#!/usr/bin/env node
/**
* aPaaS Configuration Validation Script
*
* This script validates your aPaaS setup for TikTok analytics dashboard.
* It checks:
* - Environment variables
* - API connectivity
* - Bitable access
* - Data structure
* - Chart configurations
*
* Usage:
* node scripts/validate-apaas-setup.js
*
* Requirements:
* - .env file with APP_ID, APP_SECRET, or PERSONAL_BASE_TOKEN
* - Network access to Feishu APIs
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
// Color codes for terminal output
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
bold: '\x1b[1m'
};
// Configuration
const config = {
app_token: 'C8kmbTsqoa6rBesTKRpl8nV8gHd',
table_id: 'tblG4uuUvbwfvI9Z',
apaas_app_id: 'Dffwb10dwaz6UZs6c4HlWyV3g7c',
apaas_page_id: 'pgeEOroex4nCBQxc',
required_fields: ['Date', 'Views', 'Likes', 'Comments', 'Shares', 'Followers', 'Engagement_Rate']
};
// Test results tracker
const results = {
passed: 0,
failed: 0,
warnings: 0,
tests: []
};
/**
* Logging utilities
*/
function log(message, type = 'info') {
const timestamp = new Date().toISOString();
let color = colors.reset;
let prefix = '[INFO]';
switch (type) {
case 'success':
color = colors.green;
prefix = '[PASS]';
break;
case 'error':
color = colors.red;
prefix = '[FAIL]';
break;
case 'warning':
color = colors.yellow;
prefix = '[WARN]';
break;
case 'info':
color = colors.cyan;
prefix = '[INFO]';
break;
}
console.log(`${color}${prefix}${colors.reset} ${message}`);
}
function logSection(title) {
console.log(`\n${colors.bold}${colors.blue}${'='.repeat(60)}${colors.reset}`);
console.log(`${colors.bold}${colors.blue}${title}${colors.reset}`);
console.log(`${colors.bold}${colors.blue}${'='.repeat(60)}${colors.reset}\n`);
}
/**
* Load environment variables
*/
function loadEnvVars() {
try {
const envPath = path.join(__dirname, '..', '.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
envContent.split('\n').forEach(line => {
const match = line.match(/^([^=]+)=(.*)$/);
if (match) {
process.env[match[1].trim()] = match[2].trim();
}
});
return true;
}
return false;
} catch (error) {
return false;
}
}
/**
* Make HTTPS request
*/
function makeRequest(options, postData = null) {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: JSON.parse(data)
});
} catch (error) {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: data
});
}
});
});
req.on('error', (error) => {
reject(error);
});
if (postData) {
req.write(postData);
}
req.end();
});
}
/**
* Test 1: Check environment variables
*/
async function testEnvironmentVariables() {
logSection('Test 1: Environment Variables');
let envLoaded = loadEnvVars();
if (!envLoaded) {
log('No .env file found - will check process environment', 'warning');
results.warnings++;
}
const hasAppCredentials = process.env.APP_ID && process.env.APP_SECRET;
const hasPersonalToken = process.env.PERSONAL_BASE_TOKEN;
if (hasAppCredentials) {
log('✓ Found APP_ID and APP_SECRET', 'success');
results.passed++;
return { type: 'tenant', appId: process.env.APP_ID, appSecret: process.env.APP_SECRET };
} else if (hasPersonalToken) {
log('✓ Found PERSONAL_BASE_TOKEN', 'success');
results.passed++;
return { type: 'personal', token: process.env.PERSONAL_BASE_TOKEN };
} else {
log('✗ Missing authentication credentials', 'error');
log(' Please set either (APP_ID + APP_SECRET) or PERSONAL_BASE_TOKEN in .env file', 'error');
results.failed++;
return null;
}
}
/**
* Test 2: Get tenant access token
*/
async function testTenantAccessToken(credentials) {
logSection('Test 2: Tenant Access Token');
if (credentials.type !== 'tenant') {
log('Skipping - using personal base token', 'info');
return credentials.token;
}
try {
const postData = JSON.stringify({
app_id: credentials.appId,
app_secret: credentials.appSecret
});
const options = {
hostname: 'open.feishu.cn',
path: '/open-apis/auth/v3/tenant_access_token/internal',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const response = await makeRequest(options, postData);
if (response.statusCode === 200 && response.body.code === 0) {
log('✓ Successfully obtained tenant access token', 'success');
log(` Token expires in: ${response.body.expire} seconds`, 'info');
results.passed++;
return response.body.tenant_access_token;
} else {
log('✗ Failed to get tenant access token', 'error');
log(` Error: ${response.body.msg || 'Unknown error'}`, 'error');
results.failed++;
return null;
}
} catch (error) {
log('✗ Request failed: ' + error.message, 'error');
results.failed++;
return null;
}
}
/**
* Test 3: Verify Bitable access
*/
async function testBitableAccess(token) {
logSection('Test 3: Bitable Access');
if (!token) {
log('✗ Skipping - no access token available', 'error');
results.failed++;
return null;
}
try {
const postData = JSON.stringify({
page_size: 1
});
const options = {
hostname: 'open.feishu.cn',
path: `/open-apis/bitable/v1/apps/${config.app_token}/tables/${config.table_id}/records/search`,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const response = await makeRequest(options, postData);
if (response.statusCode === 200 && response.body.code === 0) {
log('✓ Successfully accessed Bitable table', 'success');
log(` Total records: ${response.body.data.total}`, 'info');
results.passed++;
return response.body.data;
} else {
log('✗ Failed to access Bitable', 'error');
log(` Status: ${response.statusCode}`, 'error');
log(` Error: ${response.body.msg || 'Unknown error'}`, 'error');
if (response.statusCode === 403) {
log(' This usually means:', 'warning');
log(' - App does not have permission to access this Bitable', 'warning');
log(' - Bitable is not shared with the app', 'warning');
log(' - App does not have required scopes (bitable:app:readonly)', 'warning');
}
results.failed++;
return null;
}
} catch (error) {
log('✗ Request failed: ' + error.message, 'error');
results.failed++;
return null;
}
}
/**
* Test 4: Verify data structure
*/
async function testDataStructure(token) {
logSection('Test 4: Data Structure Validation');
if (!token) {
log('✗ Skipping - no access token available', 'error');
results.failed++;
return false;
}
try {
const postData = JSON.stringify({
field_names: config.required_fields,
page_size: 10
});
const options = {
hostname: 'open.feishu.cn',
path: `/open-apis/bitable/v1/apps/${config.app_token}/tables/${config.table_id}/records/search`,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const response = await makeRequest(options, postData);
if (response.statusCode === 200 && response.body.code === 0) {
const items = response.body.data.items || [];
if (items.length === 0) {
log('⚠ Table is empty - no data to validate', 'warning');
results.warnings++;
return false;
}
// Check if all required fields exist
const sampleRecord = items[0].fields;
const foundFields = Object.keys(sampleRecord);
const missingFields = config.required_fields.filter(f => !foundFields.includes(f));
if (missingFields.length === 0) {
log('✓ All required fields present', 'success');
results.passed++;
} else {
log('✗ Missing required fields:', 'error');
missingFields.forEach(field => {
log(` - ${field}`, 'error');
});
results.failed++;
return false;
}
// Validate field types
log('\nField type validation:', 'info');
config.required_fields.forEach(field => {
const value = sampleRecord[field];
const type = typeof value;
log(` ${field}: ${type} (value: ${value})`, 'info');
});
// Check for null/undefined values
let hasNulls = false;
config.required_fields.forEach(field => {
if (sampleRecord[field] == null) {
log(` ⚠ Field "${field}" has null value`, 'warning');
hasNulls = true;
results.warnings++;
}
});
if (!hasNulls) {
log('✓ No null values in required fields', 'success');
results.passed++;
}
return true;
} else {
log('✗ Failed to fetch data', 'error');
results.failed++;
return false;
}
} catch (error) {
log('✗ Request failed: ' + error.message, 'error');
results.failed++;
return false;
}
}
/**
* Test 5: Verify chart configuration files
*/
async function testChartConfigurations() {
logSection('Test 5: Chart Configuration Files');
const chartsDir = path.join(__dirname, '..', 'config', 'apaas-charts');
const expectedFiles = [
'01-metric-card-total-views.json',
'02-metric-card-total-likes.json',
'03-metric-card-avg-engagement.json',
'04-metric-card-total-followers.json',
'05-line-chart-views-engagement.json',
'06-bar-chart-engagement-breakdown.json',
'07-pie-chart-engagement-distribution.json',
'08-table-video-performance.json'
];
if (!fs.existsSync(chartsDir)) {
log('✗ Charts directory not found: ' + chartsDir, 'error');
results.failed++;
return false;
}
let allValid = true;
for (const filename of expectedFiles) {
const filepath = path.join(chartsDir, filename);
if (!fs.existsSync(filepath)) {
log(`✗ Missing configuration: ${filename}`, 'error');
results.failed++;
allValid = false;
continue;
}
try {
const content = fs.readFileSync(filepath, 'utf8');
const config = JSON.parse(content);
// Validate required fields
if (!config.component || !config.id || !config.config) {
log(`✗ Invalid configuration in ${filename}`, 'error');
results.failed++;
allValid = false;
} else {
log(`✓ Valid configuration: ${filename}`, 'success');
results.passed++;
}
} catch (error) {
log(`✗ Invalid JSON in ${filename}: ${error.message}`, 'error');
results.failed++;
allValid = false;
}
}
return allValid;
}
/**
* Test 6: Validate data request configuration
*/
async function testDataRequestConfig() {
logSection('Test 6: Data Request Configuration');
const configPath = path.join(__dirname, '..', 'config', 'apaas-data-request.json');
if (!fs.existsSync(configPath)) {
log('✗ Data request configuration not found', 'error');
results.failed++;
return false;
}
try {
const content = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(content);
// Check required sections
const requiredSections = ['dataRequest', 'comments', 'quickStart', 'exampleUsage'];
let allPresent = true;
requiredSections.forEach(section => {
if (!config[section]) {
log(`✗ Missing section: ${section}`, 'error');
allPresent = false;
results.failed++;
}
});
if (allPresent) {
log('✓ All required sections present', 'success');
results.passed++;
}
// Validate API endpoint
if (config.dataRequest && config.dataRequest.api) {
const url = config.dataRequest.api.url;
if (url.includes(config.app_token) && url.includes(config.table_id)) {
log('✓ API endpoint configured correctly', 'success');
results.passed++;
} else {
log('⚠ API endpoint may not match expected app_token/table_id', 'warning');
results.warnings++;
}
}
return true;
} catch (error) {
log('✗ Invalid JSON: ' + error.message, 'error');
results.failed++;
return false;
}
}
/**
* Print summary
*/
function printSummary() {
logSection('Validation Summary');
console.log(`${colors.green}Passed: ${results.passed}${colors.reset}`);
console.log(`${colors.red}Failed: ${results.failed}${colors.reset}`);
console.log(`${colors.yellow}Warnings: ${results.warnings}${colors.reset}`);
console.log('\n');
if (results.failed === 0) {
console.log(`${colors.green}${colors.bold}✓ All tests passed! Your aPaaS setup is ready.${colors.reset}\n`);
console.log('Next steps:');
console.log('1. Open aPaaS console: https://apaas.feishu.cn');
console.log('2. Navigate to your app: Dffwb10dwaz6UZs6c4HlWyV3g7c');
console.log('3. Open page: pgeEOroex4nCBQxc');
console.log('4. Import chart configurations from config/apaas-charts/');
console.log('5. Follow APPROACH_B_COMPLETE.md for detailed setup');
return 0;
} else {
console.log(`${colors.red}${colors.bold}✗ Validation failed with ${results.failed} error(s).${colors.reset}\n`);
console.log('Please fix the errors above and run validation again.');
console.log('For help, see:');
console.log('- config/INTEGRATION_GUIDE.md');
console.log('- APAAS_CHART_CONFIG.md');
return 1;
}
}
/**
* Main execution
*/
async function main() {
console.log(`${colors.bold}${colors.cyan}`);
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ aPaaS Configuration Validation Script ║');
console.log('║ TikTok Analytics Dashboard Setup ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(colors.reset);
log('Starting validation...', 'info');
log(`App Token: ${config.app_token}`, 'info');
log(`Table ID: ${config.table_id}`, 'info');
try {
// Run all tests
const credentials = await testEnvironmentVariables();
const token = credentials ? await testTenantAccessToken(credentials) : null;
await testBitableAccess(token);
await testDataStructure(token);
await testChartConfigurations();
await testDataRequestConfig();
// Print summary
const exitCode = printSummary();
process.exit(exitCode);
} catch (error) {
log('Unexpected error: ' + error.message, 'error');
console.error(error);
process.exit(1);
}
}
// Run the script
if (require.main === module) {
main();
}
module.exports = { testEnvironmentVariables, testBitableAccess, testDataStructure };