@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,15 +8,15 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["create", "download-all", "download", "references", "update", "move", "renditions", "retention", "hold"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'create': { method: 'POST', path: '/v1/files', summary: 'Upload a file (streaming, memory-efficient)', pathParams: [], hasBody: false, isMultipart: true },
12
- 'download-all': { method: 'GET', path: '/v1/files/download/*', summary: 'Download file directly by storage key', pathParams: [], hasBody: false, isMultipart: false },
13
- 'download': { method: 'GET', path: '/v1/files/{id}/download', summary: 'Get a signed download URL for a file', pathParams: ['id'], hasBody: false, isMultipart: false },
14
- 'references': { method: 'POST', path: '/v1/files/references', summary: 'Register a file reference to an existing storage object', pathParams: [], hasBody: true, isMultipart: false },
15
- 'update': { method: 'PATCH', path: '/v1/files/{fileId}', summary: 'Update file metadata (type, parent, sortOrder)', pathParams: ['fileId'], hasBody: true, isMultipart: false },
16
- 'move': { method: 'POST', path: '/v1/files/{fileId}/move', summary: 'Move file between storage zones', pathParams: ['fileId'], hasBody: true, isMultipart: false },
17
- 'renditions': { method: 'POST', path: '/v1/files/{fileId}/renditions', summary: 'Register a business-significant rendition', pathParams: ['fileId'], hasBody: true, isMultipart: false },
18
- 'retention': { method: 'POST', path: '/v1/files/{fileId}/retention', summary: 'Set or override retention period on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false },
19
- 'hold': { method: 'POST', path: '/v1/files/{fileId}/hold', summary: 'Set or release legal hold on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false },
11
+ 'create': { method: 'POST', path: '/v1/files', summary: 'Upload a file (streaming, memory-efficient)', pathParams: [], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'download-all': { method: 'GET', path: '/v1/files/download/*', summary: 'Download file directly by storage key', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'download': { method: 'GET', path: '/v1/files/{id}/download', summary: 'Get a signed download URL for a file', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'references': { method: 'POST', path: '/v1/files/references', summary: 'Register a file reference to an existing storage object', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
+ 'update': { method: 'PATCH', path: '/v1/files/{fileId}', summary: 'Update file metadata (type, parent, sortOrder)', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
+ 'move': { method: 'POST', path: '/v1/files/{fileId}/move', summary: 'Move file between storage zones', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
+ 'renditions': { method: 'POST', path: '/v1/files/{fileId}/renditions', summary: 'Register a business-significant rendition', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
+ 'retention': { method: 'POST', path: '/v1/files/{fileId}/retention', summary: 'Set or override retention period on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
+ 'hold': { method: 'POST', path: '/v1/files/{fileId}/hold', summary: 'Set or release legal hold on a file', pathParams: ['fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
20
  };
21
21
  export default class FilesInternal extends Command {
22
22
  static description = 'Manage files internal';
@@ -82,11 +82,6 @@ export default class FilesInternal extends Command {
82
82
  const endpoint = ENDPOINTS[args.action];
83
83
  if (!endpoint)
84
84
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
85
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
86
- if (!context)
87
- this.error('No context configured. Run: aderis auth login --new');
88
- if (!context.tokens)
89
- this.error('Not logged in. Run: aderis auth login');
90
85
  let apiPath = endpoint.path;
91
86
  for (let i = 0; i < endpoint.pathParams.length; i++) {
92
87
  const param = endpoint.pathParams[i];
@@ -105,6 +100,11 @@ export default class FilesInternal extends Command {
105
100
  }
106
101
  apiPath = apiPath.replace(`{${param}}`, String(value));
107
102
  }
103
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
104
+ if (!context)
105
+ this.error('No context configured. Run: aderis auth login --new');
106
+ if (!context.tokens)
107
+ this.error('Not logged in. Run: aderis auth login');
108
108
  const queryParams = {};
109
109
  if (flags.page)
110
110
  queryParams.page = flags.page;
@@ -131,15 +131,7 @@ export default class FilesInternal extends Command {
131
131
  if (eqIndex === -1)
132
132
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
133
133
  const key = f.slice(0, eqIndex);
134
- let value = f.slice(eqIndex + 1);
135
- // Check for operator prefix (with or without $)
136
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
137
- if (opMatch) {
138
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
139
- }
140
- else {
141
- value = `\$eq:${value}`;
142
- }
134
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
143
135
  const filterKey = `filter.${key}`;
144
136
  if (filterKey in queryParams) {
145
137
  // Multiple filters on same field: collect as array
@@ -172,7 +164,9 @@ export default class FilesInternal extends Command {
172
164
  data = form;
173
165
  }
174
166
  else if (endpoint.hasBody) {
175
- if (flags.file) {
167
+ // For binary-response endpoints, --file is the output path, not a JSON
168
+ // input file — skip the read-as-body branch.
169
+ if (flags.file && !endpoint.isBinaryResponse) {
176
170
  const fsModule = await import('node:fs');
177
171
  try {
178
172
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -194,6 +188,7 @@ export default class FilesInternal extends Command {
194
188
  if (!data)
195
189
  data = {};
196
190
  const body = data;
191
+ const arrayFields = new Set(endpoint.bodyArrayFields);
197
192
  for (const pair of flags.set) {
198
193
  const eqIndex = pair.indexOf('=');
199
194
  if (eqIndex === -1)
@@ -205,6 +200,19 @@ export default class FilesInternal extends Command {
205
200
  value = JSON.parse(value);
206
201
  }
207
202
  catch { /* keep as string */ }
203
+ // For top-level keys typed as array in the API schema, split bare
204
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
205
+ // wrap a lone scalar in a single-element array. Skips when --set
206
+ // already provided a JSON array.
207
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
208
+ if (isArrayField && !Array.isArray(value)) {
209
+ if (typeof value === 'string') {
210
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
211
+ }
212
+ else if (value !== undefined && value !== null) {
213
+ value = [value];
214
+ }
215
+ }
208
216
  // Support nested keys like "address.city"
209
217
  const keys = key.split('.');
210
218
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -255,6 +263,26 @@ export default class FilesInternal extends Command {
255
263
  process.stderr.write('\n');
256
264
  }
257
265
  try {
266
+ // Binary response: download to file
267
+ if (endpoint.isBinaryResponse) {
268
+ if (!flags.file) {
269
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
270
+ }
271
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
272
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
273
+ data,
274
+ contextName: flags.context,
275
+ binary: true,
276
+ });
277
+ const fsModule = await import('node:fs');
278
+ const binData = binResponse.data;
279
+ const buffer = Buffer.isBuffer(binData)
280
+ ? binData
281
+ : Buffer.from(binData);
282
+ fsModule.writeFileSync(flags.file, buffer);
283
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
284
+ return;
285
+ }
258
286
  const response = await apiRequest(endpoint.method, apiPath, {
259
287
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
260
288
  data,
@@ -345,6 +373,7 @@ export default class FilesInternal extends Command {
345
373
  }
346
374
  else if (flags.set && flags.set.length > 0) {
347
375
  const item = {};
376
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
348
377
  for (const pair of flags.set) {
349
378
  const eqIndex = pair.indexOf('=');
350
379
  if (eqIndex === -1)
@@ -355,6 +384,15 @@ export default class FilesInternal extends Command {
355
384
  value = JSON.parse(value);
356
385
  }
357
386
  catch { /* keep as string */ }
387
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
388
+ if (isArrayField && !Array.isArray(value)) {
389
+ if (typeof value === 'string') {
390
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
391
+ }
392
+ else if (value !== undefined && value !== null) {
393
+ value = [value];
394
+ }
395
+ }
358
396
  const keys = key.split('.');
359
397
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
360
398
  this.error(`Invalid --set key: ${key}`);
@@ -504,14 +542,7 @@ export default class FilesInternal extends Command {
504
542
  if (eqIndex === -1)
505
543
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
506
544
  const key = f.slice(0, eqIndex);
507
- let value = f.slice(eqIndex + 1);
508
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
509
- if (opMatch) {
510
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
511
- }
512
- else {
513
- value = `\$eq:${value}`;
514
- }
545
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
515
546
  const filterKey = `filter.${key}`;
516
547
  if (filterKey in baseParams) {
517
548
  const existing = baseParams[filterKey];
@@ -688,6 +719,7 @@ export default class FilesInternal extends Command {
688
719
  }
689
720
  else if (flags.set && flags.set.length > 0) {
690
721
  const item = {};
722
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
691
723
  for (const pair of flags.set) {
692
724
  const eqIndex = pair.indexOf('=');
693
725
  if (eqIndex === -1)
@@ -698,6 +730,15 @@ export default class FilesInternal extends Command {
698
730
  value = JSON.parse(value);
699
731
  }
700
732
  catch { /* keep as string */ }
733
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
734
+ if (isArrayField && !Array.isArray(value)) {
735
+ if (typeof value === 'string') {
736
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
737
+ }
738
+ else if (value !== undefined && value !== null) {
739
+ value = [value];
740
+ }
741
+ }
701
742
  const keys = key.split('.');
702
743
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
703
744
  this.error(`Invalid --set key: ${key}`);
@@ -817,6 +858,21 @@ export default class FilesInternal extends Command {
817
858
  this.log('');
818
859
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
819
860
  }
861
+ parseFilterValue(value) {
862
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
863
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
864
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
865
+ // no operator prefix is present.
866
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
867
+ const m = value.match(opRe);
868
+ if (!m)
869
+ return `\$eq:${value}`;
870
+ const op = m[1];
871
+ const rest = value.slice(m[0].length);
872
+ if (rest === '')
873
+ return `\$${op}`;
874
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
875
+ }
820
876
  filterFields(data, fields) {
821
877
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
822
878
  const pick = (obj, keys) => {
@@ -30,6 +30,7 @@ export default class Health extends Command {
30
30
  private runExport;
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
+ private parseFilterValue;
33
34
  private filterFields;
34
35
  private sanitizeOutput;
35
36
  private getColumns;
@@ -8,11 +8,11 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["get", "get-all", "ready", "get-3", "v1"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'get': { method: 'GET', path: '/', summary: 'GET /', pathParams: [], hasBody: false, isMultipart: false },
12
- 'get-all': { method: 'GET', path: '/health', summary: 'GET /health', pathParams: [], hasBody: false, isMultipart: false },
13
- 'ready': { method: 'GET', path: '/ready', summary: 'GET /ready', pathParams: [], hasBody: false, isMultipart: false },
14
- 'get-3': { method: 'GET', path: '/v1/status', summary: 'GET /v1/status', pathParams: [], hasBody: false, isMultipart: false },
15
- 'v1': { method: 'GET', path: '/v1', summary: 'GET /v1', pathParams: [], hasBody: false, isMultipart: false },
11
+ 'get': { method: 'GET', path: '/', summary: 'GET /', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'get-all': { method: 'GET', path: '/health', summary: 'GET /health', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'ready': { method: 'GET', path: '/ready', summary: 'GET /ready', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'get-3': { method: 'GET', path: '/v1/status', summary: 'GET /v1/status', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
+ 'v1': { method: 'GET', path: '/v1', summary: 'GET /v1', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class Health extends Command {
18
18
  static description = 'Manage health';
@@ -76,11 +76,6 @@ export default class Health extends Command {
76
76
  const endpoint = ENDPOINTS[args.action];
77
77
  if (!endpoint)
78
78
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
79
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
80
- if (!context)
81
- this.error('No context configured. Run: aderis auth login --new');
82
- if (!context.tokens)
83
- this.error('Not logged in. Run: aderis auth login');
84
79
  let apiPath = endpoint.path;
85
80
  for (let i = 0; i < endpoint.pathParams.length; i++) {
86
81
  const param = endpoint.pathParams[i];
@@ -99,6 +94,11 @@ export default class Health extends Command {
99
94
  }
100
95
  apiPath = apiPath.replace(`{${param}}`, String(value));
101
96
  }
97
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
98
+ if (!context)
99
+ this.error('No context configured. Run: aderis auth login --new');
100
+ if (!context.tokens)
101
+ this.error('Not logged in. Run: aderis auth login');
102
102
  const queryParams = {};
103
103
  if (flags.page)
104
104
  queryParams.page = flags.page;
@@ -125,15 +125,7 @@ export default class Health extends Command {
125
125
  if (eqIndex === -1)
126
126
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
127
127
  const key = f.slice(0, eqIndex);
128
- let value = f.slice(eqIndex + 1);
129
- // Check for operator prefix (with or without $)
130
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
131
- if (opMatch) {
132
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
133
- }
134
- else {
135
- value = `\$eq:${value}`;
136
- }
128
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
137
129
  const filterKey = `filter.${key}`;
138
130
  if (filterKey in queryParams) {
139
131
  // Multiple filters on same field: collect as array
@@ -166,7 +158,9 @@ export default class Health extends Command {
166
158
  data = form;
167
159
  }
168
160
  else if (endpoint.hasBody) {
169
- if (flags.file) {
161
+ // For binary-response endpoints, --file is the output path, not a JSON
162
+ // input file — skip the read-as-body branch.
163
+ if (flags.file && !endpoint.isBinaryResponse) {
170
164
  const fsModule = await import('node:fs');
171
165
  try {
172
166
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -188,6 +182,7 @@ export default class Health extends Command {
188
182
  if (!data)
189
183
  data = {};
190
184
  const body = data;
185
+ const arrayFields = new Set(endpoint.bodyArrayFields);
191
186
  for (const pair of flags.set) {
192
187
  const eqIndex = pair.indexOf('=');
193
188
  if (eqIndex === -1)
@@ -199,6 +194,19 @@ export default class Health extends Command {
199
194
  value = JSON.parse(value);
200
195
  }
201
196
  catch { /* keep as string */ }
197
+ // For top-level keys typed as array in the API schema, split bare
198
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
199
+ // wrap a lone scalar in a single-element array. Skips when --set
200
+ // already provided a JSON array.
201
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
202
+ if (isArrayField && !Array.isArray(value)) {
203
+ if (typeof value === 'string') {
204
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
205
+ }
206
+ else if (value !== undefined && value !== null) {
207
+ value = [value];
208
+ }
209
+ }
202
210
  // Support nested keys like "address.city"
203
211
  const keys = key.split('.');
204
212
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -249,6 +257,26 @@ export default class Health extends Command {
249
257
  process.stderr.write('\n');
250
258
  }
251
259
  try {
260
+ // Binary response: download to file
261
+ if (endpoint.isBinaryResponse) {
262
+ if (!flags.file) {
263
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
264
+ }
265
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
266
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
267
+ data,
268
+ contextName: flags.context,
269
+ binary: true,
270
+ });
271
+ const fsModule = await import('node:fs');
272
+ const binData = binResponse.data;
273
+ const buffer = Buffer.isBuffer(binData)
274
+ ? binData
275
+ : Buffer.from(binData);
276
+ fsModule.writeFileSync(flags.file, buffer);
277
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
278
+ return;
279
+ }
252
280
  const response = await apiRequest(endpoint.method, apiPath, {
253
281
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
254
282
  data,
@@ -339,6 +367,7 @@ export default class Health extends Command {
339
367
  }
340
368
  else if (flags.set && flags.set.length > 0) {
341
369
  const item = {};
370
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
342
371
  for (const pair of flags.set) {
343
372
  const eqIndex = pair.indexOf('=');
344
373
  if (eqIndex === -1)
@@ -349,6 +378,15 @@ export default class Health extends Command {
349
378
  value = JSON.parse(value);
350
379
  }
351
380
  catch { /* keep as string */ }
381
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
382
+ if (isArrayField && !Array.isArray(value)) {
383
+ if (typeof value === 'string') {
384
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
385
+ }
386
+ else if (value !== undefined && value !== null) {
387
+ value = [value];
388
+ }
389
+ }
352
390
  const keys = key.split('.');
353
391
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
354
392
  this.error(`Invalid --set key: ${key}`);
@@ -498,14 +536,7 @@ export default class Health extends Command {
498
536
  if (eqIndex === -1)
499
537
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
500
538
  const key = f.slice(0, eqIndex);
501
- let value = f.slice(eqIndex + 1);
502
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
503
- if (opMatch) {
504
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
505
- }
506
- else {
507
- value = `\$eq:${value}`;
508
- }
539
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
509
540
  const filterKey = `filter.${key}`;
510
541
  if (filterKey in baseParams) {
511
542
  const existing = baseParams[filterKey];
@@ -682,6 +713,7 @@ export default class Health extends Command {
682
713
  }
683
714
  else if (flags.set && flags.set.length > 0) {
684
715
  const item = {};
716
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
685
717
  for (const pair of flags.set) {
686
718
  const eqIndex = pair.indexOf('=');
687
719
  if (eqIndex === -1)
@@ -692,6 +724,15 @@ export default class Health extends Command {
692
724
  value = JSON.parse(value);
693
725
  }
694
726
  catch { /* keep as string */ }
727
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
728
+ if (isArrayField && !Array.isArray(value)) {
729
+ if (typeof value === 'string') {
730
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
731
+ }
732
+ else if (value !== undefined && value !== null) {
733
+ value = [value];
734
+ }
735
+ }
695
736
  const keys = key.split('.');
696
737
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
697
738
  this.error(`Invalid --set key: ${key}`);
@@ -811,6 +852,21 @@ export default class Health extends Command {
811
852
  this.log('');
812
853
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
813
854
  }
855
+ parseFilterValue(value) {
856
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
857
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
858
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
859
+ // no operator prefix is present.
860
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
861
+ const m = value.match(opRe);
862
+ if (!m)
863
+ return `\$eq:${value}`;
864
+ const op = m[1];
865
+ const rest = value.slice(m[0].length);
866
+ if (rest === '')
867
+ return `\$${op}`;
868
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
869
+ }
814
870
  filterFields(data, fields) {
815
871
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
816
872
  const pick = (obj, keys) => {
@@ -30,6 +30,7 @@ export default class Inbox extends Command {
30
30
  private runExport;
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
+ private parseFilterValue;
33
34
  private filterFields;
34
35
  private sanitizeOutput;
35
36
  private getColumns;
@@ -8,11 +8,11 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["upload", "email-webhook", "document-types", "list-processing-logs", "processing-logs"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'upload': { method: 'POST', path: '/v1/inbox/upload', summary: 'Upload document(s) to inbox for processing', pathParams: [], hasBody: false, isMultipart: true },
12
- 'email-webhook': { method: 'POST', path: '/v1/inbox/email-webhook/{secret}', summary: 'Email webhook for document ingestion (SendGrid inbound-parse)', pathParams: ['secret'], hasBody: false, isMultipart: false },
13
- 'document-types': { method: 'GET', path: '/v1/inbox/document-types', summary: 'Get available document types and per-parent allowed types', pathParams: [], hasBody: false, isMultipart: false },
14
- 'list-processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs', summary: 'List recent inbox processing logs (newest first)', pathParams: [], hasBody: false, isMultipart: false },
15
- 'processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs/{processingId}', summary: 'Get a processing run by its processing identifier', pathParams: ['processingId'], hasBody: false, isMultipart: false },
11
+ 'upload': { method: 'POST', path: '/v1/inbox/upload', summary: 'Upload document(s) to inbox for processing', pathParams: [], hasBody: false, isMultipart: true, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'email-webhook': { method: 'POST', path: '/v1/inbox/email-webhook/{secret}', summary: 'Email webhook for document ingestion (SendGrid inbound-parse)', pathParams: ['secret'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'document-types': { method: 'GET', path: '/v1/inbox/document-types', summary: 'Get available document types and per-parent allowed types', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'list-processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs', summary: 'List recent inbox processing logs (newest first)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
+ 'processing-logs': { method: 'GET', path: '/v1/inbox/processing-logs/{processingId}', summary: 'Get a processing run by its processing identifier', pathParams: ['processingId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class Inbox extends Command {
18
18
  static description = 'Manage inbox';
@@ -74,11 +74,6 @@ export default class Inbox extends Command {
74
74
  const endpoint = ENDPOINTS[args.action];
75
75
  if (!endpoint)
76
76
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
77
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
78
- if (!context)
79
- this.error('No context configured. Run: aderis auth login --new');
80
- if (!context.tokens)
81
- this.error('Not logged in. Run: aderis auth login');
82
77
  let apiPath = endpoint.path;
83
78
  for (let i = 0; i < endpoint.pathParams.length; i++) {
84
79
  const param = endpoint.pathParams[i];
@@ -97,6 +92,11 @@ export default class Inbox extends Command {
97
92
  }
98
93
  apiPath = apiPath.replace(`{${param}}`, String(value));
99
94
  }
95
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
96
+ if (!context)
97
+ this.error('No context configured. Run: aderis auth login --new');
98
+ if (!context.tokens)
99
+ this.error('Not logged in. Run: aderis auth login');
100
100
  const queryParams = {};
101
101
  if (flags.page)
102
102
  queryParams.page = flags.page;
@@ -123,15 +123,7 @@ export default class Inbox extends Command {
123
123
  if (eqIndex === -1)
124
124
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
125
125
  const key = f.slice(0, eqIndex);
126
- let value = f.slice(eqIndex + 1);
127
- // Check for operator prefix (with or without $)
128
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
129
- if (opMatch) {
130
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
131
- }
132
- else {
133
- value = `\$eq:${value}`;
134
- }
126
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
135
127
  const filterKey = `filter.${key}`;
136
128
  if (filterKey in queryParams) {
137
129
  // Multiple filters on same field: collect as array
@@ -164,7 +156,9 @@ export default class Inbox extends Command {
164
156
  data = form;
165
157
  }
166
158
  else if (endpoint.hasBody) {
167
- if (flags.file) {
159
+ // For binary-response endpoints, --file is the output path, not a JSON
160
+ // input file — skip the read-as-body branch.
161
+ if (flags.file && !endpoint.isBinaryResponse) {
168
162
  const fsModule = await import('node:fs');
169
163
  try {
170
164
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -186,6 +180,7 @@ export default class Inbox extends Command {
186
180
  if (!data)
187
181
  data = {};
188
182
  const body = data;
183
+ const arrayFields = new Set(endpoint.bodyArrayFields);
189
184
  for (const pair of flags.set) {
190
185
  const eqIndex = pair.indexOf('=');
191
186
  if (eqIndex === -1)
@@ -197,6 +192,19 @@ export default class Inbox extends Command {
197
192
  value = JSON.parse(value);
198
193
  }
199
194
  catch { /* keep as string */ }
195
+ // For top-level keys typed as array in the API schema, split bare
196
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
197
+ // wrap a lone scalar in a single-element array. Skips when --set
198
+ // already provided a JSON array.
199
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
200
+ if (isArrayField && !Array.isArray(value)) {
201
+ if (typeof value === 'string') {
202
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
203
+ }
204
+ else if (value !== undefined && value !== null) {
205
+ value = [value];
206
+ }
207
+ }
200
208
  // Support nested keys like "address.city"
201
209
  const keys = key.split('.');
202
210
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -247,6 +255,26 @@ export default class Inbox extends Command {
247
255
  process.stderr.write('\n');
248
256
  }
249
257
  try {
258
+ // Binary response: download to file
259
+ if (endpoint.isBinaryResponse) {
260
+ if (!flags.file) {
261
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
262
+ }
263
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
264
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
265
+ data,
266
+ contextName: flags.context,
267
+ binary: true,
268
+ });
269
+ const fsModule = await import('node:fs');
270
+ const binData = binResponse.data;
271
+ const buffer = Buffer.isBuffer(binData)
272
+ ? binData
273
+ : Buffer.from(binData);
274
+ fsModule.writeFileSync(flags.file, buffer);
275
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
276
+ return;
277
+ }
250
278
  const response = await apiRequest(endpoint.method, apiPath, {
251
279
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
252
280
  data,
@@ -337,6 +365,7 @@ export default class Inbox extends Command {
337
365
  }
338
366
  else if (flags.set && flags.set.length > 0) {
339
367
  const item = {};
368
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
340
369
  for (const pair of flags.set) {
341
370
  const eqIndex = pair.indexOf('=');
342
371
  if (eqIndex === -1)
@@ -347,6 +376,15 @@ export default class Inbox extends Command {
347
376
  value = JSON.parse(value);
348
377
  }
349
378
  catch { /* keep as string */ }
379
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
380
+ if (isArrayField && !Array.isArray(value)) {
381
+ if (typeof value === 'string') {
382
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
383
+ }
384
+ else if (value !== undefined && value !== null) {
385
+ value = [value];
386
+ }
387
+ }
350
388
  const keys = key.split('.');
351
389
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
352
390
  this.error(`Invalid --set key: ${key}`);
@@ -496,14 +534,7 @@ export default class Inbox extends Command {
496
534
  if (eqIndex === -1)
497
535
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
498
536
  const key = f.slice(0, eqIndex);
499
- let value = f.slice(eqIndex + 1);
500
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
501
- if (opMatch) {
502
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
503
- }
504
- else {
505
- value = `\$eq:${value}`;
506
- }
537
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
507
538
  const filterKey = `filter.${key}`;
508
539
  if (filterKey in baseParams) {
509
540
  const existing = baseParams[filterKey];
@@ -680,6 +711,7 @@ export default class Inbox extends Command {
680
711
  }
681
712
  else if (flags.set && flags.set.length > 0) {
682
713
  const item = {};
714
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
683
715
  for (const pair of flags.set) {
684
716
  const eqIndex = pair.indexOf('=');
685
717
  if (eqIndex === -1)
@@ -690,6 +722,15 @@ export default class Inbox extends Command {
690
722
  value = JSON.parse(value);
691
723
  }
692
724
  catch { /* keep as string */ }
725
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
726
+ if (isArrayField && !Array.isArray(value)) {
727
+ if (typeof value === 'string') {
728
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
729
+ }
730
+ else if (value !== undefined && value !== null) {
731
+ value = [value];
732
+ }
733
+ }
693
734
  const keys = key.split('.');
694
735
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
695
736
  this.error(`Invalid --set key: ${key}`);
@@ -809,6 +850,21 @@ export default class Inbox extends Command {
809
850
  this.log('');
810
851
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
811
852
  }
853
+ parseFilterValue(value) {
854
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
855
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
856
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
857
+ // no operator prefix is present.
858
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
859
+ const m = value.match(opRe);
860
+ if (!m)
861
+ return `\$eq:${value}`;
862
+ const op = m[1];
863
+ const rest = value.slice(m[0].length);
864
+ if (rest === '')
865
+ return `\$${op}`;
866
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
867
+ }
812
868
  filterFields(data, fields) {
813
869
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
814
870
  const pick = (obj, keys) => {