@aderis-erp/cli 1.1.0 → 1.1.2

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 (152) hide show
  1. package/dist/commands/access-groups.d.ts +8 -0
  2. package/dist/commands/access-groups.js +56 -8
  3. package/dist/commands/account-transaction-allocations.d.ts +8 -0
  4. package/dist/commands/account-transaction-allocations.js +52 -5
  5. package/dist/commands/account-transactions.d.ts +8 -0
  6. package/dist/commands/account-transactions.js +55 -7
  7. package/dist/commands/accounts.d.ts +8 -0
  8. package/dist/commands/accounts.js +52 -5
  9. package/dist/commands/agent-platform.d.ts +8 -0
  10. package/dist/commands/agent-platform.js +52 -5
  11. package/dist/commands/agent-runs.d.ts +8 -0
  12. package/dist/commands/agent-runs.js +52 -5
  13. package/dist/commands/analytics.d.ts +8 -0
  14. package/dist/commands/analytics.js +52 -5
  15. package/dist/commands/api-keys.d.ts +8 -0
  16. package/dist/commands/api-keys.js +52 -5
  17. package/dist/commands/async-jobs.d.ts +8 -0
  18. package/dist/commands/async-jobs.js +52 -5
  19. package/dist/commands/audit-log.d.ts +8 -0
  20. package/dist/commands/audit-log.js +52 -5
  21. package/dist/commands/bank-accounts.d.ts +8 -0
  22. package/dist/commands/bank-accounts.js +52 -5
  23. package/dist/commands/bank-integrations.d.ts +8 -0
  24. package/dist/commands/bank-integrations.js +52 -5
  25. package/dist/commands/business-partners.d.ts +9 -0
  26. package/dist/commands/business-partners.js +66 -9
  27. package/dist/commands/chat.d.ts +8 -0
  28. package/dist/commands/chat.js +52 -5
  29. package/dist/commands/credit-card-accounts.d.ts +8 -0
  30. package/dist/commands/credit-card-accounts.js +52 -5
  31. package/dist/commands/delivery-notes.d.ts +8 -0
  32. package/dist/commands/delivery-notes.js +52 -5
  33. package/dist/commands/dunning-configs.d.ts +8 -0
  34. package/dist/commands/dunning-configs.js +52 -5
  35. package/dist/commands/dunning-reminders.d.ts +8 -0
  36. package/dist/commands/dunning-reminders.js +52 -5
  37. package/dist/commands/dunning-runs.d.ts +8 -0
  38. package/dist/commands/dunning-runs.js +52 -5
  39. package/dist/commands/email-transactions.d.ts +8 -0
  40. package/dist/commands/email-transactions.js +52 -5
  41. package/dist/commands/employees.d.ts +8 -0
  42. package/dist/commands/employees.js +52 -5
  43. package/dist/commands/entity-configurations.d.ts +8 -0
  44. package/dist/commands/entity-configurations.js +52 -5
  45. package/dist/commands/event-processor-mappings.d.ts +8 -0
  46. package/dist/commands/event-processor-mappings.js +52 -5
  47. package/dist/commands/files.d.ts +8 -0
  48. package/dist/commands/files.js +53 -6
  49. package/dist/commands/health.d.ts +8 -0
  50. package/dist/commands/health.js +52 -5
  51. package/dist/commands/imports.d.ts +8 -0
  52. package/dist/commands/imports.js +53 -8
  53. package/dist/commands/inbox.d.ts +8 -0
  54. package/dist/commands/inbox.js +53 -7
  55. package/dist/commands/integration-identities.d.ts +8 -0
  56. package/dist/commands/integration-identities.js +52 -5
  57. package/dist/commands/integrations.d.ts +8 -0
  58. package/dist/commands/integrations.js +52 -5
  59. package/dist/commands/item-categories.d.ts +8 -0
  60. package/dist/commands/item-categories.js +52 -5
  61. package/dist/commands/items.d.ts +8 -0
  62. package/dist/commands/items.js +52 -5
  63. package/dist/commands/journal-lines.d.ts +8 -0
  64. package/dist/commands/journal-lines.js +52 -5
  65. package/dist/commands/journals.d.ts +8 -0
  66. package/dist/commands/journals.js +52 -5
  67. package/dist/commands/ledgers.d.ts +8 -0
  68. package/dist/commands/ledgers.js +57 -9
  69. package/dist/commands/my-account.d.ts +8 -0
  70. package/dist/commands/my-account.js +52 -5
  71. package/dist/commands/office-locations.d.ts +8 -0
  72. package/dist/commands/office-locations.js +52 -5
  73. package/dist/commands/payment-methods.d.ts +8 -0
  74. package/dist/commands/payment-methods.js +52 -5
  75. package/dist/commands/payment-runs.d.ts +8 -0
  76. package/dist/commands/payment-runs.js +54 -6
  77. package/dist/commands/payment-terms.d.ts +8 -0
  78. package/dist/commands/payment-terms.js +52 -5
  79. package/dist/commands/payments.d.ts +8 -0
  80. package/dist/commands/payments.js +52 -5
  81. package/dist/commands/payroll-runs.d.ts +8 -0
  82. package/dist/commands/payroll-runs.js +52 -5
  83. package/dist/commands/permissions.d.ts +8 -0
  84. package/dist/commands/permissions.js +52 -5
  85. package/dist/commands/price-lists.d.ts +8 -0
  86. package/dist/commands/price-lists.js +52 -5
  87. package/dist/commands/processor-mappings.d.ts +8 -0
  88. package/dist/commands/processor-mappings.js +52 -5
  89. package/dist/commands/processors.d.ts +8 -0
  90. package/dist/commands/processors.js +52 -5
  91. package/dist/commands/purchase-order-types.d.ts +8 -0
  92. package/dist/commands/purchase-order-types.js +52 -5
  93. package/dist/commands/purchase-orders.d.ts +8 -0
  94. package/dist/commands/purchase-orders.js +52 -5
  95. package/dist/commands/reports.d.ts +8 -0
  96. package/dist/commands/reports.js +52 -5
  97. package/dist/commands/roles.d.ts +8 -0
  98. package/dist/commands/roles.js +52 -5
  99. package/dist/commands/salary-types.d.ts +8 -0
  100. package/dist/commands/salary-types.js +52 -5
  101. package/dist/commands/sales-invoices.d.ts +10 -0
  102. package/dist/commands/sales-invoices.js +62 -6
  103. package/dist/commands/sales-orders.d.ts +8 -0
  104. package/dist/commands/sales-orders.js +52 -5
  105. package/dist/commands/saved-views.d.ts +8 -0
  106. package/dist/commands/saved-views.js +52 -5
  107. package/dist/commands/scheduled-processor-mappings.d.ts +8 -0
  108. package/dist/commands/scheduled-processor-mappings.js +52 -5
  109. package/dist/commands/schedules.d.ts +8 -0
  110. package/dist/commands/schedules.js +52 -5
  111. package/dist/commands/support/end.d.ts +9 -0
  112. package/dist/commands/support/end.js +29 -0
  113. package/dist/commands/support/start.d.ts +14 -0
  114. package/dist/commands/support/start.js +50 -0
  115. package/dist/commands/support/status.d.ts +10 -0
  116. package/dist/commands/support/status.js +45 -0
  117. package/dist/commands/support/tenants.d.ts +10 -0
  118. package/dist/commands/support/tenants.js +39 -0
  119. package/dist/commands/tags.d.ts +8 -0
  120. package/dist/commands/tags.js +56 -8
  121. package/dist/commands/tax-codes.d.ts +8 -0
  122. package/dist/commands/tax-codes.js +52 -5
  123. package/dist/commands/tax-matrix.d.ts +8 -0
  124. package/dist/commands/tax-matrix.js +52 -5
  125. package/dist/commands/templates.d.ts +8 -0
  126. package/dist/commands/templates.js +52 -5
  127. package/dist/commands/tenants.d.ts +8 -0
  128. package/dist/commands/tenants.js +52 -5
  129. package/dist/commands/tools.d.ts +8 -0
  130. package/dist/commands/tools.js +52 -5
  131. package/dist/commands/users.d.ts +8 -0
  132. package/dist/commands/users.js +52 -5
  133. package/dist/commands/vat-declarations.d.ts +8 -0
  134. package/dist/commands/vat-declarations.js +52 -5
  135. package/dist/commands/vendor-invoices.d.ts +9 -0
  136. package/dist/commands/vendor-invoices.js +60 -8
  137. package/dist/commands/webhooks.d.ts +8 -0
  138. package/dist/commands/webhooks.js +52 -5
  139. package/dist/commands/workflows.d.ts +8 -0
  140. package/dist/commands/workflows.js +52 -5
  141. package/dist/lib/auth/constants.d.ts +12 -0
  142. package/dist/lib/auth/constants.js +12 -0
  143. package/dist/lib/auth/index.d.ts +11 -1
  144. package/dist/lib/auth/index.js +90 -2
  145. package/dist/lib/auth/keycloak-provider.d.ts +22 -0
  146. package/dist/lib/auth/keycloak-provider.js +108 -3
  147. package/dist/lib/config.d.ts +18 -0
  148. package/dist/lib/config.js +10 -0
  149. package/oclif.manifest.json +640 -216
  150. package/package.json +4 -1
  151. package/dist/commands/tag-configurations.d.ts +0 -39
  152. package/dist/commands/tag-configurations.js +0 -1155
@@ -38,6 +38,7 @@ export default class Tools extends Command {
38
38
  query: Flags.string({ description: 'Additional query params as JSON' }),
39
39
  fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
40
40
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
41
+ meta: Flags.boolean({ description: 'Keep the API envelope in JSON output: data plus page, totals and sums' }),
41
42
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
42
43
  };
43
44
  async run() {
@@ -330,9 +331,15 @@ export default class Tools extends Command {
330
331
  }
331
332
  catch { /* keep as string */ }
332
333
  }
333
- // Extract data array from paginated wrapper (e.g., { data: [...], total: 3 })
334
+ // A paginated answer is { data, meta, links, totals }. The records are
335
+ // unwrapped for output, but what the API said about the page is kept:
336
+ // dropping it silently let a script read one page as the whole result
337
+ // (AderisERP/product#61).
338
+ let envelope;
334
339
  if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
335
- output = output.data;
340
+ const page = output;
341
+ envelope = page;
342
+ output = page.data;
336
343
  }
337
344
  // For table/csv on non-array responses, auto-extract the primary array
338
345
  // e.g., { payments: [...], total: 3 } → extract payments array
@@ -349,12 +356,24 @@ export default class Tools extends Command {
349
356
  output = this.sanitizeOutput(output);
350
357
  }
351
358
  const fieldList = flags.fields?.split(',').map(f => f.trim());
352
- if (flags.csv || flags.output === 'csv')
359
+ if (envelope)
360
+ this.warnShortPage(envelope, flags.limit);
361
+ if (flags.csv || flags.output === 'csv') {
353
362
  this.printCsv(output, fieldList);
354
- else if (flags.table || flags.output === 'table')
363
+ }
364
+ else if (flags.table || flags.output === 'table') {
355
365
  this.printTable(output, fieldList);
356
- else
366
+ if (envelope)
367
+ this.printPageSummary(envelope);
368
+ }
369
+ else if (envelope && flags.meta) {
370
+ // The whole envelope, for a caller that asked for it; the default stays
371
+ // a bare array so existing scripts are unaffected.
372
+ this.log(JSON.stringify({ ...envelope, data: output }, null, 2));
373
+ }
374
+ else {
357
375
  this.log(JSON.stringify(output, null, 2));
376
+ }
358
377
  }
359
378
  catch (error) {
360
379
  if (error instanceof ApiError) {
@@ -983,6 +1002,34 @@ export default class Tools extends Command {
983
1002
  return `\$${op}`;
984
1003
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
985
1004
  }
1005
+ /** The page line under a table: which page, how many, and any sums the API returned. */
1006
+ printPageSummary(envelope) {
1007
+ const meta = envelope.meta;
1008
+ if (meta && typeof meta.currentPage === 'number' && typeof meta.totalPages === 'number') {
1009
+ const shown = Array.isArray(envelope.data) ? envelope.data.length : 0;
1010
+ this.log(`
1011
+ Page ${meta.currentPage}/${meta.totalPages} — ${shown} of ${meta.totalItems ?? '?'} records`);
1012
+ }
1013
+ for (const total of envelope.totals ?? []) {
1014
+ const { currency, ...sums } = total;
1015
+ const parts = Object.entries(sums).map(([column, value]) => `${column} ${value}`).join(', ');
1016
+ this.log(`Total ${currency ?? ''}: ${parts}`.trim());
1017
+ }
1018
+ }
1019
+ /**
1020
+ * The API caps how many records a page may hold. Asking for more and getting
1021
+ * fewer looks like "that is everything" unless it is said out loud.
1022
+ */
1023
+ warnShortPage(envelope, requestedLimit) {
1024
+ const asked = requestedLimit === undefined ? undefined : Number(requestedLimit);
1025
+ const perPage = envelope.meta?.itemsPerPage;
1026
+ if (asked === undefined || Number.isNaN(asked) || typeof perPage !== 'number' || perPage >= asked)
1027
+ return;
1028
+ const pages = envelope.meta?.totalPages;
1029
+ const more = typeof pages === 'number' && pages > 1 ? ` Use --page, or the export action for every page.` : '';
1030
+ process.stderr.write(`! Asked for ${asked} records per page; the API returned at most ${perPage}.${more}
1031
+ `);
1032
+ }
986
1033
  sanitizeOutput(data) {
987
1034
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
988
1035
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -26,6 +26,7 @@ export default class Users extends Command {
26
26
  query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
27
27
  fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
28
  sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
+ meta: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
30
  'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
30
31
  };
31
32
  run(): Promise<void>;
@@ -34,6 +35,13 @@ export default class Users extends Command {
34
35
  private runCamt053Import;
35
36
  private runNestedImport;
36
37
  private parseFilterValue;
38
+ /** The page line under a table: which page, how many, and any sums the API returned. */
39
+ private printPageSummary;
40
+ /**
41
+ * The API caps how many records a page may hold. Asking for more and getting
42
+ * fewer looks like "that is everything" unless it is said out loud.
43
+ */
44
+ private warnShortPage;
37
45
  private sanitizeOutput;
38
46
  private getColumns;
39
47
  private getValue;
@@ -65,6 +65,7 @@ export default class Users extends Command {
65
65
  query: Flags.string({ description: 'Additional query params as JSON' }),
66
66
  fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
67
67
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
68
+ meta: Flags.boolean({ description: 'Keep the API envelope in JSON output: data plus page, totals and sums' }),
68
69
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
69
70
  };
70
71
  async run() {
@@ -357,9 +358,15 @@ export default class Users extends Command {
357
358
  }
358
359
  catch { /* keep as string */ }
359
360
  }
360
- // Extract data array from paginated wrapper (e.g., { data: [...], total: 3 })
361
+ // A paginated answer is { data, meta, links, totals }. The records are
362
+ // unwrapped for output, but what the API said about the page is kept:
363
+ // dropping it silently let a script read one page as the whole result
364
+ // (AderisERP/product#61).
365
+ let envelope;
361
366
  if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
362
- output = output.data;
367
+ const page = output;
368
+ envelope = page;
369
+ output = page.data;
363
370
  }
364
371
  // For table/csv on non-array responses, auto-extract the primary array
365
372
  // e.g., { payments: [...], total: 3 } → extract payments array
@@ -376,12 +383,24 @@ export default class Users extends Command {
376
383
  output = this.sanitizeOutput(output);
377
384
  }
378
385
  const fieldList = flags.fields?.split(',').map(f => f.trim());
379
- if (flags.csv || flags.output === 'csv')
386
+ if (envelope)
387
+ this.warnShortPage(envelope, flags.limit);
388
+ if (flags.csv || flags.output === 'csv') {
380
389
  this.printCsv(output, fieldList);
381
- else if (flags.table || flags.output === 'table')
390
+ }
391
+ else if (flags.table || flags.output === 'table') {
382
392
  this.printTable(output, fieldList);
383
- else
393
+ if (envelope)
394
+ this.printPageSummary(envelope);
395
+ }
396
+ else if (envelope && flags.meta) {
397
+ // The whole envelope, for a caller that asked for it; the default stays
398
+ // a bare array so existing scripts are unaffected.
399
+ this.log(JSON.stringify({ ...envelope, data: output }, null, 2));
400
+ }
401
+ else {
384
402
  this.log(JSON.stringify(output, null, 2));
403
+ }
385
404
  }
386
405
  catch (error) {
387
406
  if (error instanceof ApiError) {
@@ -1010,6 +1029,34 @@ export default class Users extends Command {
1010
1029
  return `\$${op}`;
1011
1030
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
1012
1031
  }
1032
+ /** The page line under a table: which page, how many, and any sums the API returned. */
1033
+ printPageSummary(envelope) {
1034
+ const meta = envelope.meta;
1035
+ if (meta && typeof meta.currentPage === 'number' && typeof meta.totalPages === 'number') {
1036
+ const shown = Array.isArray(envelope.data) ? envelope.data.length : 0;
1037
+ this.log(`
1038
+ Page ${meta.currentPage}/${meta.totalPages} — ${shown} of ${meta.totalItems ?? '?'} records`);
1039
+ }
1040
+ for (const total of envelope.totals ?? []) {
1041
+ const { currency, ...sums } = total;
1042
+ const parts = Object.entries(sums).map(([column, value]) => `${column} ${value}`).join(', ');
1043
+ this.log(`Total ${currency ?? ''}: ${parts}`.trim());
1044
+ }
1045
+ }
1046
+ /**
1047
+ * The API caps how many records a page may hold. Asking for more and getting
1048
+ * fewer looks like "that is everything" unless it is said out loud.
1049
+ */
1050
+ warnShortPage(envelope, requestedLimit) {
1051
+ const asked = requestedLimit === undefined ? undefined : Number(requestedLimit);
1052
+ const perPage = envelope.meta?.itemsPerPage;
1053
+ if (asked === undefined || Number.isNaN(asked) || typeof perPage !== 'number' || perPage >= asked)
1054
+ return;
1055
+ const pages = envelope.meta?.totalPages;
1056
+ const more = typeof pages === 'number' && pages > 1 ? ` Use --page, or the export action for every page.` : '';
1057
+ process.stderr.write(`! Asked for ${asked} records per page; the API returned at most ${perPage}.${more}
1058
+ `);
1059
+ }
1013
1060
  sanitizeOutput(data) {
1014
1061
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1015
1062
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -28,6 +28,7 @@ export default class VatDeclarations extends Command {
28
28
  query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
29
  fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
30
  sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
+ meta: import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
32
  'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
32
33
  };
33
34
  run(): Promise<void>;
@@ -36,6 +37,13 @@ export default class VatDeclarations extends Command {
36
37
  private runCamt053Import;
37
38
  private runNestedImport;
38
39
  private parseFilterValue;
40
+ /** The page line under a table: which page, how many, and any sums the API returned. */
41
+ private printPageSummary;
42
+ /**
43
+ * The API caps how many records a page may hold. Asking for more and getting
44
+ * fewer looks like "that is everything" unless it is said out loud.
45
+ */
46
+ private warnShortPage;
39
47
  private sanitizeOutput;
40
48
  private getColumns;
41
49
  private getValue;
@@ -70,6 +70,7 @@ export default class VatDeclarations extends Command {
70
70
  query: Flags.string({ description: 'Additional query params as JSON' }),
71
71
  fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
72
72
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
73
+ meta: Flags.boolean({ description: 'Keep the API envelope in JSON output: data plus page, totals and sums' }),
73
74
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
74
75
  };
75
76
  async run() {
@@ -362,9 +363,15 @@ export default class VatDeclarations extends Command {
362
363
  }
363
364
  catch { /* keep as string */ }
364
365
  }
365
- // Extract data array from paginated wrapper (e.g., { data: [...], total: 3 })
366
+ // A paginated answer is { data, meta, links, totals }. The records are
367
+ // unwrapped for output, but what the API said about the page is kept:
368
+ // dropping it silently let a script read one page as the whole result
369
+ // (AderisERP/product#61).
370
+ let envelope;
366
371
  if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
367
- output = output.data;
372
+ const page = output;
373
+ envelope = page;
374
+ output = page.data;
368
375
  }
369
376
  // For table/csv on non-array responses, auto-extract the primary array
370
377
  // e.g., { payments: [...], total: 3 } → extract payments array
@@ -381,12 +388,24 @@ export default class VatDeclarations extends Command {
381
388
  output = this.sanitizeOutput(output);
382
389
  }
383
390
  const fieldList = flags.fields?.split(',').map(f => f.trim());
384
- if (flags.csv || flags.output === 'csv')
391
+ if (envelope)
392
+ this.warnShortPage(envelope, flags.limit);
393
+ if (flags.csv || flags.output === 'csv') {
385
394
  this.printCsv(output, fieldList);
386
- else if (flags.table || flags.output === 'table')
395
+ }
396
+ else if (flags.table || flags.output === 'table') {
387
397
  this.printTable(output, fieldList);
388
- else
398
+ if (envelope)
399
+ this.printPageSummary(envelope);
400
+ }
401
+ else if (envelope && flags.meta) {
402
+ // The whole envelope, for a caller that asked for it; the default stays
403
+ // a bare array so existing scripts are unaffected.
404
+ this.log(JSON.stringify({ ...envelope, data: output }, null, 2));
405
+ }
406
+ else {
389
407
  this.log(JSON.stringify(output, null, 2));
408
+ }
390
409
  }
391
410
  catch (error) {
392
411
  if (error instanceof ApiError) {
@@ -1015,6 +1034,34 @@ export default class VatDeclarations extends Command {
1015
1034
  return `\$${op}`;
1016
1035
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
1017
1036
  }
1037
+ /** The page line under a table: which page, how many, and any sums the API returned. */
1038
+ printPageSummary(envelope) {
1039
+ const meta = envelope.meta;
1040
+ if (meta && typeof meta.currentPage === 'number' && typeof meta.totalPages === 'number') {
1041
+ const shown = Array.isArray(envelope.data) ? envelope.data.length : 0;
1042
+ this.log(`
1043
+ Page ${meta.currentPage}/${meta.totalPages} — ${shown} of ${meta.totalItems ?? '?'} records`);
1044
+ }
1045
+ for (const total of envelope.totals ?? []) {
1046
+ const { currency, ...sums } = total;
1047
+ const parts = Object.entries(sums).map(([column, value]) => `${column} ${value}`).join(', ');
1048
+ this.log(`Total ${currency ?? ''}: ${parts}`.trim());
1049
+ }
1050
+ }
1051
+ /**
1052
+ * The API caps how many records a page may hold. Asking for more and getting
1053
+ * fewer looks like "that is everything" unless it is said out loud.
1054
+ */
1055
+ warnShortPage(envelope, requestedLimit) {
1056
+ const asked = requestedLimit === undefined ? undefined : Number(requestedLimit);
1057
+ const perPage = envelope.meta?.itemsPerPage;
1058
+ if (asked === undefined || Number.isNaN(asked) || typeof perPage !== 'number' || perPage >= asked)
1059
+ return;
1060
+ const pages = envelope.meta?.totalPages;
1061
+ const more = typeof pages === 'number' && pages > 1 ? ` Use --page, or the export action for every page.` : '';
1062
+ process.stderr.write(`! Asked for ${asked} records per page; the API returned at most ${perPage}.${more}
1063
+ `);
1064
+ }
1018
1065
  sanitizeOutput(data) {
1019
1066
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1020
1067
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -18,6 +18,7 @@ export default class VendorInvoices extends Command {
18
18
  'note-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
19
  'file-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
20
  'payment-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'offset-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
22
  page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
23
  limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
24
  include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -27,6 +28,7 @@ export default class VendorInvoices extends Command {
27
28
  query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
29
  fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
30
  sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
+ meta: import("@oclif/core/interfaces").BooleanFlag<boolean>;
30
32
  'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
33
  };
32
34
  run(): Promise<void>;
@@ -35,6 +37,13 @@ export default class VendorInvoices extends Command {
35
37
  private runCamt053Import;
36
38
  private runNestedImport;
37
39
  private parseFilterValue;
40
+ /** The page line under a table: which page, how many, and any sums the API returned. */
41
+ private printPageSummary;
42
+ /**
43
+ * The API caps how many records a page may hold. Asking for more and getting
44
+ * fewer looks like "that is everything" unless it is said out loud.
45
+ */
46
+ private warnShortPage;
38
47
  private sanitizeOutput;
39
48
  private getColumns;
40
49
  private getValue;
@@ -6,7 +6,7 @@ const HAS_IMPORT = true;
6
6
  const HAS_EXPORT = true;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["line-items", "notes", "files", "payments"];
9
- const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "payment-derivation", "submit", "approve", "decline", "book", "rebuild-journal", "cancel", "change-vendor", "recalculate-payment-status", "validate", "create-line-items", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-payments", "create-payments", "get-payments", "update-payments", "delete-payments", "payments-approve", "payments-reopen", "payments-cancel"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "payment-derivation", "beneficiary-check", "submit", "approve", "decline", "book", "rebuild-journal", "cancel", "change-vendor", "recalculate-payment-status", "validate", "create-line-items", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-payments", "create-payments", "get-payments", "update-payments", "delete-payments", "payments-approve", "payments-reopen", "payments-cancel", "create-offsets", "delete-offsets", "offset-candidates"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
11
  'list': { method: 'GET', path: '/v1/vendor-invoices', summary: 'Get all vendor invoices with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
12
  'create': { method: 'POST', path: '/v1/vendor-invoices', summary: 'Create a new vendor invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'payments'] },
@@ -14,6 +14,7 @@ const ENDPOINTS = {
14
14
  'update': { method: 'PATCH', path: '/v1/vendor-invoices/{id}', summary: 'Update a vendor invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'removeFileIds'] },
15
15
  'delete': { method: 'DELETE', path: '/v1/vendor-invoices/{id}', summary: 'Delete a vendor invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
16
  'payment-derivation': { method: 'GET', path: '/v1/vendor-invoices/{id}/payment-derivation', summary: 'Explain how a vendor invoice\'s paid amount is derived', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'beneficiary-check': { method: 'GET', path: '/v1/vendor-invoices/{id}/beneficiary-check', summary: 'Check a vendor invoice\'s beneficiary account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
18
  'submit': { method: 'POST', path: '/v1/vendor-invoices/{id}/submit', summary: 'Submit a vendor invoice for approval (transitions DRAFT to PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
19
  'approve': { method: 'POST', path: '/v1/vendor-invoices/{id}/approve', summary: 'Approve a vendor invoice (transitions DRAFT/PENDING_APPROVAL to APPROVED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'removeFileIds'] },
19
20
  'decline': { method: 'POST', path: '/v1/vendor-invoices/{id}/decline', summary: 'Decline a vendor invoice (transitions PENDING_APPROVAL to DECLINED)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
@@ -23,8 +24,8 @@ const ENDPOINTS = {
23
24
  'change-vendor': { method: 'POST', path: '/v1/vendor-invoices/{id}/change-vendor', summary: 'Change the vendor on a vendor invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
25
  'recalculate-payment-status': { method: 'POST', path: '/v1/vendor-invoices/{id}/recalculate-payment-status', summary: 'Recalculate a vendor invoice\'s open amount and payment status', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
26
  'validate': { method: 'POST', path: '/v1/vendor-invoices/{id}/validate', summary: 'Validate a vendor invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'totalAmountByTaxRate', 'files', 'notes', 'lineItems', 'tagIds', 'tagCodes', 'removeFileIds'] },
26
- 'create-line-items': { method: 'POST', path: '/v1/vendor-invoices/{invoiceId}/line-items', summary: 'Add a line item to a vendor invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'purchaseOrderLineItems'] },
27
- 'update-line-items': { method: 'PATCH', path: '/v1/vendor-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a vendor invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'purchaseOrderLineItems'] },
27
+ 'create-line-items': { method: 'POST', path: '/v1/vendor-invoices/{invoiceId}/line-items', summary: 'Add a line item to a vendor invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'purchaseOrderLineItems', 'creditedVendorInvoiceLineItems'] },
28
+ 'update-line-items': { method: 'PATCH', path: '/v1/vendor-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a vendor invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'purchaseOrderLineItems', 'creditedVendorInvoiceLineItems'] },
28
29
  'delete-line-items': { method: 'DELETE', path: '/v1/vendor-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Remove a line item from a vendor invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
30
  'list-notes': { method: 'GET', path: '/v1/vendor-invoices/{vendorInvoiceId}/notes', summary: 'Get all notes for a vendor invoice', pathParams: ['vendorInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
31
  'create-notes': { method: 'POST', path: '/v1/vendor-invoices/{vendorInvoiceId}/notes', summary: 'Create a note for a vendor invoice', pathParams: ['vendorInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
@@ -45,6 +46,9 @@ const ENDPOINTS = {
45
46
  'payments-approve': { method: 'POST', path: '/v1/vendor-invoices/{vendorInvoiceId}/payments/{paymentId}/approve', summary: 'Approve a payment entry (OPEN → APPROVED)', pathParams: ['vendorInvoiceId', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
46
47
  'payments-reopen': { method: 'POST', path: '/v1/vendor-invoices/{vendorInvoiceId}/payments/{paymentId}/reopen', summary: 'Reopen an approved payment entry (APPROVED → OPEN)', pathParams: ['vendorInvoiceId', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
47
48
  'payments-cancel': { method: 'POST', path: '/v1/vendor-invoices/{vendorInvoiceId}/payments/{paymentId}/cancel', summary: 'Cancel a sent payment entry (SENT -> ERROR)', pathParams: ['vendorInvoiceId', 'paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
49
+ 'create-offsets': { method: 'POST', path: '/v1/vendor-invoices/{vendorInvoiceId}/offsets', summary: 'Offset a prepayment or credit note against a vendor invoice', pathParams: ['vendorInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
50
+ 'delete-offsets': { method: 'DELETE', path: '/v1/vendor-invoices/{vendorInvoiceId}/offsets/{offsetId}', summary: 'Remove an offset of a prepayment or credit note from a vendor invoice', pathParams: ['vendorInvoiceId', 'offsetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
51
+ 'offset-candidates': { method: 'GET', path: '/v1/vendor-invoices/{vendorInvoiceId}/offset-candidates', summary: 'List the prepayments and credit notes that can be offset against a vendor invoice', pathParams: ['vendorInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
48
52
  };
49
53
  export default class VendorInvoices extends Command {
50
54
  static description = 'Manage vendor invoices';
@@ -72,6 +76,7 @@ export default class VendorInvoices extends Command {
72
76
  'note-id': Flags.string({ description: 'Path param: noteId' }),
73
77
  'file-id': Flags.string({ description: 'Path param: fileId' }),
74
78
  'payment-id': Flags.string({ description: 'Path param: paymentId' }),
79
+ 'offset-id': Flags.string({ description: 'Path param: offsetId' }),
75
80
  page: Flags.string({ description: 'Page number' }),
76
81
  limit: Flags.string({ description: 'Items per page' }),
77
82
  include: Flags.string({ description: 'Relations to include' }),
@@ -81,6 +86,7 @@ export default class VendorInvoices extends Command {
81
86
  query: Flags.string({ description: 'Additional query params as JSON' }),
82
87
  fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
83
88
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
89
+ meta: Flags.boolean({ description: 'Keep the API envelope in JSON output: data plus page, totals and sums' }),
84
90
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
85
91
  };
86
92
  async run() {
@@ -373,9 +379,15 @@ export default class VendorInvoices extends Command {
373
379
  }
374
380
  catch { /* keep as string */ }
375
381
  }
376
- // Extract data array from paginated wrapper (e.g., { data: [...], total: 3 })
382
+ // A paginated answer is { data, meta, links, totals }. The records are
383
+ // unwrapped for output, but what the API said about the page is kept:
384
+ // dropping it silently let a script read one page as the whole result
385
+ // (AderisERP/product#61).
386
+ let envelope;
377
387
  if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
378
- output = output.data;
388
+ const page = output;
389
+ envelope = page;
390
+ output = page.data;
379
391
  }
380
392
  // For table/csv on non-array responses, auto-extract the primary array
381
393
  // e.g., { payments: [...], total: 3 } → extract payments array
@@ -392,12 +404,24 @@ export default class VendorInvoices extends Command {
392
404
  output = this.sanitizeOutput(output);
393
405
  }
394
406
  const fieldList = flags.fields?.split(',').map(f => f.trim());
395
- if (flags.csv || flags.output === 'csv')
407
+ if (envelope)
408
+ this.warnShortPage(envelope, flags.limit);
409
+ if (flags.csv || flags.output === 'csv') {
396
410
  this.printCsv(output, fieldList);
397
- else if (flags.table || flags.output === 'table')
411
+ }
412
+ else if (flags.table || flags.output === 'table') {
398
413
  this.printTable(output, fieldList);
399
- else
414
+ if (envelope)
415
+ this.printPageSummary(envelope);
416
+ }
417
+ else if (envelope && flags.meta) {
418
+ // The whole envelope, for a caller that asked for it; the default stays
419
+ // a bare array so existing scripts are unaffected.
420
+ this.log(JSON.stringify({ ...envelope, data: output }, null, 2));
421
+ }
422
+ else {
400
423
  this.log(JSON.stringify(output, null, 2));
424
+ }
401
425
  }
402
426
  catch (error) {
403
427
  if (error instanceof ApiError) {
@@ -1026,6 +1050,34 @@ export default class VendorInvoices extends Command {
1026
1050
  return `\$${op}`;
1027
1051
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
1028
1052
  }
1053
+ /** The page line under a table: which page, how many, and any sums the API returned. */
1054
+ printPageSummary(envelope) {
1055
+ const meta = envelope.meta;
1056
+ if (meta && typeof meta.currentPage === 'number' && typeof meta.totalPages === 'number') {
1057
+ const shown = Array.isArray(envelope.data) ? envelope.data.length : 0;
1058
+ this.log(`
1059
+ Page ${meta.currentPage}/${meta.totalPages} — ${shown} of ${meta.totalItems ?? '?'} records`);
1060
+ }
1061
+ for (const total of envelope.totals ?? []) {
1062
+ const { currency, ...sums } = total;
1063
+ const parts = Object.entries(sums).map(([column, value]) => `${column} ${value}`).join(', ');
1064
+ this.log(`Total ${currency ?? ''}: ${parts}`.trim());
1065
+ }
1066
+ }
1067
+ /**
1068
+ * The API caps how many records a page may hold. Asking for more and getting
1069
+ * fewer looks like "that is everything" unless it is said out loud.
1070
+ */
1071
+ warnShortPage(envelope, requestedLimit) {
1072
+ const asked = requestedLimit === undefined ? undefined : Number(requestedLimit);
1073
+ const perPage = envelope.meta?.itemsPerPage;
1074
+ if (asked === undefined || Number.isNaN(asked) || typeof perPage !== 'number' || perPage >= asked)
1075
+ return;
1076
+ const pages = envelope.meta?.totalPages;
1077
+ const more = typeof pages === 'number' && pages > 1 ? ` Use --page, or the export action for every page.` : '';
1078
+ process.stderr.write(`! Asked for ${asked} records per page; the API returned at most ${perPage}.${more}
1079
+ `);
1080
+ }
1029
1081
  sanitizeOutput(data) {
1030
1082
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1031
1083
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -23,6 +23,7 @@ export default class Webhooks extends Command {
23
23
  query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
24
  fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
25
  sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ meta: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
27
  'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
28
  };
28
29
  run(): Promise<void>;
@@ -31,6 +32,13 @@ export default class Webhooks extends Command {
31
32
  private runCamt053Import;
32
33
  private runNestedImport;
33
34
  private parseFilterValue;
35
+ /** The page line under a table: which page, how many, and any sums the API returned. */
36
+ private printPageSummary;
37
+ /**
38
+ * The API caps how many records a page may hold. Asking for more and getting
39
+ * fewer looks like "that is everything" unless it is said out loud.
40
+ */
41
+ private warnShortPage;
34
42
  private sanitizeOutput;
35
43
  private getColumns;
36
44
  private getValue;
@@ -47,6 +47,7 @@ export default class Webhooks extends Command {
47
47
  query: Flags.string({ description: 'Additional query params as JSON' }),
48
48
  fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
49
49
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
50
+ meta: Flags.boolean({ description: 'Keep the API envelope in JSON output: data plus page, totals and sums' }),
50
51
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
51
52
  };
52
53
  async run() {
@@ -339,9 +340,15 @@ export default class Webhooks extends Command {
339
340
  }
340
341
  catch { /* keep as string */ }
341
342
  }
342
- // Extract data array from paginated wrapper (e.g., { data: [...], total: 3 })
343
+ // A paginated answer is { data, meta, links, totals }. The records are
344
+ // unwrapped for output, but what the API said about the page is kept:
345
+ // dropping it silently let a script read one page as the whole result
346
+ // (AderisERP/product#61).
347
+ let envelope;
343
348
  if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
344
- output = output.data;
349
+ const page = output;
350
+ envelope = page;
351
+ output = page.data;
345
352
  }
346
353
  // For table/csv on non-array responses, auto-extract the primary array
347
354
  // e.g., { payments: [...], total: 3 } → extract payments array
@@ -358,12 +365,24 @@ export default class Webhooks extends Command {
358
365
  output = this.sanitizeOutput(output);
359
366
  }
360
367
  const fieldList = flags.fields?.split(',').map(f => f.trim());
361
- if (flags.csv || flags.output === 'csv')
368
+ if (envelope)
369
+ this.warnShortPage(envelope, flags.limit);
370
+ if (flags.csv || flags.output === 'csv') {
362
371
  this.printCsv(output, fieldList);
363
- else if (flags.table || flags.output === 'table')
372
+ }
373
+ else if (flags.table || flags.output === 'table') {
364
374
  this.printTable(output, fieldList);
365
- else
375
+ if (envelope)
376
+ this.printPageSummary(envelope);
377
+ }
378
+ else if (envelope && flags.meta) {
379
+ // The whole envelope, for a caller that asked for it; the default stays
380
+ // a bare array so existing scripts are unaffected.
381
+ this.log(JSON.stringify({ ...envelope, data: output }, null, 2));
382
+ }
383
+ else {
366
384
  this.log(JSON.stringify(output, null, 2));
385
+ }
367
386
  }
368
387
  catch (error) {
369
388
  if (error instanceof ApiError) {
@@ -992,6 +1011,34 @@ export default class Webhooks extends Command {
992
1011
  return `\$${op}`;
993
1012
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
994
1013
  }
1014
+ /** The page line under a table: which page, how many, and any sums the API returned. */
1015
+ printPageSummary(envelope) {
1016
+ const meta = envelope.meta;
1017
+ if (meta && typeof meta.currentPage === 'number' && typeof meta.totalPages === 'number') {
1018
+ const shown = Array.isArray(envelope.data) ? envelope.data.length : 0;
1019
+ this.log(`
1020
+ Page ${meta.currentPage}/${meta.totalPages} — ${shown} of ${meta.totalItems ?? '?'} records`);
1021
+ }
1022
+ for (const total of envelope.totals ?? []) {
1023
+ const { currency, ...sums } = total;
1024
+ const parts = Object.entries(sums).map(([column, value]) => `${column} ${value}`).join(', ');
1025
+ this.log(`Total ${currency ?? ''}: ${parts}`.trim());
1026
+ }
1027
+ }
1028
+ /**
1029
+ * The API caps how many records a page may hold. Asking for more and getting
1030
+ * fewer looks like "that is everything" unless it is said out loud.
1031
+ */
1032
+ warnShortPage(envelope, requestedLimit) {
1033
+ const asked = requestedLimit === undefined ? undefined : Number(requestedLimit);
1034
+ const perPage = envelope.meta?.itemsPerPage;
1035
+ if (asked === undefined || Number.isNaN(asked) || typeof perPage !== 'number' || perPage >= asked)
1036
+ return;
1037
+ const pages = envelope.meta?.totalPages;
1038
+ const more = typeof pages === 'number' && pages > 1 ? ` Use --page, or the export action for every page.` : '';
1039
+ process.stderr.write(`! Asked for ${asked} records per page; the API returned at most ${perPage}.${more}
1040
+ `);
1041
+ }
995
1042
  sanitizeOutput(data) {
996
1043
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
997
1044
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -23,6 +23,7 @@ export default class Workflows extends Command {
23
23
  query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
24
  fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
25
  sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ meta: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
27
  'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
28
  };
28
29
  run(): Promise<void>;
@@ -31,6 +32,13 @@ export default class Workflows extends Command {
31
32
  private runCamt053Import;
32
33
  private runNestedImport;
33
34
  private parseFilterValue;
35
+ /** The page line under a table: which page, how many, and any sums the API returned. */
36
+ private printPageSummary;
37
+ /**
38
+ * The API caps how many records a page may hold. Asking for more and getting
39
+ * fewer looks like "that is everything" unless it is said out loud.
40
+ */
41
+ private warnShortPage;
34
42
  private sanitizeOutput;
35
43
  private getColumns;
36
44
  private getValue;