@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,32 +1,42 @@
|
|
|
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 = true;
|
|
8
|
-
const NESTED_IMPORTS = ["allocations", "allocations-notes"];
|
|
9
|
-
const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "allocations-reprocess-unbooked", "allocations-unbooked", "list-allocations", "create-allocations", "get-allocations", "update-allocations", "delete-allocations", "allocations-confirm", "import-camt053", "list-allocations-notes", "create-allocations-notes", "get-allocations-notes", "update-allocations-notes", "delete-allocations-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
8
|
+
const NESTED_IMPORTS = ["allocations", "allocations-notes", "notes"];
|
|
9
|
+
const SUBCOMMANDS = [...["retry-booking", "recover-booking", "list", "create", "get", "update", "delete", "reconcile", "allocations-reprocess-unbooked", "allocations-unbooked", "list-allocations", "create-allocations", "get-allocations", "update-allocations", "delete-allocations", "list-allocations-settlement-accounts", "allocations-confirm", "import", "import-camt053", "list-allocations-notes", "create-allocations-notes", "get-allocations-notes", "update-allocations-notes", "delete-allocations-notes", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'allocations
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
11
|
+
'retry-booking': { method: 'POST', path: '/v1/account-transactions/{transactionId}/retry-booking', summary: 'Retry complete booking of an account transaction', pathParams: ['transactionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['allocations'] },
|
|
12
|
+
'recover-booking': { method: 'POST', path: '/v1/account-transactions/{transactionId}/recover-booking', summary: 'Verify existing accounting and book missing entries together', pathParams: ['transactionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['allocations'] },
|
|
13
|
+
'list': { method: 'GET', path: '/v1/account-transactions', summary: 'Get all account transactions with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
14
|
+
'create': { method: 'POST', path: '/v1/account-transactions', summary: 'Create a new account transaction', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['allocations'] },
|
|
15
|
+
'get': { method: 'GET', path: '/v1/account-transactions/{id}', summary: 'Get an account transaction by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
|
+
'update': { method: 'PATCH', path: '/v1/account-transactions/{id}', summary: 'Update an account transaction', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['allocations'] },
|
|
17
|
+
'delete': { method: 'DELETE', path: '/v1/account-transactions/{id}', summary: 'Delete an account transaction', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
18
|
+
'reconcile': { method: 'POST', path: '/v1/account-transactions/reconcile', summary: 'Send selected transactions to the reconciliation agent', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['transactionIds'] },
|
|
19
|
+
'allocations-reprocess-unbooked': { method: 'POST', path: '/v1/account-transactions/allocations/reprocess-unbooked', summary: 'Internal — re-book or repair allocations stuck at CONFIRMED', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['allocationIds'] },
|
|
20
|
+
'allocations-unbooked': { method: 'GET', path: '/v1/account-transactions/allocations/unbooked', summary: 'Internal — list allocations stuck at CONFIRMED', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
21
|
+
'list-allocations': { method: 'GET', path: '/v1/account-transactions/{transactionId}/allocations', summary: 'Get all allocations for an account transaction', pathParams: ['transactionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
22
|
+
'create-allocations': { method: 'POST', path: '/v1/account-transactions/{transactionId}/allocations', summary: 'Create an allocation for an account transaction', pathParams: ['transactionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['notes'] },
|
|
23
|
+
'get-allocations': { method: 'GET', path: '/v1/account-transactions/{transactionId}/allocations/{allocationId}', summary: 'Get an allocation by ID', pathParams: ['transactionId', 'allocationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
24
|
+
'update-allocations': { method: 'PATCH', path: '/v1/account-transactions/{transactionId}/allocations/{allocationId}', summary: 'Update an allocation', pathParams: ['transactionId', 'allocationId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['notes'] },
|
|
25
|
+
'delete-allocations': { method: 'DELETE', path: '/v1/account-transactions/{transactionId}/allocations/{allocationId}', summary: 'Delete an allocation', pathParams: ['transactionId', 'allocationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
26
|
+
'list-allocations-settlement-accounts': { method: 'GET', path: '/v1/account-transactions/{transactionId}/allocations/{allocationId}/settlement-accounts', summary: 'Which account each ledger would settle the allocated document against', pathParams: ['transactionId', 'allocationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
27
|
+
'allocations-confirm': { method: 'POST', path: '/v1/account-transactions/{transactionId}/allocations/{allocationId}/confirm', summary: 'Confirm an allocation (transition from REVIEW to CONFIRMED)', pathParams: ['transactionId', 'allocationId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['counterAccounts'] },
|
|
28
|
+
'import': { method: 'POST', path: '/v1/account-transactions/import', summary: 'Import a batch of account transactions', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['transactions'] },
|
|
29
|
+
'import-camt053': { method: 'POST', path: '/v1/account-transactions/import/camt053', summary: 'Import CAMT.053 bank statement (ISO 20022 format)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
30
|
+
'list-allocations-notes': { method: 'GET', path: '/v1/account-transactions/allocations/{allocationId}/notes', summary: 'Get all notes for an allocation', pathParams: ['allocationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
31
|
+
'create-allocations-notes': { method: 'POST', path: '/v1/account-transactions/allocations/{allocationId}/notes', summary: 'Create a note for an allocation', pathParams: ['allocationId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
32
|
+
'get-allocations-notes': { method: 'GET', path: '/v1/account-transactions/allocations/{allocationId}/notes/{noteId}', summary: 'Get a specific note for an allocation', pathParams: ['allocationId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
33
|
+
'update-allocations-notes': { method: 'PATCH', path: '/v1/account-transactions/allocations/{allocationId}/notes/{noteId}', summary: 'Update a note for an allocation', pathParams: ['allocationId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
34
|
+
'delete-allocations-notes': { method: 'DELETE', path: '/v1/account-transactions/allocations/{allocationId}/notes/{noteId}', summary: 'Delete a note from an allocation', pathParams: ['allocationId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
35
|
+
'list-notes': { method: 'GET', path: '/v1/account-transactions/{transactionId}/notes', summary: 'Get all notes for an account transaction', pathParams: ['transactionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
36
|
+
'create-notes': { method: 'POST', path: '/v1/account-transactions/{transactionId}/notes', summary: 'Create a note for an account transaction', pathParams: ['transactionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
37
|
+
'get-notes': { method: 'GET', path: '/v1/account-transactions/{transactionId}/notes/{noteId}', summary: 'Get a specific note for an account transaction', pathParams: ['transactionId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
38
|
+
'update-notes': { method: 'PATCH', path: '/v1/account-transactions/{transactionId}/notes/{noteId}', summary: 'Update a note for an account transaction', pathParams: ['transactionId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
39
|
+
'delete-notes': { method: 'DELETE', path: '/v1/account-transactions/{transactionId}/notes/{noteId}', summary: 'Delete a note from an account transaction', pathParams: ['transactionId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
30
40
|
};
|
|
31
41
|
export default class AccountTransactions extends Command {
|
|
32
42
|
static description = 'Manage account transactions';
|
|
@@ -59,7 +69,7 @@ export default class AccountTransactions extends Command {
|
|
|
59
69
|
'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
|
|
60
70
|
filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
|
|
61
71
|
query: Flags.string({ description: 'Additional query params as JSON' }),
|
|
62
|
-
fields: Flags.string({ description: 'Comma-separated list of fields to
|
|
72
|
+
fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
|
|
63
73
|
sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
|
|
64
74
|
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
65
75
|
};
|
|
@@ -131,6 +141,8 @@ export default class AccountTransactions extends Command {
|
|
|
131
141
|
queryParams.search = flags.search;
|
|
132
142
|
if (flags['sort-by'])
|
|
133
143
|
queryParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
144
|
+
if (flags.fields)
|
|
145
|
+
queryParams.fields = flags.fields;
|
|
134
146
|
if (flags.query) {
|
|
135
147
|
try {
|
|
136
148
|
Object.assign(queryParams, JSON.parse(flags.query));
|
|
@@ -278,10 +290,11 @@ export default class AccountTransactions extends Command {
|
|
|
278
290
|
process.stderr.write('\n');
|
|
279
291
|
}
|
|
280
292
|
try {
|
|
281
|
-
// Binary response:
|
|
293
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
282
294
|
if (endpoint.isBinaryResponse) {
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
const toStdout = !flags.file;
|
|
296
|
+
if (toStdout && process.stdout.isTTY) {
|
|
297
|
+
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.`);
|
|
285
298
|
}
|
|
286
299
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
287
300
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -289,24 +302,55 @@ export default class AccountTransactions extends Command {
|
|
|
289
302
|
contextName: flags.context,
|
|
290
303
|
binary: true,
|
|
291
304
|
});
|
|
292
|
-
const fsModule = await import('node:fs');
|
|
293
305
|
const binData = binResponse.data;
|
|
294
306
|
const buffer = Buffer.isBuffer(binData)
|
|
295
307
|
? binData
|
|
296
308
|
: Buffer.from(binData);
|
|
309
|
+
if (toStdout) {
|
|
310
|
+
// Nothing else may reach stdout here — it is the file.
|
|
311
|
+
process.stdout.write(buffer);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const fsModule = await import('node:fs');
|
|
297
315
|
fsModule.writeFileSync(flags.file, buffer);
|
|
298
316
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
299
317
|
return;
|
|
300
318
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
319
|
+
let response;
|
|
320
|
+
if (endpoint.hasBinaryVariant) {
|
|
321
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
322
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
323
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
324
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
325
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
326
|
+
data,
|
|
327
|
+
contextName: flags.context,
|
|
328
|
+
binary: true,
|
|
329
|
+
});
|
|
330
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
331
|
+
const rawData = raw.data;
|
|
332
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
333
|
+
if (!isTextualMediaType(rawType)) {
|
|
334
|
+
if (process.stdout.isTTY) {
|
|
335
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
336
|
+
}
|
|
337
|
+
// Nothing else may reach stdout here — it is the file.
|
|
338
|
+
process.stdout.write(buffer);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
345
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
346
|
+
data,
|
|
347
|
+
contextName: flags.context,
|
|
348
|
+
acceptAny: true,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
307
351
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
308
352
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
309
|
-
if (contentType
|
|
353
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
310
354
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
311
355
|
this.log(String(response.data));
|
|
312
356
|
return;
|
|
@@ -333,11 +377,6 @@ export default class AccountTransactions extends Command {
|
|
|
333
377
|
output = obj[arrayKeys[0]];
|
|
334
378
|
}
|
|
335
379
|
}
|
|
336
|
-
// Filter fields if --fields flag is provided
|
|
337
|
-
if (flags.fields) {
|
|
338
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
339
|
-
output = this.filterFields(output, fieldList);
|
|
340
|
-
}
|
|
341
380
|
// Sanitize output if requested
|
|
342
381
|
if (flags.sanitize) {
|
|
343
382
|
output = this.sanitizeOutput(output);
|
|
@@ -592,6 +631,8 @@ export default class AccountTransactions extends Command {
|
|
|
592
631
|
baseParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
593
632
|
if (flags.include)
|
|
594
633
|
baseParams.include = flags.include;
|
|
634
|
+
if (flags.fields)
|
|
635
|
+
baseParams.fields = flags.fields;
|
|
595
636
|
if (flags.query) {
|
|
596
637
|
try {
|
|
597
638
|
Object.assign(baseParams, JSON.parse(flags.query));
|
|
@@ -646,12 +687,7 @@ export default class AccountTransactions extends Command {
|
|
|
646
687
|
}
|
|
647
688
|
}
|
|
648
689
|
process.stderr.write(' done\n');
|
|
649
|
-
// Filter fields if specified
|
|
650
690
|
let output = allItems;
|
|
651
|
-
if (flags.fields) {
|
|
652
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
653
|
-
output = this.filterFields(allItems, fieldList);
|
|
654
|
-
}
|
|
655
691
|
// Sanitize output if requested
|
|
656
692
|
if (flags.sanitize) {
|
|
657
693
|
output = this.sanitizeOutput(output);
|
|
@@ -980,56 +1016,6 @@ export default class AccountTransactions extends Command {
|
|
|
980
1016
|
return `\$${op}`;
|
|
981
1017
|
return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
|
|
982
1018
|
}
|
|
983
|
-
filterFields(data, fields) {
|
|
984
|
-
const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
|
|
985
|
-
const pick = (obj, keys) => {
|
|
986
|
-
// Group keys by their first segment so sibling sub-paths
|
|
987
|
-
// (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
|
|
988
|
-
// and don't clobber each other on merge.
|
|
989
|
-
const directKeys = new Set();
|
|
990
|
-
const nestedKeys = new Map();
|
|
991
|
-
for (const key of keys) {
|
|
992
|
-
if (key.split('.').some(isUnsafeKey))
|
|
993
|
-
continue;
|
|
994
|
-
if (key.includes('.')) {
|
|
995
|
-
const [first, ...rest] = key.split('.');
|
|
996
|
-
if (isUnsafeKey(first))
|
|
997
|
-
continue;
|
|
998
|
-
const list = nestedKeys.get(first);
|
|
999
|
-
if (list) {
|
|
1000
|
-
list.push(rest.join('.'));
|
|
1001
|
-
}
|
|
1002
|
-
else {
|
|
1003
|
-
nestedKeys.set(first, [rest.join('.')]);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
else {
|
|
1007
|
-
directKeys.add(key);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
const result = {};
|
|
1011
|
-
for (const key of directKeys) {
|
|
1012
|
-
if (key in obj)
|
|
1013
|
-
result[key] = obj[key];
|
|
1014
|
-
}
|
|
1015
|
-
for (const [first, restKeys] of nestedKeys) {
|
|
1016
|
-
const val = obj[first];
|
|
1017
|
-
if (!val || typeof val !== 'object')
|
|
1018
|
-
continue;
|
|
1019
|
-
if (Array.isArray(val)) {
|
|
1020
|
-
result[first] = val.map(item => pick(item, restKeys));
|
|
1021
|
-
}
|
|
1022
|
-
else {
|
|
1023
|
-
result[first] = pick(val, restKeys);
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
return result;
|
|
1027
|
-
};
|
|
1028
|
-
if (Array.isArray(data)) {
|
|
1029
|
-
return data.map(item => pick(item, fields));
|
|
1030
|
-
}
|
|
1031
|
-
return pick(data, fields);
|
|
1032
|
-
}
|
|
1033
1019
|
sanitizeOutput(data) {
|
|
1034
1020
|
const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
1035
1021
|
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 = true;
|
|
@@ -8,16 +8,16 @@ const HAS_CAMT053 = false;
|
|
|
8
8
|
const NESTED_IMPORTS = ["notes"];
|
|
9
9
|
const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'list': { method: 'GET', path: '/v1/accounts', summary: 'List all accounts', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'create': { method: 'POST', path: '/v1/accounts', summary: 'Create a new account', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
|
|
13
|
-
'get': { method: 'GET', path: '/v1/accounts/{id}', summary: 'Get an account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
14
|
-
'update': { method: 'PATCH', path: '/v1/accounts/{id}', summary: 'Update an account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['tagIds', 'tagCodes'] },
|
|
15
|
-
'delete': { method: 'DELETE', path: '/v1/accounts/{id}', summary: 'Delete an account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
16
|
-
'list-notes': { method: 'GET', path: '/v1/accounts/{accountId}/notes', summary: 'Get all notes for an account', pathParams: ['accountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
17
|
-
'create-notes': { method: 'POST', path: '/v1/accounts/{accountId}/notes', summary: 'Create a note for an account', pathParams: ['accountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
18
|
-
'get-notes': { method: 'GET', path: '/v1/accounts/{accountId}/notes/{noteId}', summary: 'Get a specific note for an account', pathParams: ['accountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
19
|
-
'update-notes': { method: 'PATCH', path: '/v1/accounts/{accountId}/notes/{noteId}', summary: 'Update a note for an account', pathParams: ['accountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
20
|
-
'delete-notes': { method: 'DELETE', path: '/v1/accounts/{accountId}/notes/{noteId}', summary: 'Delete a note from an account', pathParams: ['accountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'list': { method: 'GET', path: '/v1/accounts', summary: 'List all accounts', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/accounts', summary: 'Create a new account', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes', 'notes'] },
|
|
13
|
+
'get': { method: 'GET', path: '/v1/accounts/{id}', summary: 'Get an account by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
14
|
+
'update': { method: 'PATCH', path: '/v1/accounts/{id}', summary: 'Update an account', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['tagIds', 'tagCodes'] },
|
|
15
|
+
'delete': { method: 'DELETE', path: '/v1/accounts/{id}', summary: 'Delete an account', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
|
+
'list-notes': { method: 'GET', path: '/v1/accounts/{accountId}/notes', summary: 'Get all notes for an account', pathParams: ['accountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
17
|
+
'create-notes': { method: 'POST', path: '/v1/accounts/{accountId}/notes', summary: 'Create a note for an account', pathParams: ['accountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
18
|
+
'get-notes': { method: 'GET', path: '/v1/accounts/{accountId}/notes/{noteId}', summary: 'Get a specific note for an account', pathParams: ['accountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
19
|
+
'update-notes': { method: 'PATCH', path: '/v1/accounts/{accountId}/notes/{noteId}', summary: 'Update a note for an account', pathParams: ['accountId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
20
|
+
'delete-notes': { method: 'DELETE', path: '/v1/accounts/{accountId}/notes/{noteId}', summary: 'Delete a note from an account', pathParams: ['accountId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
21
21
|
};
|
|
22
22
|
export default class Accounts extends Command {
|
|
23
23
|
static description = 'Manage accounts';
|
|
@@ -49,7 +49,7 @@ export default class Accounts extends Command {
|
|
|
49
49
|
'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
|
|
50
50
|
filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
|
|
51
51
|
query: Flags.string({ description: 'Additional query params as JSON' }),
|
|
52
|
-
fields: Flags.string({ description: 'Comma-separated list of fields to
|
|
52
|
+
fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
|
|
53
53
|
sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
|
|
54
54
|
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
55
55
|
};
|
|
@@ -121,6 +121,8 @@ export default class Accounts extends Command {
|
|
|
121
121
|
queryParams.search = flags.search;
|
|
122
122
|
if (flags['sort-by'])
|
|
123
123
|
queryParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
124
|
+
if (flags.fields)
|
|
125
|
+
queryParams.fields = flags.fields;
|
|
124
126
|
if (flags.query) {
|
|
125
127
|
try {
|
|
126
128
|
Object.assign(queryParams, JSON.parse(flags.query));
|
|
@@ -268,10 +270,11 @@ export default class Accounts extends Command {
|
|
|
268
270
|
process.stderr.write('\n');
|
|
269
271
|
}
|
|
270
272
|
try {
|
|
271
|
-
// Binary response:
|
|
273
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
272
274
|
if (endpoint.isBinaryResponse) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
const toStdout = !flags.file;
|
|
276
|
+
if (toStdout && process.stdout.isTTY) {
|
|
277
|
+
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.`);
|
|
275
278
|
}
|
|
276
279
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
277
280
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -279,24 +282,55 @@ export default class Accounts extends Command {
|
|
|
279
282
|
contextName: flags.context,
|
|
280
283
|
binary: true,
|
|
281
284
|
});
|
|
282
|
-
const fsModule = await import('node:fs');
|
|
283
285
|
const binData = binResponse.data;
|
|
284
286
|
const buffer = Buffer.isBuffer(binData)
|
|
285
287
|
? binData
|
|
286
288
|
: Buffer.from(binData);
|
|
289
|
+
if (toStdout) {
|
|
290
|
+
// Nothing else may reach stdout here — it is the file.
|
|
291
|
+
process.stdout.write(buffer);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const fsModule = await import('node:fs');
|
|
287
295
|
fsModule.writeFileSync(flags.file, buffer);
|
|
288
296
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
289
297
|
return;
|
|
290
298
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
let response;
|
|
300
|
+
if (endpoint.hasBinaryVariant) {
|
|
301
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
302
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
303
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
304
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
305
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
306
|
+
data,
|
|
307
|
+
contextName: flags.context,
|
|
308
|
+
binary: true,
|
|
309
|
+
});
|
|
310
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
311
|
+
const rawData = raw.data;
|
|
312
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
313
|
+
if (!isTextualMediaType(rawType)) {
|
|
314
|
+
if (process.stdout.isTTY) {
|
|
315
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
316
|
+
}
|
|
317
|
+
// Nothing else may reach stdout here — it is the file.
|
|
318
|
+
process.stdout.write(buffer);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
325
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
326
|
+
data,
|
|
327
|
+
contextName: flags.context,
|
|
328
|
+
acceptAny: true,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
297
331
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
298
332
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
299
|
-
if (contentType
|
|
333
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
300
334
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
301
335
|
this.log(String(response.data));
|
|
302
336
|
return;
|
|
@@ -323,11 +357,6 @@ export default class Accounts extends Command {
|
|
|
323
357
|
output = obj[arrayKeys[0]];
|
|
324
358
|
}
|
|
325
359
|
}
|
|
326
|
-
// Filter fields if --fields flag is provided
|
|
327
|
-
if (flags.fields) {
|
|
328
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
329
|
-
output = this.filterFields(output, fieldList);
|
|
330
|
-
}
|
|
331
360
|
// Sanitize output if requested
|
|
332
361
|
if (flags.sanitize) {
|
|
333
362
|
output = this.sanitizeOutput(output);
|
|
@@ -582,6 +611,8 @@ export default class Accounts extends Command {
|
|
|
582
611
|
baseParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
583
612
|
if (flags.include)
|
|
584
613
|
baseParams.include = flags.include;
|
|
614
|
+
if (flags.fields)
|
|
615
|
+
baseParams.fields = flags.fields;
|
|
585
616
|
if (flags.query) {
|
|
586
617
|
try {
|
|
587
618
|
Object.assign(baseParams, JSON.parse(flags.query));
|
|
@@ -636,12 +667,7 @@ export default class Accounts extends Command {
|
|
|
636
667
|
}
|
|
637
668
|
}
|
|
638
669
|
process.stderr.write(' done\n');
|
|
639
|
-
// Filter fields if specified
|
|
640
670
|
let output = allItems;
|
|
641
|
-
if (flags.fields) {
|
|
642
|
-
const fieldList = flags.fields.split(',').map(f => f.trim());
|
|
643
|
-
output = this.filterFields(allItems, fieldList);
|
|
644
|
-
}
|
|
645
671
|
// Sanitize output if requested
|
|
646
672
|
if (flags.sanitize) {
|
|
647
673
|
output = this.sanitizeOutput(output);
|
|
@@ -970,56 +996,6 @@ export default class Accounts extends Command {
|
|
|
970
996
|
return `\$${op}`;
|
|
971
997
|
return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
|
|
972
998
|
}
|
|
973
|
-
filterFields(data, fields) {
|
|
974
|
-
const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
|
|
975
|
-
const pick = (obj, keys) => {
|
|
976
|
-
// Group keys by their first segment so sibling sub-paths
|
|
977
|
-
// (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
|
|
978
|
-
// and don't clobber each other on merge.
|
|
979
|
-
const directKeys = new Set();
|
|
980
|
-
const nestedKeys = new Map();
|
|
981
|
-
for (const key of keys) {
|
|
982
|
-
if (key.split('.').some(isUnsafeKey))
|
|
983
|
-
continue;
|
|
984
|
-
if (key.includes('.')) {
|
|
985
|
-
const [first, ...rest] = key.split('.');
|
|
986
|
-
if (isUnsafeKey(first))
|
|
987
|
-
continue;
|
|
988
|
-
const list = nestedKeys.get(first);
|
|
989
|
-
if (list) {
|
|
990
|
-
list.push(rest.join('.'));
|
|
991
|
-
}
|
|
992
|
-
else {
|
|
993
|
-
nestedKeys.set(first, [rest.join('.')]);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
else {
|
|
997
|
-
directKeys.add(key);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
const result = {};
|
|
1001
|
-
for (const key of directKeys) {
|
|
1002
|
-
if (key in obj)
|
|
1003
|
-
result[key] = obj[key];
|
|
1004
|
-
}
|
|
1005
|
-
for (const [first, restKeys] of nestedKeys) {
|
|
1006
|
-
const val = obj[first];
|
|
1007
|
-
if (!val || typeof val !== 'object')
|
|
1008
|
-
continue;
|
|
1009
|
-
if (Array.isArray(val)) {
|
|
1010
|
-
result[first] = val.map(item => pick(item, restKeys));
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
result[first] = pick(val, restKeys);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
return result;
|
|
1017
|
-
};
|
|
1018
|
-
if (Array.isArray(data)) {
|
|
1019
|
-
return data.map(item => pick(item, fields));
|
|
1020
|
-
}
|
|
1021
|
-
return pick(data, fields);
|
|
1022
|
-
}
|
|
1023
999
|
sanitizeOutput(data) {
|
|
1024
1000
|
const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
1025
1001
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|