diff --git a/.vscode/settings.json b/.vscode/settings.json index a8e4676..74a7dfb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,8 @@ "scrollback", "skus", "snyk", + "subkey", + "subval", "tseslint", "tsimp", "wcutils", diff --git a/src/cli.ts b/src/cli.ts index f603665..4d27458 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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) => void) { const cfg = await loadConfig(); @@ -118,6 +89,7 @@ async function loadConfig(): Promise { 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... diff --git a/src/commands/get.test.ts b/src/commands/get.test.ts index ecf1ef5..a002ed5 100644 --- a/src/commands/get.test.ts +++ b/src/commands/get.test.ts @@ -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 }, }, ]); @@ -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(); diff --git a/src/commands/get.ts b/src/commands/get.ts index 3cc81e5..dd7067c 100644 --- a/src/commands/get.ts +++ b/src/commands/get.ts @@ -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; @@ -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. @@ -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 = [ @@ -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>((s) => ({ - // value: `meta["${s}"].value`, - value: `meta["${s}"]`, - label: s, - meta: { - allBlank: true, - allIdentical: true, - maximumWidth: s.length, - }, - })); + collectMetaFields(items: WooItem[]): AugmentedFieldInfo[] { + // 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> = {}; + + 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; @@ -712,6 +790,6 @@ function listFieldValues( ); } -function exists(value: T | undefined | null | false | ''): value is T { - return !!value; -} +// function exists(value: T | undefined | null | false | ''): value is T { +// return !!value; +// }