amazon-modern-widgets
Version:
Amazon Modern Widgets for Amazon affiliate websites based on Amazon PAAPI v5
340 lines • 12.1 kB
JavaScript
;
/**
* API Implementation.
* ----------------------------------------------
* Amazon Modern Widgets (AMW).
*
* @author : Ludovic Toinel <ludovic@toinel.com>
* @src : https://github.com/ltoinel/amw
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AmwApi = void 0;
// Lets import our required libraries
const config_1 = __importDefault(require("config"));
const path_1 = __importDefault(require("path"));
const Paapi_1 = require("./Paapi");
const ConfigLog4j_1 = require("../utils/ConfigLog4j");
// Custom error classes
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
/**
* API Implementation.
*/
class AmwApi {
/**
* Main AmwApi constructor.
*/
constructor(cache) {
// Initialize the logger
this.log = (0, ConfigLog4j_1.getLogger)("AmwApi");
// The cache to optimize the API calls to Amazon
this.cache = cache;
// Initialize the Paapi client
this.paapi = new Paapi_1.Paapi();
// Get TTL configuration with fallback
this.ttl = Number(config_1.default.get('Redis.expire')) || 3600; // 1 hour default
// Validate configuration
this.validateConfiguration();
this.log.info("AmwApi initialized successfully");
}
/**
* Validate configuration settings
*/
validateConfiguration() {
if (!AmwApi.PROJECT_DIR) {
throw new Error('Server.projectDir configuration is required');
}
if (this.ttl <= 0) {
throw new Error('Redis.expire must be a positive number');
}
}
/**
* Set the API Search endpoint.
*
* @param req The request object.
* @param res The response object.
*/
setProductEndpoint(req, res) {
return __awaiter(this, void 0, void 0, function* () {
try {
// Validate and extract parameters
const params = this.validateAndExtractParams(req);
// Log the request
this.logRequest(req, params);
// Handle product search by ID or keyword
if (params.id) {
yield this.handleProductById(params.id, req, res);
}
else if (params.keyword) {
yield this.handleProductByKeyword(params.keyword, req, res);
}
}
catch (error) {
this.handleError(error, req, res);
}
});
}
/**
* Validate and extract request parameters
*/
validateAndExtractParams(req) {
const { id, keyword } = req.query;
// Check if at least one parameter is provided
if (!id && !keyword) {
throw new ValidationError('Missing required parameter: id or keyword');
}
// Validate parameter types
if (id && typeof id !== 'string') {
throw new ValidationError('Parameter id must be a string');
}
if (keyword && typeof keyword !== 'string') {
throw new ValidationError('Parameter keyword must be a string');
}
// Validate parameter lengths
if (id && id.length > AmwApi.MAX_ID_LENGTH) {
throw new ValidationError(`ID too long (max ${AmwApi.MAX_ID_LENGTH} characters)`);
}
if (keyword && keyword.length > AmwApi.MAX_KEYWORD_LENGTH) {
throw new ValidationError(`Keyword too long (max ${AmwApi.MAX_KEYWORD_LENGTH} characters)`);
}
// Sanitize parameters
const sanitizedParams = {};
if (id) {
sanitizedParams.id = this.sanitizeInput(id);
}
if (keyword) {
sanitizedParams.keyword = this.sanitizeInput(keyword);
}
return sanitizedParams;
}
/**
* Sanitize input to prevent injection attacks
*/
sanitizeInput(input) {
return input.trim().replace(/[<>"']/g, '');
}
/**
* Log the incoming request
*/
logRequest(req, params) {
var _a;
const ip = ((_a = req.headers['x-forwarded-for']) === null || _a === void 0 ? void 0 : _a.toString().split(',')[0].trim()) || req.ip || 'unknown';
const referer = req.get('referer') || 'none';
this.log.info(`GET /product | id=${params.id || 'none'} | keyword=${params.keyword || 'none'} | IP=${ip} | Referer=${referer}`);
}
/**
* Handle product search by ID
*/
handleProductById(id, req, res) {
return __awaiter(this, void 0, void 0, function* () {
const cacheKey = this.generateCacheKey('id', id);
// Check cache first
const cachedProduct = yield this.findInCache(cacheKey);
if (cachedProduct) {
this.log.info(`Product found in cache: ${id}`);
this.sendSuccessResponse(res, cachedProduct);
return;
}
// Fetch from Amazon API
try {
const product = yield Promise.race([
this.paapi.getItemApi(id),
this.createTimeout(AmwApi.REQUEST_TIMEOUT)
]);
yield this.handleApiResponse(cacheKey, product, res);
}
catch (error) {
this.log.error(`Error fetching product by ID ${id}:`, error);
throw error;
}
});
}
/**
* Handle product search by keyword
*/
handleProductByKeyword(keyword, req, res) {
return __awaiter(this, void 0, void 0, function* () {
const cacheKey = this.generateCacheKey('keyword', keyword);
// Check cache first
const cachedProduct = yield this.findInCache(cacheKey);
if (cachedProduct) {
this.log.info(`Product found in cache: ${keyword}`);
this.sendSuccessResponse(res, cachedProduct);
return;
}
// Fetch from Amazon API
try {
const product = yield Promise.race([
this.paapi.searchItemApi(keyword),
this.createTimeout(AmwApi.REQUEST_TIMEOUT)
]);
yield this.handleApiResponse(cacheKey, product, res);
}
catch (error) {
this.log.error(`Error searching product by keyword ${keyword}:`, error);
throw error;
}
});
}
/**
* Handle API response and caching
*/
handleApiResponse(cacheKey, product, res) {
return __awaiter(this, void 0, void 0, function* () {
if (product && product !== null && product !== undefined) {
// Save to cache
yield this.saveInCache(cacheKey, product);
// Send response
this.sendSuccessResponse(res, product);
}
else {
this.sendNotFoundResponse(res, cacheKey);
}
});
}
/**
* Generate cache key with prefix
*/
generateCacheKey(type, value) {
return `${AmwApi.CACHE_KEY_PREFIX}${type}:${value}`;
}
/**
* Create timeout promise for API requests
*/
createTimeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Request timeout after ${ms}ms`)), ms);
});
}
/**
* Send successful response with product data
*/
sendSuccessResponse(res, product) {
res.status(200).json(product);
}
/**
* Send not found response
*/
sendNotFoundResponse(res, identifier) {
this.log.info(`Product not found in Amazon: ${identifier}`);
res.status(404).json({
error: "Product Not Found",
message: `No product found for identifier: ${identifier}`,
timestamp: new Date().toISOString()
});
}
/**
* Handle errors and send appropriate response
*/
handleError(error, req, res) {
const err = error instanceof Error ? error : new Error(String(error));
this.log.error(`API Error: ${err.message}`, err);
if (error instanceof ValidationError) {
res.status(400).json({
error: "Validation Error",
message: err.message,
timestamp: new Date().toISOString()
});
}
else if (err.message.includes('timeout')) {
res.status(504).json({
error: "Gateway Timeout",
message: "Request timed out while fetching product data",
timestamp: new Date().toISOString()
});
}
else {
res.status(500).json({
error: "Internal Server Error",
message: "An unexpected error occurred",
timestamp: new Date().toISOString()
});
}
}
/**
* Set the Widget endpoint.
*
* @param req The request object.
* @param res The response object.
*/
setWidgetEndpoint(req, res) {
try {
res.type('application/javascript');
res.sendFile(path_1.default.join(AmwApi.PROJECT_DIR, 'dist', 'widgets', 'widget.js'));
}
catch (error) {
this.handleError(error, req, res);
}
}
/**
* Find product in cache.
*
* @param key The key to find in the cache.
* @returns The cached product if found, null otherwise.
*/
findInCache(key) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.cache) {
return null;
}
try {
const cachedData = yield this.cache.get(key);
if (cachedData) {
this.log.info(`Product found in cache: ${key}`);
return JSON.parse(cachedData);
}
this.log.info(`Product not found in cache: ${key}`);
return null;
}
catch (error) {
this.log.error(`Error accessing cache for key ${key}:`, error);
return null;
}
});
}
/**
* Save the product in the cache.
*
* @param key The key to save in the cache.
* @param product The product to save.
*/
saveInCache(key, product) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.cache) {
return;
}
try {
this.log.info(`Saving product in cache: ${key}`);
yield this.cache.set(key, JSON.stringify(product), 'EX', this.ttl);
}
catch (error) {
this.log.error(`Error saving to cache for key ${key}:`, error);
// Don't throw error, caching failure shouldn't break the request
}
});
}
}
exports.AmwApi = AmwApi;
// Static attributes
AmwApi.PROJECT_DIR = config_1.default.get('Server.projectDir');
AmwApi.CACHE_KEY_PREFIX = 'amw:product:';
AmwApi.MAX_KEYWORD_LENGTH = 100;
AmwApi.MAX_ID_LENGTH = 50;
AmwApi.REQUEST_TIMEOUT = 30000; // 30 seconds
//# sourceMappingURL=AmwApi.js.map