campaign-cli 0.7.2

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.
@@ -0,0 +1,131 @@
1
+ import CampaignError from "./CampaignError.js";
2
+
3
+ /**
4
+ * Campaign CLI class for managing ACC (Campaign Classic) instances.
5
+ * Provides authentication, instance management, and connection capabilities.
6
+ *
7
+ * @class CampaignAuth
8
+ * @classdesc Main class for interacting with ACC instances
9
+ */
10
+ class CampaignAuth {
11
+ /**
12
+ * Configuration key for storing instances
13
+ * @type {string}
14
+ * @private
15
+ */
16
+ INSTANCES_KEY = "instances";
17
+
18
+ /**
19
+ * Creates a new CampaignAuth instance.
20
+ *
21
+ * @param {Object} sdk - ACC JS SDK instance
22
+ * @param {Object} config - Configstore instance for persistent storage
23
+ * @throws {CampaignError} Throws if SDK or config parameters are missing
24
+ *
25
+ * @example
26
+ * const auth = new CampaignAuth(sdk, config);
27
+ */
28
+ constructor(sdk, config) {
29
+ if (!sdk || !config) {
30
+ throw new CampaignError(
31
+ "SDK and Configstore instances are required to initialize CampaignAuth.",
32
+ );
33
+ }
34
+ this.sdk = sdk;
35
+ this.config = config;
36
+ this.instances = config.get(this.INSTANCES_KEY) || {};
37
+ this.instanceIds = Object.keys(this.instances);
38
+ }
39
+
40
+ async ip(){
41
+ console.log(`Fetching IP address...`);
42
+ const ip = await this.sdk.ip();
43
+ console.log(ip);
44
+ }
45
+
46
+ /**
47
+ * Initializes a new ACC instance with the provided credentials.
48
+ *
49
+ * @param {Object} options - Initialization options
50
+ * @param {string} options.alias - Local alias for this instance (e.g., 'prod', 'staging')
51
+ * @param {string} options.host - URL of ACC root (e.g., 'http://localhost:8080')
52
+ * @param {string} options.user - Operator username
53
+ * @param {string} options.password - Operator password
54
+ * @returns {Promise<void>} Resolves when instance is initialized and logged in
55
+ * @throws {CampaignError} Throws if instance with alias already exists
56
+ *
57
+ * @example
58
+ * await auth.init({
59
+ * alias: 'prod',
60
+ * host: 'http://localhost:8080',
61
+ * user: 'admin',
62
+ * password: 'password'
63
+ * });
64
+ */
65
+ async init(options) {
66
+ if (this.instanceIds.includes(options.alias)) {
67
+ throw new CampaignError(
68
+ `Instance with alias ${options.alias} already exists. Please choose a different alias.`,
69
+ );
70
+ }
71
+ const { alias, host, user, password } = options;
72
+ this.config.set(`${this.INSTANCES_KEY}.${alias}`, { host, user, password });
73
+ console.log(`✅ Instance ${alias} added successfully.`);
74
+ return this.login(options);
75
+ }
76
+
77
+ /**
78
+ * Logs in to an existing ACC instance.
79
+ *
80
+ * @param {Object} options - Login options
81
+ * @param {string} options.alias - Alias of the instance to log in to
82
+ * @returns {Promise<Object>} Resolves with the authenticated client
83
+ * @throws {CampaignError} Throws if instance doesn't exist or login fails
84
+ *
85
+ * @example
86
+ * const client = await auth.login({ alias: 'prod' });
87
+ */
88
+ async login(options) {
89
+ const { host, user, password } =
90
+ this.config.get(`instances.${options.alias}`) || {};
91
+ if (!host || !user || !password) {
92
+ throw new CampaignError(
93
+ `Authentication with alias "${options.alias}" doesn't exist. Use "acc auth list" to see all configured instances.`,
94
+ );
95
+ }
96
+ console.log(`↔️ Connecting ${user}@${host}...`);
97
+ const connectionParameters =
98
+ this.sdk.ConnectionParameters.ofUserAndPassword(host, user, password);
99
+ const client = await this.sdk.init(connectionParameters);
100
+ await client.logon();
101
+ const serverInfo = client.getSessionInfo().serverInfo;
102
+ if (!serverInfo) {
103
+ throw new CampaignError(`Unable to get server info.`);
104
+ }
105
+ console.log(
106
+ `✅ Logged in to ${serverInfo.instanceName} (${serverInfo.releaseName} build ${serverInfo.buildNumber}) successfully.`,
107
+ );
108
+ return client;
109
+ }
110
+
111
+ /**
112
+ * Lists all configured ACC instances.
113
+ *
114
+ * @returns {void} Outputs list of instances to console
115
+ *
116
+ * @example
117
+ * auth.list(); // Lists all configured instances
118
+ */
119
+ list() {
120
+ console.log(`📚 Reading ${this.instanceIds.length} instance(s)`);
121
+ if(this.instanceIds.length === 0) {
122
+ console.log(` No instances configured yet. Use "campaign auth init" to add an instance.`);
123
+ return;
124
+ }
125
+ for (const [key, value] of Object.entries(this.instances)) {
126
+ console.log(` - "${key}": ${value.user}@${value.host}`);
127
+ }
128
+ }
129
+ }
130
+
131
+ export default CampaignAuth;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Custom error class for Campaign CLI operations.
3
+ * Extends the standard JavaScript Error class.
4
+ *
5
+ * @class CampaignError
6
+ * @extends Error
7
+ * @classdesc Custom error class for handling ACC-related errors
8
+ *
9
+ * @example
10
+ * throw new CampaignError("Instance already exists");
11
+ *
12
+ * @example
13
+ * try {
14
+ * await auth.login({ alias: "nonexistent" });
15
+ * } catch (err) {
16
+ * if (err instanceof CampaignError) {
17
+ * console.error("Campaign error:", err.message);
18
+ * }
19
+ * }
20
+ */
21
+ class CampaignError extends Error {
22
+ /**
23
+ * Creates a new CampaignError instance.
24
+ *
25
+ * @param {string} [message] - Error message
26
+ * @param {Object} [options] - Error options
27
+ * @param {string} [options.cause] - Underlying cause of the error
28
+ *
29
+ * @example
30
+ * // Simple error
31
+ * throw new CampaignError("Instance not found");
32
+ *
33
+ * @example
34
+ * // Error with cause
35
+ * throw new CampaignError("Login failed", { cause: originalError });
36
+ */
37
+ constructor(message, options) {
38
+ super(message, options);
39
+ this.name = "CampaignError";
40
+ }
41
+ }
42
+
43
+ export default CampaignError;
@@ -0,0 +1,386 @@
1
+ // npm
2
+ import fs from "fs-extra";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+ // sdk
6
+ import sdk from "@adobe/acc-js-sdk";
7
+ const { DomUtil } = sdk;
8
+
9
+ /**
10
+ * Campaign Instance class for interacting with ACC instances.
11
+ * Handles data checking, pulling, and downloading from ACC schemas.
12
+ * - pull():
13
+ * - paginates by batch of 10 (startLine, lineCount)
14
+ * - download()
15
+ * - sdk.xml.xtkQueryDef.create(schema)
16
+ * - sdk.xml.xtkQueryDef.selectAll()
17
+ * - sdk.xml.xtkQueryDef.executeQuery()
18
+ * - for each XML record:
19
+ * - parse()
20
+ *
21
+ * @class CampaignInstance
22
+ * @classdesc Class for managing data operations with ACC instances
23
+ */
24
+ class CampaignInstance {
25
+ REGEX_CONFIG_ATTRIBUTE = /{(.+?)}/g;
26
+ CONFIG_XPATH_SEP = "/";
27
+ CONFIG_XPATH_ATTR = "@";
28
+
29
+ /**
30
+ * Creates a new CampaignInstance.
31
+ *
32
+ * @param {Object} client - Authenticated ACC client
33
+ * @param {Object} accConfig - Configuration object defining schemas and download options
34
+ * @param {Object} [accConfig.*] - Schema-specific configurations
35
+ *
36
+ * @example
37
+ * const instance = new CampaignInstance(client, { schemas: [
38
+ * { schemaId: "nms:recipient", filename: "recipient_%name%.xml" }
39
+ * ]});
40
+ */
41
+ constructor(client, accConfig, options = { verbose: false }) {
42
+ this.client = client;
43
+ this.accConfig = accConfig;
44
+ this.verbose = options.verbose;
45
+ this.downloadPath = options.path;
46
+ /**
47
+ * Array of schema names to process (excluding default config)
48
+ * @type {string[]}
49
+ */
50
+ this.schemas = Object.keys(this.accConfig);
51
+
52
+ this.client.registerObserver({
53
+ onSOAPCall: (soapCall, safeRequestData) => {
54
+ // this.saveArchiveRequest(soapCall.request.data);
55
+ },
56
+ onSOAPCallSuccess: (soapCall, safeResponseData) => {
57
+ // this.saveArchiveResponse(soapCall.response);
58
+ },
59
+ onSOAPCallFailure: (soapCall, error) => {
60
+ // this.saveArchiveResponse(soapCall.response);
61
+ },
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Gets query definition for a specific schema, merging with default config.
67
+ *
68
+ * @param {string} schema - Schema name (e.g., 'nms:recipient')
69
+ * @param {Object} baseQueryDef - Base query definition
70
+ * @returns {Object} Merged query definition
71
+ *
72
+ * @example
73
+ * const queryDef = instance._getQueryDefForSchema('nms:recipient', {
74
+ * schema: 'nms:recipient',
75
+ * operation: 'count'
76
+ * });
77
+ */
78
+ _getQueryDefForSchema(schemaConfig, baseQueryDef) {
79
+ const configQueryDef = schemaConfig.queryDef ? schemaConfig.queryDef : {};
80
+
81
+ return {
82
+ ...baseQueryDef,
83
+ ...configQueryDef,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Pulls data from all schemas in the ACC instance.
89
+ * Implements pagination to handle large datasets.
90
+ *
91
+ * @returns {Promise<void>} Resolves when pull operation is complete
92
+ *
93
+ * @example
94
+ * await instance.pull('/path/to/download');
95
+ */
96
+ async pull(isPreview) {
97
+ this.log(
98
+ `✨ ${isPreview ? "Previewing" : "Pulling"} data to ${this.downloadPath}`,
99
+ );
100
+
101
+ for (const schemaConfig of this.accConfig.schemas) {
102
+ const lineCount = schemaConfig.queryDef?.lineCount || 10;
103
+ let startLine = 1;
104
+ let recordsLengthTotal = 0;
105
+ let recordsLengthCurrent = 0;
106
+ do {
107
+ if (this.verbose) {
108
+ this.log(
109
+ ` Querying instance for records from ${startLine} to ${startLine + lineCount - 1}...`,
110
+ );
111
+ }
112
+ recordsLengthCurrent = await this.downloadAndParse(
113
+ schemaConfig,
114
+ startLine,
115
+ lineCount,
116
+ isPreview,
117
+ );
118
+ startLine += lineCount;
119
+ recordsLengthTotal += recordsLengthCurrent;
120
+ } while (recordsLengthCurrent >= lineCount);
121
+ this.log(
122
+ `✅ ${schemaConfig.filename}: ${chalk.bgCyan(schemaConfig.schemaId)} ${recordsLengthTotal} records`,
123
+ );
124
+ // new line when verbose
125
+ if (this.verbose) {
126
+ this.log("");
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Downloads records from a specific schema and saves them as XML files.
133
+ *
134
+ * @param {Object} schemaConfig - Schema download config
135
+ * @param {number} startLine - Starting line number for pagination
136
+ * @param {number} lineCount - Size of pagination
137
+ * @returns {Promise<number>} Number of records downloaded
138
+ *
139
+ * @example
140
+ * const count = await instance.download('nms:recipient', '/path/to/save', 1);
141
+ */
142
+ async downloadAndParse(schemaConfig, startLine, lineCount, isPreview) {
143
+ const { schemaId } = schemaConfig;
144
+ const baseQueryDef = {
145
+ schema: schemaId,
146
+ operation: "select",
147
+ select: {
148
+ node: [{ expr: "data" }],
149
+ },
150
+ startLine: startLine,
151
+ lineCount: lineCount,
152
+ };
153
+ const queryDef = this._getQueryDefForSchema(schemaConfig, baseQueryDef);
154
+ // console.log("queryDef", JSON.stringify(queryDef));
155
+ const queryDefXml = DomUtil.fromJSON("queryDef", queryDef, "SimpleJson");
156
+ // console.log("queryDefXml", DomUtil.toXMLString(queryDefXml));
157
+
158
+ const query = this.client.NLWS.xml.xtkQueryDef.create(queryDefXml);
159
+
160
+ let message = "";
161
+ var recordsLength = 0;
162
+ try {
163
+ await query.selectAll(false); // @see https://opensource.adobe.com/acc-js-sdk/xtkQueryDef.html
164
+ const records = await query.executeQuery(); // DOMElement <srcSchema-collection><srcSchema></srcSchema>...
165
+ // console.log("records", DomUtil.toXMLString(records));
166
+ if (this.verbose) {
167
+ this.log(
168
+ `Parsing XML Response with ${records.childElementCount} children`,
169
+ );
170
+ }
171
+ var child = DomUtil.getFirstChildElement(records); // @see https://opensource.adobe.com/acc-js-sdk/domHelper.html
172
+ while (child) {
173
+ recordsLength++;
174
+
175
+ this.parse(child, schemaConfig, isPreview);
176
+
177
+ child = DomUtil.getNextSiblingElement(child);
178
+ }
179
+
180
+ message = `${recordsLength} saved.`;
181
+ } catch (err) {
182
+ message = `⚠️ Error executing query: ${err.message}.`;
183
+ } finally {
184
+ if (this.verbose) {
185
+ this.log(` => ${message}`);
186
+ }
187
+ }
188
+ return recordsLength;
189
+ }
190
+
191
+ /**
192
+ * manual xpath to return the last Element
193
+ * - abc/def => def
194
+ * - abc/def/@ghi => def
195
+ *
196
+ * @param {Element} element
197
+ * @param {string} xpath
198
+ * @return Element
199
+ */
200
+ _getLastElement(element, xpath) {
201
+ let childTraverse = element;
202
+ xpath.split(this.CONFIG_XPATH_SEP).forEach((xp) => {
203
+ if (xp.startsWith(this.CONFIG_XPATH_ATTR)) {
204
+ return;
205
+ }
206
+ childTraverse = DomUtil.getFirstChildElement(childTraverse, xp);
207
+ });
208
+ return childTraverse;
209
+ }
210
+
211
+ parse(childElement, schemaConfig, isPreview) {
212
+ // console.log(`>>> parse with isPreview:${isPreview}`);
213
+ const { filename, decompose, excludeXPaths } = schemaConfig;
214
+ const configAttributes = this._getAttributesFromSchemaConfig(schemaConfig); // [ '@name', '@namespace' ]
215
+
216
+ const computedFilename = this._computeFilename(
217
+ filename,
218
+ configAttributes,
219
+ childElement,
220
+ );
221
+ const filenameOnly = path.basename(computedFilename);
222
+ const datapath = path.join(this.downloadPath, computedFilename);
223
+
224
+ // prepare XML by removing excluded attributes
225
+ if (excludeXPaths) {
226
+ for (let xpath of excludeXPaths) {
227
+ const chunks = xpath.split(this.CONFIG_XPATH_SEP);
228
+ const childTraverse = this._getLastElement(childElement, xpath);
229
+
230
+ // remove attribute
231
+ if (xpath.includes(this.CONFIG_XPATH_ATTR)) {
232
+ const attribute = chunks[chunks.length - 1];
233
+ const attributeName = attribute.replace(this.CONFIG_XPATH_ATTR, "");
234
+ if (!childTraverse.hasAttribute(attributeName)) {
235
+ continue;
236
+ }
237
+ childTraverse.setAttribute(attributeName, "");
238
+ }
239
+ // remove element
240
+ else {
241
+ }
242
+ }
243
+ }
244
+
245
+ // no decomposition: save raw XML
246
+ if (!decompose) {
247
+ const raw = DomUtil.toXMLString(childElement);
248
+ if (!isPreview) {
249
+ fs.outputFileSync(datapath, raw);
250
+ }
251
+ }
252
+ // with decomposition: save each xpath, then save the clean meta
253
+ else {
254
+ // 1. save each xpath + removeElement
255
+ for (const [xpath, filenameTemplate] of Object.entries(decompose)) {
256
+ try {
257
+ // compute filename
258
+ const decomposedFilename = this._computeFilename(
259
+ filenameTemplate,
260
+ configAttributes,
261
+ childElement,
262
+ );
263
+ // then traverse xpath
264
+ let childTraverse = this._getLastElement(childElement, xpath);
265
+ const elementValue = DomUtil.elementValue(childTraverse);
266
+ // save to file
267
+ const datapath = path.join(this.downloadPath, decomposedFilename);
268
+ if (!isPreview) {
269
+ fs.outputFileSync(datapath, elementValue);
270
+ }
271
+ const decomposedFilenameOnly = path.basename(decomposedFilename);
272
+ if (this.verbose) {
273
+ this.log(`${chalk.underline(decomposedFilenameOnly)} `, false);
274
+ }
275
+ // removeElement
276
+ if (childTraverse) {
277
+ childTraverse.textContent = ""; // @since 0.5.1, instead of removeChild that removed attributes
278
+ }
279
+ } catch (err) {
280
+ this.log(`(⚠️ warning:parse ${err.message})`);
281
+ }
282
+ }
283
+ // 2. save meta
284
+ const metaContent = DomUtil.toXMLString(childElement);
285
+ if (!isPreview) {
286
+ fs.outputFileSync(datapath, metaContent);
287
+ }
288
+ }
289
+
290
+ if (this.verbose) {
291
+ this.log(`${chalk.underline(filenameOnly)} `, false);
292
+ }
293
+ }
294
+
295
+ _getAttributesFromSchemaConfig(schemaConfig) {
296
+ const configAttributesRe = schemaConfig.filename.matchAll(
297
+ this.REGEX_CONFIG_ATTRIBUTE,
298
+ ); // [object RegExp String Iterator]
299
+ const configAttributesArr = Array.from(configAttributesRe); // [ [ '@name', '@name' ], ... ]
300
+ return configAttributesArr.map((attr) => attr[1]); // [ '@name', '@namespace' ]
301
+ }
302
+
303
+ _computeFilename(configFilename, configAttributes, record) {
304
+ var filename = configFilename;
305
+ for (let configAttribute of configAttributes) {
306
+ const value = DomUtil.getAttributeAsString(
307
+ record,
308
+ configAttribute.replace(this.CONFIG_XPATH_ATTR, ""),
309
+ );
310
+ filename = filename.replace(`{${configAttribute}}`, value);
311
+ }
312
+ return filename;
313
+ }
314
+
315
+ /**
316
+ * Saves SOAP request to archive file with timestamp.
317
+ *
318
+ * @param {string} rawRequest - Raw SOAP request XML
319
+ * @returns {void}
320
+ *
321
+ * @example
322
+ * instance.saveArchiveRequest('<soap:Envelope>...</soap:Envelope>');
323
+ */
324
+ saveArchiveRequest(rawRequest) {
325
+ const archiveRequest =
326
+ "archives/" + this.getArchiveDate() + "-CampaignInstance-request.xml";
327
+ fs.outputFileSync(archiveRequest, rawRequest, function (errFs) {
328
+ throw errFs;
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Saves SOAP response to archive file with timestamp.
334
+ *
335
+ * @param {string} rawResponse - Raw SOAP response XML
336
+ * @returns {void}
337
+ *
338
+ * @example
339
+ * instance.saveArchiveResponse('<soap:Envelope>...</soap:Envelope>');
340
+ */
341
+ saveArchiveResponse(rawResponse) {
342
+ const archiveResponse =
343
+ "archives/" + this.getArchiveDate() + "-CampaignInstance-response.xml";
344
+ fs.outputFileSync(archiveResponse, rawResponse, function (errFs) {
345
+ throw errFs;
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Generates timestamp string for archive files in format: YYYY/MM/DD/HH-mm-ss_ms
351
+ *
352
+ * @returns {string} Formatted timestamp string
353
+ *
354
+ * @example
355
+ * const timestamp = instance.getArchiveDate(); // "2023/01/15/14-30-45_123"
356
+ */
357
+ getArchiveDate() {
358
+ var ts_hms = new Date();
359
+
360
+ return (
361
+ ts_hms.getFullYear() +
362
+ "/" +
363
+ ("0" + (ts_hms.getMonth() + 1)).slice(-2) +
364
+ "/" +
365
+ ("0" + ts_hms.getDate()).slice(-2) +
366
+ "/" +
367
+ ("0" + ts_hms.getHours()).slice(-2) +
368
+ "-" +
369
+ ("0" + ts_hms.getMinutes()).slice(-2) +
370
+ "-" +
371
+ ("0" + ts_hms.getSeconds()).slice(-2) +
372
+ "_" +
373
+ ts_hms.getMilliseconds()
374
+ );
375
+ }
376
+
377
+ log(text, newLine = true) {
378
+ if (newLine) {
379
+ console.log(text);
380
+ } else {
381
+ process.stdout.write(text);
382
+ }
383
+ }
384
+ }
385
+
386
+ export default CampaignInstance;