@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
@@ -31,6 +31,7 @@ export default class Integrations extends Command {
31
31
  private runExport;
32
32
  private runCamt053Import;
33
33
  private runNestedImport;
34
+ private parseFilterValue;
34
35
  private filterFields;
35
36
  private sanitizeOutput;
36
37
  private getColumns;
@@ -8,9 +8,9 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["get", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'get': { method: 'GET', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Get integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false },
12
- 'update': { method: 'PUT', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Create or update integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: true, isMultipart: false },
13
- 'delete': { method: 'DELETE', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Delete integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false },
11
+ 'get': { method: 'GET', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Get integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'update': { method: 'PUT', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Create or update integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'delete': { method: 'DELETE', path: '/v1/integrations/{integrationName}/{configKey}', summary: 'Delete integration configuration', pathParams: ['integrationName', 'configKey'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
14
  };
15
15
  export default class Integrations extends Command {
16
16
  static description = 'Manage integrations';
@@ -76,11 +76,6 @@ export default class Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 Integrations 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 ItemCategories 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 = [...["list", "create", "get", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/item-categories', summary: 'Get all item categories with pagination', pathParams: [], hasBody: false, isMultipart: false },
12
- 'create': { method: 'POST', path: '/v1/item-categories', summary: 'Create a new item category', pathParams: [], hasBody: true, isMultipart: false },
13
- 'get': { method: 'GET', path: '/v1/item-categories/{id}', summary: 'Get an item category by ID', pathParams: ['id'], hasBody: false, isMultipart: false },
14
- 'update': { method: 'PATCH', path: '/v1/item-categories/{id}', summary: 'Update an item category', pathParams: ['id'], hasBody: true, isMultipart: false },
15
- 'delete': { method: 'DELETE', path: '/v1/item-categories/{id}', summary: 'Delete an item category', pathParams: ['id'], hasBody: false, isMultipart: false },
11
+ 'list': { method: 'GET', path: '/v1/item-categories', summary: 'Get all item categories with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/item-categories', summary: 'Create a new item category', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'get': { method: 'GET', path: '/v1/item-categories/{id}', summary: 'Get an item category by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/item-categories/{id}', summary: 'Update an item category', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
+ 'delete': { method: 'DELETE', path: '/v1/item-categories/{id}', summary: 'Delete an item category', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class ItemCategories extends Command {
18
18
  static description = 'Manage item categories';
@@ -81,11 +81,6 @@ export default class ItemCategories extends Command {
81
81
  const endpoint = ENDPOINTS[args.action];
82
82
  if (!endpoint)
83
83
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
84
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
85
- if (!context)
86
- this.error('No context configured. Run: aderis auth login --new');
87
- if (!context.tokens)
88
- this.error('Not logged in. Run: aderis auth login');
89
84
  let apiPath = endpoint.path;
90
85
  for (let i = 0; i < endpoint.pathParams.length; i++) {
91
86
  const param = endpoint.pathParams[i];
@@ -104,6 +99,11 @@ export default class ItemCategories extends Command {
104
99
  }
105
100
  apiPath = apiPath.replace(`{${param}}`, String(value));
106
101
  }
102
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
103
+ if (!context)
104
+ this.error('No context configured. Run: aderis auth login --new');
105
+ if (!context.tokens)
106
+ this.error('Not logged in. Run: aderis auth login');
107
107
  const queryParams = {};
108
108
  if (flags.page)
109
109
  queryParams.page = flags.page;
@@ -130,15 +130,7 @@ export default class ItemCategories extends Command {
130
130
  if (eqIndex === -1)
131
131
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
132
132
  const key = f.slice(0, eqIndex);
133
- let value = f.slice(eqIndex + 1);
134
- // Check for operator prefix (with or without $)
135
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
136
- if (opMatch) {
137
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
138
- }
139
- else {
140
- value = `\$eq:${value}`;
141
- }
133
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
142
134
  const filterKey = `filter.${key}`;
143
135
  if (filterKey in queryParams) {
144
136
  // Multiple filters on same field: collect as array
@@ -171,7 +163,9 @@ export default class ItemCategories extends Command {
171
163
  data = form;
172
164
  }
173
165
  else if (endpoint.hasBody) {
174
- if (flags.file) {
166
+ // For binary-response endpoints, --file is the output path, not a JSON
167
+ // input file — skip the read-as-body branch.
168
+ if (flags.file && !endpoint.isBinaryResponse) {
175
169
  const fsModule = await import('node:fs');
176
170
  try {
177
171
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -193,6 +187,7 @@ export default class ItemCategories extends Command {
193
187
  if (!data)
194
188
  data = {};
195
189
  const body = data;
190
+ const arrayFields = new Set(endpoint.bodyArrayFields);
196
191
  for (const pair of flags.set) {
197
192
  const eqIndex = pair.indexOf('=');
198
193
  if (eqIndex === -1)
@@ -204,6 +199,19 @@ export default class ItemCategories extends Command {
204
199
  value = JSON.parse(value);
205
200
  }
206
201
  catch { /* keep as string */ }
202
+ // For top-level keys typed as array in the API schema, split bare
203
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
204
+ // wrap a lone scalar in a single-element array. Skips when --set
205
+ // already provided a JSON array.
206
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
207
+ if (isArrayField && !Array.isArray(value)) {
208
+ if (typeof value === 'string') {
209
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
210
+ }
211
+ else if (value !== undefined && value !== null) {
212
+ value = [value];
213
+ }
214
+ }
207
215
  // Support nested keys like "address.city"
208
216
  const keys = key.split('.');
209
217
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -254,6 +262,26 @@ export default class ItemCategories extends Command {
254
262
  process.stderr.write('\n');
255
263
  }
256
264
  try {
265
+ // Binary response: download to file
266
+ if (endpoint.isBinaryResponse) {
267
+ if (!flags.file) {
268
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
269
+ }
270
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
271
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
272
+ data,
273
+ contextName: flags.context,
274
+ binary: true,
275
+ });
276
+ const fsModule = await import('node:fs');
277
+ const binData = binResponse.data;
278
+ const buffer = Buffer.isBuffer(binData)
279
+ ? binData
280
+ : Buffer.from(binData);
281
+ fsModule.writeFileSync(flags.file, buffer);
282
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
283
+ return;
284
+ }
257
285
  const response = await apiRequest(endpoint.method, apiPath, {
258
286
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
259
287
  data,
@@ -344,6 +372,7 @@ export default class ItemCategories extends Command {
344
372
  }
345
373
  else if (flags.set && flags.set.length > 0) {
346
374
  const item = {};
375
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
347
376
  for (const pair of flags.set) {
348
377
  const eqIndex = pair.indexOf('=');
349
378
  if (eqIndex === -1)
@@ -354,6 +383,15 @@ export default class ItemCategories extends Command {
354
383
  value = JSON.parse(value);
355
384
  }
356
385
  catch { /* keep as string */ }
386
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
387
+ if (isArrayField && !Array.isArray(value)) {
388
+ if (typeof value === 'string') {
389
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
390
+ }
391
+ else if (value !== undefined && value !== null) {
392
+ value = [value];
393
+ }
394
+ }
357
395
  const keys = key.split('.');
358
396
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
359
397
  this.error(`Invalid --set key: ${key}`);
@@ -503,14 +541,7 @@ export default class ItemCategories extends Command {
503
541
  if (eqIndex === -1)
504
542
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
505
543
  const key = f.slice(0, eqIndex);
506
- let value = f.slice(eqIndex + 1);
507
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
508
- if (opMatch) {
509
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
510
- }
511
- else {
512
- value = `\$eq:${value}`;
513
- }
544
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
514
545
  const filterKey = `filter.${key}`;
515
546
  if (filterKey in baseParams) {
516
547
  const existing = baseParams[filterKey];
@@ -687,6 +718,7 @@ export default class ItemCategories extends Command {
687
718
  }
688
719
  else if (flags.set && flags.set.length > 0) {
689
720
  const item = {};
721
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
690
722
  for (const pair of flags.set) {
691
723
  const eqIndex = pair.indexOf('=');
692
724
  if (eqIndex === -1)
@@ -697,6 +729,15 @@ export default class ItemCategories extends Command {
697
729
  value = JSON.parse(value);
698
730
  }
699
731
  catch { /* keep as string */ }
732
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
733
+ if (isArrayField && !Array.isArray(value)) {
734
+ if (typeof value === 'string') {
735
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
736
+ }
737
+ else if (value !== undefined && value !== null) {
738
+ value = [value];
739
+ }
740
+ }
700
741
  const keys = key.split('.');
701
742
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
702
743
  this.error(`Invalid --set key: ${key}`);
@@ -816,6 +857,21 @@ export default class ItemCategories extends Command {
816
857
  this.log('');
817
858
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
818
859
  }
860
+ parseFilterValue(value) {
861
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
862
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
863
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
864
+ // no operator prefix is present.
865
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
866
+ const m = value.match(opRe);
867
+ if (!m)
868
+ return `\$eq:${value}`;
869
+ const op = m[1];
870
+ const rest = value.slice(m[0].length);
871
+ if (rest === '')
872
+ return `\$${op}`;
873
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
874
+ }
819
875
  filterFields(data, fields) {
820
876
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
821
877
  const pick = (obj, keys) => {
@@ -30,6 +30,7 @@ export default class Items 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 = [...["list", "create", "get", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/items', summary: 'Get all items with pagination', pathParams: [], hasBody: false, isMultipart: false },
12
- 'create': { method: 'POST', path: '/v1/items', summary: 'Create a new item', pathParams: [], hasBody: true, isMultipart: false },
13
- 'get': { method: 'GET', path: '/v1/items/{id}', summary: 'Get an item by ID', pathParams: ['id'], hasBody: false, isMultipart: false },
14
- 'update': { method: 'PATCH', path: '/v1/items/{id}', summary: 'Update an item', pathParams: ['id'], hasBody: true, isMultipart: false },
15
- 'delete': { method: 'DELETE', path: '/v1/items/{id}', summary: 'Delete an item', pathParams: ['id'], hasBody: false, isMultipart: false },
11
+ 'list': { method: 'GET', path: '/v1/items', summary: 'Get all items with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/items', summary: 'Create a new item', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds'] },
13
+ 'get': { method: 'GET', path: '/v1/items/{id}', summary: 'Get an item by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'update': { method: 'PATCH', path: '/v1/items/{id}', summary: 'Update an item', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds'] },
15
+ 'delete': { method: 'DELETE', path: '/v1/items/{id}', summary: 'Delete an item', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
16
  };
17
17
  export default class Items extends Command {
18
18
  static description = 'Manage items';
@@ -81,11 +81,6 @@ export default class Items extends Command {
81
81
  const endpoint = ENDPOINTS[args.action];
82
82
  if (!endpoint)
83
83
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
84
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
85
- if (!context)
86
- this.error('No context configured. Run: aderis auth login --new');
87
- if (!context.tokens)
88
- this.error('Not logged in. Run: aderis auth login');
89
84
  let apiPath = endpoint.path;
90
85
  for (let i = 0; i < endpoint.pathParams.length; i++) {
91
86
  const param = endpoint.pathParams[i];
@@ -104,6 +99,11 @@ export default class Items extends Command {
104
99
  }
105
100
  apiPath = apiPath.replace(`{${param}}`, String(value));
106
101
  }
102
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
103
+ if (!context)
104
+ this.error('No context configured. Run: aderis auth login --new');
105
+ if (!context.tokens)
106
+ this.error('Not logged in. Run: aderis auth login');
107
107
  const queryParams = {};
108
108
  if (flags.page)
109
109
  queryParams.page = flags.page;
@@ -130,15 +130,7 @@ export default class Items extends Command {
130
130
  if (eqIndex === -1)
131
131
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
132
132
  const key = f.slice(0, eqIndex);
133
- let value = f.slice(eqIndex + 1);
134
- // Check for operator prefix (with or without $)
135
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
136
- if (opMatch) {
137
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
138
- }
139
- else {
140
- value = `\$eq:${value}`;
141
- }
133
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
142
134
  const filterKey = `filter.${key}`;
143
135
  if (filterKey in queryParams) {
144
136
  // Multiple filters on same field: collect as array
@@ -171,7 +163,9 @@ export default class Items extends Command {
171
163
  data = form;
172
164
  }
173
165
  else if (endpoint.hasBody) {
174
- if (flags.file) {
166
+ // For binary-response endpoints, --file is the output path, not a JSON
167
+ // input file — skip the read-as-body branch.
168
+ if (flags.file && !endpoint.isBinaryResponse) {
175
169
  const fsModule = await import('node:fs');
176
170
  try {
177
171
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -193,6 +187,7 @@ export default class Items extends Command {
193
187
  if (!data)
194
188
  data = {};
195
189
  const body = data;
190
+ const arrayFields = new Set(endpoint.bodyArrayFields);
196
191
  for (const pair of flags.set) {
197
192
  const eqIndex = pair.indexOf('=');
198
193
  if (eqIndex === -1)
@@ -204,6 +199,19 @@ export default class Items extends Command {
204
199
  value = JSON.parse(value);
205
200
  }
206
201
  catch { /* keep as string */ }
202
+ // For top-level keys typed as array in the API schema, split bare
203
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
204
+ // wrap a lone scalar in a single-element array. Skips when --set
205
+ // already provided a JSON array.
206
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
207
+ if (isArrayField && !Array.isArray(value)) {
208
+ if (typeof value === 'string') {
209
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
210
+ }
211
+ else if (value !== undefined && value !== null) {
212
+ value = [value];
213
+ }
214
+ }
207
215
  // Support nested keys like "address.city"
208
216
  const keys = key.split('.');
209
217
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -254,6 +262,26 @@ export default class Items extends Command {
254
262
  process.stderr.write('\n');
255
263
  }
256
264
  try {
265
+ // Binary response: download to file
266
+ if (endpoint.isBinaryResponse) {
267
+ if (!flags.file) {
268
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
269
+ }
270
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
271
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
272
+ data,
273
+ contextName: flags.context,
274
+ binary: true,
275
+ });
276
+ const fsModule = await import('node:fs');
277
+ const binData = binResponse.data;
278
+ const buffer = Buffer.isBuffer(binData)
279
+ ? binData
280
+ : Buffer.from(binData);
281
+ fsModule.writeFileSync(flags.file, buffer);
282
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
283
+ return;
284
+ }
257
285
  const response = await apiRequest(endpoint.method, apiPath, {
258
286
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
259
287
  data,
@@ -344,6 +372,7 @@ export default class Items extends Command {
344
372
  }
345
373
  else if (flags.set && flags.set.length > 0) {
346
374
  const item = {};
375
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
347
376
  for (const pair of flags.set) {
348
377
  const eqIndex = pair.indexOf('=');
349
378
  if (eqIndex === -1)
@@ -354,6 +383,15 @@ export default class Items extends Command {
354
383
  value = JSON.parse(value);
355
384
  }
356
385
  catch { /* keep as string */ }
386
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
387
+ if (isArrayField && !Array.isArray(value)) {
388
+ if (typeof value === 'string') {
389
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
390
+ }
391
+ else if (value !== undefined && value !== null) {
392
+ value = [value];
393
+ }
394
+ }
357
395
  const keys = key.split('.');
358
396
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
359
397
  this.error(`Invalid --set key: ${key}`);
@@ -503,14 +541,7 @@ export default class Items extends Command {
503
541
  if (eqIndex === -1)
504
542
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
505
543
  const key = f.slice(0, eqIndex);
506
- let value = f.slice(eqIndex + 1);
507
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
508
- if (opMatch) {
509
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
510
- }
511
- else {
512
- value = `\$eq:${value}`;
513
- }
544
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
514
545
  const filterKey = `filter.${key}`;
515
546
  if (filterKey in baseParams) {
516
547
  const existing = baseParams[filterKey];
@@ -687,6 +718,7 @@ export default class Items extends Command {
687
718
  }
688
719
  else if (flags.set && flags.set.length > 0) {
689
720
  const item = {};
721
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
690
722
  for (const pair of flags.set) {
691
723
  const eqIndex = pair.indexOf('=');
692
724
  if (eqIndex === -1)
@@ -697,6 +729,15 @@ export default class Items extends Command {
697
729
  value = JSON.parse(value);
698
730
  }
699
731
  catch { /* keep as string */ }
732
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
733
+ if (isArrayField && !Array.isArray(value)) {
734
+ if (typeof value === 'string') {
735
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
736
+ }
737
+ else if (value !== undefined && value !== null) {
738
+ value = [value];
739
+ }
740
+ }
700
741
  const keys = key.split('.');
701
742
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
702
743
  this.error(`Invalid --set key: ${key}`);
@@ -816,6 +857,21 @@ export default class Items extends Command {
816
857
  this.log('');
817
858
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
818
859
  }
860
+ parseFilterValue(value) {
861
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
862
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
863
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
864
+ // no operator prefix is present.
865
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
866
+ const m = value.match(opRe);
867
+ if (!m)
868
+ return `\$eq:${value}`;
869
+ const op = m[1];
870
+ const rest = value.slice(m[0].length);
871
+ if (rest === '')
872
+ return `\$${op}`;
873
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
874
+ }
819
875
  filterFields(data, fields) {
820
876
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
821
877
  const pick = (obj, keys) => {
@@ -30,6 +30,7 @@ export default class JournalLines 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;