sfcc-cip-analytics-client
Version:
SFCC Commerce Intelligence Platform Analytics Client
568 lines (552 loc) • 21.8 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.main = main;
const dotenv = __importStar(require("dotenv"));
dotenv.config({ quiet: true });
const util_1 = require("util");
const cip_client_1 = require("./cip-client");
const utils_1 = require("./utils");
const types_1 = require("./data/types");
const fs_1 = require("fs");
const customer_registration_analytics_1 = require("./data/aggregate/customer_registration_analytics");
const product_analytics_1 = require("./data/aggregate/product_analytics");
const promotion_analytics_1 = require("./data/aggregate/promotion_analytics");
// Recommendation analytics functions removed - no corresponding SQL blocks found
const technical_performance_analytics_1 = require("./data/aggregate/technical_performance_analytics");
const sales_analytics_1 = require("./data/aggregate/sales_analytics");
const search_analytics_1 = require("./data/aggregate/search_analytics");
const payment_method_analytics_1 = require("./data/aggregate/payment_method_analytics");
const traffic_source_analytics_1 = require("./data/aggregate/traffic_source_analytics");
// Registry of available enhanced query functions (only functions with corresponding SQL blocks)
const availableQueries = [
// Customer Analytics
customer_registration_analytics_1.queryCustomerRegistrationTrends,
// Product Analytics
product_analytics_1.queryTopSellingProducts,
product_analytics_1.queryProductCoPurchaseAnalysis,
// Promotion Analytics
promotion_analytics_1.queryPromotionDiscountAnalysis,
// Technical Analytics
technical_performance_analytics_1.queryOcapiRequests,
// Sales Analytics
sales_analytics_1.querySalesAnalytics,
sales_analytics_1.querySalesSummary,
// Search Analytics
search_analytics_1.querySearchQueryPerformance,
// Payment Analytics
payment_method_analytics_1.queryPaymentMethodPerformance,
// Traffic Analytics
traffic_source_analytics_1.queryTopReferrers,
];
function getQueryByName(name) {
const queryFn = availableQueries.find(q => q.metadata.name === name);
if (!queryFn)
return undefined;
return {
name: queryFn.metadata.name,
description: queryFn.metadata.description,
category: queryFn.metadata.category,
requiredParams: queryFn.metadata.requiredParams,
optionalParams: queryFn.metadata.optionalParams,
execute: (client, params) => {
// Only handle dateRange conversion - pass everything else through directly
const queryParams = {
...params,
dateRange: params.from && params.to ? {
startDate: new Date(params.from),
endDate: new Date(params.to)
} : undefined
};
// Remove the CLI-specific from/to params since they're now in dateRange
delete queryParams.from;
delete queryParams.to;
// Call the query function with the standardized params
return queryFn(client, queryParams, params.batchSize || 100);
}
};
}
function listQueries() {
return availableQueries.map(queryFn => ({
name: queryFn.metadata.name,
description: queryFn.metadata.description,
category: queryFn.metadata.category,
requiredParams: queryFn.metadata.requiredParams,
optionalParams: queryFn.metadata.optionalParams,
execute: getQueryByName(queryFn.metadata.name).execute
}));
}
function getQueriesByCategory() {
const byCategory = new Map();
for (const queryDef of listQueries()) {
const categoryQueries = byCategory.get(queryDef.category) || [];
categoryQueries.push(queryDef);
byCategory.set(queryDef.category, categoryQueries);
}
return byCategory;
}
function parseDate(input) {
const timestamp = Date.parse(input);
if (isNaN(timestamp)) {
throw new Error(`Unable to parse date: "${input}". Use standard date formats like "2024-01-15", "Jan 15, 2024", "2024-01-15T00:00:00"`);
}
const date = new Date(timestamp);
return { date, formatted: (0, types_1.formatDateForSQL)(date) };
}
function replacePlaceholders(sql, fromDate, toDate) {
let result = sql;
if (fromDate) {
result = result.replace(/<FROM>/g, `'${fromDate}'`);
}
if (toDate) {
result = result.replace(/<TO>/g, `'${toDate}'`);
}
return result;
}
function outputAsCSV(data) {
if (data.length === 0) {
console.log('No data');
return;
}
const headers = Object.keys(data[0]);
console.log(headers.join(','));
for (const row of data) {
const values = headers.map(header => {
const value = row[header];
if (value === null || value === undefined)
return '';
const stringValue = String(value);
// Escape quotes and wrap in quotes if contains comma or quote
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
});
console.log(values.join(','));
}
}
function outputAsJSON(data) {
console.log(JSON.stringify(data, null, 2));
}
function outputAsTable(data) {
if (data.length === 0) {
console.log('No data');
return;
}
console.table(data);
}
async function readStdin() {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
let chunk;
while (null !== (chunk = process.stdin.read())) {
data += chunk;
}
});
process.stdin.on('end', () => {
resolve(data);
});
process.stdin.on('error', reject);
});
}
function showHelp() {
console.log(`
CIP Analytics Client CLI
Usage:
cip-query <command> [options]
Commands:
sql Execute arbitrary SQL queries
query Execute predefined business queries
SQL Command:
cip-query sql [options] <sql>
cip-query sql [options] --file <file.sql>
cip-query sql [options] < file.sql
echo "SELECT * FROM table" | cip-query sql [options]
Options:
--file <path> Read SQL from file (strips shebang if present)
--from <date> From date for <FROM> placeholder
--to <date> To date for <TO> placeholder
--format <type> Output format: table (default), json, csv
Query Command:
cip-query query --name <query-name> [options]
cip-query query --list
Options:
--name <name> Name of the predefined query to execute
--list List all available queries
--from <date> From date (maps to 'from' parameter)
--to <date> To date (maps to 'to' parameter)
--param <k=v> Additional query parameters (can be used multiple times)
--format <type> Output format: table (default), json, csv
Common parameters:
--param siteId=<id> Site ID
--param deviceClassCode=<code> Device class code
--param batchSize=<size> Batch size for results
Note: The --from and --to flags automatically provide the 'from' and 'to' parameters to queries.
Global Options:
--help Show this help message
--client-id SFCC client ID (overrides SFCC_CLIENT_ID env var)
--client-secret SFCC client secret (overrides SFCC_CLIENT_SECRET env var)
--instance SFCC CIP instance (overrides SFCC_CIP_INSTANCE env var)
Environment Variables:
SFCC_CLIENT_ID Your SFCC client ID (required unless --client-id is provided)
SFCC_CLIENT_SECRET Your SFCC client secret (required unless --client-secret is provided)
SFCC_CIP_INSTANCE Your SFCC CIP instance (required unless --instance is provided)
SFCC_DEBUG Enable debug logging (optional)
Note: Environment variables can be set in a .env file in the current directory
Examples:
# Execute arbitrary SQL with CLI options
cip-query sql --client-id my-id --client-secret my-secret --instance my-instance \\
"SELECT * FROM ccdw_aggr_ocapi_request LIMIT 10"
# Execute SQL with date placeholders
cip-query sql --format json --from "2024-01-01" --to "2024-01-02" <<SQL
SELECT * FROM ccdw_aggr_ocapi_request
WHERE request_date >= <FROM> AND request_date <= <TO>
SQL
# List available business queries
cip-query query --list
# Execute a business query
cip-query query --name customer-registration-trends \\
--param siteId=my-site \\
--from 2024-01-01 \\
--to 2024-01-31 \\
--format csv
`);
}
function parseParams(paramArgs) {
const params = {};
for (const arg of paramArgs) {
const [key, ...valueParts] = arg.split('=');
const value = valueParts.join('='); // Handle values with '=' in them
if (!key || !value) {
throw new Error(`Invalid parameter format: "${arg}". Use key=value format.`);
}
params[key] = value;
}
return params;
}
function listAvailableQueries() {
const queriesByCategory = getQueriesByCategory();
console.log('\nAvailable Queries:\n');
for (const [category, queries] of queriesByCategory) {
console.log(`${category}:`);
for (const query of queries) {
console.log(` ${query.name}`);
console.log(` ${query.description}`);
console.log(` Required: ${query.requiredParams.join(', ') || 'none'}`);
if (query.optionalParams && query.optionalParams.length > 0) {
console.log(` Optional: ${query.optionalParams.join(', ')}`);
}
console.log();
}
}
}
async function executeSqlCommand(args) {
const { values, positionals } = (0, util_1.parseArgs)({
args,
options: {
from: { type: 'string' },
to: { type: 'string' },
format: { type: 'string', default: 'table' },
file: { type: 'string' },
'client-id': { type: 'string' },
'client-secret': { type: 'string' },
instance: { type: 'string' },
help: { type: 'boolean', short: 'h' }
},
allowPositionals: true
});
if (values.help) {
showHelp();
return;
}
// Get SQL from file, positional args, or stdin
let sql;
if (values.file) {
// Read from file
try {
let fileContent = (0, fs_1.readFileSync)(values.file, 'utf-8');
// Strip shebang line if present
if (fileContent.startsWith('#!')) {
const lines = fileContent.split('\n');
lines.shift(); // Remove the first line (shebang)
fileContent = lines.join('\n');
}
sql = fileContent;
}
catch (error) {
console.error(`Error reading file "${values.file}": ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
else if (positionals.length === 0) {
// Check if stdin has data
if (process.stdin.isTTY) {
console.error('Error: SQL query is required (provide as argument, --file, or via stdin)');
showHelp();
process.exit(1);
}
// Read from stdin
let stdinContent = await readStdin();
if (!stdinContent.trim()) {
console.error('Error: No SQL query provided via stdin');
process.exit(1);
}
// Strip shebang line if present
if (stdinContent.startsWith('#!')) {
const lines = stdinContent.split('\n');
lines.shift(); // Remove the first line (shebang)
stdinContent = lines.join('\n');
}
sql = stdinContent;
}
else {
sql = positionals.join(' ');
}
sql = (0, types_1.cleanSQL)(sql);
const format = values.format;
if (!['table', 'json', 'csv'].includes(format)) {
console.error('Error: Format must be one of: table, json, csv');
process.exit(1);
}
// Parse dates if provided
let fromDate;
let toDate;
if (values.from) {
const parsed = parseDate(values.from);
fromDate = parsed.formatted;
console.info(`Using from date: ${parsed.formatted} (parsed from "${values.from}")`);
}
if (values.to) {
const parsed = parseDate(values.to);
toDate = parsed.formatted;
console.info(`Using to date: ${parsed.formatted} (parsed from "${values.to}")`);
}
// Replace placeholders in SQL
const finalSQL = replacePlaceholders(sql, fromDate, toDate);
console.info(`Executing SQL: ${finalSQL}`);
// Get credentials from CLI options or environment variables
const clientId = values['client-id'] || process.env.SFCC_CLIENT_ID;
const clientSecret = values['client-secret'] || process.env.SFCC_CLIENT_SECRET;
const instance = values.instance || process.env.SFCC_CIP_INSTANCE;
if (!clientId || !clientSecret || !instance) {
console.error('Error: Required credentials not provided.');
console.error('Provide via CLI options (--client-id, --client-secret, --instance) or environment variables (SFCC_CLIENT_ID, SFCC_CLIENT_SECRET, SFCC_CIP_INSTANCE)');
process.exit(1);
}
// Create client and execute query
const client = new cip_client_1.CIPClient(clientId, clientSecret, instance);
try {
await client.openConnection({});
const statementId = await client.createStatement();
// Execute query and collect all results
const executeResponse = await client.execute(statementId, finalSQL);
const allData = [];
if (executeResponse.results && executeResponse.results.length > 0) {
const result = executeResponse.results[0];
if (result.firstFrame) {
const firstFrameData = (0, utils_1.processFrame)(result.signature, result.firstFrame);
allData.push(...firstFrameData);
let done = result.firstFrame.done;
let currentFrame = result.firstFrame;
// Fetch remaining data
while (!done && currentFrame) {
const currentOffset = currentFrame.offset || 0;
const currentRowCount = currentFrame.rows?.length || 0;
const nextResponse = await client.fetch(result.statementId || 0, currentOffset + currentRowCount, 1000 // larger batch size for CLI
);
currentFrame = nextResponse.frame;
if (!currentFrame)
break;
const nextData = (0, utils_1.processFrame)(result.signature, currentFrame);
allData.push(...nextData);
done = currentFrame.done;
}
}
}
await client.closeStatement(statementId);
// Output results in requested format
switch (format) {
case 'json':
outputAsJSON(allData);
break;
case 'csv':
outputAsCSV(allData);
break;
default:
outputAsTable(allData);
break;
}
console.info(`\nQuery completed. Retrieved ${allData.length} rows.`);
}
finally {
await client.closeConnection();
}
}
async function executeQueryCommand(args) {
const { values } = (0, util_1.parseArgs)({
args,
options: {
name: { type: 'string' },
list: { type: 'boolean' },
param: { type: 'string', multiple: true },
from: { type: 'string' },
to: { type: 'string' },
format: { type: 'string', default: 'table' },
'client-id': { type: 'string' },
'client-secret': { type: 'string' },
instance: { type: 'string' },
help: { type: 'boolean', short: 'h' }
},
allowPositionals: false
});
if (values.help) {
showHelp();
return;
}
if (values.list) {
listAvailableQueries();
return;
}
if (!values.name) {
console.error('Error: --name or --list is required for query command');
showHelp();
process.exit(1);
}
const queryDef = getQueryByName(values.name);
if (!queryDef) {
console.error(`Error: Unknown query "${values.name}"`);
console.error('Use --list to see available queries');
process.exit(1);
}
// Parse parameters
const params = parseParams(values.param || []);
// Add date parameters if provided
if (values.from) {
const parsed = parseDate(values.from);
params.from = parsed.formatted;
console.info(`Using from date: ${parsed.formatted} (parsed from "${values.from}")`);
}
if (values.to) {
const parsed = parseDate(values.to);
params.to = parsed.formatted;
console.info(`Using to date: ${parsed.formatted} (parsed from "${values.to}")`);
}
// Validate required parameters
const missingParams = queryDef.requiredParams.filter(p => !params[p]);
if (missingParams.length > 0) {
console.error(`Error: Missing required parameters: ${missingParams.join(', ')}`);
console.error(`Required parameters for ${queryDef.name}: ${queryDef.requiredParams.join(', ')}`);
if (queryDef.optionalParams && queryDef.optionalParams.length > 0) {
console.error(`Optional parameters: ${queryDef.optionalParams.join(', ')}`);
}
process.exit(1);
}
const format = values.format;
if (!['table', 'json', 'csv'].includes(format)) {
console.error('Error: Format must be one of: table, json, csv');
process.exit(1);
}
// Get credentials from CLI options or environment variables
const clientId = values['client-id'] || process.env.SFCC_CLIENT_ID;
const clientSecret = values['client-secret'] || process.env.SFCC_CLIENT_SECRET;
const instance = values.instance || process.env.SFCC_CIP_INSTANCE;
if (!clientId || !clientSecret || !instance) {
console.error('Error: Required credentials not provided.');
console.error('Provide via CLI options (--client-id, --client-secret, --instance) or environment variables (SFCC_CLIENT_ID, SFCC_CLIENT_SECRET, SFCC_CIP_INSTANCE)');
process.exit(1);
}
console.info(`Executing query: ${queryDef.name}`);
console.info(`Parameters: ${JSON.stringify(params)}`);
// Create client and execute query
const client = new cip_client_1.CIPClient(clientId, clientSecret, instance);
try {
await client.openConnection({});
// Execute the business query
const allData = [];
for await (const batch of queryDef.execute(client, params)) {
allData.push(...batch);
}
// Output results in requested format
switch (format) {
case 'json':
outputAsJSON(allData);
break;
case 'csv':
outputAsCSV(allData);
break;
default:
outputAsTable(allData);
break;
}
console.info(`\nQuery completed. Retrieved ${allData.length} rows.`);
}
finally {
await client.closeConnection();
}
}
async function main() {
try {
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
showHelp();
return;
}
const command = args[0];
const commandArgs = args.slice(1);
switch (command) {
case 'sql':
await executeSqlCommand(commandArgs);
break;
case 'query':
await executeQueryCommand(commandArgs);
break;
default:
console.error(`Error: Unknown command "${command}"`);
console.error('Valid commands: sql, query');
showHelp();
process.exit(1);
}
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
if (require.main === module) {
main();
}