@aderis-erp/cli 1.0.3 → 1.0.7

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 (111) hide show
  1. package/README.md +6 -0
  2. package/dist/commands/access-groups.d.ts +1 -0
  3. package/dist/commands/access-groups.js +84 -28
  4. package/dist/commands/{async-endpoints-internal.d.ts → account-transaction-allocations.d.ts} +3 -2
  5. package/dist/commands/{async-endpoints-internal.js → account-transaction-allocations.js} +93 -33
  6. package/dist/commands/account-transactions.d.ts +1 -0
  7. package/dist/commands/account-transactions.js +96 -40
  8. package/dist/commands/accounts.d.ts +1 -0
  9. package/dist/commands/accounts.js +89 -33
  10. package/dist/commands/agent-runs.d.ts +1 -0
  11. package/dist/commands/agent-runs.js +85 -27
  12. package/dist/commands/audit-log.d.ts +1 -0
  13. package/dist/commands/audit-log.js +82 -26
  14. package/dist/commands/auth/login.js +1 -2
  15. package/dist/commands/auth/tenant.d.ts +1 -1
  16. package/dist/commands/auth/tenant.js +11 -11
  17. package/dist/commands/auth.js +1 -1
  18. package/dist/commands/bank-accounts.d.ts +1 -0
  19. package/dist/commands/bank-accounts.js +96 -40
  20. package/dist/commands/bank-integrations.d.ts +1 -0
  21. package/dist/commands/bank-integrations.js +97 -41
  22. package/dist/commands/business-partners---contacts.d.ts +1 -0
  23. package/dist/commands/business-partners---contacts.js +89 -33
  24. package/dist/commands/business-partners.d.ts +1 -0
  25. package/dist/commands/business-partners.js +89 -33
  26. package/dist/commands/credit-card-accounts.d.ts +1 -0
  27. package/dist/commands/credit-card-accounts.js +94 -38
  28. package/dist/commands/documents.d.ts +1 -0
  29. package/dist/commands/documents.js +85 -29
  30. package/dist/commands/dunning-configs.d.ts +1 -0
  31. package/dist/commands/dunning-configs.js +84 -28
  32. package/dist/commands/dunning-reminders.d.ts +1 -0
  33. package/dist/commands/dunning-reminders.js +87 -31
  34. package/dist/commands/dunning-runs.d.ts +1 -0
  35. package/dist/commands/dunning-runs.js +80 -24
  36. package/dist/commands/employees.d.ts +1 -0
  37. package/dist/commands/employees.js +111 -55
  38. package/dist/commands/event-processor-mappings.d.ts +1 -0
  39. package/dist/commands/event-processor-mappings.js +84 -28
  40. package/dist/commands/files-internal.d.ts +1 -0
  41. package/dist/commands/files-internal.js +88 -32
  42. package/dist/commands/health.d.ts +1 -0
  43. package/dist/commands/health.js +84 -28
  44. package/dist/commands/inbox.d.ts +1 -0
  45. package/dist/commands/inbox.js +84 -28
  46. package/dist/commands/integrations.d.ts +1 -0
  47. package/dist/commands/integrations.js +82 -26
  48. package/dist/commands/item-categories.d.ts +1 -0
  49. package/dist/commands/item-categories.js +84 -28
  50. package/dist/commands/items.d.ts +1 -0
  51. package/dist/commands/items.js +84 -28
  52. package/dist/commands/journal-lines.d.ts +1 -0
  53. package/dist/commands/journal-lines.js +80 -24
  54. package/dist/commands/journals.d.ts +1 -0
  55. package/dist/commands/journals.js +98 -42
  56. package/dist/commands/ledgers.d.ts +1 -0
  57. package/dist/commands/ledgers.js +95 -39
  58. package/dist/commands/office-locations.d.ts +1 -0
  59. package/dist/commands/office-locations.js +84 -28
  60. package/dist/commands/payment-method-ledger-mappings.d.ts +1 -0
  61. package/dist/commands/payment-method-ledger-mappings.js +84 -28
  62. package/dist/commands/payment-methods.d.ts +1 -0
  63. package/dist/commands/payment-methods.js +84 -28
  64. package/dist/commands/payment-runs.d.ts +1 -0
  65. package/dist/commands/payment-runs.js +97 -41
  66. package/dist/commands/payment-terms.d.ts +1 -0
  67. package/dist/commands/payment-terms.js +85 -29
  68. package/dist/commands/price-lists.d.ts +1 -0
  69. package/dist/commands/price-lists.js +89 -33
  70. package/dist/commands/processor-mappings.d.ts +1 -0
  71. package/dist/commands/processor-mappings.js +83 -27
  72. package/dist/commands/processors.d.ts +1 -0
  73. package/dist/commands/processors.js +88 -32
  74. package/dist/commands/reconciliation.d.ts +1 -0
  75. package/dist/commands/reconciliation.js +85 -29
  76. package/dist/commands/reports.d.ts +1 -0
  77. package/dist/commands/reports.js +85 -26
  78. package/dist/commands/salary-types.d.ts +1 -0
  79. package/dist/commands/salary-types.js +84 -28
  80. package/dist/commands/sales-invoices.d.ts +1 -0
  81. package/dist/commands/sales-invoices.js +109 -53
  82. package/dist/commands/sales-orders.d.ts +1 -0
  83. package/dist/commands/sales-orders.js +107 -51
  84. package/dist/commands/tags.d.ts +1 -0
  85. package/dist/commands/tags.js +84 -28
  86. package/dist/commands/tax-codes.d.ts +1 -0
  87. package/dist/commands/tax-codes.js +87 -31
  88. package/dist/commands/tax-matrix.d.ts +1 -0
  89. package/dist/commands/tax-matrix.js +84 -28
  90. package/dist/commands/tenants.d.ts +1 -0
  91. package/dist/commands/tenants.js +84 -28
  92. package/dist/commands/text-templates.d.ts +1 -0
  93. package/dist/commands/text-templates.js +84 -28
  94. package/dist/commands/tools.d.ts +1 -0
  95. package/dist/commands/tools.js +81 -25
  96. package/dist/commands/vat-declaration.d.ts +41 -0
  97. package/dist/commands/vat-declaration.js +1079 -0
  98. package/dist/commands/vendor-invoices.d.ts +1 -0
  99. package/dist/commands/vendor-invoices.js +113 -57
  100. package/dist/commands/webhooks.d.ts +1 -0
  101. package/dist/commands/webhooks.js +86 -30
  102. package/dist/commands/workflows.d.ts +1 -0
  103. package/dist/commands/workflows.js +82 -26
  104. package/dist/lib/api-client.d.ts +6 -1
  105. package/dist/lib/api-client.js +23 -5
  106. package/dist/lib/auth.js +18 -0
  107. package/dist/lib/config.js +1 -1
  108. package/oclif.manifest.json +229 -52
  109. package/package.json +15 -13
  110. package/dist/index 2.js +0 -1
  111. package/dist/index.d 2.ts +0 -1
@@ -8,7 +8,7 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/journal-lines', summary: 'List all journal lines (global view across all journals)', pathParams: [], hasBody: false, isMultipart: false },
11
+ 'list': { method: 'GET', path: '/v1/journal-lines', summary: 'List all journal lines (global view across all journals)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
12
  };
13
13
  export default class JournalLines extends Command {
14
14
  static description = 'Manage journal lines';
@@ -73,11 +73,6 @@ export default class JournalLines extends Command {
73
73
  const endpoint = ENDPOINTS[args.action];
74
74
  if (!endpoint)
75
75
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
76
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
77
- if (!context)
78
- this.error('No context configured. Run: aderis auth login --new');
79
- if (!context.tokens)
80
- this.error('Not logged in. Run: aderis auth login');
81
76
  let apiPath = endpoint.path;
82
77
  for (let i = 0; i < endpoint.pathParams.length; i++) {
83
78
  const param = endpoint.pathParams[i];
@@ -96,6 +91,11 @@ export default class JournalLines extends Command {
96
91
  }
97
92
  apiPath = apiPath.replace(`{${param}}`, String(value));
98
93
  }
94
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
95
+ if (!context)
96
+ this.error('No context configured. Run: aderis auth login --new');
97
+ if (!context.tokens)
98
+ this.error('Not logged in. Run: aderis auth login');
99
99
  const queryParams = {};
100
100
  if (flags.page)
101
101
  queryParams.page = flags.page;
@@ -122,15 +122,7 @@ export default class JournalLines extends Command {
122
122
  if (eqIndex === -1)
123
123
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
124
124
  const key = f.slice(0, eqIndex);
125
- let value = f.slice(eqIndex + 1);
126
- // Check for operator prefix (with or without $)
127
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
128
- if (opMatch) {
129
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
130
- }
131
- else {
132
- value = `\$eq:${value}`;
133
- }
125
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
134
126
  const filterKey = `filter.${key}`;
135
127
  if (filterKey in queryParams) {
136
128
  // Multiple filters on same field: collect as array
@@ -163,7 +155,9 @@ export default class JournalLines extends Command {
163
155
  data = form;
164
156
  }
165
157
  else if (endpoint.hasBody) {
166
- if (flags.file) {
158
+ // For binary-response endpoints, --file is the output path, not a JSON
159
+ // input file — skip the read-as-body branch.
160
+ if (flags.file && !endpoint.isBinaryResponse) {
167
161
  const fsModule = await import('node:fs');
168
162
  try {
169
163
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -185,6 +179,7 @@ export default class JournalLines extends Command {
185
179
  if (!data)
186
180
  data = {};
187
181
  const body = data;
182
+ const arrayFields = new Set(endpoint.bodyArrayFields);
188
183
  for (const pair of flags.set) {
189
184
  const eqIndex = pair.indexOf('=');
190
185
  if (eqIndex === -1)
@@ -196,6 +191,19 @@ export default class JournalLines extends Command {
196
191
  value = JSON.parse(value);
197
192
  }
198
193
  catch { /* keep as string */ }
194
+ // For top-level keys typed as array in the API schema, split bare
195
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
196
+ // wrap a lone scalar in a single-element array. Skips when --set
197
+ // already provided a JSON array.
198
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
199
+ if (isArrayField && !Array.isArray(value)) {
200
+ if (typeof value === 'string') {
201
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
202
+ }
203
+ else if (value !== undefined && value !== null) {
204
+ value = [value];
205
+ }
206
+ }
199
207
  // Support nested keys like "address.city"
200
208
  const keys = key.split('.');
201
209
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -246,6 +254,26 @@ export default class JournalLines extends Command {
246
254
  process.stderr.write('\n');
247
255
  }
248
256
  try {
257
+ // Binary response: download to file
258
+ if (endpoint.isBinaryResponse) {
259
+ if (!flags.file) {
260
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
261
+ }
262
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
263
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
264
+ data,
265
+ contextName: flags.context,
266
+ binary: true,
267
+ });
268
+ const fsModule = await import('node:fs');
269
+ const binData = binResponse.data;
270
+ const buffer = Buffer.isBuffer(binData)
271
+ ? binData
272
+ : Buffer.from(binData);
273
+ fsModule.writeFileSync(flags.file, buffer);
274
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
275
+ return;
276
+ }
249
277
  const response = await apiRequest(endpoint.method, apiPath, {
250
278
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
251
279
  data,
@@ -336,6 +364,7 @@ export default class JournalLines extends Command {
336
364
  }
337
365
  else if (flags.set && flags.set.length > 0) {
338
366
  const item = {};
367
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
339
368
  for (const pair of flags.set) {
340
369
  const eqIndex = pair.indexOf('=');
341
370
  if (eqIndex === -1)
@@ -346,6 +375,15 @@ export default class JournalLines extends Command {
346
375
  value = JSON.parse(value);
347
376
  }
348
377
  catch { /* keep as string */ }
378
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
379
+ if (isArrayField && !Array.isArray(value)) {
380
+ if (typeof value === 'string') {
381
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
382
+ }
383
+ else if (value !== undefined && value !== null) {
384
+ value = [value];
385
+ }
386
+ }
349
387
  const keys = key.split('.');
350
388
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
351
389
  this.error(`Invalid --set key: ${key}`);
@@ -495,14 +533,7 @@ export default class JournalLines extends Command {
495
533
  if (eqIndex === -1)
496
534
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
497
535
  const key = f.slice(0, eqIndex);
498
- let value = f.slice(eqIndex + 1);
499
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
500
- if (opMatch) {
501
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
502
- }
503
- else {
504
- value = `\$eq:${value}`;
505
- }
536
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
506
537
  const filterKey = `filter.${key}`;
507
538
  if (filterKey in baseParams) {
508
539
  const existing = baseParams[filterKey];
@@ -679,6 +710,7 @@ export default class JournalLines extends Command {
679
710
  }
680
711
  else if (flags.set && flags.set.length > 0) {
681
712
  const item = {};
713
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
682
714
  for (const pair of flags.set) {
683
715
  const eqIndex = pair.indexOf('=');
684
716
  if (eqIndex === -1)
@@ -689,6 +721,15 @@ export default class JournalLines extends Command {
689
721
  value = JSON.parse(value);
690
722
  }
691
723
  catch { /* keep as string */ }
724
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
725
+ if (isArrayField && !Array.isArray(value)) {
726
+ if (typeof value === 'string') {
727
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
728
+ }
729
+ else if (value !== undefined && value !== null) {
730
+ value = [value];
731
+ }
732
+ }
692
733
  const keys = key.split('.');
693
734
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
694
735
  this.error(`Invalid --set key: ${key}`);
@@ -808,6 +849,21 @@ export default class JournalLines extends Command {
808
849
  this.log('');
809
850
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
810
851
  }
852
+ parseFilterValue(value) {
853
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
854
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
855
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
856
+ // no operator prefix is present.
857
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
858
+ const m = value.match(opRe);
859
+ if (!m)
860
+ return `\$eq:${value}`;
861
+ const op = m[1];
862
+ const rest = value.slice(m[0].length);
863
+ if (rest === '')
864
+ return `\$${op}`;
865
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
866
+ }
811
867
  filterFields(data, fields) {
812
868
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
813
869
  const pick = (obj, keys) => {
@@ -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';
@@ -97,11 +97,6 @@ export default class Journals extends Command {
97
97
  const endpoint = ENDPOINTS[args.action];
98
98
  if (!endpoint)
99
99
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
100
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
101
- if (!context)
102
- this.error('No context configured. Run: aderis auth login --new');
103
- if (!context.tokens)
104
- this.error('Not logged in. Run: aderis auth login');
105
100
  let apiPath = endpoint.path;
106
101
  for (let i = 0; i < endpoint.pathParams.length; i++) {
107
102
  const param = endpoint.pathParams[i];
@@ -120,6 +115,11 @@ export default class Journals extends Command {
120
115
  }
121
116
  apiPath = apiPath.replace(`{${param}}`, String(value));
122
117
  }
118
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
119
+ if (!context)
120
+ this.error('No context configured. Run: aderis auth login --new');
121
+ if (!context.tokens)
122
+ this.error('Not logged in. Run: aderis auth login');
123
123
  const queryParams = {};
124
124
  if (flags.page)
125
125
  queryParams.page = flags.page;
@@ -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,6 +873,21 @@ 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) => {
@@ -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;