@aderis-erp/cli 1.0.15 → 1.1.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.
Files changed (182) hide show
  1. package/README.md +30 -8
  2. package/dist/commands/access-groups.d.ts +0 -1
  3. package/dist/commands/access-groups.js +54 -78
  4. package/dist/commands/account-transaction-allocations.d.ts +0 -1
  5. package/dist/commands/account-transaction-allocations.js +51 -75
  6. package/dist/commands/account-transactions.d.ts +0 -1
  7. package/dist/commands/account-transactions.js +80 -94
  8. package/dist/commands/accounts.d.ts +0 -1
  9. package/dist/commands/accounts.js +59 -83
  10. package/dist/commands/agent-platform.d.ts +0 -1
  11. package/dist/commands/agent-platform.js +58 -82
  12. package/dist/commands/agent-runs.d.ts +39 -0
  13. package/dist/commands/agent-runs.js +1155 -0
  14. package/dist/commands/analytics.d.ts +0 -1
  15. package/dist/commands/analytics.js +52 -76
  16. package/dist/commands/{text-templates.d.ts → api-keys.d.ts} +1 -2
  17. package/dist/commands/api-keys.js +1150 -0
  18. package/dist/commands/async-jobs.d.ts +39 -0
  19. package/dist/commands/async-jobs.js +1158 -0
  20. package/dist/commands/audit-log.d.ts +0 -1
  21. package/dist/commands/audit-log.js +51 -75
  22. package/dist/commands/auth/login.d.ts +9 -0
  23. package/dist/commands/auth/login.js +73 -20
  24. package/dist/commands/auth/logout.d.ts +4 -0
  25. package/dist/commands/auth/logout.js +4 -0
  26. package/dist/commands/auth/status.js +26 -17
  27. package/dist/commands/auth/tenant.js +3 -6
  28. package/dist/commands/auth.d.ts +0 -1
  29. package/dist/commands/auth.js +5 -34
  30. package/dist/commands/bank-accounts.d.ts +0 -1
  31. package/dist/commands/bank-accounts.js +68 -91
  32. package/dist/commands/bank-integrations.d.ts +1 -1
  33. package/dist/commands/bank-integrations.js +71 -92
  34. package/dist/commands/business-partners.d.ts +1 -1
  35. package/dist/commands/business-partners.js +84 -100
  36. package/dist/commands/chat.d.ts +0 -1
  37. package/dist/commands/chat.js +57 -80
  38. package/dist/commands/config/add.d.ts +3 -0
  39. package/dist/commands/config/add.js +34 -16
  40. package/dist/commands/config/current.js +3 -0
  41. package/dist/commands/config/list.js +3 -0
  42. package/dist/commands/credit-card-accounts.d.ts +1 -1
  43. package/dist/commands/credit-card-accounts.js +72 -90
  44. package/dist/commands/delivery-notes.d.ts +0 -1
  45. package/dist/commands/delivery-notes.js +83 -105
  46. package/dist/commands/dunning-configs.d.ts +0 -1
  47. package/dist/commands/dunning-configs.js +54 -78
  48. package/dist/commands/dunning-reminders.d.ts +0 -1
  49. package/dist/commands/dunning-reminders.js +57 -81
  50. package/dist/commands/dunning-runs.d.ts +0 -1
  51. package/dist/commands/dunning-runs.js +50 -74
  52. package/dist/commands/email-transactions.d.ts +0 -1
  53. package/dist/commands/email-transactions.js +52 -76
  54. package/dist/commands/employees.d.ts +0 -1
  55. package/dist/commands/employees.js +81 -105
  56. package/dist/commands/entity-configurations.d.ts +0 -1
  57. package/dist/commands/entity-configurations.js +52 -76
  58. package/dist/commands/event-processor-mappings.d.ts +0 -1
  59. package/dist/commands/event-processor-mappings.js +54 -78
  60. package/dist/commands/files.d.ts +0 -1
  61. package/dist/commands/files.js +61 -85
  62. package/dist/commands/health.d.ts +0 -1
  63. package/dist/commands/health.js +54 -78
  64. package/dist/commands/imports.d.ts +39 -0
  65. package/dist/commands/imports.js +1151 -0
  66. package/dist/commands/inbox.d.ts +0 -1
  67. package/dist/commands/inbox.js +57 -80
  68. package/dist/commands/integration-identities.d.ts +40 -0
  69. package/dist/commands/integration-identities.js +1151 -0
  70. package/dist/commands/integrations.d.ts +0 -1
  71. package/dist/commands/integrations.js +52 -76
  72. package/dist/commands/item-categories.d.ts +0 -1
  73. package/dist/commands/item-categories.js +54 -78
  74. package/dist/commands/items.d.ts +0 -1
  75. package/dist/commands/items.js +54 -78
  76. package/dist/commands/journal-lines.d.ts +0 -1
  77. package/dist/commands/journal-lines.js +50 -74
  78. package/dist/commands/journals.d.ts +1 -1
  79. package/dist/commands/journals.js +75 -96
  80. package/dist/commands/ledgers.d.ts +3 -2
  81. package/dist/commands/ledgers.js +100 -95
  82. package/dist/commands/my-account.d.ts +39 -0
  83. package/dist/commands/my-account.js +1160 -0
  84. package/dist/commands/office-locations.d.ts +0 -1
  85. package/dist/commands/office-locations.js +54 -78
  86. package/dist/commands/payment-methods.d.ts +0 -1
  87. package/dist/commands/payment-methods.js +59 -83
  88. package/dist/commands/payment-runs.d.ts +0 -1
  89. package/dist/commands/payment-runs.js +66 -90
  90. package/dist/commands/payment-terms.d.ts +0 -1
  91. package/dist/commands/payment-terms.js +55 -79
  92. package/dist/commands/payments.d.ts +0 -1
  93. package/dist/commands/payments.js +56 -80
  94. package/dist/commands/payroll-runs.d.ts +43 -0
  95. package/dist/commands/payroll-runs.js +1188 -0
  96. package/dist/commands/permissions.d.ts +39 -0
  97. package/dist/commands/permissions.js +1146 -0
  98. package/dist/commands/price-lists.d.ts +0 -1
  99. package/dist/commands/price-lists.js +59 -83
  100. package/dist/commands/processor-mappings.d.ts +0 -1
  101. package/dist/commands/processor-mappings.js +53 -77
  102. package/dist/commands/processors.d.ts +0 -1
  103. package/dist/commands/processors.js +60 -85
  104. package/dist/commands/purchase-order-types.d.ts +0 -1
  105. package/dist/commands/purchase-order-types.js +54 -78
  106. package/dist/commands/purchase-orders.d.ts +0 -1
  107. package/dist/commands/purchase-orders.js +74 -97
  108. package/dist/commands/reports.d.ts +0 -1
  109. package/dist/commands/reports.js +54 -78
  110. package/dist/commands/roles.d.ts +39 -0
  111. package/dist/commands/{text-templates.js → roles.js} +64 -90
  112. package/dist/commands/salary-types.d.ts +0 -1
  113. package/dist/commands/salary-types.js +54 -78
  114. package/dist/commands/sales-invoices.d.ts +0 -1
  115. package/dist/commands/sales-invoices.js +90 -110
  116. package/dist/commands/sales-orders.d.ts +0 -1
  117. package/dist/commands/sales-orders.js +83 -105
  118. package/dist/commands/saved-views.d.ts +0 -1
  119. package/dist/commands/saved-views.js +52 -76
  120. package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
  121. package/dist/commands/scheduled-processor-mappings.js +54 -78
  122. package/dist/commands/schedules.d.ts +0 -1
  123. package/dist/commands/schedules.js +58 -82
  124. package/dist/commands/switch.js +4 -4
  125. package/dist/commands/tag-configurations.d.ts +0 -1
  126. package/dist/commands/tag-configurations.js +56 -79
  127. package/dist/commands/tags.d.ts +0 -1
  128. package/dist/commands/tags.js +54 -78
  129. package/dist/commands/tax-codes.d.ts +0 -1
  130. package/dist/commands/tax-codes.js +57 -81
  131. package/dist/commands/tax-matrix.d.ts +0 -1
  132. package/dist/commands/tax-matrix.js +54 -78
  133. package/dist/commands/templates.d.ts +39 -0
  134. package/dist/commands/templates.js +1157 -0
  135. package/dist/commands/tenants.d.ts +0 -1
  136. package/dist/commands/tenants.js +58 -81
  137. package/dist/commands/tools.d.ts +0 -1
  138. package/dist/commands/tools.js +51 -75
  139. package/dist/commands/users.d.ts +42 -0
  140. package/dist/commands/users.js +1174 -0
  141. package/dist/commands/vat-declarations.d.ts +3 -1
  142. package/dist/commands/vat-declarations.js +78 -93
  143. package/dist/commands/vendor-invoices.d.ts +0 -1
  144. package/dist/commands/vendor-invoices.js +87 -110
  145. package/dist/commands/webhooks.d.ts +0 -1
  146. package/dist/commands/webhooks.js +56 -80
  147. package/dist/commands/workflows.d.ts +0 -1
  148. package/dist/commands/workflows.js +52 -76
  149. package/dist/lib/api-client.d.ts +6 -0
  150. package/dist/lib/api-client.js +16 -6
  151. package/dist/lib/auth/constants.d.ts +11 -0
  152. package/dist/lib/auth/constants.js +11 -0
  153. package/dist/lib/auth/errors.d.ts +17 -0
  154. package/dist/lib/auth/errors.js +22 -0
  155. package/dist/lib/auth/flow.d.ts +27 -0
  156. package/dist/lib/auth/flow.js +163 -0
  157. package/dist/lib/auth/guards.d.ts +5 -0
  158. package/dist/lib/auth/guards.js +14 -0
  159. package/dist/lib/auth/http.d.ts +16 -0
  160. package/dist/lib/auth/http.js +49 -0
  161. package/dist/lib/auth/index.d.ts +49 -0
  162. package/dist/lib/auth/index.js +228 -0
  163. package/dist/lib/auth/jwks.d.ts +19 -0
  164. package/dist/lib/auth/jwks.js +112 -0
  165. package/dist/lib/auth/keycloak-provider.d.ts +91 -0
  166. package/dist/lib/auth/keycloak-provider.js +244 -0
  167. package/dist/lib/auth/pages.d.ts +25 -0
  168. package/dist/lib/auth/pages.js +123 -0
  169. package/dist/lib/auth/pkce.d.ts +11 -0
  170. package/dist/lib/auth/pkce.js +17 -0
  171. package/dist/lib/auth/tokens.d.ts +59 -0
  172. package/dist/lib/auth/tokens.js +105 -0
  173. package/dist/lib/auth/types.d.ts +61 -0
  174. package/dist/lib/auth/types.js +1 -0
  175. package/dist/lib/auth/validated-url.d.ts +6 -0
  176. package/dist/lib/auth/validated-url.js +53 -0
  177. package/dist/lib/config.d.ts +13 -3
  178. package/dist/lib/config.js +24 -19
  179. package/oclif.manifest.json +2447 -641
  180. package/package.json +5 -4
  181. package/dist/lib/auth.d.ts +0 -43
  182. package/dist/lib/auth.js +0 -610
@@ -32,7 +32,6 @@ export default class PriceLists extends Command {
32
32
  private runCamt053Import;
33
33
  private runNestedImport;
34
34
  private parseFilterValue;
35
- private filterFields;
36
35
  private sanitizeOutput;
37
36
  private getColumns;
38
37
  private getValue;
@@ -1,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
@@ -8,16 +8,16 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["prices"];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-prices", "create-prices", "get-prices", "update-prices", "delete-prices"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/price-lists', summary: 'Get all price lists with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/price-lists', summary: 'Create a new price list', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'get': { method: 'GET', path: '/v1/price-lists/{id}', summary: 'Get a price list by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'update': { method: 'PATCH', path: '/v1/price-lists/{id}', summary: 'Update a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'delete': { method: 'DELETE', path: '/v1/price-lists/{id}', summary: 'Delete a price list', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'list-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices', summary: 'Get all prices in a price list with pagination', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'create-prices': { method: 'POST', path: '/v1/price-lists/{id}/prices', summary: 'Create a new price in a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'get-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Get a price by ID', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'update-prices': { method: 'PATCH', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Update a price', pathParams: ['id', 'priceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'delete-prices': { method: 'DELETE', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Delete a price', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/price-lists', summary: 'Get all price lists with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/price-lists', summary: 'Create a new price list', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/price-lists/{id}', summary: 'Get a price list by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/price-lists/{id}', summary: 'Update a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/price-lists/{id}', summary: 'Delete a price list', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'list-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices', summary: 'Get all prices in a price list with pagination', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'create-prices': { method: 'POST', path: '/v1/price-lists/{id}/prices', summary: 'Create a new price in a price list', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'get-prices': { method: 'GET', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Get a price by ID', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'update-prices': { method: 'PATCH', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Update a price', pathParams: ['id', 'priceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'delete-prices': { method: 'DELETE', path: '/v1/price-lists/{id}/prices/{priceId}', summary: 'Delete a price', pathParams: ['id', 'priceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
21
  };
22
22
  export default class PriceLists extends Command {
23
23
  static description = 'Manage price lists';
@@ -49,7 +49,7 @@ export default class PriceLists extends Command {
49
49
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
50
50
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
51
51
  query: Flags.string({ description: 'Additional query params as JSON' }),
52
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
52
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
53
53
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
54
54
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
55
55
  };
@@ -121,6 +121,8 @@ export default class PriceLists extends Command {
121
121
  queryParams.search = flags.search;
122
122
  if (flags['sort-by'])
123
123
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
124
+ if (flags.fields)
125
+ queryParams.fields = flags.fields;
124
126
  if (flags.query) {
125
127
  try {
126
128
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -268,10 +270,11 @@ export default class PriceLists extends Command {
268
270
  process.stderr.write('\n');
269
271
  }
270
272
  try {
271
- // Binary response: download to file
273
+ // Binary response: to -f / --file, or to stdout when it is redirected.
272
274
  if (endpoint.isBinaryResponse) {
273
- if (!flags.file) {
274
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
275
+ const toStdout = !flags.file;
276
+ if (toStdout && process.stdout.isTTY) {
277
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
275
278
  }
276
279
  const binResponse = await apiRequest(endpoint.method, apiPath, {
277
280
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -279,24 +282,55 @@ export default class PriceLists extends Command {
279
282
  contextName: flags.context,
280
283
  binary: true,
281
284
  });
282
- const fsModule = await import('node:fs');
283
285
  const binData = binResponse.data;
284
286
  const buffer = Buffer.isBuffer(binData)
285
287
  ? binData
286
288
  : Buffer.from(binData);
289
+ if (toStdout) {
290
+ // Nothing else may reach stdout here — it is the file.
291
+ process.stdout.write(buffer);
292
+ return;
293
+ }
294
+ const fsModule = await import('node:fs');
287
295
  fsModule.writeFileSync(flags.file, buffer);
288
296
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
289
297
  return;
290
298
  }
291
- const response = await apiRequest(endpoint.method, apiPath, {
292
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
293
- data,
294
- contextName: flags.context,
295
- acceptAny: true,
296
- });
299
+ let response;
300
+ if (endpoint.hasBinaryVariant) {
301
+ // This route answers JSON or a file depending on the request body, so the
302
+ // bytes are read first and the response's own content-type decides. Asking
303
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
304
+ const raw = await apiRequest(endpoint.method, apiPath, {
305
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
306
+ data,
307
+ contextName: flags.context,
308
+ binary: true,
309
+ });
310
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
311
+ const rawData = raw.data;
312
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
313
+ if (!isTextualMediaType(rawType)) {
314
+ if (process.stdout.isTTY) {
315
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
316
+ }
317
+ // Nothing else may reach stdout here — it is the file.
318
+ process.stdout.write(buffer);
319
+ return;
320
+ }
321
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
322
+ }
323
+ else {
324
+ response = await apiRequest(endpoint.method, apiPath, {
325
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
326
+ data,
327
+ contextName: flags.context,
328
+ acceptAny: true,
329
+ });
330
+ }
297
331
  // Check content-type for non-JSON responses (XML, text, etc.)
298
332
  const contentType = (response.headers['content-type'] || '').toLowerCase();
299
- if (contentType.includes('xml') || contentType.includes('text/')) {
333
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
300
334
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
301
335
  this.log(String(response.data));
302
336
  return;
@@ -323,11 +357,6 @@ export default class PriceLists extends Command {
323
357
  output = obj[arrayKeys[0]];
324
358
  }
325
359
  }
326
- // Filter fields if --fields flag is provided
327
- if (flags.fields) {
328
- const fieldList = flags.fields.split(',').map(f => f.trim());
329
- output = this.filterFields(output, fieldList);
330
- }
331
360
  // Sanitize output if requested
332
361
  if (flags.sanitize) {
333
362
  output = this.sanitizeOutput(output);
@@ -582,6 +611,8 @@ export default class PriceLists extends Command {
582
611
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
583
612
  if (flags.include)
584
613
  baseParams.include = flags.include;
614
+ if (flags.fields)
615
+ baseParams.fields = flags.fields;
585
616
  if (flags.query) {
586
617
  try {
587
618
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -636,12 +667,7 @@ export default class PriceLists extends Command {
636
667
  }
637
668
  }
638
669
  process.stderr.write(' done\n');
639
- // Filter fields if specified
640
670
  let output = allItems;
641
- if (flags.fields) {
642
- const fieldList = flags.fields.split(',').map(f => f.trim());
643
- output = this.filterFields(allItems, fieldList);
644
- }
645
671
  // Sanitize output if requested
646
672
  if (flags.sanitize) {
647
673
  output = this.sanitizeOutput(output);
@@ -970,56 +996,6 @@ export default class PriceLists extends Command {
970
996
  return `\$${op}`;
971
997
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
972
998
  }
973
- filterFields(data, fields) {
974
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
975
- const pick = (obj, keys) => {
976
- // Group keys by their first segment so sibling sub-paths
977
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
978
- // and don't clobber each other on merge.
979
- const directKeys = new Set();
980
- const nestedKeys = new Map();
981
- for (const key of keys) {
982
- if (key.split('.').some(isUnsafeKey))
983
- continue;
984
- if (key.includes('.')) {
985
- const [first, ...rest] = key.split('.');
986
- if (isUnsafeKey(first))
987
- continue;
988
- const list = nestedKeys.get(first);
989
- if (list) {
990
- list.push(rest.join('.'));
991
- }
992
- else {
993
- nestedKeys.set(first, [rest.join('.')]);
994
- }
995
- }
996
- else {
997
- directKeys.add(key);
998
- }
999
- }
1000
- const result = {};
1001
- for (const key of directKeys) {
1002
- if (key in obj)
1003
- result[key] = obj[key];
1004
- }
1005
- for (const [first, restKeys] of nestedKeys) {
1006
- const val = obj[first];
1007
- if (!val || typeof val !== 'object')
1008
- continue;
1009
- if (Array.isArray(val)) {
1010
- result[first] = val.map(item => pick(item, restKeys));
1011
- }
1012
- else {
1013
- result[first] = pick(val, restKeys);
1014
- }
1015
- }
1016
- return result;
1017
- };
1018
- if (Array.isArray(data)) {
1019
- return data.map(item => pick(item, fields));
1020
- }
1021
- return pick(data, fields);
1022
- }
1023
999
  sanitizeOutput(data) {
1024
1000
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1025
1001
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -31,7 +31,6 @@ export default class ProcessorMappings extends Command {
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
33
  private parseFilterValue;
34
- private filterFields;
35
34
  private sanitizeOutput;
36
35
  private getColumns;
37
36
  private getValue;
@@ -1,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
@@ -8,10 +8,10 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "create", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/processor-mappings', summary: 'List all document type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/processor-mappings', summary: 'Create a new document type processor mapping', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['documentTypes'] },
13
- 'update': { method: 'PATCH', path: '/v1/processor-mappings/{id}', summary: 'Update a document type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['documentTypes'] },
14
- 'delete': { method: 'DELETE', path: '/v1/processor-mappings/{id}', summary: 'Delete a document type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/processor-mappings', summary: 'List all document type processor mappings for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/processor-mappings', summary: 'Create a new document type processor mapping', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['documentTypes'] },
13
+ 'update': { method: 'PATCH', path: '/v1/processor-mappings/{id}', summary: 'Update a document type processor mapping', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['documentTypes'] },
14
+ 'delete': { method: 'DELETE', path: '/v1/processor-mappings/{id}', summary: 'Delete a document type processor mapping', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
15
  };
16
16
  export default class ProcessorMappings extends Command {
17
17
  static description = 'Manage processor mappings';
@@ -41,7 +41,7 @@ export default class ProcessorMappings extends Command {
41
41
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
42
42
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
43
43
  query: Flags.string({ description: 'Additional query params as JSON' }),
44
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
44
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
45
45
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
46
46
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
47
47
  };
@@ -113,6 +113,8 @@ export default class ProcessorMappings extends Command {
113
113
  queryParams.search = flags.search;
114
114
  if (flags['sort-by'])
115
115
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
116
+ if (flags.fields)
117
+ queryParams.fields = flags.fields;
116
118
  if (flags.query) {
117
119
  try {
118
120
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -260,10 +262,11 @@ export default class ProcessorMappings extends Command {
260
262
  process.stderr.write('\n');
261
263
  }
262
264
  try {
263
- // Binary response: download to file
265
+ // Binary response: to -f / --file, or to stdout when it is redirected.
264
266
  if (endpoint.isBinaryResponse) {
265
- if (!flags.file) {
266
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
267
+ const toStdout = !flags.file;
268
+ if (toStdout && process.stdout.isTTY) {
269
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
267
270
  }
268
271
  const binResponse = await apiRequest(endpoint.method, apiPath, {
269
272
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -271,24 +274,55 @@ export default class ProcessorMappings extends Command {
271
274
  contextName: flags.context,
272
275
  binary: true,
273
276
  });
274
- const fsModule = await import('node:fs');
275
277
  const binData = binResponse.data;
276
278
  const buffer = Buffer.isBuffer(binData)
277
279
  ? binData
278
280
  : Buffer.from(binData);
281
+ if (toStdout) {
282
+ // Nothing else may reach stdout here — it is the file.
283
+ process.stdout.write(buffer);
284
+ return;
285
+ }
286
+ const fsModule = await import('node:fs');
279
287
  fsModule.writeFileSync(flags.file, buffer);
280
288
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
281
289
  return;
282
290
  }
283
- const response = await apiRequest(endpoint.method, apiPath, {
284
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
285
- data,
286
- contextName: flags.context,
287
- acceptAny: true,
288
- });
291
+ let response;
292
+ if (endpoint.hasBinaryVariant) {
293
+ // This route answers JSON or a file depending on the request body, so the
294
+ // bytes are read first and the response's own content-type decides. Asking
295
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
296
+ const raw = await apiRequest(endpoint.method, apiPath, {
297
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
298
+ data,
299
+ contextName: flags.context,
300
+ binary: true,
301
+ });
302
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
303
+ const rawData = raw.data;
304
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
305
+ if (!isTextualMediaType(rawType)) {
306
+ if (process.stdout.isTTY) {
307
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
308
+ }
309
+ // Nothing else may reach stdout here — it is the file.
310
+ process.stdout.write(buffer);
311
+ return;
312
+ }
313
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
314
+ }
315
+ else {
316
+ response = await apiRequest(endpoint.method, apiPath, {
317
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
318
+ data,
319
+ contextName: flags.context,
320
+ acceptAny: true,
321
+ });
322
+ }
289
323
  // Check content-type for non-JSON responses (XML, text, etc.)
290
324
  const contentType = (response.headers['content-type'] || '').toLowerCase();
291
- if (contentType.includes('xml') || contentType.includes('text/')) {
325
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
292
326
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
293
327
  this.log(String(response.data));
294
328
  return;
@@ -315,11 +349,6 @@ export default class ProcessorMappings extends Command {
315
349
  output = obj[arrayKeys[0]];
316
350
  }
317
351
  }
318
- // Filter fields if --fields flag is provided
319
- if (flags.fields) {
320
- const fieldList = flags.fields.split(',').map(f => f.trim());
321
- output = this.filterFields(output, fieldList);
322
- }
323
352
  // Sanitize output if requested
324
353
  if (flags.sanitize) {
325
354
  output = this.sanitizeOutput(output);
@@ -574,6 +603,8 @@ export default class ProcessorMappings extends Command {
574
603
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
575
604
  if (flags.include)
576
605
  baseParams.include = flags.include;
606
+ if (flags.fields)
607
+ baseParams.fields = flags.fields;
577
608
  if (flags.query) {
578
609
  try {
579
610
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -628,12 +659,7 @@ export default class ProcessorMappings extends Command {
628
659
  }
629
660
  }
630
661
  process.stderr.write(' done\n');
631
- // Filter fields if specified
632
662
  let output = allItems;
633
- if (flags.fields) {
634
- const fieldList = flags.fields.split(',').map(f => f.trim());
635
- output = this.filterFields(allItems, fieldList);
636
- }
637
663
  // Sanitize output if requested
638
664
  if (flags.sanitize) {
639
665
  output = this.sanitizeOutput(output);
@@ -962,56 +988,6 @@ export default class ProcessorMappings extends Command {
962
988
  return `\$${op}`;
963
989
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
964
990
  }
965
- filterFields(data, fields) {
966
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
967
- const pick = (obj, keys) => {
968
- // Group keys by their first segment so sibling sub-paths
969
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
970
- // and don't clobber each other on merge.
971
- const directKeys = new Set();
972
- const nestedKeys = new Map();
973
- for (const key of keys) {
974
- if (key.split('.').some(isUnsafeKey))
975
- continue;
976
- if (key.includes('.')) {
977
- const [first, ...rest] = key.split('.');
978
- if (isUnsafeKey(first))
979
- continue;
980
- const list = nestedKeys.get(first);
981
- if (list) {
982
- list.push(rest.join('.'));
983
- }
984
- else {
985
- nestedKeys.set(first, [rest.join('.')]);
986
- }
987
- }
988
- else {
989
- directKeys.add(key);
990
- }
991
- }
992
- const result = {};
993
- for (const key of directKeys) {
994
- if (key in obj)
995
- result[key] = obj[key];
996
- }
997
- for (const [first, restKeys] of nestedKeys) {
998
- const val = obj[first];
999
- if (!val || typeof val !== 'object')
1000
- continue;
1001
- if (Array.isArray(val)) {
1002
- result[first] = val.map(item => pick(item, restKeys));
1003
- }
1004
- else {
1005
- result[first] = pick(val, restKeys);
1006
- }
1007
- }
1008
- return result;
1009
- };
1010
- if (Array.isArray(data)) {
1011
- return data.map(item => pick(item, fields));
1012
- }
1013
- return pick(data, fields);
1014
- }
1015
991
  sanitizeOutput(data) {
1016
992
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1017
993
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -31,7 +31,6 @@ export default class Processors extends Command {
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
33
  private parseFilterValue;
34
- private filterFields;
35
34
  private sanitizeOutput;
36
35
  private getColumns;
37
36
  private getValue;