@aderis-erp/cli 1.0.15 → 1.1.0
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 +30 -8
- package/dist/commands/access-groups.d.ts +0 -1
- package/dist/commands/access-groups.js +54 -78
- package/dist/commands/account-transaction-allocations.d.ts +0 -1
- package/dist/commands/account-transaction-allocations.js +51 -75
- package/dist/commands/account-transactions.d.ts +0 -1
- package/dist/commands/account-transactions.js +80 -94
- package/dist/commands/accounts.d.ts +0 -1
- package/dist/commands/accounts.js +59 -83
- package/dist/commands/agent-platform.d.ts +0 -1
- package/dist/commands/agent-platform.js +58 -82
- package/dist/commands/agent-runs.d.ts +39 -0
- package/dist/commands/agent-runs.js +1155 -0
- package/dist/commands/analytics.d.ts +0 -1
- package/dist/commands/analytics.js +52 -76
- package/dist/commands/{text-templates.d.ts → api-keys.d.ts} +1 -2
- package/dist/commands/api-keys.js +1150 -0
- package/dist/commands/async-jobs.d.ts +39 -0
- package/dist/commands/async-jobs.js +1158 -0
- package/dist/commands/audit-log.d.ts +0 -1
- package/dist/commands/audit-log.js +51 -75
- package/dist/commands/auth/login.d.ts +9 -0
- package/dist/commands/auth/login.js +73 -20
- package/dist/commands/auth/logout.d.ts +4 -0
- package/dist/commands/auth/logout.js +4 -0
- package/dist/commands/auth/status.js +26 -17
- package/dist/commands/auth/tenant.js +3 -6
- package/dist/commands/auth.d.ts +0 -1
- package/dist/commands/auth.js +5 -34
- package/dist/commands/bank-accounts.d.ts +0 -1
- package/dist/commands/bank-accounts.js +68 -91
- package/dist/commands/bank-integrations.d.ts +1 -1
- package/dist/commands/bank-integrations.js +71 -92
- package/dist/commands/business-partners.d.ts +1 -1
- package/dist/commands/business-partners.js +84 -100
- package/dist/commands/chat.d.ts +0 -1
- package/dist/commands/chat.js +57 -80
- package/dist/commands/config/add.d.ts +3 -0
- package/dist/commands/config/add.js +34 -16
- package/dist/commands/config/current.js +3 -0
- package/dist/commands/config/list.js +3 -0
- package/dist/commands/credit-card-accounts.d.ts +1 -1
- package/dist/commands/credit-card-accounts.js +72 -90
- package/dist/commands/delivery-notes.d.ts +0 -1
- package/dist/commands/delivery-notes.js +83 -105
- package/dist/commands/dunning-configs.d.ts +0 -1
- package/dist/commands/dunning-configs.js +54 -78
- package/dist/commands/dunning-reminders.d.ts +0 -1
- package/dist/commands/dunning-reminders.js +57 -81
- package/dist/commands/dunning-runs.d.ts +0 -1
- package/dist/commands/dunning-runs.js +50 -74
- package/dist/commands/email-transactions.d.ts +0 -1
- package/dist/commands/email-transactions.js +52 -76
- package/dist/commands/employees.d.ts +0 -1
- package/dist/commands/employees.js +81 -105
- package/dist/commands/entity-configurations.d.ts +0 -1
- package/dist/commands/entity-configurations.js +52 -76
- package/dist/commands/event-processor-mappings.d.ts +0 -1
- package/dist/commands/event-processor-mappings.js +54 -78
- package/dist/commands/files.d.ts +0 -1
- package/dist/commands/files.js +61 -85
- package/dist/commands/health.d.ts +0 -1
- package/dist/commands/health.js +54 -78
- package/dist/commands/imports.d.ts +39 -0
- package/dist/commands/imports.js +1151 -0
- package/dist/commands/inbox.d.ts +0 -1
- package/dist/commands/inbox.js +57 -80
- package/dist/commands/integration-identities.d.ts +40 -0
- package/dist/commands/integration-identities.js +1151 -0
- package/dist/commands/integrations.d.ts +0 -1
- package/dist/commands/integrations.js +52 -76
- package/dist/commands/item-categories.d.ts +0 -1
- package/dist/commands/item-categories.js +54 -78
- package/dist/commands/items.d.ts +0 -1
- package/dist/commands/items.js +54 -78
- package/dist/commands/journal-lines.d.ts +0 -1
- package/dist/commands/journal-lines.js +50 -74
- package/dist/commands/journals.d.ts +1 -1
- package/dist/commands/journals.js +75 -96
- package/dist/commands/ledgers.d.ts +3 -2
- package/dist/commands/ledgers.js +100 -95
- package/dist/commands/my-account.d.ts +39 -0
- package/dist/commands/my-account.js +1160 -0
- package/dist/commands/office-locations.d.ts +0 -1
- package/dist/commands/office-locations.js +54 -78
- package/dist/commands/payment-methods.d.ts +0 -1
- package/dist/commands/payment-methods.js +59 -83
- package/dist/commands/payment-runs.d.ts +0 -1
- package/dist/commands/payment-runs.js +66 -90
- package/dist/commands/payment-terms.d.ts +0 -1
- package/dist/commands/payment-terms.js +55 -79
- package/dist/commands/payments.d.ts +0 -1
- package/dist/commands/payments.js +56 -80
- 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/permissions.js +1146 -0
- package/dist/commands/price-lists.d.ts +0 -1
- package/dist/commands/price-lists.js +59 -83
- package/dist/commands/processor-mappings.d.ts +0 -1
- package/dist/commands/processor-mappings.js +53 -77
- package/dist/commands/processors.d.ts +0 -1
- package/dist/commands/processors.js +60 -85
- package/dist/commands/purchase-order-types.d.ts +0 -1
- package/dist/commands/purchase-order-types.js +54 -78
- package/dist/commands/purchase-orders.d.ts +0 -1
- package/dist/commands/purchase-orders.js +74 -97
- package/dist/commands/reports.d.ts +0 -1
- package/dist/commands/reports.js +54 -78
- package/dist/commands/roles.d.ts +39 -0
- package/dist/commands/{text-templates.js → roles.js} +64 -90
- package/dist/commands/salary-types.d.ts +0 -1
- package/dist/commands/salary-types.js +54 -78
- package/dist/commands/sales-invoices.d.ts +0 -1
- package/dist/commands/sales-invoices.js +90 -110
- package/dist/commands/sales-orders.d.ts +0 -1
- package/dist/commands/sales-orders.js +83 -105
- package/dist/commands/saved-views.d.ts +0 -1
- package/dist/commands/saved-views.js +52 -76
- package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
- package/dist/commands/scheduled-processor-mappings.js +54 -78
- package/dist/commands/schedules.d.ts +0 -1
- package/dist/commands/schedules.js +58 -82
- package/dist/commands/switch.js +4 -4
- package/dist/commands/tag-configurations.d.ts +0 -1
- package/dist/commands/tag-configurations.js +56 -79
- package/dist/commands/tags.d.ts +0 -1
- package/dist/commands/tags.js +54 -78
- package/dist/commands/tax-codes.d.ts +0 -1
- package/dist/commands/tax-codes.js +57 -81
- package/dist/commands/tax-matrix.d.ts +0 -1
- package/dist/commands/tax-matrix.js +54 -78
- package/dist/commands/templates.d.ts +39 -0
- package/dist/commands/templates.js +1157 -0
- package/dist/commands/tenants.d.ts +0 -1
- package/dist/commands/tenants.js +58 -81
- package/dist/commands/tools.d.ts +0 -1
- package/dist/commands/tools.js +51 -75
- package/dist/commands/users.d.ts +42 -0
- package/dist/commands/users.js +1174 -0
- package/dist/commands/vat-declarations.d.ts +3 -1
- package/dist/commands/vat-declarations.js +78 -93
- package/dist/commands/vendor-invoices.d.ts +0 -1
- package/dist/commands/vendor-invoices.js +87 -110
- package/dist/commands/webhooks.d.ts +0 -1
- package/dist/commands/webhooks.js +56 -80
- package/dist/commands/workflows.d.ts +0 -1
- package/dist/commands/workflows.js +52 -76
- package/dist/lib/api-client.d.ts +6 -0
- package/dist/lib/api-client.js +16 -6
- package/dist/lib/auth/constants.d.ts +11 -0
- package/dist/lib/auth/constants.js +11 -0
- package/dist/lib/auth/errors.d.ts +17 -0
- package/dist/lib/auth/errors.js +22 -0
- package/dist/lib/auth/flow.d.ts +27 -0
- package/dist/lib/auth/flow.js +163 -0
- package/dist/lib/auth/guards.d.ts +5 -0
- package/dist/lib/auth/guards.js +14 -0
- package/dist/lib/auth/http.d.ts +16 -0
- package/dist/lib/auth/http.js +49 -0
- package/dist/lib/auth/index.d.ts +49 -0
- package/dist/lib/auth/index.js +228 -0
- 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 +91 -0
- package/dist/lib/auth/keycloak-provider.js +244 -0
- package/dist/lib/auth/pages.d.ts +25 -0
- package/dist/lib/auth/pages.js +123 -0
- package/dist/lib/auth/pkce.d.ts +11 -0
- package/dist/lib/auth/pkce.js +17 -0
- package/dist/lib/auth/tokens.d.ts +59 -0
- package/dist/lib/auth/tokens.js +105 -0
- package/dist/lib/auth/types.d.ts +61 -0
- package/dist/lib/auth/types.js +1 -0
- package/dist/lib/auth/validated-url.d.ts +6 -0
- package/dist/lib/auth/validated-url.js +53 -0
- package/dist/lib/config.d.ts +13 -3
- package/dist/lib/config.js +24 -19
- package/oclif.manifest.json +2447 -641
- package/package.json +5 -4
- package/dist/lib/auth.d.ts +0 -43
- package/dist/lib/auth.js +0 -610
|
@@ -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,14 +8,14 @@ const HAS_CAMT053 = false;
|
|
|
8
8
|
const NESTED_IMPORTS = [];
|
|
9
9
|
const SUBCOMMANDS = [...["pending", "ongoing", "cancelled", "get", "update", "list", "approve", "cancel"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'pending': { method: 'GET', path: '/v1/dunning-reminders/pending', summary: 'Get pending reminders (awaiting approval) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'ongoing': { method: 'GET', path: '/v1/dunning-reminders/ongoing', summary: 'Get ongoing reminders (pending, sent, or failed) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
13
|
-
'cancelled': { method: 'GET', path: '/v1/dunning-reminders/cancelled', summary: 'Get cancelled reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
14
|
-
'get': { method: 'GET', path: '/v1/dunning-reminders/{id}', summary: 'Get a single dunning reminder by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
15
|
-
'update': { method: 'PATCH', path: '/v1/dunning-reminders/{id}', summary: 'Update recipient or action on a dunning reminder', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
16
|
-
'list': { method: 'GET', path: '/v1/dunning-reminders', summary: 'Get all dunning reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
17
|
-
'approve': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/approve', summary: 'Approve a dunning reminder for sending', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
18
|
-
'cancel': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/cancel', summary: 'Cancel a dunning reminder', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'pending': { method: 'GET', path: '/v1/dunning-reminders/pending', summary: 'Get pending reminders (awaiting approval) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'ongoing': { method: 'GET', path: '/v1/dunning-reminders/ongoing', summary: 'Get ongoing reminders (pending, sent, or failed) with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
13
|
+
'cancelled': { method: 'GET', path: '/v1/dunning-reminders/cancelled', summary: 'Get cancelled reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
14
|
+
'get': { method: 'GET', path: '/v1/dunning-reminders/{id}', summary: 'Get a single dunning reminder by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
15
|
+
'update': { method: 'PATCH', path: '/v1/dunning-reminders/{id}', summary: 'Update recipient or action on a dunning reminder', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
|
+
'list': { method: 'GET', path: '/v1/dunning-reminders', summary: 'Get all dunning reminders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
17
|
+
'approve': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/approve', summary: 'Approve a dunning reminder for sending', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
18
|
+
'cancel': { method: 'POST', path: '/v1/dunning-reminders/{reminderId}/cancel', summary: 'Cancel a dunning reminder', pathParams: ['reminderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
19
19
|
};
|
|
20
20
|
export default class DunningReminders extends Command {
|
|
21
21
|
static description = 'Manage dunning reminders';
|
|
@@ -44,7 +44,7 @@ export default class DunningReminders extends Command {
|
|
|
44
44
|
'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
|
|
45
45
|
filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
|
|
46
46
|
query: Flags.string({ description: 'Additional query params as JSON' }),
|
|
47
|
-
fields: Flags.string({ description: 'Comma-separated list of fields to
|
|
47
|
+
fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
|
|
48
48
|
sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
|
|
49
49
|
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
50
50
|
};
|
|
@@ -116,6 +116,8 @@ export default class DunningReminders extends Command {
|
|
|
116
116
|
queryParams.search = flags.search;
|
|
117
117
|
if (flags['sort-by'])
|
|
118
118
|
queryParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
119
|
+
if (flags.fields)
|
|
120
|
+
queryParams.fields = flags.fields;
|
|
119
121
|
if (flags.query) {
|
|
120
122
|
try {
|
|
121
123
|
Object.assign(queryParams, JSON.parse(flags.query));
|
|
@@ -263,10 +265,11 @@ export default class DunningReminders extends Command {
|
|
|
263
265
|
process.stderr.write('\n');
|
|
264
266
|
}
|
|
265
267
|
try {
|
|
266
|
-
// Binary response:
|
|
268
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
267
269
|
if (endpoint.isBinaryResponse) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
const toStdout = !flags.file;
|
|
271
|
+
if (toStdout && process.stdout.isTTY) {
|
|
272
|
+
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.`);
|
|
270
273
|
}
|
|
271
274
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
272
275
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -274,24 +277,55 @@ export default class DunningReminders extends Command {
|
|
|
274
277
|
contextName: flags.context,
|
|
275
278
|
binary: true,
|
|
276
279
|
});
|
|
277
|
-
const fsModule = await import('node:fs');
|
|
278
280
|
const binData = binResponse.data;
|
|
279
281
|
const buffer = Buffer.isBuffer(binData)
|
|
280
282
|
? binData
|
|
281
283
|
: Buffer.from(binData);
|
|
284
|
+
if (toStdout) {
|
|
285
|
+
// Nothing else may reach stdout here — it is the file.
|
|
286
|
+
process.stdout.write(buffer);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const fsModule = await import('node:fs');
|
|
282
290
|
fsModule.writeFileSync(flags.file, buffer);
|
|
283
291
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
284
292
|
return;
|
|
285
293
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
let response;
|
|
295
|
+
if (endpoint.hasBinaryVariant) {
|
|
296
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
297
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
298
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
299
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
300
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
301
|
+
data,
|
|
302
|
+
contextName: flags.context,
|
|
303
|
+
binary: true,
|
|
304
|
+
});
|
|
305
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
306
|
+
const rawData = raw.data;
|
|
307
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
308
|
+
if (!isTextualMediaType(rawType)) {
|
|
309
|
+
if (process.stdout.isTTY) {
|
|
310
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
311
|
+
}
|
|
312
|
+
// Nothing else may reach stdout here — it is the file.
|
|
313
|
+
process.stdout.write(buffer);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
320
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
321
|
+
data,
|
|
322
|
+
contextName: flags.context,
|
|
323
|
+
acceptAny: true,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
292
326
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
293
327
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
294
|
-
if (contentType
|
|
328
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
295
329
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
296
330
|
this.log(String(response.data));
|
|
297
331
|
return;
|
|
@@ -318,11 +352,6 @@ export default class DunningReminders extends Command {
|
|
|
318
352
|
output = obj[arrayKeys[0]];
|
|
319
353
|
}
|
|
320
354
|
}
|
|
321
|
-
// Filter fields if --fields flag is provided
|
|
322
|
-
if (flags.fields) {
|
|
323
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
324
|
-
output = this.filterFields(output, fieldList);
|
|
325
|
-
}
|
|
326
355
|
// Sanitize output if requested
|
|
327
356
|
if (flags.sanitize) {
|
|
328
357
|
output = this.sanitizeOutput(output);
|
|
@@ -577,6 +606,8 @@ export default class DunningReminders extends Command {
|
|
|
577
606
|
baseParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
578
607
|
if (flags.include)
|
|
579
608
|
baseParams.include = flags.include;
|
|
609
|
+
if (flags.fields)
|
|
610
|
+
baseParams.fields = flags.fields;
|
|
580
611
|
if (flags.query) {
|
|
581
612
|
try {
|
|
582
613
|
Object.assign(baseParams, JSON.parse(flags.query));
|
|
@@ -631,12 +662,7 @@ export default class DunningReminders extends Command {
|
|
|
631
662
|
}
|
|
632
663
|
}
|
|
633
664
|
process.stderr.write(' done\n');
|
|
634
|
-
// Filter fields if specified
|
|
635
665
|
let output = allItems;
|
|
636
|
-
if (flags.fields) {
|
|
637
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
638
|
-
output = this.filterFields(allItems, fieldList);
|
|
639
|
-
}
|
|
640
666
|
// Sanitize output if requested
|
|
641
667
|
if (flags.sanitize) {
|
|
642
668
|
output = this.sanitizeOutput(output);
|
|
@@ -965,56 +991,6 @@ export default class DunningReminders extends Command {
|
|
|
965
991
|
return `\$${op}`;
|
|
966
992
|
return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
|
|
967
993
|
}
|
|
968
|
-
filterFields(data, fields) {
|
|
969
|
-
const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
|
|
970
|
-
const pick = (obj, keys) => {
|
|
971
|
-
// Group keys by their first segment so sibling sub-paths
|
|
972
|
-
// (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
|
|
973
|
-
// and don't clobber each other on merge.
|
|
974
|
-
const directKeys = new Set();
|
|
975
|
-
const nestedKeys = new Map();
|
|
976
|
-
for (const key of keys) {
|
|
977
|
-
if (key.split('.').some(isUnsafeKey))
|
|
978
|
-
continue;
|
|
979
|
-
if (key.includes('.')) {
|
|
980
|
-
const [first, ...rest] = key.split('.');
|
|
981
|
-
if (isUnsafeKey(first))
|
|
982
|
-
continue;
|
|
983
|
-
const list = nestedKeys.get(first);
|
|
984
|
-
if (list) {
|
|
985
|
-
list.push(rest.join('.'));
|
|
986
|
-
}
|
|
987
|
-
else {
|
|
988
|
-
nestedKeys.set(first, [rest.join('.')]);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
else {
|
|
992
|
-
directKeys.add(key);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
const result = {};
|
|
996
|
-
for (const key of directKeys) {
|
|
997
|
-
if (key in obj)
|
|
998
|
-
result[key] = obj[key];
|
|
999
|
-
}
|
|
1000
|
-
for (const [first, restKeys] of nestedKeys) {
|
|
1001
|
-
const val = obj[first];
|
|
1002
|
-
if (!val || typeof val !== 'object')
|
|
1003
|
-
continue;
|
|
1004
|
-
if (Array.isArray(val)) {
|
|
1005
|
-
result[first] = val.map(item => pick(item, restKeys));
|
|
1006
|
-
}
|
|
1007
|
-
else {
|
|
1008
|
-
result[first] = pick(val, restKeys);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return result;
|
|
1012
|
-
};
|
|
1013
|
-
if (Array.isArray(data)) {
|
|
1014
|
-
return data.map(item => pick(item, fields));
|
|
1015
|
-
}
|
|
1016
|
-
return pick(data, fields);
|
|
1017
|
-
}
|
|
1018
994
|
sanitizeOutput(data) {
|
|
1019
995
|
const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
1020
996
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -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,7 +8,7 @@ const HAS_CAMT053 = false;
|
|
|
8
8
|
const NESTED_IMPORTS = [];
|
|
9
9
|
const SUBCOMMANDS = [...["trigger"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'trigger': { method: 'POST', path: '/v1/dunning-runs/trigger', summary: 'Manually trigger a dunning run', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'trigger': { method: 'POST', path: '/v1/dunning-runs/trigger', summary: 'Manually trigger a dunning run', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
12
|
};
|
|
13
13
|
export default class DunningRuns extends Command {
|
|
14
14
|
static description = 'Manage dunning runs';
|
|
@@ -32,7 +32,7 @@ export default class DunningRuns extends Command {
|
|
|
32
32
|
'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
|
|
33
33
|
filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
|
|
34
34
|
query: Flags.string({ description: 'Additional query params as JSON' }),
|
|
35
|
-
fields: Flags.string({ description: 'Comma-separated list of fields to
|
|
35
|
+
fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
|
|
36
36
|
sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
|
|
37
37
|
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
38
38
|
};
|
|
@@ -104,6 +104,8 @@ export default class DunningRuns extends Command {
|
|
|
104
104
|
queryParams.search = flags.search;
|
|
105
105
|
if (flags['sort-by'])
|
|
106
106
|
queryParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
107
|
+
if (flags.fields)
|
|
108
|
+
queryParams.fields = flags.fields;
|
|
107
109
|
if (flags.query) {
|
|
108
110
|
try {
|
|
109
111
|
Object.assign(queryParams, JSON.parse(flags.query));
|
|
@@ -251,10 +253,11 @@ export default class DunningRuns extends Command {
|
|
|
251
253
|
process.stderr.write('\n');
|
|
252
254
|
}
|
|
253
255
|
try {
|
|
254
|
-
// Binary response:
|
|
256
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
255
257
|
if (endpoint.isBinaryResponse) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
const toStdout = !flags.file;
|
|
259
|
+
if (toStdout && process.stdout.isTTY) {
|
|
260
|
+
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.`);
|
|
258
261
|
}
|
|
259
262
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
260
263
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -262,24 +265,55 @@ export default class DunningRuns extends Command {
|
|
|
262
265
|
contextName: flags.context,
|
|
263
266
|
binary: true,
|
|
264
267
|
});
|
|
265
|
-
const fsModule = await import('node:fs');
|
|
266
268
|
const binData = binResponse.data;
|
|
267
269
|
const buffer = Buffer.isBuffer(binData)
|
|
268
270
|
? binData
|
|
269
271
|
: Buffer.from(binData);
|
|
272
|
+
if (toStdout) {
|
|
273
|
+
// Nothing else may reach stdout here — it is the file.
|
|
274
|
+
process.stdout.write(buffer);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const fsModule = await import('node:fs');
|
|
270
278
|
fsModule.writeFileSync(flags.file, buffer);
|
|
271
279
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
272
280
|
return;
|
|
273
281
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
282
|
+
let response;
|
|
283
|
+
if (endpoint.hasBinaryVariant) {
|
|
284
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
285
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
286
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
287
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
288
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
289
|
+
data,
|
|
290
|
+
contextName: flags.context,
|
|
291
|
+
binary: true,
|
|
292
|
+
});
|
|
293
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
294
|
+
const rawData = raw.data;
|
|
295
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
296
|
+
if (!isTextualMediaType(rawType)) {
|
|
297
|
+
if (process.stdout.isTTY) {
|
|
298
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
299
|
+
}
|
|
300
|
+
// Nothing else may reach stdout here — it is the file.
|
|
301
|
+
process.stdout.write(buffer);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
308
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
309
|
+
data,
|
|
310
|
+
contextName: flags.context,
|
|
311
|
+
acceptAny: true,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
280
314
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
281
315
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
282
|
-
if (contentType
|
|
316
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
283
317
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
284
318
|
this.log(String(response.data));
|
|
285
319
|
return;
|
|
@@ -306,11 +340,6 @@ export default class DunningRuns extends Command {
|
|
|
306
340
|
output = obj[arrayKeys[0]];
|
|
307
341
|
}
|
|
308
342
|
}
|
|
309
|
-
// Filter fields if --fields flag is provided
|
|
310
|
-
if (flags.fields) {
|
|
311
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
312
|
-
output = this.filterFields(output, fieldList);
|
|
313
|
-
}
|
|
314
343
|
// Sanitize output if requested
|
|
315
344
|
if (flags.sanitize) {
|
|
316
345
|
output = this.sanitizeOutput(output);
|
|
@@ -565,6 +594,8 @@ export default class DunningRuns extends Command {
|
|
|
565
594
|
baseParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
566
595
|
if (flags.include)
|
|
567
596
|
baseParams.include = flags.include;
|
|
597
|
+
if (flags.fields)
|
|
598
|
+
baseParams.fields = flags.fields;
|
|
568
599
|
if (flags.query) {
|
|
569
600
|
try {
|
|
570
601
|
Object.assign(baseParams, JSON.parse(flags.query));
|
|
@@ -619,12 +650,7 @@ export default class DunningRuns extends Command {
|
|
|
619
650
|
}
|
|
620
651
|
}
|
|
621
652
|
process.stderr.write(' done\n');
|
|
622
|
-
// Filter fields if specified
|
|
623
653
|
let output = allItems;
|
|
624
|
-
if (flags.fields) {
|
|
625
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
626
|
-
output = this.filterFields(allItems, fieldList);
|
|
627
|
-
}
|
|
628
654
|
// Sanitize output if requested
|
|
629
655
|
if (flags.sanitize) {
|
|
630
656
|
output = this.sanitizeOutput(output);
|
|
@@ -953,56 +979,6 @@ export default class DunningRuns extends Command {
|
|
|
953
979
|
return `\$${op}`;
|
|
954
980
|
return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
|
|
955
981
|
}
|
|
956
|
-
filterFields(data, fields) {
|
|
957
|
-
const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
|
|
958
|
-
const pick = (obj, keys) => {
|
|
959
|
-
// Group keys by their first segment so sibling sub-paths
|
|
960
|
-
// (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
|
|
961
|
-
// and don't clobber each other on merge.
|
|
962
|
-
const directKeys = new Set();
|
|
963
|
-
const nestedKeys = new Map();
|
|
964
|
-
for (const key of keys) {
|
|
965
|
-
if (key.split('.').some(isUnsafeKey))
|
|
966
|
-
continue;
|
|
967
|
-
if (key.includes('.')) {
|
|
968
|
-
const [first, ...rest] = key.split('.');
|
|
969
|
-
if (isUnsafeKey(first))
|
|
970
|
-
continue;
|
|
971
|
-
const list = nestedKeys.get(first);
|
|
972
|
-
if (list) {
|
|
973
|
-
list.push(rest.join('.'));
|
|
974
|
-
}
|
|
975
|
-
else {
|
|
976
|
-
nestedKeys.set(first, [rest.join('.')]);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
directKeys.add(key);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
const result = {};
|
|
984
|
-
for (const key of directKeys) {
|
|
985
|
-
if (key in obj)
|
|
986
|
-
result[key] = obj[key];
|
|
987
|
-
}
|
|
988
|
-
for (const [first, restKeys] of nestedKeys) {
|
|
989
|
-
const val = obj[first];
|
|
990
|
-
if (!val || typeof val !== 'object')
|
|
991
|
-
continue;
|
|
992
|
-
if (Array.isArray(val)) {
|
|
993
|
-
result[first] = val.map(item => pick(item, restKeys));
|
|
994
|
-
}
|
|
995
|
-
else {
|
|
996
|
-
result[first] = pick(val, restKeys);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return result;
|
|
1000
|
-
};
|
|
1001
|
-
if (Array.isArray(data)) {
|
|
1002
|
-
return data.map(item => pick(item, fields));
|
|
1003
|
-
}
|
|
1004
|
-
return pick(data, fields);
|
|
1005
|
-
}
|
|
1006
982
|
sanitizeOutput(data) {
|
|
1007
983
|
const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
1008
984
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -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,9 +8,9 @@ const HAS_CAMT053 = false;
|
|
|
8
8
|
const NESTED_IMPORTS = [];
|
|
9
9
|
const SUBCOMMANDS = [...["event-webhook", "list", "get"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'event-webhook': { method: 'POST', path: '/v1/email-transactions/event-webhook', summary: 'SendGrid delivery-event webhook (ECDSA signature-verified, no bearer token)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'list': { method: 'GET', path: '/v1/email-transactions', summary: 'List email transactions for the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
13
|
-
'get': { method: 'GET', path: '/v1/email-transactions/{id}', summary: 'Get a single email transaction (404 if not in this tenant)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'event-webhook': { method: 'POST', path: '/v1/email-transactions/event-webhook', summary: 'SendGrid delivery-event webhook (ECDSA signature-verified, no bearer token)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'list': { method: 'GET', path: '/v1/email-transactions', summary: 'List email transactions for the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
13
|
+
'get': { method: 'GET', path: '/v1/email-transactions/{id}', summary: 'Get a single email transaction (404 if not in this tenant)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
14
14
|
};
|
|
15
15
|
export default class EmailTransactions extends Command {
|
|
16
16
|
static description = 'Manage email transactions';
|
|
@@ -38,7 +38,7 @@ export default class EmailTransactions extends Command {
|
|
|
38
38
|
'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
|
|
39
39
|
filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
|
|
40
40
|
query: Flags.string({ description: 'Additional query params as JSON' }),
|
|
41
|
-
fields: Flags.string({ description: 'Comma-separated list of fields to
|
|
41
|
+
fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
|
|
42
42
|
sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
|
|
43
43
|
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
44
44
|
};
|
|
@@ -110,6 +110,8 @@ export default class EmailTransactions extends Command {
|
|
|
110
110
|
queryParams.search = flags.search;
|
|
111
111
|
if (flags['sort-by'])
|
|
112
112
|
queryParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
113
|
+
if (flags.fields)
|
|
114
|
+
queryParams.fields = flags.fields;
|
|
113
115
|
if (flags.query) {
|
|
114
116
|
try {
|
|
115
117
|
Object.assign(queryParams, JSON.parse(flags.query));
|
|
@@ -257,10 +259,11 @@ export default class EmailTransactions extends Command {
|
|
|
257
259
|
process.stderr.write('\n');
|
|
258
260
|
}
|
|
259
261
|
try {
|
|
260
|
-
// Binary response:
|
|
262
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
261
263
|
if (endpoint.isBinaryResponse) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
+
const toStdout = !flags.file;
|
|
265
|
+
if (toStdout && process.stdout.isTTY) {
|
|
266
|
+
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
267
|
}
|
|
265
268
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
266
269
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -268,24 +271,55 @@ export default class EmailTransactions extends Command {
|
|
|
268
271
|
contextName: flags.context,
|
|
269
272
|
binary: true,
|
|
270
273
|
});
|
|
271
|
-
const fsModule = await import('node:fs');
|
|
272
274
|
const binData = binResponse.data;
|
|
273
275
|
const buffer = Buffer.isBuffer(binData)
|
|
274
276
|
? binData
|
|
275
277
|
: Buffer.from(binData);
|
|
278
|
+
if (toStdout) {
|
|
279
|
+
// Nothing else may reach stdout here — it is the file.
|
|
280
|
+
process.stdout.write(buffer);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const fsModule = await import('node:fs');
|
|
276
284
|
fsModule.writeFileSync(flags.file, buffer);
|
|
277
285
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
278
286
|
return;
|
|
279
287
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
288
|
+
let response;
|
|
289
|
+
if (endpoint.hasBinaryVariant) {
|
|
290
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
291
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
292
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
293
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
294
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
295
|
+
data,
|
|
296
|
+
contextName: flags.context,
|
|
297
|
+
binary: true,
|
|
298
|
+
});
|
|
299
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
300
|
+
const rawData = raw.data;
|
|
301
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
302
|
+
if (!isTextualMediaType(rawType)) {
|
|
303
|
+
if (process.stdout.isTTY) {
|
|
304
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
305
|
+
}
|
|
306
|
+
// Nothing else may reach stdout here — it is the file.
|
|
307
|
+
process.stdout.write(buffer);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
314
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
315
|
+
data,
|
|
316
|
+
contextName: flags.context,
|
|
317
|
+
acceptAny: true,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
286
320
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
287
321
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
288
|
-
if (contentType
|
|
322
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
289
323
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
290
324
|
this.log(String(response.data));
|
|
291
325
|
return;
|
|
@@ -312,11 +346,6 @@ export default class EmailTransactions extends Command {
|
|
|
312
346
|
output = obj[arrayKeys[0]];
|
|
313
347
|
}
|
|
314
348
|
}
|
|
315
|
-
// Filter fields if --fields flag is provided
|
|
316
|
-
if (flags.fields) {
|
|
317
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
318
|
-
output = this.filterFields(output, fieldList);
|
|
319
|
-
}
|
|
320
349
|
// Sanitize output if requested
|
|
321
350
|
if (flags.sanitize) {
|
|
322
351
|
output = this.sanitizeOutput(output);
|
|
@@ -571,6 +600,8 @@ export default class EmailTransactions extends Command {
|
|
|
571
600
|
baseParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
572
601
|
if (flags.include)
|
|
573
602
|
baseParams.include = flags.include;
|
|
603
|
+
if (flags.fields)
|
|
604
|
+
baseParams.fields = flags.fields;
|
|
574
605
|
if (flags.query) {
|
|
575
606
|
try {
|
|
576
607
|
Object.assign(baseParams, JSON.parse(flags.query));
|
|
@@ -625,12 +656,7 @@ export default class EmailTransactions extends Command {
|
|
|
625
656
|
}
|
|
626
657
|
}
|
|
627
658
|
process.stderr.write(' done\n');
|
|
628
|
-
// Filter fields if specified
|
|
629
659
|
let output = allItems;
|
|
630
|
-
if (flags.fields) {
|
|
631
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
632
|
-
output = this.filterFields(allItems, fieldList);
|
|
633
|
-
}
|
|
634
660
|
// Sanitize output if requested
|
|
635
661
|
if (flags.sanitize) {
|
|
636
662
|
output = this.sanitizeOutput(output);
|
|
@@ -959,56 +985,6 @@ export default class EmailTransactions extends Command {
|
|
|
959
985
|
return `\$${op}`;
|
|
960
986
|
return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
|
|
961
987
|
}
|
|
962
|
-
filterFields(data, fields) {
|
|
963
|
-
const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
|
|
964
|
-
const pick = (obj, keys) => {
|
|
965
|
-
// Group keys by their first segment so sibling sub-paths
|
|
966
|
-
// (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
|
|
967
|
-
// and don't clobber each other on merge.
|
|
968
|
-
const directKeys = new Set();
|
|
969
|
-
const nestedKeys = new Map();
|
|
970
|
-
for (const key of keys) {
|
|
971
|
-
if (key.split('.').some(isUnsafeKey))
|
|
972
|
-
continue;
|
|
973
|
-
if (key.includes('.')) {
|
|
974
|
-
const [first, ...rest] = key.split('.');
|
|
975
|
-
if (isUnsafeKey(first))
|
|
976
|
-
continue;
|
|
977
|
-
const list = nestedKeys.get(first);
|
|
978
|
-
if (list) {
|
|
979
|
-
list.push(rest.join('.'));
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
nestedKeys.set(first, [rest.join('.')]);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
else {
|
|
986
|
-
directKeys.add(key);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
const result = {};
|
|
990
|
-
for (const key of directKeys) {
|
|
991
|
-
if (key in obj)
|
|
992
|
-
result[key] = obj[key];
|
|
993
|
-
}
|
|
994
|
-
for (const [first, restKeys] of nestedKeys) {
|
|
995
|
-
const val = obj[first];
|
|
996
|
-
if (!val || typeof val !== 'object')
|
|
997
|
-
continue;
|
|
998
|
-
if (Array.isArray(val)) {
|
|
999
|
-
result[first] = val.map(item => pick(item, restKeys));
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
result[first] = pick(val, restKeys);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return result;
|
|
1006
|
-
};
|
|
1007
|
-
if (Array.isArray(data)) {
|
|
1008
|
-
return data.map(item => pick(item, fields));
|
|
1009
|
-
}
|
|
1010
|
-
return pick(data, fields);
|
|
1011
|
-
}
|
|
1012
988
|
sanitizeOutput(data) {
|
|
1013
989
|
const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
1014
990
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|