@aderis-erp/cli 1.0.2 → 1.0.4

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 (191) hide show
  1. package/README.md +6 -0
  2. package/dist/commands/access-groups 3.d.ts +40 -0
  3. package/dist/commands/{access-groups 2.js → access-groups 3.js } +106 -40
  4. package/dist/commands/access-groups.d.ts +40 -0
  5. package/dist/commands/access-groups.js +1074 -0
  6. package/dist/commands/account-transactions.d.ts +42 -0
  7. package/dist/commands/account-transactions.js +1088 -0
  8. package/dist/commands/accounts 2.d.ts +41 -0
  9. package/dist/commands/accounts 2.js +1078 -0
  10. package/dist/commands/accounts.d.ts +41 -0
  11. package/dist/commands/accounts.js +1080 -0
  12. package/dist/commands/agent-runs.d.ts +40 -0
  13. package/dist/commands/agent-runs.js +1071 -0
  14. package/dist/commands/async-endpoints-internal 3.d.ts +40 -0
  15. package/dist/commands/{processing-logs.js → async-endpoints-internal 3.js } +110 -46
  16. package/dist/commands/async-endpoints-internal.d.ts +40 -0
  17. package/dist/commands/async-endpoints-internal.js +1064 -0
  18. package/dist/commands/audit-log 3.d.ts +40 -0
  19. package/dist/commands/audit-log 3.js +1066 -0
  20. package/dist/commands/audit-log.d.ts +40 -0
  21. package/dist/commands/audit-log.js +1068 -0
  22. package/dist/commands/auth/login.d.ts +14 -0
  23. package/dist/commands/auth/login.js +116 -0
  24. package/dist/commands/auth/logout.d.ts +10 -0
  25. package/dist/commands/auth/logout.js +58 -0
  26. package/dist/commands/auth/status.d.ts +12 -0
  27. package/dist/commands/auth/status.js +101 -0
  28. package/dist/commands/auth/tenant.d.ts +19 -0
  29. package/dist/commands/auth/tenant.js +184 -0
  30. package/dist/commands/auth.d.ts +37 -0
  31. package/dist/commands/auth.js +871 -0
  32. package/dist/commands/bank-accounts.d.ts +42 -0
  33. package/dist/commands/bank-accounts.js +1088 -0
  34. package/dist/commands/bank-integrations.d.ts +1 -0
  35. package/dist/commands/bank-integrations.js +122 -54
  36. package/dist/commands/business-partners 3.d.ts +41 -0
  37. package/dist/commands/business-partners 3.js +1078 -0
  38. package/dist/commands/business-partners---contacts 3.d.ts +42 -0
  39. package/dist/commands/business-partners---contacts 3.js +1072 -0
  40. package/dist/commands/business-partners---contacts.d.ts +1 -0
  41. package/dist/commands/business-partners---contacts.js +114 -46
  42. package/dist/commands/business-partners.d.ts +1 -0
  43. package/dist/commands/business-partners.js +114 -46
  44. package/dist/commands/config/add.d.ts +13 -0
  45. package/dist/commands/config/add.js +120 -0
  46. package/dist/commands/config/current.d.ts +9 -0
  47. package/dist/commands/config/current.js +43 -0
  48. package/dist/commands/config/delete.d.ts +12 -0
  49. package/dist/commands/config/delete.js +67 -0
  50. package/dist/commands/config/list.d.ts +9 -0
  51. package/dist/commands/config/list.js +50 -0
  52. package/dist/commands/config/use.d.ts +9 -0
  53. package/dist/commands/config/use.js +40 -0
  54. package/dist/commands/credit-card-accounts 3.d.ts +42 -0
  55. package/dist/commands/credit-card-accounts 3.js +1084 -0
  56. package/dist/commands/credit-card-accounts.d.ts +1 -0
  57. package/dist/commands/credit-card-accounts.js +119 -51
  58. package/dist/commands/documents.d.ts +1 -0
  59. package/dist/commands/documents.js +110 -42
  60. package/dist/commands/dunning-configs 3.d.ts +40 -0
  61. package/dist/commands/dunning-configs 3.js +1072 -0
  62. package/dist/commands/dunning-configs.d.ts +1 -0
  63. package/dist/commands/dunning-configs.js +109 -41
  64. package/dist/commands/dunning-reminders.d.ts +1 -0
  65. package/dist/commands/dunning-reminders.js +112 -44
  66. package/dist/commands/dunning-runs 3.d.ts +40 -0
  67. package/dist/commands/{pubsub.js → dunning-runs 3.js } +108 -43
  68. package/dist/commands/dunning-runs.d.ts +1 -0
  69. package/dist/commands/dunning-runs.js +105 -37
  70. package/dist/commands/employees.d.ts +1 -0
  71. package/dist/commands/employees.js +136 -68
  72. package/dist/commands/event-processor-mappings 3.d.ts +40 -0
  73. package/dist/commands/event-processor-mappings 3.js +1071 -0
  74. package/dist/commands/event-processor-mappings.d.ts +1 -0
  75. package/dist/commands/event-processor-mappings.js +109 -41
  76. package/dist/commands/files-internal 3.d.ts +40 -0
  77. package/dist/commands/files-internal 3.js +1073 -0
  78. package/dist/commands/files-internal.d.ts +1 -0
  79. package/dist/commands/files-internal.js +113 -45
  80. package/dist/commands/health 3.d.ts +40 -0
  81. package/dist/commands/health 3.js +1067 -0
  82. package/dist/commands/health.d.ts +1 -0
  83. package/dist/commands/health.js +109 -41
  84. package/dist/commands/{pubsub.d.ts → inbox 3.d.ts } +2 -1
  85. package/dist/commands/inbox 3.js +1065 -0
  86. package/dist/commands/inbox.d.ts +1 -0
  87. package/dist/commands/inbox.js +110 -40
  88. package/dist/commands/integrations 3.d.ts +41 -0
  89. package/dist/commands/integrations 3.js +1067 -0
  90. package/dist/commands/integrations.d.ts +1 -0
  91. package/dist/commands/integrations.js +107 -39
  92. package/dist/commands/item-categories 3.d.ts +40 -0
  93. package/dist/commands/item-categories 3.js +1072 -0
  94. package/dist/commands/item-categories.d.ts +1 -0
  95. package/dist/commands/item-categories.js +109 -41
  96. package/dist/commands/items 3.d.ts +40 -0
  97. package/dist/commands/items 3.js +1072 -0
  98. package/dist/commands/items.d.ts +1 -0
  99. package/dist/commands/items.js +109 -41
  100. package/dist/commands/journal-lines.d.ts +1 -0
  101. package/dist/commands/journal-lines.js +105 -37
  102. package/dist/commands/journals 3.d.ts +42 -0
  103. package/dist/commands/journals 3.js +1088 -0
  104. package/dist/commands/journals.d.ts +1 -0
  105. package/dist/commands/journals.js +123 -55
  106. package/dist/commands/ledgers.d.ts +1 -0
  107. package/dist/commands/ledgers.js +120 -52
  108. package/dist/commands/office-locations 3.d.ts +40 -0
  109. package/dist/commands/office-locations 3.js +1072 -0
  110. package/dist/commands/office-locations.d.ts +1 -0
  111. package/dist/commands/office-locations.js +109 -41
  112. package/dist/commands/payment-method-ledger-mappings 3.d.ts +41 -0
  113. package/dist/commands/payment-method-ledger-mappings 3.js +1066 -0
  114. package/dist/commands/payment-method-ledger-mappings.d.ts +1 -0
  115. package/dist/commands/payment-method-ledger-mappings.js +109 -41
  116. package/dist/commands/payment-methods 3.d.ts +40 -0
  117. package/dist/commands/payment-methods 3.js +1072 -0
  118. package/dist/commands/payment-methods.d.ts +1 -0
  119. package/dist/commands/payment-methods.js +109 -41
  120. package/dist/commands/payment-runs 3.d.ts +42 -0
  121. package/dist/commands/payment-runs 3.js +1087 -0
  122. package/dist/commands/payment-runs.d.ts +1 -0
  123. package/dist/commands/payment-runs.js +122 -54
  124. package/dist/commands/payment-terms 3.d.ts +40 -0
  125. package/dist/commands/payment-terms 3.js +1073 -0
  126. package/dist/commands/payment-terms.d.ts +1 -0
  127. package/dist/commands/payment-terms.js +110 -42
  128. package/dist/commands/price-lists 3.d.ts +41 -0
  129. package/dist/commands/price-lists 3.js +1078 -0
  130. package/dist/commands/price-lists.d.ts +1 -0
  131. package/dist/commands/price-lists.js +114 -46
  132. package/dist/commands/processor-mappings 2.d.ts +40 -0
  133. package/dist/commands/processor-mappings 2.js +1070 -0
  134. package/dist/commands/processor-mappings.d.ts +1 -0
  135. package/dist/commands/processor-mappings.js +108 -40
  136. package/dist/commands/processors.d.ts +1 -0
  137. package/dist/commands/processors.js +113 -45
  138. package/dist/commands/reconciliation 3.d.ts +40 -0
  139. package/dist/commands/reconciliation 3.js +1069 -0
  140. package/dist/commands/reconciliation.d.ts +1 -0
  141. package/dist/commands/reconciliation.js +110 -42
  142. package/dist/commands/reports.d.ts +1 -0
  143. package/dist/commands/reports.js +110 -39
  144. package/dist/commands/salary-types 3.d.ts +40 -0
  145. package/dist/commands/salary-types 3.js +1072 -0
  146. package/dist/commands/salary-types.d.ts +1 -0
  147. package/dist/commands/salary-types.js +1074 -0
  148. package/dist/commands/sales-invoices 3.d.ts +43 -0
  149. package/dist/commands/sales-invoices 3.js +1100 -0
  150. package/dist/commands/sales-invoices.d.ts +1 -0
  151. package/dist/commands/sales-invoices.js +134 -66
  152. package/dist/commands/sales-orders 3.d.ts +43 -0
  153. package/dist/commands/sales-orders 3.js +1098 -0
  154. package/dist/commands/sales-orders.d.ts +1 -0
  155. package/dist/commands/sales-orders.js +133 -64
  156. package/dist/commands/switch.d.ts +1 -0
  157. package/dist/commands/switch.js +58 -3
  158. package/dist/commands/{processing-logs.d.ts → tags 3.d.ts } +2 -1
  159. package/dist/commands/tags 3.js +1072 -0
  160. package/dist/commands/tags.d.ts +1 -0
  161. package/dist/commands/tags.js +109 -41
  162. package/dist/commands/tax-codes 3.d.ts +40 -0
  163. package/dist/commands/tax-codes 3.js +1075 -0
  164. package/dist/commands/tax-codes.d.ts +1 -0
  165. package/dist/commands/tax-codes.js +112 -44
  166. package/dist/commands/tax-matrix.d.ts +1 -0
  167. package/dist/commands/tax-matrix.js +109 -41
  168. package/dist/commands/tenants.d.ts +1 -0
  169. package/dist/commands/tenants.js +109 -41
  170. package/dist/commands/text-templates 3.d.ts +40 -0
  171. package/dist/commands/text-templates 3.js +1072 -0
  172. package/dist/commands/text-templates.d.ts +1 -0
  173. package/dist/commands/text-templates.js +109 -41
  174. package/dist/commands/tools.d.ts +1 -0
  175. package/dist/commands/tools.js +106 -38
  176. package/dist/commands/vendor-invoices.d.ts +1 -0
  177. package/dist/commands/vendor-invoices.js +139 -69
  178. package/dist/commands/webhooks 3.d.ts +40 -0
  179. package/dist/commands/webhooks 3.js +1074 -0
  180. package/dist/commands/webhooks.d.ts +1 -0
  181. package/dist/commands/webhooks.js +111 -43
  182. package/dist/commands/workflows.d.ts +1 -0
  183. package/dist/commands/workflows.js +107 -39
  184. package/dist/lib/api-client.d.ts +5 -1
  185. package/dist/lib/api-client.js +19 -5
  186. package/dist/lib/auth.d.ts +16 -1
  187. package/dist/lib/auth.js +170 -45
  188. package/dist/lib/config.d.ts +7 -0
  189. package/dist/lib/config.js +7 -1
  190. package/oclif.manifest.json +7464 -653
  191. package/package.json +2 -2
@@ -32,6 +32,7 @@ export default class Journals extends Command {
32
32
  private runExport;
33
33
  private runCamt053Import;
34
34
  private runNestedImport;
35
+ private parseFilterValue;
35
36
  private filterFields;
36
37
  private sanitizeOutput;
37
38
  private getColumns;
@@ -8,25 +8,25 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["notes", "files"];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "post", "approve", "cancel", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/journals', summary: 'List all journals', pathParams: [], hasBody: false, isMultipart: false },
12
- 'create': { method: 'POST', path: '/v1/journals', summary: 'Create a new journal (with embedded lines)', pathParams: [], hasBody: true, isMultipart: false },
13
- 'get': { method: 'GET', path: '/v1/journals/{id}', summary: 'Get a journal by ID', pathParams: ['id'], hasBody: false, isMultipart: false },
14
- 'update': { method: 'PATCH', path: '/v1/journals/{id}', summary: 'Update a journal (with embedded lines upsert)', pathParams: ['id'], hasBody: true, isMultipart: false },
15
- 'delete': { method: 'DELETE', path: '/v1/journals/{id}', summary: 'Delete a journal', pathParams: ['id'], hasBody: false, isMultipart: false },
16
- 'post': { method: 'POST', path: '/v1/journals/{id}/post', summary: 'Post journal (finalize)', pathParams: ['id'], hasBody: false, isMultipart: false },
17
- 'approve': { method: 'POST', path: '/v1/journals/{id}/approve', summary: 'Approve a journal (transitions DRAFT to APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false },
18
- 'cancel': { method: 'POST', path: '/v1/journals/{id}/cancel', summary: 'Cancel a journal', pathParams: ['id'], hasBody: true, isMultipart: false },
19
- 'list-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes', summary: 'Get all notes for a journal', pathParams: ['journalId'], hasBody: false, isMultipart: false },
20
- 'create-notes': { method: 'POST', path: '/v1/journals/{journalId}/notes', summary: 'Create a note for a journal', pathParams: ['journalId'], hasBody: true, isMultipart: false },
21
- 'get-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Get a specific note for a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false },
22
- 'update-notes': { method: 'PATCH', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Update a note for a journal', pathParams: ['journalId', 'noteId'], hasBody: true, isMultipart: false },
23
- 'delete-notes': { method: 'DELETE', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Delete a note from a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false },
24
- 'list-files': { method: 'GET', path: '/v1/journals/{parentId}/files', summary: 'List file attachments for a journal', pathParams: ['parentId'], hasBody: false, isMultipart: false },
25
- 'create-files': { method: 'POST', path: '/v1/journals/{parentId}/files', summary: 'Upload a file attachment to a journal', pathParams: ['parentId'], hasBody: false, isMultipart: true },
26
- 'get-files': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Get a file attachment for a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false },
27
- 'update-files': { method: 'PATCH', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a journal', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false },
28
- 'delete-files': { method: 'DELETE', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false },
29
- 'files-download': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false },
11
+ 'list': { method: 'GET', path: '/v1/journals', summary: 'List all journals', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/journals', summary: 'Create a new journal (with embedded lines)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'lines', 'notes'] },
13
+ 'get': { method: 'GET', path: '/v1/journals/{id}', summary: 'Get a journal by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/journals/{id}', summary: 'Update a journal (with embedded lines upsert)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'lines'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/journals/{id}', summary: 'Delete a journal', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
+ 'post': { method: 'POST', path: '/v1/journals/{id}/post', summary: 'Post journal (finalize)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
+ 'approve': { method: 'POST', path: '/v1/journals/{id}/approve', summary: 'Approve a journal (transitions DRAFT to APPROVED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
+ 'cancel': { method: 'POST', path: '/v1/journals/{id}/cancel', summary: 'Cancel a journal', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
+ 'list-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes', summary: 'Get all notes for a journal', pathParams: ['journalId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
+ 'create-notes': { method: 'POST', path: '/v1/journals/{journalId}/notes', summary: 'Create a note for a journal', pathParams: ['journalId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
+ 'get-notes': { method: 'GET', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Get a specific note for a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
+ 'update-notes': { method: 'PATCH', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Update a note for a journal', pathParams: ['journalId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
+ 'delete-notes': { method: 'DELETE', path: '/v1/journals/{journalId}/notes/{noteId}', summary: 'Delete a note from a journal', pathParams: ['journalId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
+ 'list-files': { method: 'GET', path: '/v1/journals/{parentId}/files', summary: 'List file attachments for a journal', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
25
+ 'create-files': { method: 'POST', path: '/v1/journals/{parentId}/files', summary: 'Upload a file attachment to a journal', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
26
+ 'get-files': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Get a file attachment for a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
+ 'update-files': { method: 'PATCH', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a journal', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
28
+ 'delete-files': { method: 'DELETE', path: '/v1/journals/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
29
+ 'files-download': { method: 'GET', path: '/v1/journals/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a journal', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
30
30
  };
31
31
  export default class Journals extends Command {
32
32
  static description = 'Manage journals';
@@ -146,15 +146,7 @@ export default class Journals extends Command {
146
146
  if (eqIndex === -1)
147
147
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
148
148
  const key = f.slice(0, eqIndex);
149
- let value = f.slice(eqIndex + 1);
150
- // Check for operator prefix (with or without $)
151
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
152
- if (opMatch) {
153
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
154
- }
155
- else {
156
- value = `\$eq:${value}`;
157
- }
149
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
158
150
  const filterKey = `filter.${key}`;
159
151
  if (filterKey in queryParams) {
160
152
  // Multiple filters on same field: collect as array
@@ -187,7 +179,9 @@ export default class Journals extends Command {
187
179
  data = form;
188
180
  }
189
181
  else if (endpoint.hasBody) {
190
- if (flags.file) {
182
+ // For binary-response endpoints, --file is the output path, not a JSON
183
+ // input file — skip the read-as-body branch.
184
+ if (flags.file && !endpoint.isBinaryResponse) {
191
185
  const fsModule = await import('node:fs');
192
186
  try {
193
187
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -209,6 +203,7 @@ export default class Journals extends Command {
209
203
  if (!data)
210
204
  data = {};
211
205
  const body = data;
206
+ const arrayFields = new Set(endpoint.bodyArrayFields);
212
207
  for (const pair of flags.set) {
213
208
  const eqIndex = pair.indexOf('=');
214
209
  if (eqIndex === -1)
@@ -220,6 +215,19 @@ export default class Journals extends Command {
220
215
  value = JSON.parse(value);
221
216
  }
222
217
  catch { /* keep as string */ }
218
+ // For top-level keys typed as array in the API schema, split bare
219
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
220
+ // wrap a lone scalar in a single-element array. Skips when --set
221
+ // already provided a JSON array.
222
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
223
+ if (isArrayField && !Array.isArray(value)) {
224
+ if (typeof value === 'string') {
225
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
226
+ }
227
+ else if (value !== undefined && value !== null) {
228
+ value = [value];
229
+ }
230
+ }
223
231
  // Support nested keys like "address.city"
224
232
  const keys = key.split('.');
225
233
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -270,6 +278,26 @@ export default class Journals extends Command {
270
278
  process.stderr.write('\n');
271
279
  }
272
280
  try {
281
+ // Binary response: download to file
282
+ if (endpoint.isBinaryResponse) {
283
+ if (!flags.file) {
284
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
285
+ }
286
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
287
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
288
+ data,
289
+ contextName: flags.context,
290
+ binary: true,
291
+ });
292
+ const fsModule = await import('node:fs');
293
+ const binData = binResponse.data;
294
+ const buffer = Buffer.isBuffer(binData)
295
+ ? binData
296
+ : Buffer.from(binData);
297
+ fsModule.writeFileSync(flags.file, buffer);
298
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
299
+ return;
300
+ }
273
301
  const response = await apiRequest(endpoint.method, apiPath, {
274
302
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
275
303
  data,
@@ -360,6 +388,7 @@ export default class Journals extends Command {
360
388
  }
361
389
  else if (flags.set && flags.set.length > 0) {
362
390
  const item = {};
391
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
363
392
  for (const pair of flags.set) {
364
393
  const eqIndex = pair.indexOf('=');
365
394
  if (eqIndex === -1)
@@ -370,6 +399,15 @@ export default class Journals extends Command {
370
399
  value = JSON.parse(value);
371
400
  }
372
401
  catch { /* keep as string */ }
402
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
403
+ if (isArrayField && !Array.isArray(value)) {
404
+ if (typeof value === 'string') {
405
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
406
+ }
407
+ else if (value !== undefined && value !== null) {
408
+ value = [value];
409
+ }
410
+ }
373
411
  const keys = key.split('.');
374
412
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
375
413
  this.error(`Invalid --set key: ${key}`);
@@ -519,14 +557,7 @@ export default class Journals extends Command {
519
557
  if (eqIndex === -1)
520
558
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
521
559
  const key = f.slice(0, eqIndex);
522
- let value = f.slice(eqIndex + 1);
523
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
524
- if (opMatch) {
525
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
526
- }
527
- else {
528
- value = `\$eq:${value}`;
529
- }
560
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
530
561
  const filterKey = `filter.${key}`;
531
562
  if (filterKey in baseParams) {
532
563
  const existing = baseParams[filterKey];
@@ -703,6 +734,7 @@ export default class Journals extends Command {
703
734
  }
704
735
  else if (flags.set && flags.set.length > 0) {
705
736
  const item = {};
737
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
706
738
  for (const pair of flags.set) {
707
739
  const eqIndex = pair.indexOf('=');
708
740
  if (eqIndex === -1)
@@ -713,6 +745,15 @@ export default class Journals extends Command {
713
745
  value = JSON.parse(value);
714
746
  }
715
747
  catch { /* keep as string */ }
748
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
749
+ if (isArrayField && !Array.isArray(value)) {
750
+ if (typeof value === 'string') {
751
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
752
+ }
753
+ else if (value !== undefined && value !== null) {
754
+ value = [value];
755
+ }
756
+ }
716
757
  const keys = key.split('.');
717
758
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
718
759
  this.error(`Invalid --set key: ${key}`);
@@ -832,35 +873,62 @@ export default class Journals extends Command {
832
873
  this.log('');
833
874
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
834
875
  }
876
+ parseFilterValue(value) {
877
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
878
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
879
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
880
+ // no operator prefix is present.
881
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
882
+ const m = value.match(opRe);
883
+ if (!m)
884
+ return `\$eq:${value}`;
885
+ const op = m[1];
886
+ const rest = value.slice(m[0].length);
887
+ if (rest === '')
888
+ return `\$${op}`;
889
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
890
+ }
835
891
  filterFields(data, fields) {
836
892
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
837
893
  const pick = (obj, keys) => {
838
- const result = {};
894
+ // Group keys by their first segment so sibling sub-paths
895
+ // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
896
+ // and don't clobber each other on merge.
897
+ const directKeys = new Set();
898
+ const nestedKeys = new Map();
839
899
  for (const key of keys) {
840
900
  if (key.split('.').some(isUnsafeKey))
841
901
  continue;
842
902
  if (key.includes('.')) {
843
- // Handle nested fields like 'customer.name' or 'allocations.salesInvoiceId'
844
903
  const [first, ...rest] = key.split('.');
845
- const val = obj[first];
846
- if (val && typeof val === 'object') {
847
- const restKey = rest.join('.');
848
- if (Array.isArray(val)) {
849
- // Map over array elements (e.g., allocations[].salesInvoiceId)
850
- const existing = Array.isArray(result[first]) ? result[first] : [];
851
- result[first] = val.map((item, i) => ({
852
- ...(existing[i] || {}),
853
- ...pick(item, [restKey]),
854
- }));
855
- }
856
- else {
857
- const nested = pick(val, [restKey]);
858
- result[first] = { ...(result[first] || {}), ...nested };
859
- }
904
+ if (isUnsafeKey(first))
905
+ continue;
906
+ const list = nestedKeys.get(first);
907
+ if (list) {
908
+ list.push(rest.join('.'));
909
+ }
910
+ else {
911
+ nestedKeys.set(first, [rest.join('.')]);
860
912
  }
861
913
  }
862
- else if (key in obj) {
914
+ else {
915
+ directKeys.add(key);
916
+ }
917
+ }
918
+ const result = {};
919
+ for (const key of directKeys) {
920
+ if (key in obj)
863
921
  result[key] = obj[key];
922
+ }
923
+ for (const [first, restKeys] of nestedKeys) {
924
+ const val = obj[first];
925
+ if (!val || typeof val !== 'object')
926
+ continue;
927
+ if (Array.isArray(val)) {
928
+ result[first] = val.map(item => pick(item, restKeys));
929
+ }
930
+ else {
931
+ result[first] = pick(val, restKeys);
864
932
  }
865
933
  }
866
934
  return result;
@@ -32,6 +32,7 @@ export default class Ledgers extends Command {
32
32
  private runExport;
33
33
  private runCamt053Import;
34
34
  private runNestedImport;
35
+ private parseFilterValue;
35
36
  private filterFields;
36
37
  private sanitizeOutput;
37
38
  private getColumns;
@@ -8,22 +8,22 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = ["notes", "closings"];
9
9
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-closings", "create-closings", "get-closings", "update-closings", "closings-close", "closings-reopen"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/ledgers', summary: 'List all ledgers', pathParams: [], hasBody: false, isMultipart: false },
12
- 'create': { method: 'POST', path: '/v1/ledgers', summary: 'Create a new ledger', pathParams: [], hasBody: true, isMultipart: false },
13
- 'get': { method: 'GET', path: '/v1/ledgers/{id}', summary: 'Get a ledger by ID', pathParams: ['id'], hasBody: false, isMultipart: false },
14
- 'update': { method: 'PATCH', path: '/v1/ledgers/{id}', summary: 'Update a ledger', pathParams: ['id'], hasBody: true, isMultipart: false },
15
- 'delete': { method: 'DELETE', path: '/v1/ledgers/{id}', summary: 'Delete a ledger', pathParams: ['id'], hasBody: false, isMultipart: false },
16
- 'list-notes': { method: 'GET', path: '/v1/ledgers/{ledgerId}/notes', summary: 'Get all notes for a ledger', pathParams: ['ledgerId'], hasBody: false, isMultipart: false },
17
- 'create-notes': { method: 'POST', path: '/v1/ledgers/{ledgerId}/notes', summary: 'Create a note for a ledger', pathParams: ['ledgerId'], hasBody: true, isMultipart: false },
18
- 'get-notes': { method: 'GET', path: '/v1/ledgers/{ledgerId}/notes/{noteId}', summary: 'Get a specific note for a ledger', pathParams: ['ledgerId', 'noteId'], hasBody: false, isMultipart: false },
19
- 'update-notes': { method: 'PATCH', path: '/v1/ledgers/{ledgerId}/notes/{noteId}', summary: 'Update a note for a ledger', pathParams: ['ledgerId', 'noteId'], hasBody: true, isMultipart: false },
20
- 'delete-notes': { method: 'DELETE', path: '/v1/ledgers/{ledgerId}/notes/{noteId}', summary: 'Delete a note from a ledger', pathParams: ['ledgerId', 'noteId'], hasBody: false, isMultipart: false },
21
- 'list-closings': { method: 'GET', path: '/v1/ledgers/{ledgerId}/closings', summary: 'List all closings for a ledger, sorted by period end ascending', pathParams: ['ledgerId'], hasBody: false, isMultipart: false },
22
- 'create-closings': { method: 'POST', path: '/v1/ledgers/{ledgerId}/closings', summary: 'Create a new closing in DRAFT status', pathParams: ['ledgerId'], hasBody: true, isMultipart: false },
23
- 'get-closings': { method: 'GET', path: '/v1/ledgers/{ledgerId}/closings/{id}', summary: 'Get a single closing by ID', pathParams: ['ledgerId', 'id'], hasBody: false, isMultipart: false },
24
- 'update-closings': { method: 'PATCH', path: '/v1/ledgers/{ledgerId}/closings/{id}', summary: 'Update closing description', pathParams: ['ledgerId', 'id'], hasBody: true, isMultipart: false },
25
- 'closings-close': { method: 'POST', path: '/v1/ledgers/{ledgerId}/closings/{id}/close', summary: 'Transition closing to CLOSED. Computes netPnl from POSTED journals. Valid from DRAFT or REOPENED.', pathParams: ['ledgerId', 'id'], hasBody: true, isMultipart: false },
26
- 'closings-reopen': { method: 'POST', path: '/v1/ledgers/{ledgerId}/closings/{id}/reopen', summary: 'Transition closing to REOPENED. netPnl snapshot is preserved. Valid from CLOSED only.', pathParams: ['ledgerId', 'id'], hasBody: true, isMultipart: false },
11
+ 'list': { method: 'GET', path: '/v1/ledgers', summary: 'List all ledgers', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/ledgers', summary: 'Create a new ledger', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['notes'] },
13
+ 'get': { method: 'GET', path: '/v1/ledgers/{id}', summary: 'Get a ledger by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/ledgers/{id}', summary: 'Update a ledger', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/ledgers/{id}', summary: 'Delete a ledger', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
+ 'list-notes': { method: 'GET', path: '/v1/ledgers/{ledgerId}/notes', summary: 'Get all notes for a ledger', pathParams: ['ledgerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
+ 'create-notes': { method: 'POST', path: '/v1/ledgers/{ledgerId}/notes', summary: 'Create a note for a ledger', pathParams: ['ledgerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
+ 'get-notes': { method: 'GET', path: '/v1/ledgers/{ledgerId}/notes/{noteId}', summary: 'Get a specific note for a ledger', pathParams: ['ledgerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
+ 'update-notes': { method: 'PATCH', path: '/v1/ledgers/{ledgerId}/notes/{noteId}', summary: 'Update a note for a ledger', pathParams: ['ledgerId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
+ 'delete-notes': { method: 'DELETE', path: '/v1/ledgers/{ledgerId}/notes/{noteId}', summary: 'Delete a note from a ledger', pathParams: ['ledgerId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
+ 'list-closings': { method: 'GET', path: '/v1/ledgers/{ledgerId}/closings', summary: 'List all closings for a ledger, sorted by period end ascending', pathParams: ['ledgerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
22
+ 'create-closings': { method: 'POST', path: '/v1/ledgers/{ledgerId}/closings', summary: 'Create a new closing in DRAFT status', pathParams: ['ledgerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
+ 'get-closings': { method: 'GET', path: '/v1/ledgers/{ledgerId}/closings/{id}', summary: 'Get a single closing by ID', pathParams: ['ledgerId', 'id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
+ 'update-closings': { method: 'PATCH', path: '/v1/ledgers/{ledgerId}/closings/{id}', summary: 'Update closing description', pathParams: ['ledgerId', 'id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
25
+ 'closings-close': { method: 'POST', path: '/v1/ledgers/{ledgerId}/closings/{id}/close', summary: 'Transition closing to CLOSED. Computes netPnl from POSTED journals. Valid from DRAFT or REOPENED.', pathParams: ['ledgerId', 'id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
26
+ 'closings-reopen': { method: 'POST', path: '/v1/ledgers/{ledgerId}/closings/{id}/reopen', summary: 'Transition closing to REOPENED. netPnl snapshot is preserved. Valid from CLOSED only.', pathParams: ['ledgerId', 'id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
27
  };
28
28
  export default class Ledgers extends Command {
29
29
  static description = 'Manage ledgers';
@@ -143,15 +143,7 @@ export default class Ledgers extends Command {
143
143
  if (eqIndex === -1)
144
144
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
145
145
  const key = f.slice(0, eqIndex);
146
- let value = f.slice(eqIndex + 1);
147
- // Check for operator prefix (with or without $)
148
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
149
- if (opMatch) {
150
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
151
- }
152
- else {
153
- value = `\$eq:${value}`;
154
- }
146
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
155
147
  const filterKey = `filter.${key}`;
156
148
  if (filterKey in queryParams) {
157
149
  // Multiple filters on same field: collect as array
@@ -184,7 +176,9 @@ export default class Ledgers extends Command {
184
176
  data = form;
185
177
  }
186
178
  else if (endpoint.hasBody) {
187
- if (flags.file) {
179
+ // For binary-response endpoints, --file is the output path, not a JSON
180
+ // input file — skip the read-as-body branch.
181
+ if (flags.file && !endpoint.isBinaryResponse) {
188
182
  const fsModule = await import('node:fs');
189
183
  try {
190
184
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -206,6 +200,7 @@ export default class Ledgers extends Command {
206
200
  if (!data)
207
201
  data = {};
208
202
  const body = data;
203
+ const arrayFields = new Set(endpoint.bodyArrayFields);
209
204
  for (const pair of flags.set) {
210
205
  const eqIndex = pair.indexOf('=');
211
206
  if (eqIndex === -1)
@@ -217,6 +212,19 @@ export default class Ledgers extends Command {
217
212
  value = JSON.parse(value);
218
213
  }
219
214
  catch { /* keep as string */ }
215
+ // For top-level keys typed as array in the API schema, split bare
216
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
217
+ // wrap a lone scalar in a single-element array. Skips when --set
218
+ // already provided a JSON array.
219
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
220
+ if (isArrayField && !Array.isArray(value)) {
221
+ if (typeof value === 'string') {
222
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
223
+ }
224
+ else if (value !== undefined && value !== null) {
225
+ value = [value];
226
+ }
227
+ }
220
228
  // Support nested keys like "address.city"
221
229
  const keys = key.split('.');
222
230
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -267,6 +275,26 @@ export default class Ledgers extends Command {
267
275
  process.stderr.write('\n');
268
276
  }
269
277
  try {
278
+ // Binary response: download to file
279
+ if (endpoint.isBinaryResponse) {
280
+ if (!flags.file) {
281
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
282
+ }
283
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
284
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
285
+ data,
286
+ contextName: flags.context,
287
+ binary: true,
288
+ });
289
+ const fsModule = await import('node:fs');
290
+ const binData = binResponse.data;
291
+ const buffer = Buffer.isBuffer(binData)
292
+ ? binData
293
+ : Buffer.from(binData);
294
+ fsModule.writeFileSync(flags.file, buffer);
295
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
296
+ return;
297
+ }
270
298
  const response = await apiRequest(endpoint.method, apiPath, {
271
299
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
272
300
  data,
@@ -357,6 +385,7 @@ export default class Ledgers extends Command {
357
385
  }
358
386
  else if (flags.set && flags.set.length > 0) {
359
387
  const item = {};
388
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
360
389
  for (const pair of flags.set) {
361
390
  const eqIndex = pair.indexOf('=');
362
391
  if (eqIndex === -1)
@@ -367,6 +396,15 @@ export default class Ledgers extends Command {
367
396
  value = JSON.parse(value);
368
397
  }
369
398
  catch { /* keep as string */ }
399
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
400
+ if (isArrayField && !Array.isArray(value)) {
401
+ if (typeof value === 'string') {
402
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
403
+ }
404
+ else if (value !== undefined && value !== null) {
405
+ value = [value];
406
+ }
407
+ }
370
408
  const keys = key.split('.');
371
409
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
372
410
  this.error(`Invalid --set key: ${key}`);
@@ -516,14 +554,7 @@ export default class Ledgers extends Command {
516
554
  if (eqIndex === -1)
517
555
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
518
556
  const key = f.slice(0, eqIndex);
519
- let value = f.slice(eqIndex + 1);
520
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
521
- if (opMatch) {
522
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
523
- }
524
- else {
525
- value = `\$eq:${value}`;
526
- }
557
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
527
558
  const filterKey = `filter.${key}`;
528
559
  if (filterKey in baseParams) {
529
560
  const existing = baseParams[filterKey];
@@ -700,6 +731,7 @@ export default class Ledgers extends Command {
700
731
  }
701
732
  else if (flags.set && flags.set.length > 0) {
702
733
  const item = {};
734
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
703
735
  for (const pair of flags.set) {
704
736
  const eqIndex = pair.indexOf('=');
705
737
  if (eqIndex === -1)
@@ -710,6 +742,15 @@ export default class Ledgers extends Command {
710
742
  value = JSON.parse(value);
711
743
  }
712
744
  catch { /* keep as string */ }
745
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
746
+ if (isArrayField && !Array.isArray(value)) {
747
+ if (typeof value === 'string') {
748
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
749
+ }
750
+ else if (value !== undefined && value !== null) {
751
+ value = [value];
752
+ }
753
+ }
713
754
  const keys = key.split('.');
714
755
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
715
756
  this.error(`Invalid --set key: ${key}`);
@@ -829,35 +870,62 @@ export default class Ledgers extends Command {
829
870
  this.log('');
830
871
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
831
872
  }
873
+ parseFilterValue(value) {
874
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
875
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
876
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
877
+ // no operator prefix is present.
878
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
879
+ const m = value.match(opRe);
880
+ if (!m)
881
+ return `\$eq:${value}`;
882
+ const op = m[1];
883
+ const rest = value.slice(m[0].length);
884
+ if (rest === '')
885
+ return `\$${op}`;
886
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
887
+ }
832
888
  filterFields(data, fields) {
833
889
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
834
890
  const pick = (obj, keys) => {
835
- const result = {};
891
+ // Group keys by their first segment so sibling sub-paths
892
+ // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
893
+ // and don't clobber each other on merge.
894
+ const directKeys = new Set();
895
+ const nestedKeys = new Map();
836
896
  for (const key of keys) {
837
897
  if (key.split('.').some(isUnsafeKey))
838
898
  continue;
839
899
  if (key.includes('.')) {
840
- // Handle nested fields like 'customer.name' or 'allocations.salesInvoiceId'
841
900
  const [first, ...rest] = key.split('.');
842
- const val = obj[first];
843
- if (val && typeof val === 'object') {
844
- const restKey = rest.join('.');
845
- if (Array.isArray(val)) {
846
- // Map over array elements (e.g., allocations[].salesInvoiceId)
847
- const existing = Array.isArray(result[first]) ? result[first] : [];
848
- result[first] = val.map((item, i) => ({
849
- ...(existing[i] || {}),
850
- ...pick(item, [restKey]),
851
- }));
852
- }
853
- else {
854
- const nested = pick(val, [restKey]);
855
- result[first] = { ...(result[first] || {}), ...nested };
856
- }
901
+ if (isUnsafeKey(first))
902
+ continue;
903
+ const list = nestedKeys.get(first);
904
+ if (list) {
905
+ list.push(rest.join('.'));
906
+ }
907
+ else {
908
+ nestedKeys.set(first, [rest.join('.')]);
857
909
  }
858
910
  }
859
- else if (key in obj) {
911
+ else {
912
+ directKeys.add(key);
913
+ }
914
+ }
915
+ const result = {};
916
+ for (const key of directKeys) {
917
+ if (key in obj)
860
918
  result[key] = obj[key];
919
+ }
920
+ for (const [first, restKeys] of nestedKeys) {
921
+ const val = obj[first];
922
+ if (!val || typeof val !== 'object')
923
+ continue;
924
+ if (Array.isArray(val)) {
925
+ result[first] = val.map(item => pick(item, restKeys));
926
+ }
927
+ else {
928
+ result[first] = pick(val, restKeys);
861
929
  }
862
930
  }
863
931
  return result;
@@ -0,0 +1,40 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class OfficeLocations extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'sort-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ 'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ };
28
+ run(): Promise<void>;
29
+ private runImport;
30
+ private runExport;
31
+ private runCamt053Import;
32
+ private runNestedImport;
33
+ private parseFilterValue;
34
+ private filterFields;
35
+ private sanitizeOutput;
36
+ private getColumns;
37
+ private getValue;
38
+ private printTable;
39
+ private printCsv;
40
+ }