mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-04-27 14:30:07 +00:00
v13 preparations: Standardize command data output
Change-type: patch Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
parent
c125e0b38d
commit
f3fb9b6bdf
@ -21,6 +21,7 @@ import {
|
|||||||
NotAvailableInOfflineModeError,
|
NotAvailableInOfflineModeError,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { stripIndent } from './utils/lazy';
|
import { stripIndent } from './utils/lazy';
|
||||||
|
import * as output from './framework/output';
|
||||||
|
|
||||||
export default abstract class BalenaCommand extends Command {
|
export default abstract class BalenaCommand extends Command {
|
||||||
/**
|
/**
|
||||||
@ -167,4 +168,7 @@ export default abstract class BalenaCommand extends Command {
|
|||||||
await this.getStdin();
|
await this.getStdin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected outputMessage = output.outputMessage;
|
||||||
|
protected outputData = output.outputData;
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,10 @@ import {
|
|||||||
appToFleetCmdMsg,
|
appToFleetCmdMsg,
|
||||||
warnify,
|
warnify,
|
||||||
} from '../../utils/messages';
|
} from '../../utils/messages';
|
||||||
|
import { isV13 } from '../../utils/version';
|
||||||
|
import type { DataOutputOptions } from '../../framework';
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef extends DataOutputOptions {
|
||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,13 +58,14 @@ export class FleetCmd extends Command {
|
|||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
|
...(isV13() ? cf.dataOutputFlags : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||||
const { args: params } =
|
const { args: params, flags: options } =
|
||||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
|
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
@ -82,16 +85,24 @@ export class FleetCmd extends Command {
|
|||||||
application.device_type = application.is_for__device_type[0].slug;
|
application.device_type = application.is_for__device_type[0].slug;
|
||||||
application.commit = application.should_be_running__release[0]?.commit;
|
application.commit = application.should_be_running__release[0]?.commit;
|
||||||
|
|
||||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
if (isV13()) {
|
||||||
console.log(`== ${application.app_name}`);
|
await this.outputData(
|
||||||
console.log(
|
application,
|
||||||
getVisuals().table.vertical(application, [
|
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||||
'id',
|
options,
|
||||||
'device_type',
|
);
|
||||||
'slug',
|
} else {
|
||||||
'commit',
|
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||||
]),
|
console.log(`== ${application.app_name}`);
|
||||||
);
|
console.log(
|
||||||
|
getVisuals().table.vertical(application, [
|
||||||
|
'id',
|
||||||
|
'device_type',
|
||||||
|
'slug',
|
||||||
|
'commit',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,18 +17,20 @@
|
|||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { flags } from '@oclif/command';
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
import type { Output as ParserOutput } from '@oclif/parser';
|
||||||
|
|
||||||
import Command from '../command';
|
import Command from '../command';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../utils/common-flags';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||||
import { appToFleetCmdMsg, warnify } from '../utils/messages';
|
import { appToFleetCmdMsg, warnify } from '../utils/messages';
|
||||||
|
import { isV13 } from '../utils/version';
|
||||||
|
import type { DataSetOutputOptions } from '../framework';
|
||||||
|
|
||||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||||
device_count?: number;
|
device_count: number;
|
||||||
online_devices?: number;
|
online_devices: number;
|
||||||
|
device_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef extends DataSetOutputOptions {
|
||||||
help: void;
|
help: void;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
}
|
}
|
||||||
@ -48,12 +50,17 @@ export class FleetsCmd extends Command {
|
|||||||
public static usage = 'fleets';
|
public static usage = 'fleets';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
...(isV13()
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
verbose: flags.boolean({
|
||||||
|
default: false,
|
||||||
|
char: 'v',
|
||||||
|
description: 'No-op since release v12.0.0',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
...(isV13() ? cf.dataSetOutputFlags : {}),
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
verbose: flags.boolean({
|
|
||||||
default: false,
|
|
||||||
char: 'v',
|
|
||||||
description: 'No-op since release v12.0.0',
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
@ -75,28 +82,39 @@ export class FleetsCmd extends Command {
|
|||||||
},
|
},
|
||||||
})) as ExtendedApplication[];
|
})) as ExtendedApplication[];
|
||||||
|
|
||||||
const _ = await import('lodash');
|
|
||||||
// Add extended properties
|
// Add extended properties
|
||||||
applications.forEach((application) => {
|
applications.forEach((application) => {
|
||||||
application.device_count = application.owns__device?.length ?? 0;
|
application.device_count = application.owns__device?.length ?? 0;
|
||||||
application.online_devices = _.sumBy(application.owns__device, (d) =>
|
application.online_devices =
|
||||||
d.is_online === true ? 1 : 0,
|
application.owns__device?.filter((d) => d.is_online).length || 0;
|
||||||
);
|
|
||||||
// @ts-expect-error
|
|
||||||
application.device_type = application.is_for__device_type[0].slug;
|
application.device_type = application.is_for__device_type[0].slug;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display
|
if (isV13()) {
|
||||||
console.log(
|
await this.outputData(
|
||||||
getVisuals().table.horizontal(applications, [
|
applications,
|
||||||
'id',
|
[
|
||||||
this.useAppWord ? 'app_name' : 'app_name => NAME',
|
'id',
|
||||||
'slug',
|
'app_name',
|
||||||
'device_type',
|
'slug',
|
||||||
'online_devices',
|
'device_type',
|
||||||
'device_count',
|
'device_count',
|
||||||
]),
|
'online_devices',
|
||||||
);
|
],
|
||||||
|
_parserOutput.flags,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
getVisuals().table.horizontal(applications, [
|
||||||
|
'id',
|
||||||
|
this.useAppWord ? 'app_name' : 'app_name => NAME',
|
||||||
|
'slug',
|
||||||
|
'device_type',
|
||||||
|
'online_devices',
|
||||||
|
'device_count',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
lib/framework/index.ts
Normal file
19
lib/framework/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Balena
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DataOutputOptions, DataSetOutputOptions } from './output';
|
||||||
|
|
||||||
|
export { DataOutputOptions, DataSetOutputOptions };
|
158
lib/framework/output.ts
Normal file
158
lib/framework/output.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Balena
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getCliUx, getChalk } from '../utils/lazy';
|
||||||
|
|
||||||
|
export interface DataOutputOptions {
|
||||||
|
fields?: string;
|
||||||
|
json?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataSetOutputOptions extends DataOutputOptions {
|
||||||
|
filter?: string;
|
||||||
|
'no-header'?: boolean;
|
||||||
|
'no-truncate'?: boolean;
|
||||||
|
sort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output message to STDERR
|
||||||
|
*/
|
||||||
|
export function outputMessage(msg: string) {
|
||||||
|
// Messages go to STDERR
|
||||||
|
console.error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output result data to STDOUT
|
||||||
|
* Supports:
|
||||||
|
* - arrays of items (displayed in a tabular way),
|
||||||
|
* - single items (displayed in a field per row format).
|
||||||
|
*
|
||||||
|
* @param data Array of data objects to output
|
||||||
|
* @param fields Array of fieldnames, specifying the fields and display order
|
||||||
|
* @param options Output options
|
||||||
|
*/
|
||||||
|
export async function outputData(
|
||||||
|
data: any[] | {},
|
||||||
|
fields: string[],
|
||||||
|
options: DataOutputOptions | DataSetOutputOptions,
|
||||||
|
) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
await outputDataSet(data, fields, options as DataSetOutputOptions);
|
||||||
|
} else {
|
||||||
|
await outputDataItem(data, fields, options as DataOutputOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the cli.ux table implementation, to output tabular data
|
||||||
|
*
|
||||||
|
* @param data Array of data objects to output
|
||||||
|
* @param fields Array of fieldnames, specifying the fields and display order
|
||||||
|
* @param options Output options
|
||||||
|
*/
|
||||||
|
async function outputDataSet(
|
||||||
|
data: any[],
|
||||||
|
fields: string[],
|
||||||
|
options: DataSetOutputOptions,
|
||||||
|
) {
|
||||||
|
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
|
||||||
|
// By replacing underscores with spaces here, we can support both header format and actual field name
|
||||||
|
// (e.g. as seen in json output).
|
||||||
|
options.fields = options.fields?.replace(/_/g, ' ');
|
||||||
|
options.filter = options.filter?.replace(/_/g, ' ');
|
||||||
|
options.sort = options.sort?.replace(/_/g, ' ');
|
||||||
|
|
||||||
|
getCliUx().table(
|
||||||
|
data,
|
||||||
|
// Convert fields array to column object keys
|
||||||
|
// that cli.ux expects. We can later add support
|
||||||
|
// for both formats if beneficial
|
||||||
|
fields.reduce((ac, a) => ({ ...ac, [a]: {} }), {}),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
...(options.json
|
||||||
|
? {
|
||||||
|
output: 'json',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
columns: options.fields,
|
||||||
|
printLine,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs a single data object (like `resin-cli-visuals table.vertical`),
|
||||||
|
* but supporting a subset of options from `cli-ux table` (--json and --fields)
|
||||||
|
*
|
||||||
|
* @param data Array of data objects to output
|
||||||
|
* @param fields Array of fieldnames, specifying the fields and display order
|
||||||
|
* @param options Output options
|
||||||
|
*/
|
||||||
|
async function outputDataItem(
|
||||||
|
data: any,
|
||||||
|
fields: string[],
|
||||||
|
options: DataOutputOptions,
|
||||||
|
) {
|
||||||
|
const outData: typeof data = {};
|
||||||
|
|
||||||
|
// Convert comma separated list of fields in `options.fields` to array of correct format.
|
||||||
|
// Note, user may have specified the true field name (e.g. `some_field`),
|
||||||
|
// or the format displayed in headers (e.g. `Some field`, case insensitive).
|
||||||
|
const userSelectedFields = options.fields?.split(',').map((f) => {
|
||||||
|
return f.toLowerCase().trim().replace(/ /g, '_');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Order and filter the fields based on `fields` parameter and `options.fields`
|
||||||
|
(userSelectedFields || fields).forEach((fieldName) => {
|
||||||
|
if (fields.includes(fieldName)) {
|
||||||
|
outData[fieldName] = data[fieldName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.json) {
|
||||||
|
printLine(JSON.stringify(outData, undefined, 2));
|
||||||
|
} else {
|
||||||
|
const chalk = getChalk();
|
||||||
|
const { capitalize } = await import('lodash');
|
||||||
|
|
||||||
|
// Find longest key, so we can align results
|
||||||
|
const longestKeyLength = getLongestObjectKeyLength(outData);
|
||||||
|
|
||||||
|
// Output one field per line
|
||||||
|
for (const [k, v] of Object.entries(outData)) {
|
||||||
|
const shim = ' '.repeat(longestKeyLength - k.length);
|
||||||
|
const kDisplay = capitalize(k.replace(/_/g, ' '));
|
||||||
|
printLine(`${chalk.bold(kDisplay) + shim} : ${v}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLongestObjectKeyLength(o: any): number {
|
||||||
|
return Object.keys(o).length >= 1
|
||||||
|
? Object.keys(o).reduce((a, b) => {
|
||||||
|
return a.length > b.length ? a : b;
|
||||||
|
}).length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printLine(s: any) {
|
||||||
|
// Duplicating oclif cli-ux's default implementation here,
|
||||||
|
// but using this one explicitly for ease of testing
|
||||||
|
process.stdout.write(s + '\n');
|
||||||
|
}
|
@ -16,11 +16,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { flags } from '@oclif/command';
|
||||||
|
|
||||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
|
||||||
import { stripIndent } from './lazy';
|
import { stripIndent } from './lazy';
|
||||||
import { lowercaseIfSlug } from './normalization';
|
import { lowercaseIfSlug } from './normalization';
|
||||||
|
|
||||||
import { isV13 } from './version';
|
import { isV13 } from './version';
|
||||||
|
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||||
|
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
|
||||||
|
|
||||||
export const v13: IBooleanFlag<boolean> = flags.boolean({
|
export const v13: IBooleanFlag<boolean> = flags.boolean({
|
||||||
description: stripIndent`\
|
description: stripIndent`\
|
||||||
@ -125,3 +126,37 @@ export const json: IBooleanFlag<boolean> = flags.boolean({
|
|||||||
description: 'produce JSON output instead of tabular output',
|
description: 'produce JSON output instead of tabular output',
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const dataOutputFlags: flags.Input<DataOutputOptions> = {
|
||||||
|
fields: flags.string({
|
||||||
|
description: 'only show provided fields (comma-separated)',
|
||||||
|
}),
|
||||||
|
json: flags.boolean({
|
||||||
|
char: 'j',
|
||||||
|
exclusive: ['no-truncate'],
|
||||||
|
description: 'output in json format',
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dataSetOutputFlags: flags.Input<DataOutputOptions> &
|
||||||
|
flags.Input<DataSetOutputOptions> = {
|
||||||
|
...dataOutputFlags,
|
||||||
|
filter: flags.string({
|
||||||
|
description:
|
||||||
|
'filter results by substring matching of a given field, eg: --filter field=foo',
|
||||||
|
}),
|
||||||
|
'no-header': flags.boolean({
|
||||||
|
exclusive: ['json'],
|
||||||
|
description: 'hide table header from output',
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
'no-truncate': flags.boolean({
|
||||||
|
exclusive: ['json'],
|
||||||
|
description: 'do not truncate output to fit screen',
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
sort: flags.string({
|
||||||
|
description: `field to sort by (prepend '-' for descending order)`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
@ -220,6 +220,7 @@
|
|||||||
"chalk": "^3.0.0",
|
"chalk": "^3.0.0",
|
||||||
"chokidar": "^3.4.3",
|
"chokidar": "^3.4.3",
|
||||||
"cli-truncate": "^2.1.0",
|
"cli-truncate": "^2.1.0",
|
||||||
|
"cli-ux": "^5.5.1",
|
||||||
"color-hash": "^1.0.3",
|
"color-hash": "^1.0.3",
|
||||||
"columnify": "^1.5.2",
|
"columnify": "^1.5.2",
|
||||||
"common-tags": "^1.7.2",
|
"common-tags": "^1.7.2",
|
||||||
|
255
tests/framework/output.spec.ts
Normal file
255
tests/framework/output.spec.ts
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020-2021 Balena Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable: prefer-const no-empty */
|
||||||
|
|
||||||
|
import rewire = require('rewire');
|
||||||
|
import sinon = require('sinon');
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
const dataItem = {
|
||||||
|
name: 'item1',
|
||||||
|
id: 1,
|
||||||
|
thing_color: 'blue',
|
||||||
|
thing_shape: 'square',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSet = [
|
||||||
|
{
|
||||||
|
name: 'item1',
|
||||||
|
id: 1,
|
||||||
|
thing_color: 'red',
|
||||||
|
thing_shape: 'square',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'item2',
|
||||||
|
id: 2,
|
||||||
|
thing_color: 'blue',
|
||||||
|
thing_shape: 'round',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('outputData', function () {
|
||||||
|
let outputData: any;
|
||||||
|
let outputDataSetSpy: any;
|
||||||
|
let outputDataItemSpy: any;
|
||||||
|
|
||||||
|
this.beforeEach(() => {
|
||||||
|
const output = rewire('../../build/framework/output');
|
||||||
|
|
||||||
|
outputDataSetSpy = sinon.spy();
|
||||||
|
outputDataItemSpy = sinon.spy();
|
||||||
|
|
||||||
|
output.__set__('outputDataSet', outputDataSetSpy);
|
||||||
|
output.__set__('outputDataItem', outputDataItemSpy);
|
||||||
|
|
||||||
|
outputData = output.__get__('outputData');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call outputDataSet function when data param is an array', async () => {
|
||||||
|
await outputData(dataSet);
|
||||||
|
expect(outputDataSetSpy.called).to.be.true;
|
||||||
|
expect(outputDataItemSpy.called).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call outputDataItem function when data param is an object', async () => {
|
||||||
|
await outputData(dataItem);
|
||||||
|
expect(outputDataSetSpy.called).to.be.false;
|
||||||
|
expect(outputDataItemSpy.called).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('outputDataSet', function () {
|
||||||
|
let outputDataSet: any;
|
||||||
|
let printLineSpy: any;
|
||||||
|
|
||||||
|
this.beforeEach(() => {
|
||||||
|
const output = rewire('../../build/framework/output');
|
||||||
|
printLineSpy = sinon.spy();
|
||||||
|
output.__set__('printLine', printLineSpy);
|
||||||
|
outputDataSet = output.__get__('outputDataSet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only output fields specified in `fields` param, in that order', async () => {
|
||||||
|
const fields = ['id', 'name', 'thing_color'];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
await outputDataSet(dataSet, fields, options);
|
||||||
|
|
||||||
|
// check correct number of rows (2 data, 2 header)
|
||||||
|
expect(printLineSpy.callCount).to.equal(4);
|
||||||
|
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
|
||||||
|
// check we have fields we specified
|
||||||
|
fields.forEach((f) => {
|
||||||
|
expect(headerLine).to.include(f.replace(/_/g, ' '));
|
||||||
|
});
|
||||||
|
// check we don't have fields we didn't specify
|
||||||
|
expect(headerLine).to.not.include('thing_shape');
|
||||||
|
// check order
|
||||||
|
// split header using the `name` column as delimiter
|
||||||
|
const splitHeader = headerLine.split('name');
|
||||||
|
expect(splitHeader[0]).to.include('id');
|
||||||
|
expect(splitHeader[1]).to.include('thing');
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
it('should output fields in the order specified in `fields` param', async () => {
|
||||||
|
const fields = ['thing_color', 'id', 'name'];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
await outputDataSet(dataSet, fields, options);
|
||||||
|
|
||||||
|
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
|
||||||
|
// split header using the `it` column as delimiter
|
||||||
|
const splitHeader = headerLine.split('id');
|
||||||
|
expect(splitHeader[0]).to.include('thing');
|
||||||
|
expect(splitHeader[1]).to.include('name');
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
it('should only output fields specified in `options.fields` if present', async () => {
|
||||||
|
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||||
|
const options = {
|
||||||
|
// test all formats
|
||||||
|
fields: 'Name,thing_color,Thing shape',
|
||||||
|
};
|
||||||
|
|
||||||
|
await outputDataSet(dataSet, fields, options);
|
||||||
|
|
||||||
|
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
|
||||||
|
// check we have fields we specified
|
||||||
|
expect(headerLine).to.include('name');
|
||||||
|
expect(headerLine).to.include('thing color');
|
||||||
|
expect(headerLine).to.include('thing shape');
|
||||||
|
// check we don't have fields we didn't specify
|
||||||
|
expect(headerLine).to.not.include('id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output records in order specified by `options.sort` if present', async () => {
|
||||||
|
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||||
|
const options = {
|
||||||
|
sort: 'thing shape',
|
||||||
|
'no-header': true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await outputDataSet(dataSet, fields, options);
|
||||||
|
|
||||||
|
// blue should come before red
|
||||||
|
expect(printLineSpy.getCall(0).firstArg).to.include('blue');
|
||||||
|
expect(printLineSpy.getCall(1).firstArg).to.include('red');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only output records that match filter specified by `options.filter` if present', async () => {
|
||||||
|
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||||
|
const options = {
|
||||||
|
filter: 'thing color=red',
|
||||||
|
'no-header': true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await outputDataSet(dataSet, fields, options);
|
||||||
|
|
||||||
|
// check correct number of rows (1 matched data, no-header)
|
||||||
|
expect(printLineSpy.callCount).to.equal(1);
|
||||||
|
expect(printLineSpy.getCall(0).firstArg).to.include('red');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output data in json format, if `options.json` true', async () => {
|
||||||
|
const fields = ['name', 'thing_color', 'thing_shape'];
|
||||||
|
const options = {
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: I've run into an oclif cli-ux bug, where numbers are output as strings in json
|
||||||
|
// (this can be seen by including 'id' in the fields list above).
|
||||||
|
// Issue opened: https://github.com/oclif/cli-ux/issues/309
|
||||||
|
// For now removing id for this test.
|
||||||
|
const clonedDataSet = JSON.parse(JSON.stringify(dataSet));
|
||||||
|
clonedDataSet.forEach((d: any) => {
|
||||||
|
delete d.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedJson = JSON.stringify(clonedDataSet, undefined, 2);
|
||||||
|
|
||||||
|
await outputDataSet(dataSet, fields, options);
|
||||||
|
|
||||||
|
expect(printLineSpy.callCount).to.equal(1);
|
||||||
|
expect(printLineSpy.getCall(0).firstArg).to.equal(expectedJson);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('outputDataItem', function () {
|
||||||
|
let outputDataItem: any;
|
||||||
|
let printLineSpy: any;
|
||||||
|
|
||||||
|
this.beforeEach(() => {
|
||||||
|
const output = rewire('../../build/framework/output');
|
||||||
|
printLineSpy = sinon.spy();
|
||||||
|
output.__set__('printLine', printLineSpy);
|
||||||
|
outputDataItem = output.__get__('outputDataItem');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only output fields specified in `fields` param, in that order', async () => {
|
||||||
|
const fields = ['id', 'name', 'thing_color'];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
await outputDataItem(dataItem, fields, options);
|
||||||
|
|
||||||
|
// check correct number of rows (3 fields)
|
||||||
|
expect(printLineSpy.callCount).to.equal(3);
|
||||||
|
// check we have fields we specified
|
||||||
|
fields.forEach((f, index) => {
|
||||||
|
const kvPair = printLineSpy.getCall(index).firstArg.split(':');
|
||||||
|
expect(kvPair[0].toLowerCase()).to.include(f.replace(/_/g, ' '));
|
||||||
|
expect(kvPair[1]).to.include((dataItem as any)[f]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only output fields specified in `options.fields` if present', async () => {
|
||||||
|
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||||
|
const options = {
|
||||||
|
// test all formats
|
||||||
|
fields: 'Name,thing_color,Thing shape',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedFields = ['name', 'thing_color', 'thing_shape'];
|
||||||
|
|
||||||
|
await outputDataItem(dataItem, fields, options);
|
||||||
|
|
||||||
|
// check correct number of rows (3 fields)
|
||||||
|
expect(printLineSpy.callCount).to.equal(3);
|
||||||
|
// check we have fields we specified
|
||||||
|
expectedFields.forEach((f, index) => {
|
||||||
|
const kvPair = printLineSpy.getCall(index).firstArg.split(':');
|
||||||
|
expect(kvPair[0].toLowerCase()).to.include(f.replace(/_/g, ' '));
|
||||||
|
expect(kvPair[1]).to.include((dataItem as any)[f]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output data in json format, if `options.json` true', async () => {
|
||||||
|
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||||
|
const options = {
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedJson = JSON.stringify(dataItem, undefined, 2);
|
||||||
|
|
||||||
|
await outputDataItem(dataItem, fields, options);
|
||||||
|
|
||||||
|
expect(printLineSpy.callCount).to.equal(1);
|
||||||
|
expect(printLineSpy.getCall(0).firstArg).to.equal(expectedJson);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user