Skip to content

Commit

Permalink
fix: support column names with commas
Browse files Browse the repository at this point in the history
  • Loading branch information
JaredReisinger committed Sep 1, 2024
1 parent 53cc3d9 commit 726aede
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 64 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"scrollback",
"skus",
"snyk",
"subkey",
"subval",
"tseslint",
"tsimp",
"wcutils",
Expand Down
30 changes: 1 addition & 29 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,6 @@ export interface Args {

export { type Argv };

// // See
// // https://exploringjs.com/nodejs-shell-scripting/ch_nodejs-path.html#detecting-if-module-is-main
// // for the logic behind this check...
// if (
// import.meta.url.startsWith('file:') &&
// process.argv[1] === fileURLToPath(import.meta.url)
// ) {
// try {
// await main();
// } catch (e) {
// if (e instanceof Error || typeof e === 'string') {
// err(e);
// } else {
// err(`unexpected error: ${typeof e}`);
// }

// if (
// !(e instanceof UserError) &&
// typeof e === 'object' &&
// e &&
// 'stack' in e
// ) {
// err(String(e.stack), chalk.white);
// }

// process.exit(1);
// }
// }

export default async function main(yargsHook?: (yargs: Argv<Args>) => void) {
const cfg = await loadConfig();

Expand Down Expand Up @@ -118,6 +89,7 @@ async function loadConfig(): Promise<ConfigFile> {
const cfg = JSON.parse(data.toString()) as ConfigFile;
cfg._filename = filename;
return cfg;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// We *could* check the error code for errno -2 (ENOENT), but really
// any failure means we should have the default config...
Expand Down
38 changes: 34 additions & 4 deletions src/commands/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,30 @@ test('Get.collectMetaFields() should return meta field descriptors', (t) => {
{ meta: { meta1: '' } },
{ meta: { meta2: '' } },
{ meta: { meta1: 'SOME-VALUE' } },
{ meta: { arr: ['one', 'two'] } },
] as unknown as WooItem[]);
t.deepEqual(result, [

// we don't test against `value`, because it's a lexically-scoped accessor
// function
t.like(result, [
{
label: 'arr',
meta: { allBlank: true, allIdentical: true, maximumWidth: 3 },
},
{
label: 'arr: one',
meta: { allBlank: true, allIdentical: true, maximumWidth: 8 },
},
{
label: 'arr: two',
meta: { allBlank: true, allIdentical: true, maximumWidth: 8 },
},
{
label: 'meta1',
value: 'meta["meta1"]',
// value analysis hasn't happened yet!
meta: { allBlank: true, allIdentical: true, maximumWidth: 5 },
},
{
label: 'meta2',
value: 'meta["meta2"]',
meta: { allBlank: true, allIdentical: true, maximumWidth: 5 },
},
]);
Expand Down Expand Up @@ -352,6 +365,23 @@ test('Get.run() can present specific columns', async (t) => {
await t.notThrowsAsync(result);
});

test('Get.run() can handle escaped-comma columns', async (t) => {
const get = createGet();
const { client, stub } = createClientStub();

stub.withArgs('data/currencies').resolves(currencies);
stub.withArgs('orders').resolves([order]);

const result = get.run(
'foo',
// @ts-expect-error -- fake args
{ columns: 'column\\, with comma' },
client
);
t.true(stub.calledWith('orders'));
await t.notThrowsAsync(result);
});

test('Get.run() can output CSV to a file', async (t) => {
const get = createGet();
const { client, stub } = createClientStub();
Expand Down
140 changes: 109 additions & 31 deletions src/commands/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ Examples:

// Determine all the fields based on the items, and analyze them so that we
// can (potentially) omit some...
const allFields = this.defineAllFields(items, currencies);
const allFields = this.discoverFields(items, currencies);

const metaOnly = argv.listSkus || argv.listStatuses || argv.listColumns;

Expand All @@ -372,11 +372,42 @@ Examples:
let fields: typeof allFields;

if (argv.columns) {
// If columns is given, create *exactly* those fields...
const cols = argv.columns.split(',').map((s) => s.trim());
fields = cols
.map((c) => allFields.find((f) => f.label === c))
.filter(exists);
// If columns is given, create *exactly* those fields... note that a
// column/meta name *can* contain a comma; we use "\," for this. We used
// to have a simple map() to process, but we need a loop to handle
// "compressing" elements together if needed.
const cols = argv.columns.split(',');
for (let i = 0; i < cols.length; i++) {
// if the column ends with \, combine, and restart processing from the
// same field (in case there's another escape!)
const s = cols[i];
if (s.endsWith('\\') && i + 1 < cols.length) {
cols[i] = `${s.substring(0, s.length - 1)},${cols[i + 1]}`;
cols.splice(i + 1, 1);
i--;
continue;
}

cols[i] = s.trim();
}

// We *used* to filter to only existing (found) columns/fields, but there
// are cases where we need a placeholder column in the CSV... for an
// occasional field, we don't want the output to change based on a
// particular range *not* having the column!
fields = cols.map(
(c) =>
allFields.find((f) => f.label === c) || {
label: c,
value: () => '',
config: {},
meta: {
allBlank: true,
allIdentical: true,
maximumWidth: c.length,
},
}
);
} else {
// Regardless of the "--omit-..." options, we *always* want to include the
// quantity and total.
Expand Down Expand Up @@ -531,8 +562,7 @@ Examples:
// .replace(printable.ansiEscapeCodes, ''); // can't do this... chalk!
}

// TODO: we need a way to customize the fields: re-order, include/omit, etc.
defineAllFields(items: WooItem[], currencies: WooCurrencies) {
discoverFields(items: WooItem[], currencies: WooCurrencies) {
// We always wants these fields first... each item is either used as both a
// key and label, or is a [label, key] pair (or [label, key, config]).
const fieldLabelKeys = [
Expand Down Expand Up @@ -618,27 +648,75 @@ Examples:
return allFields;
}

collectMetaFields(items: WooItem[]) {
// While wc/v1 included separate label/key information in the line item
// metadata, v2 and v3 have only the key. For add-on properties, it is a
// good displayable value; variation attributes, however, return the
// attribute/variation *slug* as the key. There's not much we can do about
// this. (In theory, we could introspect the product, but we don't want
// that much detailed knowledge in this tool!)
const slugs = new Set(items.flatMap((i) => Object.keys(i.meta)));
helpers.dbg(3, 'meta slugs', slugs);
const metaFields = [...slugs].map<AugmentedFieldInfo<WooItem>>((s) => ({
// value: `meta["${s}"].value`,
value: `meta["${s}"]`,
label: s,
meta: {
allBlank: true,
allIdentical: true,
maximumWidth: s.length,
},
}));
collectMetaFields(items: WooItem[]): AugmentedFieldInfo<WooItem>[] {
// For each key in the meta sub-object, create a field definition. Also be
// aware that some meta fields can contain *lists*, in which case we create
// a pseudo-field for each sub-item.
const metaFieldMap: Record<string, AugmentedFieldInfo<WooItem>> = {};

items.forEach((i) => {
Object.entries(i.meta).forEach(([key, val]) => {
// first, create a field for the main key if we haven't seen it yet
if (!metaFieldMap[key]) {
metaFieldMap[key] = {
label: key,
// we used to just use `meta["key"]` as the value accessor, but we
// now need to also handle array-containing cases!
value: (item: WooItem) => {
const itemVal = item.meta[key];
if (Array.isArray(itemVal)) {
return itemVal.join(', ');
}
return itemVal;
},

config: {},

meta: {
allBlank: true,
allIdentical: true,
maximumWidth: key.length,
},
};
}

metaFields.sort((a, b) => helpers.stringCompare(a.label, b.label));
// now, include any sub-fields...
if (Array.isArray(val)) {
val.forEach((subval: string) => {
const subkey = `${key}: ${subval}`;
if (!metaFieldMap[subkey]) {
metaFieldMap[subkey] = {
label: subkey,
// we used to just use `meta["key"]` as the value accessor, but we
// now need to also handle array-containing cases!
value: (item: WooItem) => {
const itemVal = item.meta[key];
if (
itemVal === subval ||
(Array.isArray(itemVal) && itemVal.includes(subval))
) {
return 'YES';
}
return '';
},

config: {},

meta: {
allBlank: true,
allIdentical: true,
maximumWidth: subkey.length,
},
};
}
});
}
});
});

const metaFields = Object.values(metaFieldMap).sort((a, b) =>
helpers.stringCompare(a.label, b.label)
);
helpers.dbg(3, 'metaFields', metaFields);

return metaFields;
Expand Down Expand Up @@ -712,6 +790,6 @@ function listFieldValues<T>(
);
}

function exists<T>(value: T | undefined | null | false | ''): value is T {
return !!value;
}
// function exists<T>(value: T | undefined | null | false | ''): value is T {
// return !!value;
// }

0 comments on commit 726aede

Please sign in to comment.