@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.
- package/README.md +6 -0
- package/dist/commands/access-groups.d.ts +1 -0
- package/dist/commands/access-groups.js +84 -28
- package/dist/commands/{async-endpoints-internal.d.ts → account-transaction-allocations.d.ts} +3 -2
- package/dist/commands/{async-endpoints-internal.js → account-transaction-allocations.js} +93 -33
- package/dist/commands/account-transactions.d.ts +1 -0
- package/dist/commands/account-transactions.js +96 -40
- package/dist/commands/accounts.d.ts +1 -0
- package/dist/commands/accounts.js +89 -33
- package/dist/commands/agent-runs.d.ts +1 -0
- package/dist/commands/agent-runs.js +85 -27
- package/dist/commands/audit-log.d.ts +1 -0
- package/dist/commands/audit-log.js +82 -26
- package/dist/commands/auth/login.js +1 -2
- package/dist/commands/auth/tenant.d.ts +1 -1
- package/dist/commands/auth/tenant.js +11 -11
- package/dist/commands/auth.js +1 -1
- package/dist/commands/bank-accounts.d.ts +1 -0
- package/dist/commands/bank-accounts.js +96 -40
- package/dist/commands/bank-integrations.d.ts +1 -0
- package/dist/commands/bank-integrations.js +97 -41
- package/dist/commands/business-partners---contacts.d.ts +1 -0
- package/dist/commands/business-partners---contacts.js +89 -33
- package/dist/commands/business-partners.d.ts +1 -0
- package/dist/commands/business-partners.js +89 -33
- package/dist/commands/credit-card-accounts.d.ts +1 -0
- package/dist/commands/credit-card-accounts.js +94 -38
- package/dist/commands/documents.d.ts +1 -0
- package/dist/commands/documents.js +85 -29
- package/dist/commands/dunning-configs.d.ts +1 -0
- package/dist/commands/dunning-configs.js +84 -28
- package/dist/commands/dunning-reminders.d.ts +1 -0
- package/dist/commands/dunning-reminders.js +87 -31
- package/dist/commands/dunning-runs.d.ts +1 -0
- package/dist/commands/dunning-runs.js +80 -24
- package/dist/commands/employees.d.ts +1 -0
- package/dist/commands/employees.js +111 -55
- package/dist/commands/event-processor-mappings.d.ts +1 -0
- package/dist/commands/event-processor-mappings.js +84 -28
- package/dist/commands/files-internal.d.ts +1 -0
- package/dist/commands/files-internal.js +88 -32
- package/dist/commands/health.d.ts +1 -0
- package/dist/commands/health.js +84 -28
- package/dist/commands/inbox.d.ts +1 -0
- package/dist/commands/inbox.js +84 -28
- package/dist/commands/integrations.d.ts +1 -0
- package/dist/commands/integrations.js +82 -26
- package/dist/commands/item-categories.d.ts +1 -0
- package/dist/commands/item-categories.js +84 -28
- package/dist/commands/items.d.ts +1 -0
- package/dist/commands/items.js +84 -28
- package/dist/commands/journal-lines.d.ts +1 -0
- package/dist/commands/journal-lines.js +80 -24
- package/dist/commands/journals.d.ts +1 -0
- package/dist/commands/journals.js +98 -42
- package/dist/commands/ledgers.d.ts +1 -0
- package/dist/commands/ledgers.js +95 -39
- package/dist/commands/office-locations.d.ts +1 -0
- package/dist/commands/office-locations.js +84 -28
- package/dist/commands/payment-method-ledger-mappings.d.ts +1 -0
- package/dist/commands/payment-method-ledger-mappings.js +84 -28
- package/dist/commands/payment-methods.d.ts +1 -0
- package/dist/commands/payment-methods.js +84 -28
- package/dist/commands/payment-runs.d.ts +1 -0
- package/dist/commands/payment-runs.js +97 -41
- package/dist/commands/payment-terms.d.ts +1 -0
- package/dist/commands/payment-terms.js +85 -29
- package/dist/commands/price-lists.d.ts +1 -0
- package/dist/commands/price-lists.js +89 -33
- package/dist/commands/processor-mappings.d.ts +1 -0
- package/dist/commands/processor-mappings.js +83 -27
- package/dist/commands/processors.d.ts +1 -0
- package/dist/commands/processors.js +88 -32
- package/dist/commands/reconciliation.d.ts +1 -0
- package/dist/commands/reconciliation.js +85 -29
- package/dist/commands/reports.d.ts +1 -0
- package/dist/commands/reports.js +85 -26
- package/dist/commands/salary-types.d.ts +1 -0
- package/dist/commands/salary-types.js +84 -28
- package/dist/commands/sales-invoices.d.ts +1 -0
- package/dist/commands/sales-invoices.js +109 -53
- package/dist/commands/sales-orders.d.ts +1 -0
- package/dist/commands/sales-orders.js +107 -51
- package/dist/commands/tags.d.ts +1 -0
- package/dist/commands/tags.js +84 -28
- package/dist/commands/tax-codes.d.ts +1 -0
- package/dist/commands/tax-codes.js +87 -31
- package/dist/commands/tax-matrix.d.ts +1 -0
- package/dist/commands/tax-matrix.js +84 -28
- package/dist/commands/tenants.d.ts +1 -0
- package/dist/commands/tenants.js +84 -28
- package/dist/commands/text-templates.d.ts +1 -0
- package/dist/commands/text-templates.js +84 -28
- package/dist/commands/tools.d.ts +1 -0
- package/dist/commands/tools.js +81 -25
- package/dist/commands/vat-declaration.d.ts +41 -0
- package/dist/commands/vat-declaration.js +1079 -0
- package/dist/commands/vendor-invoices.d.ts +1 -0
- package/dist/commands/vendor-invoices.js +113 -57
- package/dist/commands/webhooks.d.ts +1 -0
- package/dist/commands/webhooks.js +86 -30
- package/dist/commands/workflows.d.ts +1 -0
- package/dist/commands/workflows.js +82 -26
- package/dist/lib/api-client.d.ts +6 -1
- package/dist/lib/api-client.js +23 -5
- package/dist/lib/auth.js +18 -0
- package/dist/lib/config.js +1 -1
- package/oclif.manifest.json +229 -52
- package/package.json +15 -13
- package/dist/index 2.js +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|