UNPKG

sfcc-cip-analytics-client

Version:

SFCC Commerce Intelligence Platform Analytics Client

568 lines (552 loc) 21.8 kB
#!/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(); }