Skip to content
Merged
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"@salesforce/kit": "^3.2.2",
"@salesforce/sf-plugins-core": "^11.3.12",
"form-data": "^4.0.1",
"got": "^14.4.4",
"tough-cookie": "^4.1.4"
"got": "^14.4.4"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.2.22",
Expand Down
43 changes: 34 additions & 9 deletions src/commands/data-seeding/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages, PollingClient, SfError, StatusResult } from '@salesforce/core';
import { Duration } from '@salesforce/kit';
import { initiateDataSeed, pollSeedStatus, PollSeedResponse } from '../../../utils/api.js';
import { initiateDataSeed, pollSeedStatus, PollSeedResponse, initiateJWTMint } from '../../../utils/api.js';
import { getSeedGenerateMso, getSeedGenerateStage as getStage } from '../../../utils/mso.js';
import { DataSeedingGenerateResult } from '../../../utils/types.js';
import { GenerateRequestCache } from '../../../utils/cache.js';
Expand All @@ -23,12 +23,12 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe

public static readonly flags = {
// TODO: The org flags will need to use Flags.requiredOrg() once auth is finalized
'target-org': Flags.string({
'target-org': Flags.requiredOrg({
summary: messages.getMessage('flags.target-org.summary'),
char: 'o',
required: true,
}),
'source-org': Flags.string({
'source-org': Flags.requiredOrg({
summary: messages.getMessage('flags.source-org.summary'),
char: 's',
required: true,
Expand All @@ -55,10 +55,28 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe

public async run(): Promise<DataSeedingGenerateResult> {
const { flags } = await this.parse(DataSeedingGenerate);
const { async, 'config-file': configFile, 'source-org': sourceOrg, 'target-org': targetOrg, wait } = flags;

const { request_id: jobId } = await initiateDataSeed(configFile, 'data-generation');

const { async, 'config-file': configFile, 'source-org': srcOrgObj, 'target-org': tgtOrgObj, wait } = flags;

const sourceOrg = srcOrgObj.getOrgId();
const srcAccessToken = srcOrgObj.getConnection().accessToken as string;
const srcOrgInstUrl = srcOrgObj.getConnection().instanceUrl;

const targetOrg = tgtOrgObj.getOrgId();
const tgtAccessToken = tgtOrgObj.getConnection().accessToken as string;
const tgtOrgInstUrl = tgtOrgObj.getConnection().instanceUrl;

// Fetch Valid JWT with Data Seed Org Perm
const { jwt: jwtValue } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const { request_id: jobId } = await initiateDataSeed(
configFile,
'data-generation',
jwtValue,
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken,
sourceOrg
);
const reportMessage = messages.getMessage('report.suggestion', [jobId]);

if (!jobId) throw new Error('Failed to receive job id');
Expand All @@ -83,7 +101,13 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe

const options: PollingClient.Options = {
poll: async (): Promise<StatusResult> => {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken
);
const response = await pollSeedStatus(jobId, jwtValueNew);

mso.goto(getStage(response.step), {
startTime: response.execution_start_time,
Expand Down Expand Up @@ -134,7 +158,8 @@ export default class DataSeedingGenerate extends SfCommand<DataSeedingGenerateRe
throw err;
}
} else {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const response = await pollSeedStatus(jobId, jwtValueNew);

const mso = getSeedGenerateMso({
jsonEnabled: this.jsonEnabled(),
Expand Down
2 changes: 1 addition & 1 deletion src/commands/data-seeding/generate/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class DataSeedingGenerateReport extends SfCommand<DataSeedingRepo

if (!jobId) throw new SfError('No job ID provided or found in cache');

const response = await pollSeedStatus(jobId);
const response = await pollSeedStatus(jobId, '');

const data = {
jobId,
Expand Down
42 changes: 34 additions & 8 deletions src/commands/data-seeding/migrate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Duration } from '@salesforce/kit';
import { Messages, PollingClient, StatusResult, SfError } from '@salesforce/core';
import { initiateDataSeed, PollSeedResponse, pollSeedStatus } from '../../../utils/api.js';
import { initiateDataSeed, PollSeedResponse, pollSeedStatus, initiateJWTMint } from '../../../utils/api.js';
import { DataSeedingMigrateResult } from '../../../utils/types.js';
import { getSeedMigrateMso, getSeedMigrateStage as getStage } from '../../../utils/mso.js';
import { MigrateRequestCache } from '../../../utils/cache.js';
Expand All @@ -23,12 +23,12 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu

public static readonly flags = {
// TODO: The org flags will need to use Flags.requiredOrg() once auth is finalized
'target-org': Flags.string({
'target-org': Flags.requiredOrg({
summary: messages.getMessage('flags.target-org.summary'),
char: 'o',
required: true,
}),
'source-org': Flags.string({
'source-org': Flags.requiredOrg({
summary: messages.getMessage('flags.source-org.summary'),
char: 's',
required: true,
Expand All @@ -55,9 +55,28 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu

public async run(): Promise<DataSeedingMigrateResult> {
const { flags } = await this.parse(DataSeedingMigrate);
const { async, 'config-file': configFile, 'source-org': sourceOrg, 'target-org': targetOrg, wait } = flags;

const { request_id: jobId } = await initiateDataSeed(configFile, 'data-copy');
const { async, 'config-file': configFile, 'source-org': sourceOrgObj, 'target-org': targetOrgObj, wait } = flags;

const sourceOrg = sourceOrgObj.getOrgId();
const srcAccessToken = sourceOrgObj.getConnection().accessToken as string;
const srcOrgInstUrl = sourceOrgObj.getConnection().instanceUrl;

const targetOrg = targetOrgObj.getOrgId();
const tgtAccessToken = targetOrgObj.getConnection().accessToken as string;
const tgtOrgInstUrl = targetOrgObj.getConnection().instanceUrl;

// Fetch Valid JWT with Data Seed Org Perm
const { jwt: jwtValue } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const { request_id: jobId } = await initiateDataSeed(
configFile,
'data-copy',
jwtValue,
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken,
sourceOrg
);

if (!jobId) throw new Error('Failed to receive job id');

Expand All @@ -83,7 +102,13 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu

const options: PollingClient.Options = {
poll: async (): Promise<StatusResult> => {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(
srcOrgInstUrl,
srcAccessToken,
tgtOrgInstUrl,
tgtAccessToken
);
const response = await pollSeedStatus(jobId, jwtValueNew);

mso.goto(getStage(response.step), {
startTime: response.execution_start_time,
Expand Down Expand Up @@ -136,7 +161,8 @@ export default class DataSeedingMigrate extends SfCommand<DataSeedingMigrateResu
throw err;
}
} else {
const response = await pollSeedStatus(jobId);
const { jwt: jwtValueNew } = await initiateJWTMint(srcOrgInstUrl, srcAccessToken, tgtOrgInstUrl, tgtAccessToken);
const response = await pollSeedStatus(jobId, jwtValueNew);

const mso = getSeedMigrateMso({
jsonEnabled: this.jsonEnabled(),
Expand Down
6 changes: 3 additions & 3 deletions src/commands/data-seeding/migrate/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getSeedMigrateMso, getSeedMigrateStage as getStage } from '../../../uti
import { DataSeedingReportResult } from '../../../utils/types.js';
import { MigrateRequestCache } from '../../../utils/cache.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url)
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-data-seeding', 'data-seeding.migrate.report');

export default class DataSeedingMigrateReport extends SfCommand<DataSeedingReportResult> {
Expand Down Expand Up @@ -40,7 +40,7 @@ export default class DataSeedingMigrateReport extends SfCommand<DataSeedingRepor

if (!jobId) throw new SfError('No job ID provided or found in cache');

const response = await pollSeedStatus(jobId);
const response = await pollSeedStatus(jobId, '');

const data = {
jobId,
Expand Down Expand Up @@ -77,4 +77,4 @@ export default class DataSeedingMigrateReport extends SfCommand<DataSeedingRepor
...data,
};
}
}
}
99 changes: 72 additions & 27 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@

import fs from 'node:fs';
import got from 'got';
import { CookieJar } from 'tough-cookie';
import FormData from 'form-data';
import { SfError, Logger } from '@salesforce/core';

export type SeedResponse = {
request_id: string;
};
export type ServletResponse = {
jwt: string;
};
export type AuthServletResponse = {
statusCode: string;
body: string;
};

export type PollSeedResponse = {
execution_end_time: string;
Expand All @@ -26,41 +32,36 @@ export type PollSeedResponse = {

export type DataSeedingOperation = 'data-generation' | 'data-copy';

const baseUrl = process.env.SF_DATA_SEEDING_URL ?? 'https://data-seed-scratchpad5.sfdc-3vx9f4.svc.sfdcfc.net';
const csrfUrl = `${baseUrl}/get-csrf-token`;
const baseUrl = 'https://api.salesforce.com/platform/data-seed/v1';
const seedUrl = `${baseUrl}/data-seed`;
const pollUrl = `${baseUrl}/status`;

export const getCookieJar = async (): Promise<CookieJar> => {
const cookieJar = new CookieJar();
await got(csrfUrl, { cookieJar });
return cookieJar;
};

export const getCsrfToken = (cookieJar: CookieJar): string => {
const csrfToken = cookieJar.getCookiesSync(csrfUrl).find((cookie) => cookie.key === 'csrf_token')?.value;
if (!csrfToken) throw new SfError('Failed to obtain CSRF token');

return csrfToken;
};

export const initiateDataSeed = async (config: string, operation: DataSeedingOperation): Promise<SeedResponse> => {
const cookieJar = await getCookieJar();
const csrf = getCsrfToken(cookieJar);

const sfRegion = 'us-east-1'
export const initiateDataSeed = async (
config: string,
operation: DataSeedingOperation,
jwt: string,
srcOrgUrl: string,
srcAccessToken: string,
tgtOrgUrl: string,
tgtAccessToken: string,
srcOrgId: string
): Promise<SeedResponse> => {
const form = new FormData();
form.append('config_file', fs.createReadStream(config));
form.append('credentials_file', fs.createReadStream('ignore/credentials.txt'));
form.append('operation', operation);

form.append('source_access_token', srcAccessToken);
form.append('source_instance_url', srcOrgUrl);
form.append('target_access_token', tgtAccessToken);
form.append('target_instance_url', tgtOrgUrl);
form.append('source_org_id',srcOrgId);
// TODO: Update to use .json() instead of JSON.parse once the Error response is changed to be JSON
// Update the return type as well
const response = await got.post(seedUrl, {
throwHttpErrors: false,
cookieJar,
headers: {
...form.getHeaders(),
'X-CSRFToken': csrf,
Authorization: `Bearer ${jwt}`,
'x-salesforce-region':sfRegion,
},
body: form,
});
Expand All @@ -72,12 +73,56 @@ export const initiateDataSeed = async (config: string, operation: DataSeedingOpe
return JSON.parse(response.body) as SeedResponse;
};

export const pollSeedStatus = async (jobId: string): Promise<PollSeedResponse> => {
export const initiateJWTMint = async (
srcOrgUrl: string,
srcAccessToken: string,
tgtOrgUrl: string,
tgtAccessToken: string
): Promise<ServletResponse> => {
const srcServletUrl = `${srcOrgUrl}/dataseed/auth`;
const tgtServletUrl = `${tgtOrgUrl}/dataseed/auth`;

const [responseSrc, responseTgt] = await Promise.all([
callAuthServlet(srcServletUrl, srcAccessToken),
callAuthServlet(tgtServletUrl, tgtAccessToken),
]);

if (responseSrc.statusCode === '200') {
return JSON.parse(responseSrc.body) as ServletResponse;
}

if (responseTgt.statusCode === '200') {
return JSON.parse(responseTgt.body) as ServletResponse;
}

throw new SfError(
`Org permission for data seed not found in either the source or target org.\nSource Response: Error Code : ${responseSrc.statusCode} - ${responseSrc.body}. \nTarget Response: Error Code : ${responseTgt.statusCode} - ${responseTgt.body}`
);
};

const callAuthServlet = async (url: string, accessToken: string): Promise<AuthServletResponse> => {
const response = await got.post(url, {
throwHttpErrors: false,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return {
statusCode: response.statusCode.toString(), // Convert to string
body: response.body,
};
};

export const pollSeedStatus = async (jobId: string, jwt: string): Promise<PollSeedResponse> => {
const logger = await Logger.child('PollSeedStatus');

// TODO: Update to use .json() instead of JSON.parse once the Error response is changed to be JSON
// Update the return type as well
const response = await got.get(`${pollUrl}/${jobId}`, { throwHttpErrors: false });
const headers = {
Authorization: `Bearer ${jwt}`,
'x-salesforce-region': sfRegion,
};
const response = await got.get(`${pollUrl}/${jobId}`, { throwHttpErrors: false, headers });

if (response.statusCode !== 200) {
// TODO: Print error body once the Error response is changed to be JSON
Expand Down
1 change: 1 addition & 0 deletions src/utils/mso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type MsoGet = string | undefined;
// - They have been converted to lowercase for later comparison
// The values in this Map are used as the stage names in mso
const seedGenerateStagesMap = new Map<string, string>([
['init','Initializing'],
['querying source org', 'Querying Source Org'],
['data generation', 'Data Generation'],
['populating target org', 'Populating Target Org'],
Expand Down
10 changes: 0 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7396,16 +7396,6 @@ tough-cookie@*:
universalify "^0.2.0"
url-parse "^1.5.3"

tough-cookie@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
dependencies:
psl "^1.1.33"
punycode "^2.1.1"
universalify "^0.2.0"
url-parse "^1.5.3"

tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
Expand Down