Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(data:export): add NUTs for big exports #1145

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ jobs:
os: [ubuntu-latest, windows-latest]
command:
- 'yarn test:nuts:bulk:export'
- 'yarn test:nuts:bulk:import'
- 'yarn test:nuts:bulk:update'
- 'yarn test:nuts:data:bulk-upsert-delete'
- 'yarn test:nuts:data:create'
- 'yarn test:nuts:data:query'
- 'yarn test:nuts:data:record'
- 'yarn test:nuts:data:search'
- 'yarn test:nuts:data:tree'
# - 'yarn test:nuts:bulk:import'
# - 'yarn test:nuts:bulk:update'
# - 'yarn test:nuts:data:bulk-upsert-delete'
# - 'yarn test:nuts:data:create'
# - 'yarn test:nuts:data:query'
# - 'yarn test:nuts:data:record'
# - 'yarn test:nuts:data:search'
# - 'yarn test:nuts:data:tree'
fail-fast: false
with:
os: ${{ matrix.os }}
Expand Down
25 changes: 22 additions & 3 deletions src/bulkUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Transform, Readable } from 'node:stream';
import { createInterface } from 'node:readline';
import { pipeline } from 'node:stream/promises';
import * as fs from 'node:fs';
import { EOL } from 'node:os';
import { EOL, platform } from 'node:os';
import { HttpApi } from '@jsforce/jsforce-node/lib/http-api.js';
import { HttpResponse } from '@jsforce/jsforce-node';
import {
Expand Down Expand Up @@ -96,6 +96,7 @@ export async function exportRecords(
outputInfo: {
filePath: string;
format: 'csv' | 'json';
lineEnding: 'CRLF' | 'LF';
columnDelimiter: ColumnDelimiterKeys;
}
): Promise<QueryJobInfoV2> {
Expand All @@ -111,6 +112,11 @@ export async function exportRecords(
throw new Error('could not get job info after polling');
}

const lineEndingsMap = {
CRLF: '\r\n',
LF: '\n',
};

let locator: string | undefined;

let recordsWritten = 0;
Expand Down Expand Up @@ -164,14 +170,27 @@ export async function exportRecords(
await pipeline(
locator
? [
Readable.from(res.body.slice(res.body.indexOf(EOL) + 1)),
Readable.from(
res.body.slice(
res.body.indexOf(EOL) + 1,
platform() === 'win32' ? res.body.lastIndexOf(lineEndingsMap[outputInfo.lineEnding]) : undefined
)
),
fs.createWriteStream(outputInfo.filePath, {
// Open file for appending. The file is created if it does not exist.
// https://nodejs.org/api/fs.html#file-system-flags
flags: 'a', // append mode
}),
]
: [Readable.from(res.body), fs.createWriteStream(outputInfo.filePath)]
: [
Readable.from(
res.body.slice(
0,
platform() === 'win32' ? res.body.lastIndexOf(lineEndingsMap[outputInfo.lineEnding]) : undefined
)
),
fs.createWriteStream(outputInfo.filePath),
]
);
}

Expand Down
1 change: 1 addition & 0 deletions src/commands/data/export/bulk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export default class DataExportBulk extends SfCommand<DataExportBulkResult> {
const jobInfo = await exportRecords(conn, queryJob, {
filePath: flags['output-file'],
format: flags['result-format'],
lineEnding,
columnDelimiter,
});

Expand Down
2 changes: 2 additions & 0 deletions src/commands/data/export/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export default class DataExportResume extends SfCommand<DataExportResumeResult>
});

try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const jobInfo = await exportRecords(resumeOpts.options.connection, queryJob, resumeOpts.outputInfo);

ms.stop();
Expand Down
35 changes: 34 additions & 1 deletion test/commands/data/export/bulk.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('data export bulk NUTs', () => {
});

const soqlQuery = 'select id,name,phone, annualrevenue from account';
const soqlQueryFields = ['Id', 'Name', 'Phone', 'AnnualRevenue'];

it('should export records in csv format', async () => {
const outputFile = 'export-accounts.csv';
Expand All @@ -55,6 +56,34 @@ describe('data export bulk NUTs', () => {
await validateCsv(path.join(session.dir, 'data-project', outputFile), 'COMMA', ensureNumber(result?.totalSize));
});

it('should export +1 million records in csv format', async () => {
const outputFile = 'export-scratch-info.csv';
const command = `data export bulk -q "select id,ExpirationDate from scratchorginfo" --output-file ${outputFile} --wait 10 --json -o ${session.hubOrg.username}`;

const result = execCmd<DataExportBulkResult>(command, { ensureExitCode: 0 }).jsonOutput?.result;

expect(result?.totalSize).to.be.greaterThan(1_000_000);
expect(result?.filePath).to.equal(outputFile);

await validateCsv(path.join(session.dir, 'data-project', outputFile), 'COMMA', ensureNumber(result?.totalSize));
});

it('should export +1 million records in json format', async () => {
const outputFile = 'export-scratch-info.json';
const command = `data export bulk -q "SELECT Id,ExpirationDate FROM scratchorginfo" --output-file ${outputFile} --wait 10 --json -o ${session.hubOrg.username} --result-format json`;

const result = execCmd<DataExportBulkResult>(command, { ensureExitCode: 0 }).jsonOutput?.result;

expect(result?.totalSize).to.be.greaterThan(1_000_000);
expect(result?.filePath).to.equal(outputFile);

await validateJson(
path.join(session.dir, 'data-project', outputFile),
['Id', 'ExpirationDate'],
ensureNumber(result?.totalSize)
);
});

it('should export records in csv format with PIPE delimiter', async () => {
const outputFile = 'export-accounts.csv';
const command = `data export bulk -q "${soqlQuery}" --output-file ${outputFile} --wait 10 --column-delimiter PIPE --json`;
Expand All @@ -76,6 +105,10 @@ describe('data export bulk NUTs', () => {
expect(result?.totalSize).to.equal(totalAccountRecords);
expect(result?.filePath).to.equal(outputFile);

await validateJson(path.join(session.dir, 'data-project', outputFile), ensureNumber(totalAccountRecords));
await validateJson(
path.join(session.dir, 'data-project', outputFile),
soqlQueryFields,
ensureNumber(totalAccountRecords)
);
});
});
7 changes: 6 additions & 1 deletion test/commands/data/export/resume.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('data export resume NUTs', () => {
});

const soqlQuery = 'select id,name,phone, annualrevenue from account';
const soqlQueryFields = ['Id', 'Name', 'Phone', 'AnnualRevenue'];

it('should resume export in csv format', async () => {
const outputFile = 'export-accounts.csv';
Expand Down Expand Up @@ -85,6 +86,10 @@ describe('data export resume NUTs', () => {
expect(exportResumeResult?.totalSize).to.be.equal(totalAccountRecords);
expect(exportResumeResult?.filePath).to.equal(outputFile);

await validateJson(path.join(session.dir, 'data-project', outputFile), ensureNumber(totalAccountRecords));
await validateJson(
path.join(session.dir, 'data-project', outputFile),
soqlQueryFields,
ensureNumber(totalAccountRecords)
);
});
});
4 changes: 2 additions & 2 deletions test/commands/data/tree/dataTreeMoreThan200.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('data:tree commands with more than 200 records are batches in safe grou
ensureExitCode: 0,
}
);
expect(importResult.jsonOutput?.result.length).to.equal(10000, 'Expected 10000 records to be imported');
expect(importResult.jsonOutput?.result.length).to.equal(10_000, 'Expected 10000 records to be imported');

execCmd(
`data:export:tree --query "${query}" --prefix ${prefix} --output-dir ${path.join(
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('data:tree commands with more than 200 records are batches in safe grou
}).jsonOutput;

expect(queryResults?.result.totalSize).to.equal(
10000,
10_000,
`Expected 10000 Account objects returned by the query to org: ${importAlias}`
);
});
Expand Down
17 changes: 9 additions & 8 deletions test/testUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,17 @@ export async function validateCsv(
}

/**
* Validate that a JSON file has X records
* Validate that a JSON file has an array of records (and props)
*
* @param filePath JSON file to validate
* @param props Array of props each record should have
* @param totalqty Total amount of records in the file
*/
export async function validateJson(filePath: string, totalqty: number): Promise<void> {
export async function validateJson(filePath: string, props: string[], totalqty: number): Promise<void> {
// check records have expected fields
const fieldsRes = await exec(
`jq 'map(has("Id") and has("Name") and has("Phone") and has("AnnualRevenue")) | all' ${filePath}`,
{
shell: 'pwsh',
}
);
const fieldsRes = await exec(`jq 'map(${props.map((field) => `has("${field}")`).join(' and ')}) | all' ${filePath}`, {
shell: 'pwsh',
Copy link
Member Author

@cristiand391 cristiand391 Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small refactor to remove hardcoded fields in the jq query, the caller can now pass them as an array of string. No functional changes.

});
expect(fieldsRes.stdout.trim()).equal('true');

// check all records were written
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2651,7 +2651,7 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==

ansis@^3.3.2, ansis@^3.4.0, ansis@^3.5.2, ansis@^3.6.0:
ansis@^3.3.2, ansis@^3.5.2, ansis@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.6.0.tgz#f4d8437fb27659bf5a6adca90135801919dee764"
integrity sha512-8KluYVZM+vx19f5rInhdEBdIAjvBp7ASzyF/DoStcDpMJ3JOM55ybvUcs9nMRVP8XN2K3ABBdO7zCXezvrT0pg==
Expand Down
Loading