@heroku/heroku-cli-util 10.8.3 → 10.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ export { AmbiguousError } from '../errors/ambiguous.js';
2
2
  export { NotFound } from '../errors/not-found.js';
3
3
  export { default as AddonResolver } from './addons/addon-resolver.js';
4
4
  export { getAddonService, isAdvancedDatabase, isAdvancedPrivateDatabase, isEssentialDatabase, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, } from './addons/helpers.js';
5
+ export { default as parseKeyValue } from './parse-key-value.js';
5
6
  export { getPsqlConfigs, sshTunnel } from './pg/bastion.js';
6
7
  export { getConfigVarNameFromAttachment } from './pg/config-vars.js';
7
8
  export { default as DatabaseResolver } from './pg/databases.js';
@@ -2,6 +2,7 @@ export { AmbiguousError } from '../errors/ambiguous.js';
2
2
  export { NotFound } from '../errors/not-found.js';
3
3
  export { default as AddonResolver } from './addons/addon-resolver.js';
4
4
  export { getAddonService, isAdvancedDatabase, isAdvancedPrivateDatabase, isEssentialDatabase, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, } from './addons/helpers.js';
5
+ export { default as parseKeyValue } from './parse-key-value.js';
5
6
  export { getPsqlConfigs, sshTunnel } from './pg/bastion.js';
6
7
  export { getConfigVarNameFromAttachment } from './pg/config-vars.js';
7
8
  export { default as DatabaseResolver } from './pg/databases.js';
@@ -0,0 +1,4 @@
1
+ export default function parseKeyValue(input: string): {
2
+ key: string;
3
+ value: string;
4
+ };
@@ -0,0 +1,6 @@
1
+ export default function parseKeyValue(input) {
2
+ let [key, value] = input.split(/=(.*)/);
3
+ key = key.trim();
4
+ value = value ? value.trim() : '';
5
+ return { key, value };
6
+ }
@@ -8,7 +8,14 @@ type Column<T extends Record<string, unknown>> = {
8
8
  type Columns<T extends Record<string, unknown>> = {
9
9
  [key: string]: Partial<Column<T>>;
10
10
  };
11
- export declare function table<T extends Record<string, unknown>>(data: T[], columns: Columns<T>, options?: Omit<TableOptions<T>, 'columns' | 'data'> & {
11
+ type TableFlags = {
12
+ columns?: string;
13
+ csv?: boolean;
14
+ extended?: boolean;
15
+ filter?: string;
16
+ sort?: string;
17
+ };
18
+ export declare function table<T extends Record<string, unknown>>(data: T[], columns: Columns<T>, options?: Omit<TableOptions<T>, 'columns' | 'data' | 'filter' | 'sort'> & TableFlags & {
12
19
  printLine?(s: unknown): void;
13
20
  }): void;
14
21
  export {};
package/dist/ux/table.js CHANGED
@@ -1,26 +1,147 @@
1
+ import { ux } from '@oclif/core/ux';
1
2
  import { printTable } from '@oclif/table';
2
- export function table(data, columns, options) {
3
- const cols = Object.entries(columns).map(([key, opts]) => {
4
- if (opts.header)
5
- return { key, name: opts.header };
3
+ import { orderBy } from 'natural-orderby';
4
+ import parseKeyValue from '../utils/parse-key-value.js';
5
+ /**
6
+ * Finds a column by id (header name or property key), case-insensitively.
7
+ * Returns undefined if no match found.
8
+ */
9
+ function getColumnById(id, columns) {
10
+ const lowerCaseId = id.toLowerCase();
11
+ for (const [key, col] of Object.entries(columns)) {
12
+ if (col.header?.toLowerCase() === lowerCaseId || key.toLowerCase() === lowerCaseId) {
13
+ return [key, col];
14
+ }
15
+ }
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Selects columns based on --columns or --extended flags.
20
+ * --columns takes precedence over --extended.
21
+ */
22
+ function selectColumns(columns, flags) {
23
+ if (flags.columns) {
24
+ const columnIds = flags.columns.split(',').filter(id => id.trim());
25
+ const uniqueColumnIds = [...new Set(columnIds)];
26
+ const selectedColumns = {};
27
+ for (const id of uniqueColumnIds) {
28
+ const column = getColumnById(id, columns);
29
+ if (!column) {
30
+ throw new Error('Columns flag has an invalid value.');
31
+ }
32
+ const [key, col] = column;
33
+ selectedColumns[key] = col;
34
+ }
35
+ return selectedColumns;
36
+ }
37
+ if (!flags.extended) {
38
+ const baseColumns = {};
39
+ for (const [key, col] of Object.entries(columns)) {
40
+ if (!col.extended) {
41
+ baseColumns[key] = col;
42
+ }
43
+ }
44
+ return baseColumns;
45
+ }
46
+ return columns;
47
+ }
48
+ /**
49
+ * Builds a filter predicate function for substring matching.
50
+ */
51
+ function getFilterPredicate(columns, filter) {
52
+ const { key: columnId, value: searchValue } = parseKeyValue(filter);
53
+ if (!searchValue) {
54
+ throw new Error('Filter flag has an invalid value.');
55
+ }
56
+ const column = getColumnById(columnId, columns);
57
+ if (!column) {
58
+ throw new Error('Filter flag has an invalid value.');
59
+ }
60
+ const [key] = column;
61
+ return (row) => {
62
+ const cellValue = String(row[key] ?? '');
63
+ return cellValue.includes(searchValue);
64
+ };
65
+ }
66
+ /**
67
+ * Gets the identifiers for the sort properties.
68
+ */
69
+ function getSortIds(columns, sort) {
70
+ const sortIds = sort
71
+ .split(',')
72
+ .filter(id => id.trim())
73
+ .map(id => {
74
+ const column = getColumnById(id, columns);
75
+ if (!column) {
76
+ throw new Error('Sort flag has an invalid value.');
77
+ }
78
+ const [key] = column;
6
79
  return key;
7
80
  });
8
- const d = data.map(row => Object.fromEntries(Object.entries(columns).map(([key, { get }]) => [key, get ? get(row) : row[key]])));
9
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
- const { printLine, ...tableOptions } = options || {};
11
- printTable({
12
- ...(tableOptions?.noStyle
13
- ? {}
14
- : {
15
- borderColor: 'whiteBright',
16
- borderStyle: 'headers-only-with-underline',
17
- headerOptions: {
18
- bold: true,
19
- color: 'white', // or 'reset' to use default terminal color
20
- },
21
- }),
22
- ...tableOptions,
23
- columns: cols,
24
- data: d,
81
+ if (sortIds.length === 0) {
82
+ throw new Error('Sort flag has an invalid value.');
83
+ }
84
+ return sortIds;
85
+ }
86
+ function escapeCSV(value) {
87
+ const needsEscaping = /["\n\r,]/.test(value);
88
+ return needsEscaping ? `"${value.replaceAll('"', '""')}"` : value;
89
+ }
90
+ function outputCSV(data, columns, printLine) {
91
+ const columnEntries = Object.entries(columns);
92
+ const columnHeaders = columnEntries.map(([key, col]) => col.header || key);
93
+ printLine(columnHeaders.join(','));
94
+ for (const row of data) {
95
+ const values = columnEntries.map(([key]) => {
96
+ const value = row[key];
97
+ return escapeCSV(String(value ?? ''));
98
+ });
99
+ printLine(values.join(','));
100
+ }
101
+ }
102
+ export function table(data, columns, options) {
103
+ const selectedColumns = selectColumns(columns, {
104
+ columns: options?.columns,
105
+ extended: options?.extended,
25
106
  });
107
+ let processedData = data.map(row => Object.fromEntries(Object.entries(selectedColumns).map(([key, { get }]) => [
108
+ key,
109
+ get ? get(row) : row[key],
110
+ ])));
111
+ if (options?.filter) {
112
+ const filterPredicate = getFilterPredicate(selectedColumns, options.filter);
113
+ processedData = processedData.filter(row => filterPredicate(row));
114
+ }
115
+ if (options?.sort) {
116
+ const sortIds = getSortIds(selectedColumns, options.sort);
117
+ processedData = orderBy(processedData, sortIds, ['asc']);
118
+ }
119
+ if (options?.csv) {
120
+ const printFn = options?.printLine || ux.stdout;
121
+ outputCSV(processedData, selectedColumns, printFn);
122
+ }
123
+ else {
124
+ const cols = Object.entries(selectedColumns).map(([key, opts]) => {
125
+ if (opts.header)
126
+ return { key, name: opts.header };
127
+ return key;
128
+ });
129
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
130
+ const { columns, csv, extended, filter, printLine, sort, ...tableOptions } = options || {};
131
+ printTable({
132
+ ...(tableOptions?.noStyle
133
+ ? {}
134
+ : {
135
+ borderColor: 'whiteBright',
136
+ borderStyle: 'headers-only-with-underline',
137
+ headerOptions: {
138
+ bold: true,
139
+ color: 'white', // or 'reset' to use default terminal color
140
+ },
141
+ }),
142
+ ...tableOptions,
143
+ columns: cols,
144
+ data: processedData,
145
+ });
146
+ }
26
147
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@heroku/heroku-cli-util",
4
- "version": "10.8.3",
4
+ "version": "10.9.0",
5
5
  "description": "Set of helpful CLI utilities",
6
6
  "author": "Heroku",
7
7
  "license": "ISC",
@@ -77,6 +77,7 @@
77
77
  "ansis": "^4.1.0",
78
78
  "debug": "^4.4.0",
79
79
  "inquirer": "^12.6.1",
80
+ "natural-orderby": "^5.0.0",
80
81
  "open": "^11",
81
82
  "printf": "^0.6.1",
82
83
  "tsheredoc": "^1.0.1",