@aderis-erp/cli 1.0.17 → 1.1.1
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/dist/commands/access-groups.js +49 -17
- package/dist/commands/account-transaction-allocations.js +46 -14
- package/dist/commands/account-transactions.js +75 -35
- package/dist/commands/accounts.js +54 -22
- package/dist/commands/agent-platform.js +53 -21
- package/dist/commands/{nango-integrations.d.ts → agent-runs.d.ts} +1 -1
- package/dist/commands/agent-runs.js +1155 -0
- package/dist/commands/analytics.js +47 -15
- package/dist/commands/api-keys.js +49 -16
- package/dist/commands/{text-templates.d.ts → async-jobs.d.ts} +1 -1
- package/dist/commands/async-jobs.js +1158 -0
- package/dist/commands/audit-log.js +46 -14
- package/dist/commands/bank-accounts.js +63 -30
- package/dist/commands/bank-integrations.d.ts +1 -0
- package/dist/commands/bank-integrations.js +66 -31
- package/dist/commands/business-partners.js +77 -44
- package/dist/commands/chat.js +52 -19
- package/dist/commands/credit-card-accounts.d.ts +1 -0
- package/dist/commands/credit-card-accounts.js +67 -29
- package/dist/commands/delivery-notes.js +78 -44
- package/dist/commands/dunning-configs.js +49 -17
- package/dist/commands/dunning-reminders.js +52 -20
- package/dist/commands/dunning-runs.js +45 -13
- package/dist/commands/email-transactions.js +47 -15
- package/dist/commands/employees.js +76 -44
- package/dist/commands/entity-configurations.js +47 -15
- package/dist/commands/event-processor-mappings.js +49 -17
- package/dist/commands/files.js +56 -24
- package/dist/commands/health.js +49 -17
- package/dist/commands/{tag-configurations.d.ts → imports.d.ts} +1 -1
- package/dist/commands/{tag-configurations.js → imports.js} +59 -31
- package/dist/commands/inbox.js +52 -19
- package/dist/commands/integration-identities.d.ts +40 -0
- package/dist/commands/integration-identities.js +1151 -0
- package/dist/commands/integrations.js +47 -15
- package/dist/commands/item-categories.js +49 -17
- package/dist/commands/items.js +49 -17
- package/dist/commands/journal-lines.js +45 -13
- package/dist/commands/journals.js +68 -36
- package/dist/commands/ledgers.d.ts +3 -1
- package/dist/commands/ledgers.js +96 -34
- package/dist/commands/my-account.d.ts +39 -0
- package/dist/commands/my-account.js +1160 -0
- package/dist/commands/office-locations.js +49 -17
- package/dist/commands/payment-methods.js +54 -22
- package/dist/commands/payment-runs.js +61 -29
- package/dist/commands/payment-terms.js +50 -18
- package/dist/commands/payments.js +51 -19
- package/dist/commands/payroll-runs.d.ts +43 -0
- package/dist/commands/payroll-runs.js +1188 -0
- package/dist/commands/permissions.d.ts +39 -0
- package/dist/commands/{text-templates.js → permissions.js} +54 -30
- package/dist/commands/price-lists.js +54 -22
- package/dist/commands/processor-mappings.js +48 -16
- package/dist/commands/processors.js +55 -24
- package/dist/commands/purchase-order-types.js +49 -17
- package/dist/commands/purchase-orders.js +69 -36
- package/dist/commands/reports.js +49 -17
- package/dist/commands/roles.d.ts +39 -0
- package/dist/commands/{nango-integrations.js → roles.js} +63 -26
- package/dist/commands/salary-types.js +49 -17
- package/dist/commands/sales-invoices.js +84 -51
- package/dist/commands/sales-orders.js +78 -44
- package/dist/commands/saved-views.js +47 -15
- package/dist/commands/scheduled-processor-mappings.js +49 -17
- package/dist/commands/schedules.js +53 -21
- package/dist/commands/support/end.d.ts +9 -0
- package/dist/commands/support/end.js +29 -0
- package/dist/commands/support/start.d.ts +14 -0
- package/dist/commands/support/start.js +50 -0
- package/dist/commands/support/status.d.ts +10 -0
- package/dist/commands/support/status.js +45 -0
- package/dist/commands/support/tenants.d.ts +10 -0
- package/dist/commands/support/tenants.js +39 -0
- package/dist/commands/tags.js +51 -18
- package/dist/commands/tax-codes.js +52 -20
- package/dist/commands/tax-matrix.js +49 -17
- package/dist/commands/templates.d.ts +39 -0
- package/dist/commands/templates.js +1157 -0
- package/dist/commands/tenants.js +52 -20
- package/dist/commands/tools.js +46 -14
- package/dist/commands/users.d.ts +42 -0
- package/dist/commands/users.js +1174 -0
- package/dist/commands/vat-declarations.d.ts +2 -0
- package/dist/commands/vat-declarations.js +72 -36
- package/dist/commands/vendor-invoices.js +81 -49
- package/dist/commands/webhooks.js +51 -19
- package/dist/commands/workflows.js +47 -15
- package/dist/lib/api-client.d.ts +6 -0
- package/dist/lib/api-client.js +13 -0
- package/dist/lib/auth/constants.d.ts +12 -0
- package/dist/lib/auth/constants.js +12 -0
- package/dist/lib/auth/index.d.ts +11 -1
- package/dist/lib/auth/index.js +105 -6
- package/dist/lib/auth/jwks.d.ts +19 -0
- package/dist/lib/auth/jwks.js +112 -0
- package/dist/lib/auth/keycloak-provider.d.ts +45 -7
- package/dist/lib/auth/keycloak-provider.js +161 -27
- package/dist/lib/auth/tokens.d.ts +8 -3
- package/dist/lib/auth/tokens.js +8 -7
- package/dist/lib/auth/types.d.ts +5 -1
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.js +10 -0
- package/oclif.manifest.json +2182 -744
- package/package.json +7 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
5
|
const HAS_IMPORT = true;
|
|
@@ -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/purchase-order-types', summary: 'Get all purchase order types with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'create': { method: 'POST', path: '/v1/purchase-order-types', summary: 'Create a new purchase order type', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
13
|
-
'get': { method: 'GET', path: '/v1/purchase-order-types/{id}', summary: 'Get a purchase order type by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
14
|
-
'update': { method: 'PATCH', path: '/v1/purchase-order-types/{id}', summary: 'Update a purchase order type', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
15
|
-
'delete': { method: 'DELETE', path: '/v1/purchase-order-types/{id}', summary: 'Delete a purchase order type', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'list': { method: 'GET', path: '/v1/purchase-order-types', summary: 'Get all purchase order types with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/purchase-order-types', summary: 'Create a new purchase order type', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
13
|
+
'get': { method: 'GET', path: '/v1/purchase-order-types/{id}', summary: 'Get a purchase order type by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
14
|
+
'update': { method: 'PATCH', path: '/v1/purchase-order-types/{id}', summary: 'Update a purchase order type', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
15
|
+
'delete': { method: 'DELETE', path: '/v1/purchase-order-types/{id}', summary: 'Delete a purchase order type', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
16
|
};
|
|
17
17
|
export default class PurchaseOrderTypes extends Command {
|
|
18
18
|
static description = 'Manage purchase order types';
|
|
@@ -264,10 +264,11 @@ export default class PurchaseOrderTypes extends Command {
|
|
|
264
264
|
process.stderr.write('\n');
|
|
265
265
|
}
|
|
266
266
|
try {
|
|
267
|
-
// Binary response:
|
|
267
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
268
268
|
if (endpoint.isBinaryResponse) {
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
const toStdout = !flags.file;
|
|
270
|
+
if (toStdout && process.stdout.isTTY) {
|
|
271
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
271
272
|
}
|
|
272
273
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
273
274
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -275,24 +276,55 @@ export default class PurchaseOrderTypes extends Command {
|
|
|
275
276
|
contextName: flags.context,
|
|
276
277
|
binary: true,
|
|
277
278
|
});
|
|
278
|
-
const fsModule = await import('node:fs');
|
|
279
279
|
const binData = binResponse.data;
|
|
280
280
|
const buffer = Buffer.isBuffer(binData)
|
|
281
281
|
? binData
|
|
282
282
|
: Buffer.from(binData);
|
|
283
|
+
if (toStdout) {
|
|
284
|
+
// Nothing else may reach stdout here — it is the file.
|
|
285
|
+
process.stdout.write(buffer);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const fsModule = await import('node:fs');
|
|
283
289
|
fsModule.writeFileSync(flags.file, buffer);
|
|
284
290
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
285
291
|
return;
|
|
286
292
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
let response;
|
|
294
|
+
if (endpoint.hasBinaryVariant) {
|
|
295
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
296
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
297
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
298
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
299
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
300
|
+
data,
|
|
301
|
+
contextName: flags.context,
|
|
302
|
+
binary: true,
|
|
303
|
+
});
|
|
304
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
305
|
+
const rawData = raw.data;
|
|
306
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
307
|
+
if (!isTextualMediaType(rawType)) {
|
|
308
|
+
if (process.stdout.isTTY) {
|
|
309
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
310
|
+
}
|
|
311
|
+
// Nothing else may reach stdout here — it is the file.
|
|
312
|
+
process.stdout.write(buffer);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
319
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
320
|
+
data,
|
|
321
|
+
contextName: flags.context,
|
|
322
|
+
acceptAny: true,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
293
325
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
294
326
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
295
|
-
if (contentType
|
|
327
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
296
328
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
297
329
|
this.log(String(response.data));
|
|
298
330
|
return;
|
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
5
|
const HAS_IMPORT = true;
|
|
6
6
|
const HAS_EXPORT = true;
|
|
7
7
|
const HAS_CAMT053 = false;
|
|
8
8
|
const NESTED_IMPORTS = ["line-items", "notes", "files"];
|
|
9
|
-
const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "submit", "approve", "decline", "cancel", "create-line-items", "update-line-items", "delete-line-items", "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}`)];
|
|
9
|
+
const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "submit", "approve", "copy", "decline", "cancel", "create-line-items", "update-line-items", "delete-line-items", "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/purchase-orders', summary: 'Get all purchase orders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'create': { method: 'POST', path: '/v1/purchase-orders', summary: 'Create a new purchase order', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes', 'files', 'notes', 'discounts', 'lineItems'] },
|
|
13
|
-
'get': { method: 'GET', path: '/v1/purchase-orders/{id}', summary: 'Get a purchase order by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
14
|
-
'update': { method: 'PATCH', path: '/v1/purchase-orders/{id}', summary: 'Update a purchase order', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes', 'files', 'notes', 'discounts', 'lineItems', 'removeFileIds'] },
|
|
15
|
-
'delete': { method: 'DELETE', path: '/v1/purchase-orders/{id}', summary: 'Delete a purchase order', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
16
|
-
'submit': { method: 'POST', path: '/v1/purchase-orders/{id}/submit', summary: 'Submit order for approval (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
17
|
-
'approve': { method: 'POST', path: '/v1/purchase-orders/{id}/approve', summary: 'Approve order (PENDING_APPROVAL → APPROVED). Generates order number if missing.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'files
|
|
11
|
+
'list': { method: 'GET', path: '/v1/purchase-orders', summary: 'Get all purchase orders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/purchase-orders', summary: 'Create a new purchase order', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'files', 'notes', 'discounts', 'lineItems'] },
|
|
13
|
+
'get': { method: 'GET', path: '/v1/purchase-orders/{id}', summary: 'Get a purchase order by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
14
|
+
'update': { method: 'PATCH', path: '/v1/purchase-orders/{id}', summary: 'Update a purchase order', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'files', 'notes', 'discounts', 'lineItems', 'removeFileIds'] },
|
|
15
|
+
'delete': { method: 'DELETE', path: '/v1/purchase-orders/{id}', summary: 'Delete a purchase order', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
|
+
'submit': { method: 'POST', path: '/v1/purchase-orders/{id}/submit', summary: 'Submit order for approval (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
17
|
+
'approve': { method: 'POST', path: '/v1/purchase-orders/{id}/approve', summary: 'Approve order (PENDING_APPROVAL → APPROVED). Generates order number if missing.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
18
|
+
'copy': { method: 'POST', path: '/v1/purchase-orders/{id}/copy', summary: 'Create a copy of a purchase order as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
19
|
+
'decline': { method: 'POST', path: '/v1/purchase-orders/{id}/decline', summary: 'Decline order (PENDING_APPROVAL → DECLINED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
20
|
+
'cancel': { method: 'POST', path: '/v1/purchase-orders/{id}/cancel', summary: 'Cancel order (DRAFT, PENDING_APPROVAL or APPROVED → CANCELLED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
21
|
+
'create-line-items': { method: 'POST', path: '/v1/purchase-orders/{orderId}/line-items', summary: 'Add a line item to a purchase order', pathParams: ['orderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'tagIds', 'tagCodes'] },
|
|
22
|
+
'update-line-items': { method: 'PATCH', path: '/v1/purchase-orders/{orderId}/line-items/{lineItemId}', summary: 'Update a line item on a purchase order', pathParams: ['orderId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'tagIds', 'tagCodes'] },
|
|
23
|
+
'delete-line-items': { method: 'DELETE', path: '/v1/purchase-orders/{orderId}/line-items/{lineItemId}', summary: 'Remove a line item from a purchase order', pathParams: ['orderId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
24
|
+
'list-notes': { method: 'GET', path: '/v1/purchase-orders/{purchaseOrderId}/notes', summary: 'Get all notes for a purchase order', pathParams: ['purchaseOrderId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
25
|
+
'create-notes': { method: 'POST', path: '/v1/purchase-orders/{purchaseOrderId}/notes', summary: 'Create a note for a purchase order', pathParams: ['purchaseOrderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
26
|
+
'get-notes': { method: 'GET', path: '/v1/purchase-orders/{purchaseOrderId}/notes/{noteId}', summary: 'Get a specific note for a purchase order', pathParams: ['purchaseOrderId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
27
|
+
'update-notes': { method: 'PATCH', path: '/v1/purchase-orders/{purchaseOrderId}/notes/{noteId}', summary: 'Update a note for a purchase order', pathParams: ['purchaseOrderId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
28
|
+
'delete-notes': { method: 'DELETE', path: '/v1/purchase-orders/{purchaseOrderId}/notes/{noteId}', summary: 'Delete a note from a purchase order', pathParams: ['purchaseOrderId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
29
|
+
'list-files': { method: 'GET', path: '/v1/purchase-orders/{parentId}/files', summary: 'List file attachments for a purchase order', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
30
|
+
'create-files': { method: 'POST', path: '/v1/purchase-orders/{parentId}/files', summary: 'Upload a file attachment to a purchase order', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
31
|
+
'get-files': { method: 'GET', path: '/v1/purchase-orders/{parentId}/files/{fileId}', summary: 'Get a file attachment for a purchase order', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
32
|
+
'update-files': { method: 'PATCH', path: '/v1/purchase-orders/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a purchase order', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
33
|
+
'delete-files': { method: 'DELETE', path: '/v1/purchase-orders/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a purchase order', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
34
|
+
'files-download': { method: 'GET', path: '/v1/purchase-orders/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a purchase order', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
34
35
|
};
|
|
35
36
|
export default class PurchaseOrders extends Command {
|
|
36
37
|
static description = 'Manage purchase orders';
|
|
@@ -285,10 +286,11 @@ export default class PurchaseOrders extends Command {
|
|
|
285
286
|
process.stderr.write('\n');
|
|
286
287
|
}
|
|
287
288
|
try {
|
|
288
|
-
// Binary response:
|
|
289
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
289
290
|
if (endpoint.isBinaryResponse) {
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
const toStdout = !flags.file;
|
|
292
|
+
if (toStdout && process.stdout.isTTY) {
|
|
293
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
292
294
|
}
|
|
293
295
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
294
296
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -296,24 +298,55 @@ export default class PurchaseOrders extends Command {
|
|
|
296
298
|
contextName: flags.context,
|
|
297
299
|
binary: true,
|
|
298
300
|
});
|
|
299
|
-
const fsModule = await import('node:fs');
|
|
300
301
|
const binData = binResponse.data;
|
|
301
302
|
const buffer = Buffer.isBuffer(binData)
|
|
302
303
|
? binData
|
|
303
304
|
: Buffer.from(binData);
|
|
305
|
+
if (toStdout) {
|
|
306
|
+
// Nothing else may reach stdout here — it is the file.
|
|
307
|
+
process.stdout.write(buffer);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const fsModule = await import('node:fs');
|
|
304
311
|
fsModule.writeFileSync(flags.file, buffer);
|
|
305
312
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
306
313
|
return;
|
|
307
314
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
315
|
+
let response;
|
|
316
|
+
if (endpoint.hasBinaryVariant) {
|
|
317
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
318
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
319
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
320
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
321
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
322
|
+
data,
|
|
323
|
+
contextName: flags.context,
|
|
324
|
+
binary: true,
|
|
325
|
+
});
|
|
326
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
327
|
+
const rawData = raw.data;
|
|
328
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
329
|
+
if (!isTextualMediaType(rawType)) {
|
|
330
|
+
if (process.stdout.isTTY) {
|
|
331
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
332
|
+
}
|
|
333
|
+
// Nothing else may reach stdout here — it is the file.
|
|
334
|
+
process.stdout.write(buffer);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
341
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
342
|
+
data,
|
|
343
|
+
contextName: flags.context,
|
|
344
|
+
acceptAny: true,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
314
347
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
315
348
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
316
|
-
if (contentType
|
|
349
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
317
350
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
318
351
|
this.log(String(response.data));
|
|
319
352
|
return;
|
package/dist/commands/reports.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
5
|
const HAS_IMPORT = false;
|
|
@@ -8,11 +8,11 @@ const HAS_CAMT053 = false;
|
|
|
8
8
|
const NESTED_IMPORTS = [];
|
|
9
9
|
const SUBCOMMANDS = [...["profit-and-loss", "balance-sheet", "gl-detail", "gl-detail-export-xlsx", "gl-detail-export-pdf"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'profit-and-loss': { method: 'POST', path: '/v1/reports/profit-and-loss', summary: 'Get Profit & Loss report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['ledgerIds', 'periods'] },
|
|
12
|
-
'balance-sheet': { method: 'GET', path: '/v1/reports/balance-sheet', summary: 'Generate Balance Sheet report', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
13
|
-
'gl-detail': { method: 'POST', path: '/v1/reports/gl-detail', summary: 'Get GL Detail report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
|
|
14
|
-
'gl-detail-export-xlsx': { method: 'POST', path: '/v1/reports/gl-detail/export/xlsx', summary: 'Export GL Detail report as Excel (one sheet per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, bodyArrayFields: ['accountIds', 'accountCodes'] },
|
|
15
|
-
'gl-detail-export-pdf': { method: 'POST', path: '/v1/reports/gl-detail/export/pdf', summary: 'Export GL Detail report as PDF (one page per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, bodyArrayFields: ['accountIds', 'accountCodes'] },
|
|
11
|
+
'profit-and-loss': { method: 'POST', path: '/v1/reports/profit-and-loss', summary: 'Get Profit & Loss report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: true, bodyArrayFields: ['ledgerIds', 'periods'] },
|
|
12
|
+
'balance-sheet': { method: 'GET', path: '/v1/reports/balance-sheet', summary: 'Generate Balance Sheet report', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
13
|
+
'gl-detail': { method: 'POST', path: '/v1/reports/gl-detail', summary: 'Get GL Detail report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
|
|
14
|
+
'gl-detail-export-xlsx': { method: 'POST', path: '/v1/reports/gl-detail/export/xlsx', summary: 'Export GL Detail report as Excel (one sheet per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
|
|
15
|
+
'gl-detail-export-pdf': { method: 'POST', path: '/v1/reports/gl-detail/export/pdf', summary: 'Export GL Detail report as PDF (one page per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
|
|
16
16
|
};
|
|
17
17
|
export default class Reports extends Command {
|
|
18
18
|
static description = 'Manage reports';
|
|
@@ -257,10 +257,11 @@ export default class Reports extends Command {
|
|
|
257
257
|
process.stderr.write('\n');
|
|
258
258
|
}
|
|
259
259
|
try {
|
|
260
|
-
// Binary response:
|
|
260
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
261
261
|
if (endpoint.isBinaryResponse) {
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
const toStdout = !flags.file;
|
|
263
|
+
if (toStdout && process.stdout.isTTY) {
|
|
264
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
264
265
|
}
|
|
265
266
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
266
267
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -268,24 +269,55 @@ export default class Reports extends Command {
|
|
|
268
269
|
contextName: flags.context,
|
|
269
270
|
binary: true,
|
|
270
271
|
});
|
|
271
|
-
const fsModule = await import('node:fs');
|
|
272
272
|
const binData = binResponse.data;
|
|
273
273
|
const buffer = Buffer.isBuffer(binData)
|
|
274
274
|
? binData
|
|
275
275
|
: Buffer.from(binData);
|
|
276
|
+
if (toStdout) {
|
|
277
|
+
// Nothing else may reach stdout here — it is the file.
|
|
278
|
+
process.stdout.write(buffer);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const fsModule = await import('node:fs');
|
|
276
282
|
fsModule.writeFileSync(flags.file, buffer);
|
|
277
283
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
278
284
|
return;
|
|
279
285
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
+
let response;
|
|
287
|
+
if (endpoint.hasBinaryVariant) {
|
|
288
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
289
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
290
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
291
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
292
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
293
|
+
data,
|
|
294
|
+
contextName: flags.context,
|
|
295
|
+
binary: true,
|
|
296
|
+
});
|
|
297
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
298
|
+
const rawData = raw.data;
|
|
299
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
300
|
+
if (!isTextualMediaType(rawType)) {
|
|
301
|
+
if (process.stdout.isTTY) {
|
|
302
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
303
|
+
}
|
|
304
|
+
// Nothing else may reach stdout here — it is the file.
|
|
305
|
+
process.stdout.write(buffer);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
312
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
313
|
+
data,
|
|
314
|
+
contextName: flags.context,
|
|
315
|
+
acceptAny: true,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
286
318
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
287
319
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
288
|
-
if (contentType
|
|
320
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
289
321
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
290
322
|
this.log(String(response.data));
|
|
291
323
|
return;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Roles extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
static flags: {
|
|
10
|
+
context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
'sort-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
|
+
query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
|
+
fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
|
+
sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
26
|
+
'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
27
|
+
};
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
private runImport;
|
|
30
|
+
private runExport;
|
|
31
|
+
private runCamt053Import;
|
|
32
|
+
private runNestedImport;
|
|
33
|
+
private parseFilterValue;
|
|
34
|
+
private sanitizeOutput;
|
|
35
|
+
private getColumns;
|
|
36
|
+
private getValue;
|
|
37
|
+
private printTable;
|
|
38
|
+
private printCsv;
|
|
39
|
+
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
|
-
const HAS_IMPORT =
|
|
6
|
-
const HAS_EXPORT =
|
|
5
|
+
const HAS_IMPORT = true;
|
|
6
|
+
const HAS_EXPORT = true;
|
|
7
7
|
const HAS_CAMT053 = false;
|
|
8
8
|
const NESTED_IMPORTS = [];
|
|
9
|
-
const SUBCOMMANDS = [...["
|
|
9
|
+
const SUBCOMMANDS = [...["list", "create", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'delete
|
|
15
|
-
'webhooks': { method: 'POST', path: '/v1/nango/webhooks', summary: 'Receive Nango webhooks (auth and sync)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'list': { method: 'GET', path: '/v1/roles', summary: 'List the roles of the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/roles', summary: 'Create a role for the current tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissionIds'] },
|
|
13
|
+
'update': { method: 'PATCH', path: '/v1/roles/{roleId}', summary: 'Change a tenant-created role', pathParams: ['roleId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissionIds'] },
|
|
14
|
+
'delete': { method: 'DELETE', path: '/v1/roles/{roleId}', summary: 'Delete a tenant-created role', pathParams: ['roleId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
15
|
};
|
|
17
|
-
export default class
|
|
18
|
-
static description = 'Manage
|
|
19
|
-
static examples = [
|
|
16
|
+
export default class Roles extends Command {
|
|
17
|
+
static description = 'Manage roles';
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> roles list',
|
|
20
|
+
'<%= config.bin %> roles create --data \'{...}\'',
|
|
21
|
+
'<%= config.bin %> roles update <id> --set field=value',
|
|
22
|
+
'<%= config.bin %> roles import --file data.json',
|
|
23
|
+
'<%= config.bin %> roles export --csv > roles.csv',
|
|
24
|
+
];
|
|
20
25
|
static args = {
|
|
21
26
|
action: Args.string({ description: 'Action: ' + SUBCOMMANDS.join(', '), required: true }),
|
|
22
27
|
id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
|
|
@@ -41,7 +46,7 @@ export default class NangoIntegrations extends Command {
|
|
|
41
46
|
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
42
47
|
};
|
|
43
48
|
async run() {
|
|
44
|
-
const { args, flags } = await this.parse(
|
|
49
|
+
const { args, flags } = await this.parse(Roles);
|
|
45
50
|
// Handle import action specially
|
|
46
51
|
if (args.action === 'import') {
|
|
47
52
|
if (!HAS_IMPORT)
|
|
@@ -82,7 +87,7 @@ export default class NangoIntegrations extends Command {
|
|
|
82
87
|
// First path param always comes from positional argument
|
|
83
88
|
value = args.id;
|
|
84
89
|
if (!value)
|
|
85
|
-
this.error('Missing required argument: id. Usage: aderis
|
|
90
|
+
this.error('Missing required argument: id. Usage: aderis roles <action> <id>');
|
|
86
91
|
}
|
|
87
92
|
else {
|
|
88
93
|
const flagName = param.replace(/\./g, '-').replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
|
|
@@ -257,10 +262,11 @@ export default class NangoIntegrations extends Command {
|
|
|
257
262
|
process.stderr.write('\n');
|
|
258
263
|
}
|
|
259
264
|
try {
|
|
260
|
-
// Binary response:
|
|
265
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
261
266
|
if (endpoint.isBinaryResponse) {
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
const toStdout = !flags.file;
|
|
268
|
+
if (toStdout && process.stdout.isTTY) {
|
|
269
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
264
270
|
}
|
|
265
271
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
266
272
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -268,24 +274,55 @@ export default class NangoIntegrations extends Command {
|
|
|
268
274
|
contextName: flags.context,
|
|
269
275
|
binary: true,
|
|
270
276
|
});
|
|
271
|
-
const fsModule = await import('node:fs');
|
|
272
277
|
const binData = binResponse.data;
|
|
273
278
|
const buffer = Buffer.isBuffer(binData)
|
|
274
279
|
? binData
|
|
275
280
|
: Buffer.from(binData);
|
|
281
|
+
if (toStdout) {
|
|
282
|
+
// Nothing else may reach stdout here — it is the file.
|
|
283
|
+
process.stdout.write(buffer);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const fsModule = await import('node:fs');
|
|
276
287
|
fsModule.writeFileSync(flags.file, buffer);
|
|
277
288
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
278
289
|
return;
|
|
279
290
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
let response;
|
|
292
|
+
if (endpoint.hasBinaryVariant) {
|
|
293
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
294
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
295
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
296
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
297
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
298
|
+
data,
|
|
299
|
+
contextName: flags.context,
|
|
300
|
+
binary: true,
|
|
301
|
+
});
|
|
302
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
303
|
+
const rawData = raw.data;
|
|
304
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
305
|
+
if (!isTextualMediaType(rawType)) {
|
|
306
|
+
if (process.stdout.isTTY) {
|
|
307
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
308
|
+
}
|
|
309
|
+
// Nothing else may reach stdout here — it is the file.
|
|
310
|
+
process.stdout.write(buffer);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
317
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
318
|
+
data,
|
|
319
|
+
contextName: flags.context,
|
|
320
|
+
acceptAny: true,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
286
323
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
287
324
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
288
|
-
if (contentType
|
|
325
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
289
326
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
290
327
|
this.log(String(response.data));
|
|
291
328
|
return;
|
|
@@ -725,7 +762,7 @@ export default class NangoIntegrations extends Command {
|
|
|
725
762
|
}
|
|
726
763
|
async runNestedImport(suffix, parentId, flags) {
|
|
727
764
|
if (!parentId) {
|
|
728
|
-
this.error(`Missing required argument: id. Usage: aderis
|
|
765
|
+
this.error(`Missing required argument: id. Usage: aderis roles import-${suffix} <parent-id> --file data.json`);
|
|
729
766
|
}
|
|
730
767
|
const context = flags.context ? getContext(flags.context) : getCurrentContext();
|
|
731
768
|
if (!context)
|