@aderis-erp/cli 1.1.2 → 1.1.4
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 +1 -0
- package/dist/commands/access-groups.d.ts +11 -2
- package/dist/commands/access-groups.js +158 -24
- package/dist/commands/account-transaction-allocations.d.ts +11 -2
- package/dist/commands/account-transaction-allocations.js +157 -23
- package/dist/commands/account-transactions.d.ts +15 -2
- package/dist/commands/account-transactions.js +162 -24
- package/dist/commands/accounts.d.ts +11 -2
- package/dist/commands/accounts.js +158 -24
- package/dist/commands/agent-platform.d.ts +11 -3
- package/dist/commands/agent-platform.js +157 -32
- package/dist/commands/agent-runs.d.ts +11 -2
- package/dist/commands/agent-runs.js +157 -23
- package/dist/commands/analytics.d.ts +11 -2
- package/dist/commands/analytics.js +157 -23
- package/dist/commands/api-clients.d.ts +58 -0
- package/dist/commands/api-clients.js +1341 -0
- package/dist/commands/api-keys.d.ts +11 -2
- package/dist/commands/api-keys.js +157 -23
- package/dist/commands/async-jobs.d.ts +11 -2
- package/dist/commands/async-jobs.js +158 -24
- package/dist/commands/audit-log.d.ts +11 -2
- package/dist/commands/audit-log.js +157 -23
- package/dist/commands/bank-accounts.d.ts +11 -2
- package/dist/commands/bank-accounts.js +159 -25
- package/dist/commands/bank-integrations.d.ts +11 -2
- package/dist/commands/bank-integrations.js +158 -24
- package/dist/commands/business-partners.d.ts +11 -2
- package/dist/commands/business-partners.js +158 -24
- package/dist/commands/chat.d.ts +11 -2
- package/dist/commands/chat.js +158 -24
- package/dist/commands/credit-card-accounts.d.ts +15 -2
- package/dist/commands/credit-card-accounts.js +163 -24
- package/dist/commands/currencies.d.ts +56 -0
- package/dist/commands/currencies.js +1327 -0
- package/dist/commands/custody-accounts.d.ts +56 -0
- package/dist/commands/custody-accounts.js +1335 -0
- package/dist/commands/dashboards.d.ts +57 -0
- package/dist/commands/dashboards.js +1347 -0
- package/dist/commands/delivery-notes.d.ts +11 -2
- package/dist/commands/delivery-notes.js +158 -24
- package/dist/commands/{integration-identities.d.ts → document-checksums.d.ts} +12 -4
- package/dist/commands/{integration-identities.js → document-checksums.js} +162 -36
- package/dist/commands/dunning-configs.d.ts +11 -2
- package/dist/commands/dunning-configs.js +158 -24
- package/dist/commands/dunning-reminders.d.ts +11 -2
- package/dist/commands/dunning-reminders.js +157 -23
- package/dist/commands/dunning-runs.d.ts +11 -2
- package/dist/commands/dunning-runs.js +157 -23
- package/dist/commands/email-transactions.d.ts +11 -2
- package/dist/commands/email-transactions.js +157 -23
- package/dist/commands/employees.d.ts +13 -2
- package/dist/commands/employees.js +169 -24
- package/dist/commands/entity-configurations.d.ts +11 -2
- package/dist/commands/entity-configurations.js +157 -23
- package/dist/commands/event-mapping-rules.d.ts +56 -0
- package/dist/commands/event-mapping-rules.js +1324 -0
- package/dist/commands/event-processor-mappings.d.ts +11 -2
- package/dist/commands/event-processor-mappings.js +158 -24
- package/dist/commands/files.d.ts +11 -2
- package/dist/commands/files.js +158 -24
- package/dist/commands/financial-instrument-types.d.ts +56 -0
- package/dist/commands/financial-instrument-types.js +1335 -0
- package/dist/commands/financial-instruments.d.ts +56 -0
- package/dist/commands/financial-instruments.js +1335 -0
- package/dist/commands/health.d.ts +11 -2
- package/dist/commands/health.js +157 -23
- package/dist/commands/imports.d.ts +11 -2
- package/dist/commands/imports.js +157 -23
- package/dist/commands/inbox.d.ts +11 -2
- package/dist/commands/inbox.js +157 -23
- package/dist/commands/integrations.d.ts +11 -2
- package/dist/commands/integrations.js +157 -23
- package/dist/commands/item-categories.d.ts +11 -2
- package/dist/commands/item-categories.js +158 -24
- package/dist/commands/items.d.ts +15 -2
- package/dist/commands/items.js +165 -26
- package/dist/commands/journal-lines.d.ts +11 -2
- package/dist/commands/journal-lines.js +157 -23
- package/dist/commands/journals.d.ts +15 -2
- package/dist/commands/journals.js +163 -24
- package/dist/commands/{workflows.d.ts → kpis.d.ts} +12 -3
- package/dist/commands/kpis.js +1337 -0
- package/dist/commands/ledgers.d.ts +11 -2
- package/dist/commands/ledgers.js +159 -25
- package/dist/commands/my-account.d.ts +11 -2
- package/dist/commands/my-account.js +157 -23
- package/dist/commands/notifications.d.ts +56 -0
- package/dist/commands/notifications.js +1326 -0
- package/dist/commands/office-locations.d.ts +11 -2
- package/dist/commands/office-locations.js +158 -24
- package/dist/commands/payment-methods.d.ts +11 -2
- package/dist/commands/payment-methods.js +158 -24
- package/dist/commands/payment-runs.d.ts +11 -2
- package/dist/commands/payment-runs.js +158 -24
- package/dist/commands/payment-terms.d.ts +11 -2
- package/dist/commands/payment-terms.js +159 -25
- package/dist/commands/payments.d.ts +11 -2
- package/dist/commands/payments.js +158 -24
- package/dist/commands/payroll-runs.d.ts +11 -2
- package/dist/commands/payroll-runs.js +158 -24
- package/dist/commands/permissions.d.ts +11 -2
- package/dist/commands/permissions.js +157 -23
- package/dist/commands/price-lists.d.ts +15 -2
- package/dist/commands/price-lists.js +164 -24
- package/dist/commands/processor-mappings.d.ts +11 -2
- package/dist/commands/processor-mappings.js +158 -24
- package/dist/commands/processors.d.ts +11 -2
- package/dist/commands/processors.js +158 -24
- package/dist/commands/purchase-order-types.d.ts +11 -2
- package/dist/commands/purchase-order-types.js +158 -24
- package/dist/commands/purchase-orders.d.ts +11 -2
- package/dist/commands/purchase-orders.js +158 -24
- package/dist/commands/reports.d.ts +11 -2
- package/dist/commands/reports.js +157 -23
- package/dist/commands/roles.d.ts +11 -2
- package/dist/commands/roles.js +158 -24
- package/dist/commands/salary-types.d.ts +11 -2
- package/dist/commands/salary-types.js +158 -24
- package/dist/commands/sales-invoices.d.ts +11 -2
- package/dist/commands/sales-invoices.js +161 -24
- package/dist/commands/sales-orders.d.ts +11 -2
- package/dist/commands/sales-orders.js +159 -25
- package/dist/commands/saved-views.d.ts +11 -2
- package/dist/commands/saved-views.js +157 -23
- package/dist/commands/scheduled-processor-mappings.d.ts +11 -2
- package/dist/commands/scheduled-processor-mappings.js +158 -24
- package/dist/commands/schedules.d.ts +11 -2
- package/dist/commands/schedules.js +158 -24
- package/dist/commands/settings.d.ts +56 -0
- package/dist/commands/settings.js +1327 -0
- package/dist/commands/tags.d.ts +11 -2
- package/dist/commands/tags.js +158 -24
- package/dist/commands/tax-codes.d.ts +11 -2
- package/dist/commands/tax-codes.js +158 -24
- package/dist/commands/tax-matrix.d.ts +11 -2
- package/dist/commands/tax-matrix.js +158 -24
- package/dist/commands/templates.d.ts +11 -2
- package/dist/commands/templates.js +163 -24
- package/dist/commands/tenants.d.ts +11 -2
- package/dist/commands/tenants.js +158 -24
- package/dist/commands/tools.d.ts +11 -2
- package/dist/commands/tools.js +157 -23
- package/dist/commands/users.d.ts +11 -2
- package/dist/commands/users.js +158 -24
- package/dist/commands/vat-declarations.d.ts +11 -2
- package/dist/commands/vat-declarations.js +158 -24
- package/dist/commands/vendor-invoices.d.ts +11 -2
- package/dist/commands/vendor-invoices.js +162 -24
- package/dist/commands/webhooks.d.ts +11 -2
- package/dist/commands/webhooks.js +158 -24
- package/dist/commands/work-summary.d.ts +56 -0
- package/dist/commands/{workflows.js → work-summary.js} +164 -33
- package/dist/lib/api-client.d.ts +1 -0
- package/dist/lib/api-client.js +3 -0
- package/dist/lib/auth/index.js +5 -0
- package/dist/lib/production-guard.d.ts +42 -0
- package/dist/lib/production-guard.js +92 -0
- package/oclif.manifest.json +2358 -442
- package/package.json +1 -1
|
@@ -0,0 +1,1324 @@
|
|
|
1
|
+
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
|
+
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
|
+
// 'upsert' loops create/update from the CLI, one request per record. It exists
|
|
5
|
+
// only where the API has no import endpoint of its own; those keep the name
|
|
6
|
+
// 'import' and are listed in SERVER_IMPORTS.
|
|
7
|
+
const HAS_UPSERT = false;
|
|
8
|
+
const HAS_EXPORT = false;
|
|
9
|
+
const HAS_CAMT053 = false;
|
|
10
|
+
const SERVER_IMPORTS = [];
|
|
11
|
+
const NESTED_UPSERTS = [];
|
|
12
|
+
const SUBCOMMANDS = [...["fields"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
|
|
13
|
+
const ENDPOINTS = {
|
|
14
|
+
'fields': { method: 'GET', path: '/v1/event-mapping-rules/fields', summary: 'List the fields an event mapping’s conditions may test', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
15
|
+
};
|
|
16
|
+
export default class EventMappingRules extends Command {
|
|
17
|
+
static description = 'Manage event mapping rules';
|
|
18
|
+
static examples = [];
|
|
19
|
+
static args = {
|
|
20
|
+
action: Args.string({ description: 'Action: ' + SUBCOMMANDS.join(', '), required: true }),
|
|
21
|
+
id: Args.string({ description: 'Resource ID (for get, update, delete operations)', required: false }),
|
|
22
|
+
};
|
|
23
|
+
static flags = {
|
|
24
|
+
context: Flags.string({ char: 'c', aliases: ['config'], env: 'ADERIS_CONTEXT', description: 'Context to use for this command, overriding the current context (alias: --config)' }),
|
|
25
|
+
output: Flags.string({ char: 'o', description: 'Output format (default: table for upsert, json for other commands)', options: ['json', 'table', 'csv'] }),
|
|
26
|
+
table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
|
|
27
|
+
csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
|
|
28
|
+
data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
|
|
29
|
+
file: Flags.string({ char: 'f', description: 'Read request body from file' }),
|
|
30
|
+
set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
|
|
31
|
+
page: Flags.string({ description: 'Page number' }),
|
|
32
|
+
limit: Flags.string({ description: 'Items per page' }),
|
|
33
|
+
include: Flags.string({ description: 'Relations to include' }),
|
|
34
|
+
search: Flags.string({ description: 'Search term' }),
|
|
35
|
+
'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
|
|
36
|
+
filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
|
|
37
|
+
query: Flags.string({ description: 'Additional query params as JSON' }),
|
|
38
|
+
fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
|
|
39
|
+
sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
|
|
40
|
+
meta: Flags.boolean({ description: 'Keep the API envelope in JSON output: data plus page, totals and sums' }),
|
|
41
|
+
'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
|
|
42
|
+
};
|
|
43
|
+
async run() {
|
|
44
|
+
const { args, flags } = await this.parse(EventMappingRules);
|
|
45
|
+
// Bulk actions. 'import' is the API's own endpoint: one request, answered
|
|
46
|
+
// by a run that records what happened to every record. 'upsert' is the CLI
|
|
47
|
+
// looping create/update. A resource has one or the other, never both.
|
|
48
|
+
if (args.action === 'upsert') {
|
|
49
|
+
if (!HAS_UPSERT) {
|
|
50
|
+
this.error(SERVER_IMPORTS.includes('import')
|
|
51
|
+
? 'This resource is imported by the API. Use: aderis event-mapping-rules import --file data.json'
|
|
52
|
+
: 'Upsert needs both a create and an update endpoint, which this resource does not have');
|
|
53
|
+
}
|
|
54
|
+
await this.runUpsert(flags);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (args.action.startsWith('upsert-')) {
|
|
58
|
+
const suffix = args.action.replace('upsert-', '');
|
|
59
|
+
if (!NESTED_UPSERTS.includes(suffix)) {
|
|
60
|
+
this.error(SERVER_IMPORTS.includes('import-' + suffix)
|
|
61
|
+
? `That resource is imported by the API. Use: aderis event-mapping-rules import-${suffix} <id> --file data.json`
|
|
62
|
+
: `Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
await this.runNestedUpsert(suffix, args.id, flags);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Handle export action specially
|
|
68
|
+
if (args.action === 'export') {
|
|
69
|
+
if (!HAS_EXPORT)
|
|
70
|
+
this.error('Export not available for this resource');
|
|
71
|
+
await this.runExport(flags);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Handle CAMT.053 import specially - sends raw XML
|
|
75
|
+
if (args.action === 'import-camt053') {
|
|
76
|
+
if (!HAS_CAMT053)
|
|
77
|
+
this.error('CAMT.053 import not available for this resource');
|
|
78
|
+
await this.runCamt053Import(flags);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (SERVER_IMPORTS.includes(args.action)) {
|
|
82
|
+
await this.runServerImport(args.action, args.id, flags);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// 'import' used to name the CLI's create/update loop on every resource. It
|
|
86
|
+
// now names the API endpoint, which most resources do not have.
|
|
87
|
+
if (args.action === 'import' || args.action.startsWith('import-')) {
|
|
88
|
+
const suffix = args.action === 'import' ? '' : '-' + args.action.replace('import-', '');
|
|
89
|
+
const loops = args.action === 'import' ? HAS_UPSERT : NESTED_UPSERTS.includes(args.action.replace('import-', ''));
|
|
90
|
+
if (loops) {
|
|
91
|
+
this.error(`This resource has no import endpoint. Use: aderis event-mapping-rules upsert${suffix}${suffix ? ' <id>' : ''} --file data.json`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const endpoint = ENDPOINTS[args.action];
|
|
95
|
+
if (!endpoint)
|
|
96
|
+
this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
|
|
97
|
+
let apiPath = endpoint.path;
|
|
98
|
+
for (let i = 0; i < endpoint.pathParams.length; i++) {
|
|
99
|
+
const param = endpoint.pathParams[i];
|
|
100
|
+
let value;
|
|
101
|
+
if (i === 0) {
|
|
102
|
+
// First path param always comes from positional argument
|
|
103
|
+
value = args.id;
|
|
104
|
+
if (!value)
|
|
105
|
+
this.error('Missing required argument: id. Usage: aderis event-mapping-rules <action> <id>');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const flagName = param.replace(/\./g, '-').replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
|
|
109
|
+
value = flags[flagName];
|
|
110
|
+
if (!value)
|
|
111
|
+
this.error(`Missing required parameter: --${flagName}`);
|
|
112
|
+
}
|
|
113
|
+
apiPath = apiPath.replace(`{${param}}`, String(value));
|
|
114
|
+
}
|
|
115
|
+
const context = flags.context ? getContext(flags.context) : getCurrentContext();
|
|
116
|
+
if (!context)
|
|
117
|
+
this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
|
|
118
|
+
if (!hasCredentials(context))
|
|
119
|
+
this.error(missingCredentialsMessage(context));
|
|
120
|
+
const queryParams = {};
|
|
121
|
+
if (flags.page)
|
|
122
|
+
queryParams.page = flags.page;
|
|
123
|
+
if (flags.limit)
|
|
124
|
+
queryParams.limit = flags.limit;
|
|
125
|
+
if (flags.include)
|
|
126
|
+
queryParams.include = flags.include;
|
|
127
|
+
if (flags.search)
|
|
128
|
+
queryParams.search = flags.search;
|
|
129
|
+
if (flags['sort-by'])
|
|
130
|
+
queryParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
131
|
+
if (flags.fields)
|
|
132
|
+
queryParams.fields = flags.fields;
|
|
133
|
+
if (flags.query) {
|
|
134
|
+
try {
|
|
135
|
+
Object.assign(queryParams, JSON.parse(flags.query));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
this.error('Invalid JSON in --query');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Parse --filter flags (e.g., --filter status=ACTIVE becomes filter.status=$eq:ACTIVE)
|
|
142
|
+
if (flags.filter && flags.filter.length > 0) {
|
|
143
|
+
for (const f of flags.filter) {
|
|
144
|
+
const eqIndex = f.indexOf('=');
|
|
145
|
+
if (eqIndex === -1)
|
|
146
|
+
this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
|
|
147
|
+
const key = f.slice(0, eqIndex);
|
|
148
|
+
const value = this.parseFilterValue(f.slice(eqIndex + 1));
|
|
149
|
+
const filterKey = `filter.${key}`;
|
|
150
|
+
if (filterKey in queryParams) {
|
|
151
|
+
// Multiple filters on same field: collect as array
|
|
152
|
+
const existing = queryParams[filterKey];
|
|
153
|
+
queryParams[filterKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
queryParams[filterKey] = value;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let data;
|
|
161
|
+
if (endpoint.isMultipart) {
|
|
162
|
+
if (!flags.file)
|
|
163
|
+
this.error('Missing required --file flag for file upload');
|
|
164
|
+
const fsModule = await import('node:fs');
|
|
165
|
+
const pathModule = await import('node:path');
|
|
166
|
+
const FormDataLib = (await import('form-data')).default;
|
|
167
|
+
const form = new FormDataLib();
|
|
168
|
+
form.append('file', fsModule.createReadStream(flags.file), pathModule.basename(flags.file));
|
|
169
|
+
// Append --set fields as extra form fields (e.g. --set type=CREDIT_CARD_STATEMENT)
|
|
170
|
+
if (flags.set && flags.set.length > 0) {
|
|
171
|
+
for (const pair of flags.set) {
|
|
172
|
+
const eqIndex = pair.indexOf('=');
|
|
173
|
+
if (eqIndex === -1)
|
|
174
|
+
this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
|
|
175
|
+
form.append(pair.slice(0, eqIndex), pair.slice(eqIndex + 1));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
data = form;
|
|
179
|
+
}
|
|
180
|
+
else if (endpoint.hasBody) {
|
|
181
|
+
// For binary-response endpoints, --file is the output path, not a JSON
|
|
182
|
+
// input file — skip the read-as-body branch.
|
|
183
|
+
if (flags.file && !endpoint.isBinaryResponse) {
|
|
184
|
+
const fsModule = await import('node:fs');
|
|
185
|
+
try {
|
|
186
|
+
data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
this.error(`Failed to read file: ${flags.file}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (flags.data) {
|
|
193
|
+
try {
|
|
194
|
+
data = JSON.parse(flags.data);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
this.error('Invalid JSON in --data');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Apply --set flags (can be combined with --data or --file)
|
|
201
|
+
if (flags.set && flags.set.length > 0) {
|
|
202
|
+
if (!data)
|
|
203
|
+
data = {};
|
|
204
|
+
const body = data;
|
|
205
|
+
const arrayFields = new Set(endpoint.bodyArrayFields);
|
|
206
|
+
for (const pair of flags.set) {
|
|
207
|
+
const eqIndex = pair.indexOf('=');
|
|
208
|
+
if (eqIndex === -1)
|
|
209
|
+
this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
|
|
210
|
+
const key = pair.slice(0, eqIndex);
|
|
211
|
+
let value = pair.slice(eqIndex + 1);
|
|
212
|
+
// Try to parse as JSON for numbers, booleans, arrays, objects
|
|
213
|
+
try {
|
|
214
|
+
value = JSON.parse(value);
|
|
215
|
+
}
|
|
216
|
+
catch { /* keep as string */ }
|
|
217
|
+
// For top-level keys typed as array in the API schema, split bare
|
|
218
|
+
// comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
|
|
219
|
+
// wrap a lone scalar in a single-element array. Skips when --set
|
|
220
|
+
// already provided a JSON array.
|
|
221
|
+
const isArrayField = !key.includes('.') && arrayFields.has(key);
|
|
222
|
+
if (isArrayField && !Array.isArray(value)) {
|
|
223
|
+
if (typeof value === 'string') {
|
|
224
|
+
value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
|
|
225
|
+
}
|
|
226
|
+
else if (value !== undefined && value !== null) {
|
|
227
|
+
value = [value];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Support nested keys like "address.city"
|
|
231
|
+
const keys = key.split('.');
|
|
232
|
+
if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
|
|
233
|
+
this.error(`Invalid --set key: ${key}`);
|
|
234
|
+
}
|
|
235
|
+
let obj = body;
|
|
236
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
237
|
+
if (!(keys[i] in obj) || typeof obj[keys[i]] !== 'object') {
|
|
238
|
+
obj[keys[i]] = {};
|
|
239
|
+
}
|
|
240
|
+
obj = obj[keys[i]];
|
|
241
|
+
}
|
|
242
|
+
obj[keys[keys.length - 1]] = value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (flags.set && flags.set.length > 0) {
|
|
247
|
+
// For GET endpoints, --set adds query params
|
|
248
|
+
for (const pair of flags.set) {
|
|
249
|
+
const eqIndex = pair.indexOf('=');
|
|
250
|
+
if (eqIndex === -1)
|
|
251
|
+
this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
|
|
252
|
+
const key = pair.slice(0, eqIndex);
|
|
253
|
+
let value = pair.slice(eqIndex + 1);
|
|
254
|
+
try {
|
|
255
|
+
value = JSON.parse(value);
|
|
256
|
+
}
|
|
257
|
+
catch { /* keep as string */ }
|
|
258
|
+
queryParams[key] = value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Show HTTP request details if --show-http-request flag is set
|
|
262
|
+
if (flags['show-http-request']) {
|
|
263
|
+
const baseUrls = getBaseUrls(context.environment);
|
|
264
|
+
const queryString = Object.keys(queryParams).length > 0
|
|
265
|
+
? '?' + Object.entries(queryParams).flatMap(([k, v]) => {
|
|
266
|
+
const vals = Array.isArray(v) ? v : [v];
|
|
267
|
+
return vals.map(val => `${encodeURIComponent(k)}=${encodeURIComponent(String(val))}`);
|
|
268
|
+
}).join('&')
|
|
269
|
+
: '';
|
|
270
|
+
process.stderr.write(`\n${endpoint.method.toUpperCase()} ${baseUrls.api}${apiPath}${queryString}\n`);
|
|
271
|
+
if (endpoint.isMultipart) {
|
|
272
|
+
process.stderr.write(`Body: [multipart/form-data file upload]\n`);
|
|
273
|
+
}
|
|
274
|
+
else if (data) {
|
|
275
|
+
process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
|
|
276
|
+
}
|
|
277
|
+
process.stderr.write('\n');
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
281
|
+
if (endpoint.isBinaryResponse) {
|
|
282
|
+
const toStdout = !flags.file;
|
|
283
|
+
if (toStdout && process.stdout.isTTY) {
|
|
284
|
+
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
|
+
}
|
|
286
|
+
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
287
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
288
|
+
data,
|
|
289
|
+
contextName: flags.context,
|
|
290
|
+
binary: true,
|
|
291
|
+
});
|
|
292
|
+
const binData = binResponse.data;
|
|
293
|
+
const buffer = Buffer.isBuffer(binData)
|
|
294
|
+
? binData
|
|
295
|
+
: Buffer.from(binData);
|
|
296
|
+
if (toStdout) {
|
|
297
|
+
// Nothing else may reach stdout here — it is the file.
|
|
298
|
+
process.stdout.write(buffer);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const fsModule = await import('node:fs');
|
|
302
|
+
fsModule.writeFileSync(flags.file, buffer);
|
|
303
|
+
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let response;
|
|
307
|
+
if (endpoint.hasBinaryVariant) {
|
|
308
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
309
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
310
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
311
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
312
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
313
|
+
data,
|
|
314
|
+
contextName: flags.context,
|
|
315
|
+
binary: true,
|
|
316
|
+
});
|
|
317
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
318
|
+
const rawData = raw.data;
|
|
319
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
320
|
+
if (!isTextualMediaType(rawType)) {
|
|
321
|
+
if (process.stdout.isTTY) {
|
|
322
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
323
|
+
}
|
|
324
|
+
// Nothing else may reach stdout here — it is the file.
|
|
325
|
+
process.stdout.write(buffer);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
332
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
333
|
+
data,
|
|
334
|
+
contextName: flags.context,
|
|
335
|
+
acceptAny: true,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
339
|
+
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
340
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
341
|
+
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
342
|
+
this.log(String(response.data));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Parse JSON if response came as string (due to acceptAny)
|
|
346
|
+
let output = response.data;
|
|
347
|
+
if (typeof output === 'string') {
|
|
348
|
+
try {
|
|
349
|
+
output = JSON.parse(output);
|
|
350
|
+
}
|
|
351
|
+
catch { /* keep as string */ }
|
|
352
|
+
}
|
|
353
|
+
// A paginated answer is { data, meta, links, totals }. The records are
|
|
354
|
+
// unwrapped for output, but what the API said about the page is kept:
|
|
355
|
+
// dropping it silently let a script read one page as the whole result
|
|
356
|
+
// (AderisERP/product#61).
|
|
357
|
+
let envelope;
|
|
358
|
+
if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
|
|
359
|
+
const page = output;
|
|
360
|
+
envelope = page;
|
|
361
|
+
output = page.data;
|
|
362
|
+
}
|
|
363
|
+
// For table/csv on non-array responses, auto-extract the primary array
|
|
364
|
+
// e.g., { payments: [...], total: 3 } → extract payments array
|
|
365
|
+
if ((flags.table || flags.output === 'table' || flags.csv || flags.output === 'csv')
|
|
366
|
+
&& output && typeof output === 'object' && !Array.isArray(output)) {
|
|
367
|
+
const obj = output;
|
|
368
|
+
const arrayKeys = Object.keys(obj).filter(k => Array.isArray(obj[k]));
|
|
369
|
+
if (arrayKeys.length === 1) {
|
|
370
|
+
output = obj[arrayKeys[0]];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Sanitize output if requested
|
|
374
|
+
if (flags.sanitize) {
|
|
375
|
+
output = this.sanitizeOutput(output);
|
|
376
|
+
}
|
|
377
|
+
const fieldList = flags.fields?.split(',').map(f => f.trim());
|
|
378
|
+
if (envelope)
|
|
379
|
+
this.warnShortPage(envelope, flags.limit);
|
|
380
|
+
if (flags.csv || flags.output === 'csv') {
|
|
381
|
+
this.printCsv(output, fieldList);
|
|
382
|
+
}
|
|
383
|
+
else if (flags.table || flags.output === 'table') {
|
|
384
|
+
this.printTable(output, fieldList);
|
|
385
|
+
if (envelope)
|
|
386
|
+
this.printPageSummary(envelope);
|
|
387
|
+
}
|
|
388
|
+
else if (envelope && flags.meta) {
|
|
389
|
+
// The whole envelope, for a caller that asked for it; the default stays
|
|
390
|
+
// a bare array so existing scripts are unaffected.
|
|
391
|
+
this.log(JSON.stringify({ ...envelope, data: output }, null, 2));
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
this.log(JSON.stringify(output, null, 2));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (error instanceof ApiError) {
|
|
399
|
+
if (error.status === 404)
|
|
400
|
+
this.error(error.message);
|
|
401
|
+
this.error(`API Error (${error.status}): ${error.message}`);
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async runUpsert(flags) {
|
|
407
|
+
const context = flags.context ? getContext(flags.context) : getCurrentContext();
|
|
408
|
+
if (!context)
|
|
409
|
+
this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
|
|
410
|
+
if (!hasCredentials(context))
|
|
411
|
+
this.error(missingCredentialsMessage(context));
|
|
412
|
+
// Get data from file, --data, or --set flags
|
|
413
|
+
let items = [];
|
|
414
|
+
if (flags.file) {
|
|
415
|
+
const fsModule = await import('node:fs');
|
|
416
|
+
try {
|
|
417
|
+
const content = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
|
|
418
|
+
items = Array.isArray(content) ? content : [content];
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
this.error(`Failed to read file: ${flags.file}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (flags.data) {
|
|
425
|
+
try {
|
|
426
|
+
const content = JSON.parse(flags.data);
|
|
427
|
+
items = Array.isArray(content) ? content : [content];
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
this.error('Invalid JSON in --data');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (flags.set && flags.set.length > 0) {
|
|
434
|
+
const item = {};
|
|
435
|
+
const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
|
|
436
|
+
for (const pair of flags.set) {
|
|
437
|
+
const eqIndex = pair.indexOf('=');
|
|
438
|
+
if (eqIndex === -1)
|
|
439
|
+
this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
|
|
440
|
+
const key = pair.slice(0, eqIndex);
|
|
441
|
+
let value = pair.slice(eqIndex + 1);
|
|
442
|
+
try {
|
|
443
|
+
value = JSON.parse(value);
|
|
444
|
+
}
|
|
445
|
+
catch { /* keep as string */ }
|
|
446
|
+
const isArrayField = !key.includes('.') && arrayFields.has(key);
|
|
447
|
+
if (isArrayField && !Array.isArray(value)) {
|
|
448
|
+
if (typeof value === 'string') {
|
|
449
|
+
value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
|
|
450
|
+
}
|
|
451
|
+
else if (value !== undefined && value !== null) {
|
|
452
|
+
value = [value];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const keys = key.split('.');
|
|
456
|
+
if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
|
|
457
|
+
this.error(`Invalid --set key: ${key}`);
|
|
458
|
+
}
|
|
459
|
+
let obj = item;
|
|
460
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
461
|
+
if (!(keys[i] in obj))
|
|
462
|
+
obj[keys[i]] = {};
|
|
463
|
+
obj = obj[keys[i]];
|
|
464
|
+
}
|
|
465
|
+
obj[keys[keys.length - 1]] = value;
|
|
466
|
+
}
|
|
467
|
+
items = [item];
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
this.error('Upsert requires --file, --data, or --set flags');
|
|
471
|
+
}
|
|
472
|
+
const createEndpoint = ENDPOINTS['create'];
|
|
473
|
+
const updateEndpoint = ENDPOINTS['update'];
|
|
474
|
+
if (!createEndpoint || !updateEndpoint) {
|
|
475
|
+
this.error('Upsert requires both create and update endpoints');
|
|
476
|
+
}
|
|
477
|
+
// Helper to extract identifier from item (id, code, or xxxCode)
|
|
478
|
+
const getItemIdentifier = (item) => {
|
|
479
|
+
if (item.id)
|
|
480
|
+
return String(item.id);
|
|
481
|
+
if (item.code)
|
|
482
|
+
return String(item.code);
|
|
483
|
+
// Look for xxxCode fields (e.g., ledgerCode, accountCode)
|
|
484
|
+
for (const key of Object.keys(item)) {
|
|
485
|
+
if (key.endsWith('Code') && item[key])
|
|
486
|
+
return String(item[key]);
|
|
487
|
+
}
|
|
488
|
+
// Look for name as fallback
|
|
489
|
+
if (item.name)
|
|
490
|
+
return String(item.name).slice(0, 30);
|
|
491
|
+
return '';
|
|
492
|
+
};
|
|
493
|
+
const results = [];
|
|
494
|
+
// Stream output row-by-row so progress is visible during long imports.
|
|
495
|
+
// Pre-compute the identifier column width from the input items so the
|
|
496
|
+
// table stays aligned without buffering all rows.
|
|
497
|
+
// When stdout is piped (IDE terminals, tee, CI), Node switches to async-buffered
|
|
498
|
+
// mode and rows accumulate in the pipe buffer until it fills. Force blocking
|
|
499
|
+
// mode so each \n-terminated write flushes immediately.
|
|
500
|
+
const stdoutHandle = process.stdout._handle;
|
|
501
|
+
if (stdoutHandle?.setBlocking)
|
|
502
|
+
stdoutHandle.setBlocking(true);
|
|
503
|
+
const outputFormat = flags.output || 'table';
|
|
504
|
+
const useCsv = outputFormat === 'csv' || Boolean(flags.csv);
|
|
505
|
+
const useJson = outputFormat === 'json';
|
|
506
|
+
const statusWidth = 8;
|
|
507
|
+
const identifierWidth = Math.max(10, ...items.map(i => getItemIdentifier(i).length));
|
|
508
|
+
if (useCsv) {
|
|
509
|
+
this.log('status,identifier,id,error');
|
|
510
|
+
}
|
|
511
|
+
else if (!useJson) {
|
|
512
|
+
this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
|
|
513
|
+
this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
|
|
514
|
+
}
|
|
515
|
+
const emitRow = (r) => {
|
|
516
|
+
if (useJson)
|
|
517
|
+
return;
|
|
518
|
+
if (useCsv) {
|
|
519
|
+
const err = (r.error || '').replace(/"/g, '""');
|
|
520
|
+
this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
this.log(`${r.status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
// Server-managed fields the API rejects in create/update bodies. Stripped
|
|
527
|
+
// before every request so re-importing a previously-exported payload Just Works.
|
|
528
|
+
const STRIP_FIELDS = new Set(['id', '_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
|
|
529
|
+
const stripServerFields = (item) => {
|
|
530
|
+
const out = {};
|
|
531
|
+
for (const [k, v] of Object.entries(item)) {
|
|
532
|
+
if (!STRIP_FIELDS.has(k))
|
|
533
|
+
out[k] = v;
|
|
534
|
+
}
|
|
535
|
+
return out;
|
|
536
|
+
};
|
|
537
|
+
const processItem = async (item) => {
|
|
538
|
+
const identifier = getItemIdentifier(item);
|
|
539
|
+
const body = stripServerFields(item);
|
|
540
|
+
// If the payload carries an id (e.g. re-importing an export), PATCH directly.
|
|
541
|
+
// Fall through to POST only on 404 (so a missing record still gets created).
|
|
542
|
+
const itemId = typeof item.id === 'string' ? item.id : undefined;
|
|
543
|
+
if (itemId) {
|
|
544
|
+
try {
|
|
545
|
+
const updatePath = updateEndpoint.path.replace('{id}', itemId);
|
|
546
|
+
await apiRequest('PATCH', updatePath, {
|
|
547
|
+
data: body,
|
|
548
|
+
contextName: flags.context,
|
|
549
|
+
});
|
|
550
|
+
return { status: 'updated', id: itemId, identifier };
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
if (!(error instanceof ApiError && error.status === 404)) {
|
|
554
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
555
|
+
return { status: 'error', id: itemId, identifier, error: msg };
|
|
556
|
+
}
|
|
557
|
+
// 404 → record gone, try create below
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const response = await apiRequest('POST', createEndpoint.path, {
|
|
562
|
+
data: body,
|
|
563
|
+
contextName: flags.context,
|
|
564
|
+
});
|
|
565
|
+
const id = response.data?.id;
|
|
566
|
+
return { status: 'created', id: id ? String(id) : undefined, identifier };
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
if (error instanceof ApiError && error.status === 409) {
|
|
570
|
+
const errorData = error.data;
|
|
571
|
+
const existingId = errorData?.existingId;
|
|
572
|
+
if (existingId) {
|
|
573
|
+
try {
|
|
574
|
+
const updatePath = updateEndpoint.path.replace('{id}', existingId);
|
|
575
|
+
await apiRequest('PATCH', updatePath, {
|
|
576
|
+
data: body,
|
|
577
|
+
contextName: flags.context,
|
|
578
|
+
});
|
|
579
|
+
return { status: 'updated', id: existingId, identifier };
|
|
580
|
+
}
|
|
581
|
+
catch (updateError) {
|
|
582
|
+
const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
|
|
583
|
+
return { status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return { status: 'error', identifier, error: 'Conflict but no existingId returned' };
|
|
587
|
+
}
|
|
588
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
589
|
+
return { status: 'error', identifier, error: msg };
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
// Worker pool: keep N requests in flight at all times. Rows are emitted as
|
|
593
|
+
// each request resolves (so output order may differ from input order). The
|
|
594
|
+
// summary at the end remains accurate regardless.
|
|
595
|
+
const concurrency = 4;
|
|
596
|
+
const queue = [...items];
|
|
597
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
598
|
+
for (;;) {
|
|
599
|
+
const item = queue.shift();
|
|
600
|
+
if (!item)
|
|
601
|
+
return;
|
|
602
|
+
const r = await processItem(item);
|
|
603
|
+
results.push(r);
|
|
604
|
+
emitRow(r);
|
|
605
|
+
}
|
|
606
|
+
}));
|
|
607
|
+
// Summary counts
|
|
608
|
+
const created = results.filter(r => r.status === 'created').length;
|
|
609
|
+
const updated = results.filter(r => r.status === 'updated').length;
|
|
610
|
+
const errors = results.filter(r => r.status === 'error');
|
|
611
|
+
if (useJson) {
|
|
612
|
+
this.log(JSON.stringify(results, null, 2));
|
|
613
|
+
}
|
|
614
|
+
// Always show summary at the end
|
|
615
|
+
this.log('');
|
|
616
|
+
this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* The API's own import: the whole array goes up in one request and comes
|
|
620
|
+
* back as an import run, which records what happened to every record. A bad
|
|
621
|
+
* record lands in errorLog and the rest are still applied, and the run stays
|
|
622
|
+
* readable afterwards with: aderis imports get <id>.
|
|
623
|
+
*/
|
|
624
|
+
async runServerImport(action, parentId, flags) {
|
|
625
|
+
const endpoint = ENDPOINTS[action];
|
|
626
|
+
if (!endpoint)
|
|
627
|
+
this.error(`Unknown action: ${action}. Available: ${SUBCOMMANDS.join(', ')}`);
|
|
628
|
+
const context = flags.context ? getContext(flags.context) : getCurrentContext();
|
|
629
|
+
if (!context)
|
|
630
|
+
this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
|
|
631
|
+
if (!hasCredentials(context))
|
|
632
|
+
this.error(missingCredentialsMessage(context));
|
|
633
|
+
let apiPath = endpoint.path;
|
|
634
|
+
if (endpoint.pathParams.length > 0) {
|
|
635
|
+
if (!parentId)
|
|
636
|
+
this.error(`Missing required argument: id. Usage: aderis event-mapping-rules ${action} <parent-id> --file data.json`);
|
|
637
|
+
apiPath = apiPath.replace(`{${endpoint.pathParams[0]}}`, parentId);
|
|
638
|
+
}
|
|
639
|
+
// The endpoint takes an array. A single object is wrapped, so importing
|
|
640
|
+
// one record reads the same as importing many.
|
|
641
|
+
let records = [];
|
|
642
|
+
if (flags.file) {
|
|
643
|
+
const fsModule = await import('node:fs');
|
|
644
|
+
try {
|
|
645
|
+
const content = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
|
|
646
|
+
records = Array.isArray(content) ? content : [content];
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
this.error(`Failed to read file: ${flags.file}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
else if (flags.data) {
|
|
653
|
+
try {
|
|
654
|
+
const content = JSON.parse(flags.data);
|
|
655
|
+
records = Array.isArray(content) ? content : [content];
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
this.error('Invalid JSON in --data');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
this.error('Import requires --file or --data holding a JSON array of records');
|
|
663
|
+
}
|
|
664
|
+
if (records.length === 0)
|
|
665
|
+
this.error('Nothing to import: the input is an empty array');
|
|
666
|
+
const params = { format: 'json' };
|
|
667
|
+
if (flags.mode)
|
|
668
|
+
params.mode = flags.mode;
|
|
669
|
+
if (flags.reference)
|
|
670
|
+
params.reference = flags.reference;
|
|
671
|
+
if (flags.async)
|
|
672
|
+
params.async = true;
|
|
673
|
+
const headers = {};
|
|
674
|
+
if (flags['idempotency-key'])
|
|
675
|
+
headers['Idempotency-Key'] = flags['idempotency-key'];
|
|
676
|
+
if (flags['show-http-request']) {
|
|
677
|
+
const baseUrls = getBaseUrls(context.environment);
|
|
678
|
+
const queryString = '?' + Object.entries(params).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
|
|
679
|
+
process.stderr.write(`\n${endpoint.method.toUpperCase()} ${baseUrls.api}${apiPath}${queryString}\n`);
|
|
680
|
+
process.stderr.write(`Body: [${records.length} record${records.length === 1 ? '' : 's'}]\n\n`);
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
const response = await apiRequest(endpoint.method, apiPath, {
|
|
684
|
+
contextName: flags.context,
|
|
685
|
+
data: records,
|
|
686
|
+
headers,
|
|
687
|
+
params,
|
|
688
|
+
});
|
|
689
|
+
this.printImportRun(response.data, flags);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
if (error instanceof ApiError) {
|
|
693
|
+
this.error(`API Error (${error.status}): ${error.message}`);
|
|
694
|
+
}
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/** Renders an import run: its counts, then whatever the server refused. */
|
|
699
|
+
printImportRun(run, flags) {
|
|
700
|
+
if (flags.output === 'json') {
|
|
701
|
+
this.log(JSON.stringify(run, null, 2));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const record = (run ?? {});
|
|
705
|
+
const count = (key) => (typeof record[key] === 'number' ? record[key] : 0);
|
|
706
|
+
const id = record.id ? String(record.id) : '';
|
|
707
|
+
this.log(`Import ${id} - ${String(record.status ?? 'UNKNOWN')}${record.async ? ' (background)' : ''}`);
|
|
708
|
+
this.log(` ${count('inputSize')} submitted, ${count('createdCount')} created, ${count('updatedCount')} updated, ${count('unchangedCount')} unchanged`);
|
|
709
|
+
if (count('errorCount') > 0)
|
|
710
|
+
this.log(` ${count('errorCount')} not applied: ${count('refusedCount')} refused, ${count('failedCount')} failed`);
|
|
711
|
+
if (count('warningCount') > 0)
|
|
712
|
+
this.log(` ${count('warningCount')} applied with a warning`);
|
|
713
|
+
// errorLog is keyed by the record's identifier, then by field name.
|
|
714
|
+
const errorLog = record.errorLog;
|
|
715
|
+
if (errorLog && Object.keys(errorLog).length > 0) {
|
|
716
|
+
this.log('');
|
|
717
|
+
for (const [key, fields] of Object.entries(errorLog)) {
|
|
718
|
+
for (const [field, messages] of Object.entries(fields ?? {})) {
|
|
719
|
+
for (const message of messages ?? [])
|
|
720
|
+
this.log(` ${key} ${field}: ${message}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (record.async && id) {
|
|
725
|
+
this.log('');
|
|
726
|
+
this.log(`Follow it with: aderis imports get ${id}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async runExport(flags) {
|
|
730
|
+
const context = flags.context ? getContext(flags.context) : getCurrentContext();
|
|
731
|
+
if (!context)
|
|
732
|
+
this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
|
|
733
|
+
if (!hasCredentials(context))
|
|
734
|
+
this.error(missingCredentialsMessage(context));
|
|
735
|
+
const listEndpoint = ENDPOINTS['list'];
|
|
736
|
+
if (!listEndpoint)
|
|
737
|
+
this.error('Export requires a list endpoint');
|
|
738
|
+
const allItems = [];
|
|
739
|
+
let page = 1;
|
|
740
|
+
const limit = 100;
|
|
741
|
+
let hasMore = true;
|
|
742
|
+
// Build base query params
|
|
743
|
+
const baseParams = {};
|
|
744
|
+
if (flags.search)
|
|
745
|
+
baseParams.search = flags.search;
|
|
746
|
+
if (flags['sort-by'])
|
|
747
|
+
baseParams.sortBy = normalizeSortBy(flags['sort-by']);
|
|
748
|
+
if (flags.include)
|
|
749
|
+
baseParams.include = flags.include;
|
|
750
|
+
if (flags.fields)
|
|
751
|
+
baseParams.fields = flags.fields;
|
|
752
|
+
if (flags.query) {
|
|
753
|
+
try {
|
|
754
|
+
Object.assign(baseParams, JSON.parse(flags.query));
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
this.error('Invalid JSON in --query');
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Parse --filter flags (auto-prepend $eq: if no operator specified)
|
|
761
|
+
if (flags.filter && flags.filter.length > 0) {
|
|
762
|
+
for (const f of flags.filter) {
|
|
763
|
+
const eqIndex = f.indexOf('=');
|
|
764
|
+
if (eqIndex === -1)
|
|
765
|
+
this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
|
|
766
|
+
const key = f.slice(0, eqIndex);
|
|
767
|
+
const value = this.parseFilterValue(f.slice(eqIndex + 1));
|
|
768
|
+
const filterKey = `filter.${key}`;
|
|
769
|
+
if (filterKey in baseParams) {
|
|
770
|
+
const existing = baseParams[filterKey];
|
|
771
|
+
baseParams[filterKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
baseParams[filterKey] = value;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Fetch all pages
|
|
779
|
+
process.stderr.write('Exporting...');
|
|
780
|
+
while (hasMore) {
|
|
781
|
+
const params = { ...baseParams, page, limit };
|
|
782
|
+
const response = await apiRequest('GET', listEndpoint.path, {
|
|
783
|
+
params,
|
|
784
|
+
contextName: flags.context,
|
|
785
|
+
});
|
|
786
|
+
let items = [];
|
|
787
|
+
const data = response.data;
|
|
788
|
+
// Handle paginated response format
|
|
789
|
+
if (data && typeof data === 'object' && 'data' in data && Array.isArray(data.data)) {
|
|
790
|
+
items = data.data;
|
|
791
|
+
}
|
|
792
|
+
else if (Array.isArray(response.data)) {
|
|
793
|
+
items = response.data;
|
|
794
|
+
}
|
|
795
|
+
allItems.push(...items);
|
|
796
|
+
process.stderr.write(` ${allItems.length}`);
|
|
797
|
+
// Check if there are more pages
|
|
798
|
+
if (items.length < limit) {
|
|
799
|
+
hasMore = false;
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
page++;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
process.stderr.write(' done\n');
|
|
806
|
+
let output = allItems;
|
|
807
|
+
// Sanitize output if requested
|
|
808
|
+
if (flags.sanitize) {
|
|
809
|
+
output = this.sanitizeOutput(output);
|
|
810
|
+
}
|
|
811
|
+
// Output in requested format
|
|
812
|
+
const fieldList = flags.fields ? flags.fields.split(',').map(f => f.trim()) : undefined;
|
|
813
|
+
if (flags.csv || flags.output === 'csv') {
|
|
814
|
+
this.printCsv(output, fieldList);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
this.log(JSON.stringify(output, null, 2));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async runCamt053Import(flags) {
|
|
821
|
+
const context = flags.context ? getContext(flags.context) : getCurrentContext();
|
|
822
|
+
if (!context)
|
|
823
|
+
this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
|
|
824
|
+
if (!hasCredentials(context))
|
|
825
|
+
this.error(missingCredentialsMessage(context));
|
|
826
|
+
if (!flags.file) {
|
|
827
|
+
this.error('CAMT.053 import requires --file (-f) flag with path to XML file');
|
|
828
|
+
}
|
|
829
|
+
const fsModule = await import('node:fs');
|
|
830
|
+
let xmlContent;
|
|
831
|
+
try {
|
|
832
|
+
xmlContent = fsModule.readFileSync(flags.file, 'utf-8');
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
this.error(`Failed to read file: ${flags.file}`);
|
|
836
|
+
}
|
|
837
|
+
const endpoint = ENDPOINTS['import-camt053'];
|
|
838
|
+
try {
|
|
839
|
+
const response = await apiRequest(endpoint.method, endpoint.path, {
|
|
840
|
+
data: xmlContent,
|
|
841
|
+
contextName: flags.context,
|
|
842
|
+
contentType: 'application/xml',
|
|
843
|
+
});
|
|
844
|
+
// Extract transactions array and counts from response
|
|
845
|
+
let transactions = [];
|
|
846
|
+
let imported = 0;
|
|
847
|
+
let skipped = 0;
|
|
848
|
+
if (response.data && typeof response.data === 'object') {
|
|
849
|
+
const data = response.data;
|
|
850
|
+
if ('data' in data && Array.isArray(data.data)) {
|
|
851
|
+
transactions = data.data;
|
|
852
|
+
}
|
|
853
|
+
else if ('transactions' in data && Array.isArray(data.transactions)) {
|
|
854
|
+
transactions = data.transactions;
|
|
855
|
+
}
|
|
856
|
+
else if (Array.isArray(response.data)) {
|
|
857
|
+
transactions = response.data;
|
|
858
|
+
}
|
|
859
|
+
if (typeof data.imported === 'number')
|
|
860
|
+
imported = data.imported;
|
|
861
|
+
if (typeof data.skipped === 'number')
|
|
862
|
+
skipped = data.skipped;
|
|
863
|
+
}
|
|
864
|
+
else if (Array.isArray(response.data)) {
|
|
865
|
+
transactions = response.data;
|
|
866
|
+
}
|
|
867
|
+
if (imported === 0 && skipped === 0 && transactions.length > 0) {
|
|
868
|
+
for (const tx of transactions) {
|
|
869
|
+
const t = tx;
|
|
870
|
+
if (t.status === 'skipped' || t.skipped === true) {
|
|
871
|
+
skipped++;
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
imported++;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (imported === 0 && skipped === 0) {
|
|
878
|
+
imported = transactions.length;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
let output = transactions;
|
|
882
|
+
if (flags.sanitize) {
|
|
883
|
+
output = this.sanitizeOutput(transactions);
|
|
884
|
+
}
|
|
885
|
+
const fieldList = flags.fields ? flags.fields.split(',').map(f => f.trim()) : undefined;
|
|
886
|
+
if (flags.csv || flags.output === 'csv') {
|
|
887
|
+
this.printCsv(output, fieldList);
|
|
888
|
+
}
|
|
889
|
+
else if (flags.table || flags.output === 'table') {
|
|
890
|
+
this.printTable(output, fieldList);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
this.log(JSON.stringify(output, null, 2));
|
|
894
|
+
}
|
|
895
|
+
this.log('');
|
|
896
|
+
this.log(`Summary: ${imported} imported, ${skipped} skipped`);
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
if (error instanceof ApiError) {
|
|
900
|
+
if (error.status === 404)
|
|
901
|
+
this.error(error.message);
|
|
902
|
+
this.error(`API Error (${error.status}): ${error.message}`);
|
|
903
|
+
}
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async runNestedUpsert(suffix, parentId, flags) {
|
|
908
|
+
if (!parentId) {
|
|
909
|
+
this.error(`Missing required argument: id. Usage: aderis event-mapping-rules upsert-${suffix} <parent-id> --file data.json`);
|
|
910
|
+
}
|
|
911
|
+
const context = flags.context ? getContext(flags.context) : getCurrentContext();
|
|
912
|
+
if (!context)
|
|
913
|
+
this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
|
|
914
|
+
if (!hasCredentials(context))
|
|
915
|
+
this.error(missingCredentialsMessage(context));
|
|
916
|
+
// Get data from file, --data, or --set flags
|
|
917
|
+
let items = [];
|
|
918
|
+
if (flags.file) {
|
|
919
|
+
const fsModule = await import('node:fs');
|
|
920
|
+
try {
|
|
921
|
+
const content = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
|
|
922
|
+
items = Array.isArray(content) ? content : [content];
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
this.error(`Failed to read file: ${flags.file}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
else if (flags.data) {
|
|
929
|
+
try {
|
|
930
|
+
const content = JSON.parse(flags.data);
|
|
931
|
+
items = Array.isArray(content) ? content : [content];
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
this.error('Invalid JSON in --data');
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
else if (flags.set && flags.set.length > 0) {
|
|
938
|
+
const item = {};
|
|
939
|
+
const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
|
|
940
|
+
for (const pair of flags.set) {
|
|
941
|
+
const eqIndex = pair.indexOf('=');
|
|
942
|
+
if (eqIndex === -1)
|
|
943
|
+
this.error(`Invalid --set format: ${pair}. Use: --set key=value`);
|
|
944
|
+
const key = pair.slice(0, eqIndex);
|
|
945
|
+
let value = pair.slice(eqIndex + 1);
|
|
946
|
+
try {
|
|
947
|
+
value = JSON.parse(value);
|
|
948
|
+
}
|
|
949
|
+
catch { /* keep as string */ }
|
|
950
|
+
const isArrayField = !key.includes('.') && arrayFields.has(key);
|
|
951
|
+
if (isArrayField && !Array.isArray(value)) {
|
|
952
|
+
if (typeof value === 'string') {
|
|
953
|
+
value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
|
|
954
|
+
}
|
|
955
|
+
else if (value !== undefined && value !== null) {
|
|
956
|
+
value = [value];
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const keys = key.split('.');
|
|
960
|
+
if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
|
|
961
|
+
this.error(`Invalid --set key: ${key}`);
|
|
962
|
+
}
|
|
963
|
+
let obj = item;
|
|
964
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
965
|
+
if (!(keys[i] in obj))
|
|
966
|
+
obj[keys[i]] = {};
|
|
967
|
+
obj = obj[keys[i]];
|
|
968
|
+
}
|
|
969
|
+
obj[keys[keys.length - 1]] = value;
|
|
970
|
+
}
|
|
971
|
+
items = [item];
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
this.error('Upsert requires --file, --data, or --set flags');
|
|
975
|
+
}
|
|
976
|
+
const createEndpoint = ENDPOINTS[`create-${suffix}`];
|
|
977
|
+
const updateEndpoint = ENDPOINTS[`update-${suffix}`];
|
|
978
|
+
if (!createEndpoint || !updateEndpoint) {
|
|
979
|
+
this.error(`Upsert requires both create-${suffix} and update-${suffix} endpoints`);
|
|
980
|
+
}
|
|
981
|
+
// Build the base path with parent ID (first path param)
|
|
982
|
+
const firstParam = createEndpoint.pathParams[0];
|
|
983
|
+
const basePath = createEndpoint.path.replace(`{${firstParam}}`, parentId);
|
|
984
|
+
// Helper to extract identifier from item
|
|
985
|
+
const getItemIdentifier = (item) => {
|
|
986
|
+
if (item.id)
|
|
987
|
+
return String(item.id);
|
|
988
|
+
if (item.itemId)
|
|
989
|
+
return String(item.itemId);
|
|
990
|
+
if (item.code)
|
|
991
|
+
return String(item.code);
|
|
992
|
+
for (const key of Object.keys(item)) {
|
|
993
|
+
if (key.endsWith('Id') && item[key])
|
|
994
|
+
return String(item[key]);
|
|
995
|
+
if (key.endsWith('Code') && item[key])
|
|
996
|
+
return String(item[key]);
|
|
997
|
+
}
|
|
998
|
+
return '';
|
|
999
|
+
};
|
|
1000
|
+
const results = [];
|
|
1001
|
+
const stdoutHandle = process.stdout._handle;
|
|
1002
|
+
if (stdoutHandle?.setBlocking)
|
|
1003
|
+
stdoutHandle.setBlocking(true);
|
|
1004
|
+
const outputFormat = flags.output || 'table';
|
|
1005
|
+
const useCsv = outputFormat === 'csv' || Boolean(flags.csv);
|
|
1006
|
+
const useJson = outputFormat === 'json';
|
|
1007
|
+
const statusWidth = 8;
|
|
1008
|
+
const identifierWidth = Math.max(10, ...items.map(i => getItemIdentifier(i).length));
|
|
1009
|
+
if (useCsv) {
|
|
1010
|
+
this.log('status,identifier,id,error');
|
|
1011
|
+
}
|
|
1012
|
+
else if (!useJson) {
|
|
1013
|
+
this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
|
|
1014
|
+
this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
|
|
1015
|
+
}
|
|
1016
|
+
const emitRow = (r) => {
|
|
1017
|
+
if (useJson)
|
|
1018
|
+
return;
|
|
1019
|
+
if (useCsv) {
|
|
1020
|
+
const err = (r.error || '').replace(/"/g, '""');
|
|
1021
|
+
this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
this.log(`${r.status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const STRIP_FIELDS = new Set(['id', '_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
|
|
1028
|
+
const stripServerFields = (item) => {
|
|
1029
|
+
const out = {};
|
|
1030
|
+
for (const [k, v] of Object.entries(item)) {
|
|
1031
|
+
if (!STRIP_FIELDS.has(k))
|
|
1032
|
+
out[k] = v;
|
|
1033
|
+
}
|
|
1034
|
+
return out;
|
|
1035
|
+
};
|
|
1036
|
+
const buildUpdatePath = (existingId) => {
|
|
1037
|
+
const firstUpdateParam = updateEndpoint.pathParams[0];
|
|
1038
|
+
const nestedIdParam = updateEndpoint.pathParams.find((p, i) => i > 0);
|
|
1039
|
+
let updatePath = updateEndpoint.path.replace(`{${firstUpdateParam}}`, parentId);
|
|
1040
|
+
if (nestedIdParam) {
|
|
1041
|
+
updatePath = updatePath.replace(`{${nestedIdParam}}`, existingId);
|
|
1042
|
+
}
|
|
1043
|
+
return updatePath;
|
|
1044
|
+
};
|
|
1045
|
+
const processItem = async (item) => {
|
|
1046
|
+
const identifier = getItemIdentifier(item);
|
|
1047
|
+
const body = stripServerFields(item);
|
|
1048
|
+
const itemId = typeof item.id === 'string' ? item.id : undefined;
|
|
1049
|
+
if (itemId) {
|
|
1050
|
+
try {
|
|
1051
|
+
await apiRequest('PATCH', buildUpdatePath(itemId), {
|
|
1052
|
+
data: body,
|
|
1053
|
+
contextName: flags.context,
|
|
1054
|
+
});
|
|
1055
|
+
return { status: 'updated', id: itemId, identifier };
|
|
1056
|
+
}
|
|
1057
|
+
catch (error) {
|
|
1058
|
+
if (!(error instanceof ApiError && error.status === 404)) {
|
|
1059
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
1060
|
+
return { status: 'error', id: itemId, identifier, error: msg };
|
|
1061
|
+
}
|
|
1062
|
+
// 404 → fall through and try create
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const response = await apiRequest('POST', basePath, {
|
|
1067
|
+
data: body,
|
|
1068
|
+
contextName: flags.context,
|
|
1069
|
+
});
|
|
1070
|
+
const id = response.data?.id;
|
|
1071
|
+
return { status: 'created', id: id ? String(id) : undefined, identifier };
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
if (error instanceof ApiError && error.status === 409) {
|
|
1075
|
+
const errorData = error.data;
|
|
1076
|
+
const existingId = errorData?.existingId || item.id;
|
|
1077
|
+
if (existingId) {
|
|
1078
|
+
try {
|
|
1079
|
+
await apiRequest('PATCH', buildUpdatePath(existingId), {
|
|
1080
|
+
data: body,
|
|
1081
|
+
contextName: flags.context,
|
|
1082
|
+
});
|
|
1083
|
+
return { status: 'updated', id: existingId, identifier };
|
|
1084
|
+
}
|
|
1085
|
+
catch (updateError) {
|
|
1086
|
+
const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
|
|
1087
|
+
return { status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return { status: 'error', identifier, error: 'Conflict but no existingId returned' };
|
|
1091
|
+
}
|
|
1092
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
1093
|
+
return { status: 'error', identifier, error: msg };
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
// Worker pool: 4 requests in flight at a time. Rows are emitted as each
|
|
1097
|
+
// request resolves so output order may differ from input order.
|
|
1098
|
+
const concurrency = 4;
|
|
1099
|
+
const queue = [...items];
|
|
1100
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
1101
|
+
for (;;) {
|
|
1102
|
+
const item = queue.shift();
|
|
1103
|
+
if (!item)
|
|
1104
|
+
return;
|
|
1105
|
+
const r = await processItem(item);
|
|
1106
|
+
results.push(r);
|
|
1107
|
+
emitRow(r);
|
|
1108
|
+
}
|
|
1109
|
+
}));
|
|
1110
|
+
// Summary counts
|
|
1111
|
+
const created = results.filter(r => r.status === 'created').length;
|
|
1112
|
+
const updated = results.filter(r => r.status === 'updated').length;
|
|
1113
|
+
const errors = results.filter(r => r.status === 'error');
|
|
1114
|
+
if (useJson) {
|
|
1115
|
+
this.log(JSON.stringify(results, null, 2));
|
|
1116
|
+
}
|
|
1117
|
+
this.log('');
|
|
1118
|
+
this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
|
|
1119
|
+
}
|
|
1120
|
+
parseFilterValue(value) {
|
|
1121
|
+
// Recognises the nestjs-paginate operator set (with or without the leading $)
|
|
1122
|
+
// and recursively unwraps chained operators like 'not:null' -> '$not:$null'
|
|
1123
|
+
// or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
|
|
1124
|
+
// no operator prefix is present.
|
|
1125
|
+
const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
|
|
1126
|
+
const m = value.match(opRe);
|
|
1127
|
+
if (!m)
|
|
1128
|
+
return `\$eq:${value}`;
|
|
1129
|
+
const op = m[1];
|
|
1130
|
+
const rest = value.slice(m[0].length);
|
|
1131
|
+
if (rest === '')
|
|
1132
|
+
return `\$${op}`;
|
|
1133
|
+
return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
|
|
1134
|
+
}
|
|
1135
|
+
/** The page line under a table: which page, how many, and any sums the API returned. */
|
|
1136
|
+
printPageSummary(envelope) {
|
|
1137
|
+
const meta = envelope.meta;
|
|
1138
|
+
if (meta && typeof meta.currentPage === 'number' && typeof meta.totalPages === 'number') {
|
|
1139
|
+
const shown = Array.isArray(envelope.data) ? envelope.data.length : 0;
|
|
1140
|
+
this.log(`
|
|
1141
|
+
Page ${meta.currentPage}/${meta.totalPages} — ${shown} of ${meta.totalItems ?? '?'} records`);
|
|
1142
|
+
}
|
|
1143
|
+
for (const total of envelope.totals ?? []) {
|
|
1144
|
+
const { currency, ...sums } = total;
|
|
1145
|
+
const parts = Object.entries(sums).map(([column, value]) => `${column} ${value}`).join(', ');
|
|
1146
|
+
this.log(`Total ${currency ?? ''}: ${parts}`.trim());
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* The API caps how many records a page may hold. Asking for more and getting
|
|
1151
|
+
* fewer looks like "that is everything" unless it is said out loud.
|
|
1152
|
+
*/
|
|
1153
|
+
warnShortPage(envelope, requestedLimit) {
|
|
1154
|
+
const asked = requestedLimit === undefined ? undefined : Number(requestedLimit);
|
|
1155
|
+
const perPage = envelope.meta?.itemsPerPage;
|
|
1156
|
+
if (asked === undefined || Number.isNaN(asked) || typeof perPage !== 'number' || perPage >= asked)
|
|
1157
|
+
return;
|
|
1158
|
+
const pages = envelope.meta?.totalPages;
|
|
1159
|
+
const more = typeof pages === 'number' && pages > 1 ? ` Use --page, or the export action for every page.` : '';
|
|
1160
|
+
process.stderr.write(`! Asked for ${asked} records per page; the API returned at most ${perPage}.${more}
|
|
1161
|
+
`);
|
|
1162
|
+
}
|
|
1163
|
+
sanitizeOutput(data) {
|
|
1164
|
+
const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
1165
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1166
|
+
const sanitize = (obj, parentUuids = new Set()) => {
|
|
1167
|
+
if (obj === null || obj === undefined)
|
|
1168
|
+
return undefined;
|
|
1169
|
+
if (Array.isArray(obj)) {
|
|
1170
|
+
const result = obj.map(item => sanitize(item, new Set(parentUuids))).filter(item => item !== undefined);
|
|
1171
|
+
return result.length > 0 ? result : undefined;
|
|
1172
|
+
}
|
|
1173
|
+
if (typeof obj === 'object') {
|
|
1174
|
+
const input = obj;
|
|
1175
|
+
// Use a Map to avoid any prototype-pollution risk on the result object
|
|
1176
|
+
const collected = new Map();
|
|
1177
|
+
// Collect UUIDs from this level to pass to children
|
|
1178
|
+
const currentUuids = new Set(parentUuids);
|
|
1179
|
+
for (const [, value] of Object.entries(input)) {
|
|
1180
|
+
if (typeof value === 'string' && uuidRegex.test(value)) {
|
|
1181
|
+
currentUuids.add(value);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1185
|
+
// Skip prototype-pollution keys
|
|
1186
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
1187
|
+
continue;
|
|
1188
|
+
// Skip audit fields
|
|
1189
|
+
if (auditFields.includes(key))
|
|
1190
|
+
continue;
|
|
1191
|
+
// Skip null/undefined values
|
|
1192
|
+
if (value === null || value === undefined)
|
|
1193
|
+
continue;
|
|
1194
|
+
// Skip UUID values that exist in parent
|
|
1195
|
+
if (typeof value === 'string' && uuidRegex.test(value) && parentUuids.has(value))
|
|
1196
|
+
continue;
|
|
1197
|
+
// Recursively sanitize nested objects/arrays
|
|
1198
|
+
const sanitized = sanitize(value, currentUuids);
|
|
1199
|
+
if (sanitized !== undefined) {
|
|
1200
|
+
collected.set(key, sanitized);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (collected.size === 0)
|
|
1204
|
+
return undefined;
|
|
1205
|
+
return Object.fromEntries(collected);
|
|
1206
|
+
}
|
|
1207
|
+
return obj;
|
|
1208
|
+
};
|
|
1209
|
+
return sanitize(data) ?? (Array.isArray(data) ? [] : {});
|
|
1210
|
+
}
|
|
1211
|
+
getColumns(data, fields, flatten = false) {
|
|
1212
|
+
const items = Array.isArray(data) ? data : [data];
|
|
1213
|
+
if (items.length === 0)
|
|
1214
|
+
return { cols: [], items: [] };
|
|
1215
|
+
const first = items[0];
|
|
1216
|
+
const hiddenFields = ['tenantId', '_type'];
|
|
1217
|
+
if (fields) {
|
|
1218
|
+
return { cols: fields, items: items };
|
|
1219
|
+
}
|
|
1220
|
+
// Recursively collect all column paths, flattening objects (but not arrays)
|
|
1221
|
+
const collectPaths = (obj, prefix = '') => {
|
|
1222
|
+
const paths = [];
|
|
1223
|
+
for (const key of Object.keys(obj)) {
|
|
1224
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1225
|
+
if (hiddenFields.includes(key))
|
|
1226
|
+
continue;
|
|
1227
|
+
const val = obj[key];
|
|
1228
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
|
|
1229
|
+
// Flatten nested objects (but not arrays)
|
|
1230
|
+
if (flatten) {
|
|
1231
|
+
paths.push(...collectPaths(val, fullKey));
|
|
1232
|
+
}
|
|
1233
|
+
// Skip objects when not flattening
|
|
1234
|
+
}
|
|
1235
|
+
else if (!Array.isArray(val)) {
|
|
1236
|
+
// Include primitives (and null)
|
|
1237
|
+
paths.push(fullKey);
|
|
1238
|
+
}
|
|
1239
|
+
// Skip arrays entirely
|
|
1240
|
+
}
|
|
1241
|
+
return paths;
|
|
1242
|
+
};
|
|
1243
|
+
// Scan multiple items to discover all columns (handles fields absent/null in first item)
|
|
1244
|
+
const cols = collectPaths(first);
|
|
1245
|
+
const colSet = new Set(cols);
|
|
1246
|
+
const scanCount = Math.min(items.length, 50);
|
|
1247
|
+
for (let i = 1; i < scanCount; i++) {
|
|
1248
|
+
for (const p of collectPaths(items[i])) {
|
|
1249
|
+
if (!colSet.has(p)) {
|
|
1250
|
+
cols.push(p);
|
|
1251
|
+
colSet.add(p);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return { cols: flatten ? cols : cols.slice(0, 8), items: items };
|
|
1256
|
+
}
|
|
1257
|
+
getValue(row, col) {
|
|
1258
|
+
if (!col.includes('.'))
|
|
1259
|
+
return String(row[col] ?? '');
|
|
1260
|
+
const parts = col.split('.');
|
|
1261
|
+
// Walk the dot-path; descending through an array branches into every
|
|
1262
|
+
// element so leaf values get collected from each one. Multiple leaves are
|
|
1263
|
+
// joined with ';' (e.g. dunningReminders.id -> "uuid1;uuid2").
|
|
1264
|
+
const walk = (o, idx) => {
|
|
1265
|
+
if (idx >= parts.length)
|
|
1266
|
+
return [o];
|
|
1267
|
+
if (o == null)
|
|
1268
|
+
return [];
|
|
1269
|
+
if (Array.isArray(o))
|
|
1270
|
+
return o.flatMap(item => walk(item, idx));
|
|
1271
|
+
if (typeof o === 'object')
|
|
1272
|
+
return walk(o[parts[idx]], idx + 1);
|
|
1273
|
+
return [];
|
|
1274
|
+
};
|
|
1275
|
+
return walk(row, 0).filter(v => v !== undefined && v !== null).map(v => String(v)).join(';');
|
|
1276
|
+
}
|
|
1277
|
+
printTable(data, fields) {
|
|
1278
|
+
const { cols, items } = this.getColumns(data, fields);
|
|
1279
|
+
if (items.length === 0) {
|
|
1280
|
+
this.log('No results');
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
// Calculate column widths
|
|
1284
|
+
const widths = cols.map(c => c.length);
|
|
1285
|
+
for (const item of items) {
|
|
1286
|
+
cols.forEach((col, i) => {
|
|
1287
|
+
const val = this.getValue(item, col).slice(0, 40);
|
|
1288
|
+
widths[i] = Math.max(widths[i], val.length);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
// Print header
|
|
1292
|
+
const header = cols.map((c, i) => c.padEnd(widths[i])).join(' ');
|
|
1293
|
+
this.log(header);
|
|
1294
|
+
this.log(widths.map(w => '-'.repeat(w)).join(' '));
|
|
1295
|
+
// Print rows
|
|
1296
|
+
for (const item of items) {
|
|
1297
|
+
const row = cols.map((col, i) => {
|
|
1298
|
+
const val = this.getValue(item, col).slice(0, 40);
|
|
1299
|
+
return val.padEnd(widths[i]);
|
|
1300
|
+
}).join(' ');
|
|
1301
|
+
this.log(row);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
printCsv(data, fields) {
|
|
1305
|
+
const { cols, items } = this.getColumns(data, fields, true); // flatten=true for CSV
|
|
1306
|
+
if (items.length === 0) {
|
|
1307
|
+
this.log('No results');
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const escape = (val) => {
|
|
1311
|
+
if (val.includes(',') || val.includes('"') || val.includes('\n')) {
|
|
1312
|
+
return '"' + val.replace(/"/g, '""') + '"';
|
|
1313
|
+
}
|
|
1314
|
+
return val;
|
|
1315
|
+
};
|
|
1316
|
+
// Print header
|
|
1317
|
+
this.log(cols.map(escape).join(','));
|
|
1318
|
+
// Print rows
|
|
1319
|
+
for (const item of items) {
|
|
1320
|
+
const row = cols.map(col => escape(this.getValue(item, col)));
|
|
1321
|
+
this.log(row.join(','));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|