From 0aa6248a9bc7af95e5617fcee4a5cb96cb0c1bc4 Mon Sep 17 00:00:00 2001 From: raymond Date: Wed, 10 Sep 2025 06:20:48 +0000 Subject: [PATCH] commit miss, recover server.ts --- server.ts | 3222 +++++++++++++++++++++++------------------------------ 1 file changed, 1423 insertions(+), 1799 deletions(-) diff --git a/server.ts b/server.ts index b10045c..3661f26 100644 --- a/server.ts +++ b/server.ts @@ -1,1800 +1,1424 @@ -// server.ts - Simplified main server file -// package.json dependencies needed: -// npm install express mathjs lodash date-fns swagger-jsdoc swagger-ui-express js-yaml -// npm install -D @types/express @types/node @types/lodash typescript ts-node - -import express from 'express'; -import swaggerJsdoc from 'swagger-jsdoc'; -import swaggerUi from 'swagger-ui-express'; -import * as math from 'mathjs'; -import * as _ from 'lodash'; - -// These imports assume the files exist in the same directory -// import { KMeans, KMeansOptions } from './kmeans'; -// import { getWeekNumber, getSameWeekDayLastYear } from './time-helper'; -// import { calculateLinearRegression, generateForecast, calculatePredictionIntervals, ForecastResult } from './prediction'; -import { SignalProcessor, SmoothingOptions, EdgeDetectionOptions } from './signal_processing_convolution'; -import { convolve1D, ConvolutionKernels } from './convolution'; // Direct import for new functions - -interface KMeansOptions {} -class KMeans { - constructor(p: any, n: any, o: any) {} - run = () => ({ clusters: [] }) -} -const getWeekNumber = (d: string) => 1; -const getSameWeekDayLastYear = (d: string) => new Date().toISOString(); -interface ForecastResult {} -const calculateLinearRegression = (v: any) => ({slope: 1, intercept: 0}); -const generateForecast = (m: any, l: any, p: any) => []; -const calculatePredictionIntervals = (v: any, m: any, f: any) => []; - -const app = express(); -app.use(express.json()); -const PORT = process.env.PORT || 3000; - -const swaggerOptions = { - swaggerDefinition: { - openapi: '3.0.0', - info: { - title: 'My Express API', - version: '1.0.0', - description: 'API documentation for my awesome Express app', - }, - servers: [ - { - url: `http://localhost:${PORT}`, - }, - ], - }, - apis: ["./server.ts"], // Pointing to this file for Swagger docs -}; - -const swaggerSpec = swaggerJsdoc(swaggerOptions); - -app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); - -// ======================================== -// TYPE DEFINITIONS -// ======================================== - -interface DataSeries { - values: number[]; - labels?: string[]; -} - -interface DataMatrix { - data: number[][]; - columns?: string[]; - rows?: string[]; -} - -interface Condition { - field: string; - operator: '>' | '<' | '=' | '>=' | '<=' | '!='; - value: number | string; -} - -interface ApiResponse { - success: boolean; - data?: T; - error?: string; -} - -// ======================================== -// HELPER FUNCTIONS -// ======================================== - -const handleError = (error: unknown): string => { - return error instanceof Error ? error.message : 'Unknown error'; -}; - -const validateSeries = (series: DataSeries): void => { - if (!series || !Array.isArray(series.values) || series.values.length === 0) { - throw new Error('Series must contain at least one value'); - } -}; - -const validateMatrix = (matrix: DataMatrix): void => { - if (!matrix || !Array.isArray(matrix.data) || matrix.data.length === 0) { - throw new Error('Matrix must contain at least one row'); - } -}; - -/** - * A helper class to provide a fluent API for rolling window calculations. - */ -class RollingWindow { - private windows: number[][]; - - constructor(windows: number[][]) { - this.windows = windows; - } - - mean(): number[] { - return this.windows.map(window => Number(math.mean(window))); - } - - sum(): number[] { - return this.windows.map(window => _.sum(window)); - } - - min(): number[] { - return this.windows.map(window => Math.min(...window)); - } - - max(): number[] { - return this.windows.map(window => Math.max(...window)); - } - - toArray(): number[][] { - return this.windows; - } -} - -// ======================================== -// ANALYTICS ENGINE (Simplified) -// ======================================== - -class AnalyticsEngine { - - private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] { - if (conditions.length === 0) return series.values; - return series.values; // TODO: Implement filtering - } - - // Basic statistical functions - unique(series: DataSeries): number[] { - validateSeries(series); - return _.uniq(series.values); - } - - mean(series: DataSeries, conditions: Condition[] = []): number { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - if (filteredValues.length === 0) throw new Error('No data points match conditions'); - return Number(math.mean(filteredValues)); - } - - count(series: DataSeries, conditions: Condition[] = []): number { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - if (filteredValues.length === 0) throw new Error('No data points match conditions'); - return filteredValues.length; - } - - variance(series: DataSeries, conditions: Condition[] = []): number { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - if (filteredValues.length === 0) throw new Error('No data points match conditions'); - return Number(math.variance(filteredValues)); - } - - standardDeviation(series: DataSeries, conditions: Condition[] = []): number { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - if (filteredValues.length === 0) throw new Error('No data points match conditions'); - return Number(math.std(filteredValues)); - } - - percentile(series: DataSeries, percent: number, ascending: boolean = true, conditions: Condition[] = []): number { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - if (filteredValues.length === 0) throw new Error('No data points match conditions'); - - const sorted = ascending ? _.sortBy(filteredValues) : _.sortBy(filteredValues).reverse(); - const index = (percent / 100) * (sorted.length - 1); - const lower = Math.floor(index); - const upper = Math.ceil(index); - const weight = index % 1; - - return sorted[lower] * (1 - weight) + sorted[upper] * weight; - } - - median(series: DataSeries, conditions: Condition[] = []): number { - return this.percentile(series, 50, true, conditions); - } - - mode(series: DataSeries, conditions: Condition[] = []): number[] { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - const frequency = _.countBy(filteredValues); - const maxFreq = Math.max(...Object.values(frequency)); - - return Object.keys(frequency) - .filter(key => frequency[key] === maxFreq) - .map(Number); - } - - max(series: DataSeries, conditions: Condition[] = []): number { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - if (filteredValues.length === 0) throw new Error('No data points match conditions'); - return Math.max(...filteredValues); - } - - min(series: DataSeries, conditions: Condition[] = []): number { - validateSeries(series); - const filteredValues = this.applyConditions(series, conditions); - if (filteredValues.length === 0) throw new Error('No data points match conditions'); - return Math.min(...filteredValues); - } - - correlation(series1: DataSeries, series2: DataSeries): number { - validateSeries(series1); - validateSeries(series2); - - if (series1.values.length !== series2.values.length) { - throw new Error('Series must have same length for correlation'); - } - - const x = series1.values; - const y = series2.values; - const n = x.length; - - const sumX = _.sum(x); - const sumY = _.sum(y); - const sumXY = _.sum(x.map((xi, i) => xi * y[i])); - const sumX2 = _.sum(x.map(xi => xi * xi)); - const sumY2 = _.sum(y.map(yi => yi * yi)); - - const numerator = n * sumXY - sumX * sumY; - const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); - - return numerator / denominator; - } - - // Rolling window functions - rolling(series: DataSeries, windowSize: number): RollingWindow { - validateSeries(series); - if (windowSize <= 0) { - throw new Error('Window size must be a positive number.'); - } - if (series.values.length < windowSize) { - return new RollingWindow([]); - } - - const windows: number[][] = []; - for (let i = 0; i <= series.values.length - windowSize; i++) { - const window = series.values.slice(i, i + windowSize); - windows.push(window); - } - return new RollingWindow(windows); - } - - movingAverage(series: DataSeries, windowSize: number): number[] { - return this.rolling(series, windowSize).mean(); - } - - // K-means wrapper (uses imported KMeans class) - kmeans(matrix: DataMatrix, nClusters: number, options: KMeansOptions = {}): { clusters: number[][][], centroids: number[][] } { - validateMatrix(matrix); - const points: number[][] = matrix.data; - - // Use the new MiniBatchKMeans class - const kmeans = new KMeans(points, nClusters, options); - const result = kmeans.run(); - - const centroids = result.clusters.map(c => (c as any).centroid); - const clusters = result.clusters.map(c => (c as any).points); - - return { clusters, centroids }; - } - - // Time helper wrapper functions - getWeekNumber(dateString: string): number { - return getWeekNumber(dateString); - } - - getSameWeekDayLastYear(dateString: string): string { - return getSameWeekDayLastYear(dateString); - } - - // Retail functions - purchaseRate(productPurchases: number, totalTransactions: number): number { - if (totalTransactions === 0) throw new Error('Total transactions cannot be zero'); - return (productPurchases / totalTransactions) * 100; - } - - liftValue(jointPurchaseRate: number, productAPurchaseRate: number, productBPurchaseRate: number): number { - const expectedJointRate = productAPurchaseRate * productBPurchaseRate; - if (expectedJointRate === 0) throw new Error('Expected joint rate cannot be zero'); - return jointPurchaseRate / expectedJointRate; - } - - costRatio(cost: number, salePrice: number): number { - if (salePrice === 0) throw new Error('Sale price cannot be zero'); - return cost / salePrice; - } - - grossMarginRate(salePrice: number, cost: number): number { - if (salePrice === 0) throw new Error('Sale price cannot be zero'); - return (salePrice - cost) / salePrice; - } - - averageSpendPerCustomer(totalRevenue: number, numberOfCustomers: number): number { - if (numberOfCustomers === 0) { - throw new Error('Number of customers cannot be zero'); - } - return totalRevenue / numberOfCustomers; - } - - purchaseIndex(totalItemsSold: number, numberOfCustomers: number): number { - if (numberOfCustomers === 0) { - throw new Error('Number of customers cannot be zero'); - } - return (totalItemsSold / numberOfCustomers) * 1000; - } - - // ======================================== - // Prediction functions - // ======================================== - - timeSeriesForecast(series: DataSeries, forecastPeriods: number): ForecastResult { - validateSeries(series); - - const model = calculateLinearRegression(series.values); - const forecast = generateForecast(model, series.values.length, forecastPeriods); - const predictionIntervals = calculatePredictionIntervals(series.values, model, forecast); - - return { - forecast, - predictionIntervals, - modelParameters: { - slope: model.slope, - intercept: model.intercept, - }, - }; - } -} - -// Initialize analytics engine -const analytics = new AnalyticsEngine(); - -// ======================================== -// API ROUTES -// ======================================== - -/** - * @swagger - * /api/health: - * get: - * summary: Health check endpoint - * description: Returns the health status of the API - * tags: [Health] - * responses: - * '200': - * description: API is healthy - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: OK - * timestamp: - * type: string - * format: date-time - */ -app.get('/api/health', (req, res) => { - res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); -}); - -/** - * @swagger - * /api/unique: - * post: - * summary: Get unique values from a data series - * description: Returns an array of unique values from the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * responses: - * '200': - * description: Unique values calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/unique', (req, res) => { - try { - const result = analytics.unique(req.body.series); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/mean: - * post: - * summary: Calculate mean of a data series - * description: Returns the arithmetic mean of the provided data series, optionally filtered by conditions - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Mean calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/mean', (req, res) => { - try { - const result = analytics.mean(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/count: - * post: - * summary: Count data points in a series - * description: Returns the count of data points in the series, optionally filtered by conditions - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Count calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/count', (req, res) => { - try { - const result = analytics.count(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/variance: - * post: - * summary: Calculate variance of a data series - * description: Returns the variance of the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Variance calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/variance', (req, res) => { - try { - const result = analytics.variance(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/std: - * post: - * summary: Calculate standard deviation of a data series - * description: Returns the standard deviation of the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Standard deviation calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/std', (req, res) => { - try { - const result = analytics.standardDeviation(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/percentile: - * post: - * summary: Calculate percentile of a data series - * description: Returns the specified percentile of the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * percent: - * type: number - * description: Percentile to calculate (0-100) - * example: 95 - * ascending: - * type: boolean - * description: Sort order - * default: true - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Percentile calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/percentile', (req, res) => { - try { - const result = analytics.percentile(req.body.series, req.body.percent, req.body.ascending, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/median: - * post: - * summary: Calculate median of a data series - * description: Returns the median (50th percentile) of the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Median calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/median', (req, res) => { - try { - const result = analytics.median(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/mode: - * post: - * summary: Calculate mode of a data series - * description: Returns the mode (most frequent values) of the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Mode calculated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/mode', (req, res) => { - try { - const result = analytics.mode(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/max: - * post: - * summary: Find maximum value in a data series - * description: Returns the maximum value from the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Maximum value found successfully - * '400': - * description: Invalid input data - */ -app.post('/api/max', (req, res) => { - try { - const result = analytics.max(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/min: - * post: - * summary: Find minimum value in a data series - * description: Returns the minimum value from the provided data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * conditions: - * type: array - * items: - * $ref: '#/components/schemas/Condition' - * responses: - * '200': - * description: Minimum value found successfully - * '400': - * description: Invalid input data - */ -app.post('/api/min', (req, res) => { - try { - const result = analytics.min(req.body.series, req.body.conditions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/correlation: - * post: - * summary: Calculate correlation between two data series - * description: Returns the Pearson correlation coefficient between two data series - * tags: [Statistics] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series1: - * $ref: '#/components/schemas/DataSeries' - * series2: - * $ref: '#/components/schemas/DataSeries' - * responses: - * '200': - * description: Correlation calculated successfully - * '400': - * description: Invalid input data or series have different lengths - */ -app.post('/api/correlation', (req, res) => { - try { - const result = analytics.correlation(req.body.series1, req.body.series2); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/series/moving-average: - * post: - * summary: Calculate moving average of a data series - * description: Returns the moving average of the provided data series with specified window size - * tags: [Series Operations] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * windowSize: - * type: integer - * description: Size of the moving window - * minimum: 1 - * example: 5 - * responses: - * '200': - * description: Moving average calculated successfully - * '400': - * description: Invalid input data or window size - */ -app.post('/api/series/moving-average', (req, res) => { - try { - const { series, windowSize } = req.body; - const result = analytics.movingAverage(series, windowSize); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/series/rolling: - * post: - * summary: Get rolling windows of a data series - * description: Returns rolling windows of the provided data series with specified window size - * tags: [Series Operations] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * windowSize: - * type: integer - * description: Size of the rolling window - * minimum: 1 - * example: 3 - * responses: - * '200': - * description: Rolling windows calculated successfully - * '400': - * description: Invalid input data or window size - */ -app.post('/api/series/rolling', (req, res) => { - try { - const { series, windowSize } = req.body; - const result = analytics.rolling(series, windowSize).toArray(); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/ml/kmeans: - * post: - * summary: Perform K-means clustering - * description: Performs K-means clustering on the provided data matrix - * tags: [Machine Learning] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * matrix: - * $ref: '#/components/schemas/DataMatrix' - * nClusters: - * type: integer - * description: Number of clusters - * minimum: 1 - * example: 3 - * options: - * type: object - * description: K-means options - * responses: - * '200': - * description: K-means clustering completed successfully - * '400': - * description: Invalid input data - */ -app.post('/api/ml/kmeans', (req, res) => { - try { - const result = analytics.kmeans(req.body.matrix, req.body.nClusters, req.body.options); - res.status(200).json({ success: true, data: result } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>); - } -}); - -/** - * @swagger - * /api/time/week-number: - * post: - * summary: Get week number from date - * description: Returns the ISO week number for the provided date string - * tags: [Time] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * date: - * type: string - * format: date - * description: Date string in ISO format - * example: "2024-03-15" - * responses: - * '200': - * description: Week number calculated successfully - * '400': - * description: Invalid date format - */ -app.post('/api/time/week-number', (req, res) => { - try { - const { date } = req.body; - const result = analytics.getWeekNumber(date); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/time/same-day-last-year: - * post: - * summary: Get same day of week from last year - * description: Returns the date string for the same day of the week from the previous year - * tags: [Time] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * date: - * type: string - * format: date - * description: Date string in ISO format - * example: "2024-03-15" - * responses: - * '200': - * description: Same day last year calculated successfully - * '400': - * description: Invalid date format - */ -app.post('/api/time/same-day-last-year', (req, res) => { - try { - const { date } = req.body; - const result = analytics.getSameWeekDayLastYear(date); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/retail/purchase-rate: - * post: - * summary: Calculate purchase rate - * description: Calculates the purchase rate as a percentage of product purchases over total transactions - * tags: [Retail] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * productPurchases: - * type: number - * description: Number of product purchases - * example: 150 - * totalTransactions: - * type: number - * description: Total number of transactions - * example: 1000 - * responses: - * '200': - * description: Purchase rate calculated successfully - * '400': - * description: Invalid input data or division by zero - */ -app.post('/api/retail/purchase-rate', (req, res) => { - try { - const result = analytics.purchaseRate(req.body.productPurchases, req.body.totalTransactions); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/retail/lift-value: - * post: - * summary: Calculate lift value - * description: Calculates the lift value for market basket analysis - * tags: [Retail] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * jointPurchaseRate: - * type: number - * description: Joint purchase rate of both products - * example: 0.05 - * productAPurchaseRate: - * type: number - * description: Purchase rate of product A - * example: 0.2 - * productBPurchaseRate: - * type: number - * description: Purchase rate of product B - * example: 0.3 - * responses: - * '200': - * description: Lift value calculated successfully - * '400': - * description: Invalid input data or division by zero - */ -app.post('/api/retail/lift-value', (req, res) => { - try { - const result = analytics.liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/retail/cost-ratio: - * post: - * summary: Calculate cost ratio - * description: Calculates the cost ratio (cost divided by sale price) - * tags: [Retail] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * cost: - * type: number - * description: Cost of the product - * example: 50 - * salePrice: - * type: number - * description: Sale price of the product - * example: 100 - * responses: - * '200': - * description: Cost ratio calculated successfully - * '400': - * description: Invalid input data or division by zero - */ -app.post('/api/retail/cost-ratio', (req, res) => { - try { - const result = analytics.costRatio(req.body.cost, req.body.salePrice); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/retail/gross-margin: - * post: - * summary: Calculate gross margin rate - * description: Calculates the gross margin rate as a percentage - * tags: [Retail] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * salePrice: - * type: number - * description: Sale price of the product - * example: 100 - * cost: - * type: number - * description: Cost of the product - * example: 60 - * responses: - * '200': - * description: Gross margin rate calculated successfully - * '400': - * description: Invalid input data or division by zero - */ -app.post('/api/retail/gross-margin', (req, res) => { - try { - const result = analytics.grossMarginRate(req.body.salePrice, req.body.cost); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/retail/average-spend: - * post: - * summary: Calculate average spend per customer - * description: Calculates the average amount spent per customer - * tags: [Retail] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * totalRevenue: - * type: number - * description: Total revenue - * example: 50000 - * numberOfCustomers: - * type: number - * description: Number of customers - * example: 500 - * responses: - * '200': - * description: Average spend calculated successfully - * '400': - * description: Invalid input data or division by zero - */ -app.post('/api/retail/average-spend', (req, res) => { - try { - const { totalRevenue, numberOfCustomers } = req.body; - const result = analytics.averageSpendPerCustomer(totalRevenue, numberOfCustomers); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/retail/purchase-index: - * post: - * summary: Calculate purchase index - * description: Calculates the purchase index (items per 1000 customers) - * tags: [Retail] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * totalItemsSold: - * type: number - * description: Total number of items sold - * example: 2500 - * numberOfCustomers: - * type: number - * description: Number of customers - * example: 1000 - * responses: - * '200': - * description: Purchase index calculated successfully - * '400': - * description: Invalid input data or division by zero - */ -app.post('/api/retail/purchase-index', (req, res) => { - try { - const { totalItemsSold, numberOfCustomers } = req.body; - const result = analytics.purchaseIndex(totalItemsSold, numberOfCustomers); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/predict/forecast: - * post: - * summary: Generate time series forecast - * description: Generates a forecast for time series data using linear regression - * tags: [Prediction] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * forecastPeriods: - * type: integer - * description: Number of periods to forecast - * minimum: 1 - * example: 10 - * responses: - * '200': - * description: Forecast generated successfully - * '400': - * description: Invalid input data - */ -app.post('/api/predict/forecast', (req, res) => { - try { - const { series, forecastPeriods } = req.body; - const result = analytics.timeSeriesForecast(series, forecastPeriods); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -// ======================================== -// NEW SIGNAL & IMAGE PROCESSING ROUTES -// ======================================== - -/** - * @swagger - * /api/signal/smooth: - * post: - * summary: Smooth a 1D data series - * description: Applies a smoothing filter (Gaussian or Moving Average) to a 1D data series to reduce noise. - * tags: [Signal Processing] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * options: - * $ref: '#/components/schemas/SmoothingOptions' - * responses: - * '200': - * description: The smoothed data series - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiResponse' - * '400': - * description: Invalid input data - */ -app.post('/api/signal/smooth', (req, res) => { - try { - const { series, options } = req.body; - validateSeries(series); - const result = SignalProcessor.smooth(series.values, options); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/signal/detect-peaks: - * post: - * summary: Detect peaks in a 1D data series - * description: Identifies local maxima (peaks) in a 1D data series. More robust and accurate logic. - * tags: [Signal Processing] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * options: - * type: object - * properties: - * smoothWindow: - * type: integer - * description: Optional window size for Gaussian smoothing to reduce noise before peak detection. - * example: 3 - * minDistance: - * type: integer - * description: The minimum number of data points between two peaks. - * example: 1 - * threshold: - * type: number - * description: The minimum value for a data point to be considered a peak. - * example: 0.5 - * responses: - * '200': - * description: An array of detected peak objects, each with an index and value. - * '400': - * description: Invalid input data - */ -app.post('/api/signal/detect-peaks', (req, res) => { - try { - const { series, options } = req.body; - validateSeries(series); - const result = SignalProcessor.detectPeaksConvolution(series.values, options); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/signal/detect-valleys: - * post: - * summary: Detect valleys in a 1D data series - * description: Identifies local minima (valleys) in a 1D data series. More robust and accurate logic. - * tags: [Signal Processing] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * options: - * type: object - * properties: - * smoothWindow: - * type: integer - * description: Optional window size for Gaussian smoothing to reduce noise before valley detection. - * example: 3 - * minDistance: - * type: integer - * description: The minimum number of data points between two valleys. - * example: 1 - * threshold: - * type: number - * description: The maximum value for a data point to be considered a valley. - * example: -0.5 - * responses: - * '200': - * description: An array of detected valley objects, each with an index and value. - * '400': - * description: Invalid input data - */ -app.post('/api/signal/detect-valleys', (req, res) => { - try { - const { series, options } = req.body; - validateSeries(series); - const result = SignalProcessor.detectValleysConvolution(series.values, options); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/signal/detect-outliers: - * post: - * summary: Detect outliers in a 1D data series - * description: Identifies outliers in a 1D data series using statistically sound methods. - * tags: [Signal Processing] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * options: - * type: object - * properties: - * method: - * type: string - * enum: [local_deviation, mean_diff] - * default: local_deviation - * windowSize: - * type: integer - * default: 7 - * threshold: - * type: number - * description: "The sensitivity threshold. For 'local_deviation', this is the number of standard deviations (Z-score)." - * default: 3.0 - * responses: - * '200': - * description: An array of detected outlier objects. - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiResponse' - * '400': - * description: Invalid input data - */ -app.post('/api/signal/detect-outliers', (req, res) => { - try { - const { series, options } = req.body; - validateSeries(series); - const result = SignalProcessor.detectOutliersConvolution(series.values, options); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/signal/detect-vertices: - * post: - * summary: Detect trend vertices (turning points) in a 1D series - * description: Identifies all significant peaks and valleys in a data series trend using a robust local maxima/minima search. - * tags: [Signal Processing] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * series: - * $ref: '#/components/schemas/DataSeries' - * options: - * type: object - * properties: - * smoothingWindow: - * type: integer - * default: 5 - * description: Window size for an initial Gaussian smoothing pass to reduce noise. - * threshold: - * type: number - * description: The absolute value a peak/valley must exceed to be counted. - * default: 0 - * minDistance: - * type: integer - * default: 3 - * description: Minimum number of data points between any two vertices. - * responses: - * '200': - * description: An array of detected vertex objects, labeled as 'peak' or 'valley'. - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiResponse' - * '400': - * description: Invalid input data - */ -app.post('/api/signal/detect-vertices', (req, res) => { - try { - const { series, options } = req.body; - validateSeries(series); - const result = SignalProcessor.detectTrendVertices(series.values, options); - res.status(200).json({ success: true, data: result } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -/** - * @swagger - * /api/kernels/{name}: - * get: - * summary: Get a pre-defined convolution kernel - * description: Retrieves a standard 1D or 2D convolution kernel by its name. - * tags: [Kernels] - * parameters: - * - in: path - * name: name - * required: true - * schema: - * type: string - * enum: [sobel-x, sobel-y, laplacian, difference1d, average1d] - * description: The name of the kernel to retrieve. - * - in: query - * name: size - * schema: - * type: integer - * default: 3 - * description: The size of the kernel (for kernels like 'average1d'). - * responses: - * '200': - * description: The requested kernel as a 1D or 2D array. - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiResponse' - * '400': - * description: Unknown kernel name or invalid options. - */ -app.get('/api/kernels/:name', (req, res) => { - try { - const kernelName = req.params.name; - const size = req.query.size ? parseInt(req.query.size as string, 10) : 3; - let kernel: number[] | number[][]; - - switch (kernelName) { - case 'sobel-x': - kernel = ConvolutionKernels.sobel('x'); - break; - case 'sobel-y': - kernel = ConvolutionKernels.sobel('y'); - break; - case 'laplacian': - kernel = ConvolutionKernels.laplacian(); - break; - case 'difference1d': - kernel = ConvolutionKernels.difference1D(); - break; - case 'average1d': - kernel = ConvolutionKernels.average1D(size); - break; - default: - throw new Error(`Unknown kernel name: ${kernelName}`); - } - res.status(200).json({ success: true, data: kernel } as ApiResponse); - } catch (error) { - const errorMessage = handleError(error); - res.status(400).json({ success: false, error: errorMessage } as ApiResponse); - } -}); - -// ======================================== -// SWAGGER COMPONENTS -// ======================================== - -/** - * @swagger - * components: - * schemas: - * DataSeries: - * type: object - * required: - * - values - * properties: - * values: - * type: array - * items: - * type: number - * description: Array of numerical values - * example: [1, 2, 3, 4, 5] - * labels: - * type: array - * items: - * type: string - * description: Optional labels for the values - * example: ["Jan", "Feb", "Mar", "Apr", "May"] - * DataMatrix: - * type: object - * required: - * - data - * properties: - * data: - * type: array - * items: - * type: array - * items: - * type: number - * description: 2D array of numerical values - * example: [[1, 2], [3, 4], [5, 6]] - * columns: - * type: array - * items: - * type: string - * description: Optional column names - * example: ["x", "y"] - * rows: - * type: array - * items: - * type: string - * description: Optional row names - * example: ["row1", "row2", "row3"] - * Condition: - * type: object - * required: - * - field - * - operator - * - value - * properties: - * field: - * type: string - * description: Field name to apply condition on - * example: "value" - * operator: - * type: string - * enum: [">", "<", "=", ">=", "<=", "!="] - * description: Comparison operator - * example: ">" - * value: - * oneOf: - * - type: number - * - type: string - * description: Value to compare against - * example: 10 - * SmoothingOptions: - * type: object - * properties: - * method: - * type: string - * enum: [gaussian, moving_average] - * default: gaussian - * description: The smoothing method to use. - * windowSize: - * type: integer - * default: 5 - * description: The size of the window for the filter. Must be an odd number for Gaussian. - * sigma: - * type: number - * default: 1.0 - * description: The standard deviation for the Gaussian filter. - * EdgeDetectionOptions: - * type: object - * properties: - * method: - * type: string - * enum: [sobel, laplacian] - * default: sobel - * description: The edge detection algorithm to use. - * threshold: - * type: number - * default: 0.1 - * description: The sensitivity threshold for detecting an edge. Values below this will be set to 0. - * ApiResponse: - * type: object - * properties: - * success: - * type: boolean - * description: Whether the request was successful - * data: - * description: Response data (varies by endpoint) - * error: - * type: string - * description: Error message if success is false - */ - -/** - * @swagger - * /api/docs/export/json: - * get: - * summary: Export API documentation as JSON - * description: Returns the complete OpenAPI specification in JSON format - * tags: [Documentation] - * responses: - * '200': - * description: OpenAPI specification in JSON format - * content: - * application/json: - * schema: - * type: object - */ -app.get('/api/docs/export/json', (req, res) => { - res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.json"'); - res.setHeader('Content-Type', 'application/json'); - res.json(swaggerSpec); -}); - -/** - * @swagger - * /api/docs/export/yaml: - * get: - * summary: Export API documentation as YAML - * description: Returns the complete OpenAPI specification in YAML format - * tags: [Documentation] - * responses: - * '200': - * description: OpenAPI specification in YAML format - * content: - * text/yaml: - * schema: - * type: string - */ -app.get('/api/docs/export/yaml', (req, res) => { - const yaml = require('js-yaml'); - const yamlString = yaml.dump(swaggerSpec, { indent: 2 }); - - res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.yaml"'); - res.setHeader('Content-Type', 'text/yaml'); - res.send(yamlString); -}); - -/** - * @swagger - * /api/docs/export/html: - * get: - * summary: Export API documentation as HTML - * description: Returns a standalone HTML file with the complete API documentation - * tags: [Documentation] - * responses: - * '200': - * description: Standalone HTML documentation - * content: - * text/html: - * schema: - * type: string - */ -app.get('/api/docs/export/html', (req, res) => { - const htmlTemplate = ` - - - - - - API Documentation - - - - -
- - - - -`; - - res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.html"'); - res.setHeader('Content-Type', 'text/html'); - res.send(htmlTemplate); -}); - -// ======================================== -// ERROR HANDLING -// ======================================== - -app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { - console.error(err.stack); - res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse); -}); - -app.use('*', (req, res) => { - res.status(404).json({ success: false, error: 'Endpoint not found' }); -}); - -// ======================================== -// SERVER STARTUP -// ======================================== - -app.listen(PORT, () => { - console.log(`Analytics API server running on port ${PORT}`); - console.log(`Health check: http://localhost:${PORT}/api/health`); - console.log(`API Documentation: http://localhost:${PORT}/api-docs`); -}); - +// server.ts - Simplified main server file +// package.json dependencies needed: +// npm install express mathjs lodash date-fns +// npm install -D @types/express @types/node @types/lodash typescript ts-node + +import express from 'express'; +import swaggerJsdoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; +import * as math from 'mathjs'; +import * as _ from 'lodash'; +import { KMeans, KMeansOptions } from './kmeans'; +import { getWeekNumber, getSameWeekDayLastYear } from './time-helper'; +import { calculateLinearRegression, generateForecast, calculatePredictionIntervals, ForecastResult } from './prediction'; + +const app = express(); +app.use(express.json()); +const PORT = process.env.PORT || 3000; + +const swaggerOptions = { + swaggerDefinition: { + openapi: '3.0.0', + info: { + title: 'My Express API', + version: '1.0.0', + description: 'API documentation for my awesome Express app', + }, + servers: [ + { + url: `http://localhost:${PORT}`, + }, + ], + }, + // Paths to files containing OpenAPI definitions + apis: ["./*.ts"], // Make sure this path is correct +}; + +const swaggerSpec = swaggerJsdoc(swaggerOptions); + +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// ======================================== +// TYPE DEFINITIONS +// ======================================== + +interface DataSeries { + values: number[]; + labels?: string[]; +} + +interface DataMatrix { + data: number[][]; + columns?: string[]; + rows?: string[]; +} + +interface Condition { + field: string; + operator: '>' | '<' | '=' | '>=' | '<=' | '!='; + value: number | string; +} + +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +// ======================================== +// HELPER FUNCTIONS +// ======================================== + +const handleError = (error: unknown): string => { + return error instanceof Error ? error.message : 'Unknown error'; +}; + +const validateSeries = (series: DataSeries): void => { + if (!series || !Array.isArray(series.values) || series.values.length === 0) { + throw new Error('Series must contain at least one value'); + } +}; + +const validateMatrix = (matrix: DataMatrix): void => { + if (!matrix || !Array.isArray(matrix.data) || matrix.data.length === 0) { + throw new Error('Matrix must contain at least one row'); + } +}; + +/** + * A helper class to provide a fluent API for rolling window calculations. + */ +class RollingWindow { + private windows: number[][]; + + constructor(windows: number[][]) { + this.windows = windows; + } + + mean(): number[] { + return this.windows.map(window => Number(math.mean(window))); + } + + sum(): number[] { + return this.windows.map(window => _.sum(window)); + } + + min(): number[] { + return this.windows.map(window => Math.min(...window)); + } + + max(): number[] { + return this.windows.map(window => Math.max(...window)); + } + + toArray(): number[][] { + return this.windows; + } +} + +// ======================================== +// ANALYTICS ENGINE (Simplified) +// ======================================== + +class AnalyticsEngine { + + private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] { + if (conditions.length === 0) return series.values; + return series.values; // TODO: Implement filtering + } + + // Basic statistical functions + unique(series: DataSeries): number[] { + validateSeries(series); + return _.uniq(series.values); + } + + mean(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Number(math.mean(filteredValues)); + } + + count(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return filteredValues.length; + } + + variance(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Number(math.variance(filteredValues)); + } + + standardDeviation(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Number(math.std(filteredValues)); + } + + percentile(series: DataSeries, percent: number, ascending: boolean = true, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + + const sorted = ascending ? _.sortBy(filteredValues) : _.sortBy(filteredValues).reverse(); + const index = (percent / 100) * (sorted.length - 1); + const lower = Math.floor(index); + const upper = Math.ceil(index); + const weight = index % 1; + + return sorted[lower] * (1 - weight) + sorted[upper] * weight; + } + + median(series: DataSeries, conditions: Condition[] = []): number { + return this.percentile(series, 50, true, conditions); + } + + mode(series: DataSeries, conditions: Condition[] = []): number[] { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + const frequency = _.countBy(filteredValues); + const maxFreq = Math.max(...Object.values(frequency)); + + return Object.keys(frequency) + .filter(key => frequency[key] === maxFreq) + .map(Number); + } + + max(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Math.max(...filteredValues); + } + + min(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Math.min(...filteredValues); + } + + correlation(series1: DataSeries, series2: DataSeries): number { + validateSeries(series1); + validateSeries(series2); + + if (series1.values.length !== series2.values.length) { + throw new Error('Series must have same length for correlation'); + } + + const x = series1.values; + const y = series2.values; + const n = x.length; + + const sumX = _.sum(x); + const sumY = _.sum(y); + const sumXY = _.sum(x.map((xi, i) => xi * y[i])); + const sumX2 = _.sum(x.map(xi => xi * xi)); + const sumY2 = _.sum(y.map(yi => yi * yi)); + + const numerator = n * sumXY - sumX * sumY; + const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); + + return numerator / denominator; + } + + // Rolling window functions + rolling(series: DataSeries, windowSize: number): RollingWindow { + validateSeries(series); + if (windowSize <= 0) { + throw new Error('Window size must be a positive number.'); + } + if (series.values.length < windowSize) { + return new RollingWindow([]); + } + + const windows: number[][] = []; + for (let i = 0; i <= series.values.length - windowSize; i++) { + const window = series.values.slice(i, i + windowSize); + windows.push(window); + } + return new RollingWindow(windows); + } + + movingAverage(series: DataSeries, windowSize: number): number[] { + return this.rolling(series, windowSize).mean(); + } + + // K-means wrapper (uses imported KMeans class) + kmeans(matrix: DataMatrix, nClusters: number, options: KMeansOptions = {}): { clusters: number[][][], centroids: number[][] } { + validateMatrix(matrix); + const points: number[][] = matrix.data; + + // Use the new MiniBatchKMeans class + const kmeans = new KMeans(points, nClusters, options); + const result = kmeans.run(); + + const centroids = result.clusters.map(c => c.centroid); + const clusters = result.clusters.map(c => c.points); + + return { clusters, centroids }; + } + + // Time helper wrapper functions + getWeekNumber(dateString: string): number { + return getWeekNumber(dateString); + } + + getSameWeekDayLastYear(dateString: string): string { + return getSameWeekDayLastYear(dateString); + } + + // Retail functions + purchaseRate(productPurchases: number, totalTransactions: number): number { + if (totalTransactions === 0) throw new Error('Total transactions cannot be zero'); + return (productPurchases / totalTransactions) * 100; + } + + liftValue(jointPurchaseRate: number, productAPurchaseRate: number, productBPurchaseRate: number): number { + const expectedJointRate = productAPurchaseRate * productBPurchaseRate; + if (expectedJointRate === 0) throw new Error('Expected joint rate cannot be zero'); + return jointPurchaseRate / expectedJointRate; + } + + costRatio(cost: number, salePrice: number): number { + if (salePrice === 0) throw new Error('Sale price cannot be zero'); + return cost / salePrice; + } + + grossMarginRate(salePrice: number, cost: number): number { + if (salePrice === 0) throw new Error('Sale price cannot be zero'); + return (salePrice - cost) / salePrice; + } + + averageSpendPerCustomer(totalRevenue: number, numberOfCustomers: number): number { + if (numberOfCustomers === 0) { + throw new Error('Number of customers cannot be zero'); + } + return totalRevenue / numberOfCustomers; + } + + purchaseIndex(totalItemsSold: number, numberOfCustomers: number): number { + if (numberOfCustomers === 0) { + throw new Error('Number of customers cannot be zero'); + } + return (totalItemsSold / numberOfCustomers) * 1000; + } + + // ======================================== + // Prediction functions + // ======================================== + + timeSeriesForecast(series: DataSeries, forecastPeriods: number): ForecastResult { + validateSeries(series); + + const model = calculateLinearRegression(series.values); + const forecast = generateForecast(model, series.values.length, forecastPeriods); + const predictionIntervals = calculatePredictionIntervals(series.values, model, forecast); + + return { + forecast, + predictionIntervals, + modelParameters: { + slope: model.slope, + intercept: model.intercept, + }, + }; + } +} + +// Initialize analytics engine +const analytics = new AnalyticsEngine(); + +// ======================================== +// API ROUTES +// ======================================== + +/** + * @swagger + * /api/health: + * get: + * summary: Health check endpoint + * description: Returns the health status of the API + * responses: + * 200: + * description: API is healthy + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: OK + * timestamp: + * type: string + * format: date-time + */ +app.get('/api/health', (req, res) => { + res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); +}); + +/** + * @swagger + * /api/unique: + * post: + * summary: Get unique values from a data series + * description: Returns an array of unique values from the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * responses: + * 200: + * description: Unique values calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/unique', (req, res) => { + try { + const result = analytics.unique(req.body.series); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/mean: + * post: + * summary: Calculate mean of a data series + * description: Returns the arithmetic mean of the provided data series, optionally filtered by conditions + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Mean calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/mean', (req, res) => { + try { + const result = analytics.mean(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/count: + * post: + * summary: Count data points in a series + * description: Returns the count of data points in the series, optionally filtered by conditions + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Count calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/count', (req, res) => { + try { + const result = analytics.count(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/variance: + * post: + * summary: Calculate variance of a data series + * description: Returns the variance of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Variance calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/variance', (req, res) => { + try { + const result = analytics.variance(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/std: + * post: + * summary: Calculate standard deviation of a data series + * description: Returns the standard deviation of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Standard deviation calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/std', (req, res) => { + try { + const result = analytics.standardDeviation(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/percentile: + * post: + * summary: Calculate percentile of a data series + * description: Returns the specified percentile of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * percent: + * type: number + * description: Percentile to calculate (0-100) + * example: 95 + * ascending: + * type: boolean + * description: Sort order + * default: true + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Percentile calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/percentile', (req, res) => { + try { + const result = analytics.percentile(req.body.series, req.body.percent, req.body.ascending, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/median: + * post: + * summary: Calculate median of a data series + * description: Returns the median (50th percentile) of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Median calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/median', (req, res) => { + try { + const result = analytics.median(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/mode: + * post: + * summary: Calculate mode of a data series + * description: Returns the mode (most frequent values) of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Mode calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/mode', (req, res) => { + try { + const result = analytics.mode(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/max: + * post: + * summary: Find maximum value in a data series + * description: Returns the maximum value from the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Maximum value found successfully + * 400: + * description: Invalid input data + */ +app.post('/api/max', (req, res) => { + try { + const result = analytics.max(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/min: + * post: + * summary: Find minimum value in a data series + * description: Returns the minimum value from the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Minimum value found successfully + * 400: + * description: Invalid input data + */ +app.post('/api/min', (req, res) => { + try { + const result = analytics.min(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/correlation: + * post: + * summary: Calculate correlation between two data series + * description: Returns the Pearson correlation coefficient between two data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series1: + * $ref: '#/components/schemas/DataSeries' + * series2: + * $ref: '#/components/schemas/DataSeries' + * responses: + * 200: + * description: Correlation calculated successfully + * 400: + * description: Invalid input data or series have different lengths + */ +app.post('/api/correlation', (req, res) => { + try { + const result = analytics.correlation(req.body.series1, req.body.series2); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/series/moving-average: + * post: + * summary: Calculate moving average of a data series + * description: Returns the moving average of the provided data series with specified window size + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * windowSize: + * type: integer + * description: Size of the moving window + * minimum: 1 + * example: 5 + * responses: + * 200: + * description: Moving average calculated successfully + * 400: + * description: Invalid input data or window size + */ +app.post('/api/series/moving-average', (req, res) => { + try { + const { series, windowSize } = req.body; + const result = analytics.movingAverage(series, windowSize); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/series/rolling: + * post: + * summary: Get rolling windows of a data series + * description: Returns rolling windows of the provided data series with specified window size + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * windowSize: + * type: integer + * description: Size of the rolling window + * minimum: 1 + * example: 3 + * responses: + * 200: + * description: Rolling windows calculated successfully + * 400: + * description: Invalid input data or window size + */ +app.post('/api/series/rolling', (req, res) => { + try { + const { series, windowSize } = req.body; + const result = analytics.rolling(series, windowSize).toArray(); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/ml/kmeans: + * post: + * summary: Perform K-means clustering + * description: Performs K-means clustering on the provided data matrix + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * matrix: + * $ref: '#/components/schemas/DataMatrix' + * nClusters: + * type: integer + * description: Number of clusters + * minimum: 1 + * example: 3 + * options: + * type: object + * description: K-means options + * responses: + * 200: + * description: K-means clustering completed successfully + * 400: + * description: Invalid input data + */ +app.post('/api/ml/kmeans', (req, res) => { + try { + const result = analytics.kmeans(req.body.matrix, req.body.nClusters, req.body.options); + res.status(200).json({ success: true, data: result } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>); + } +}); + +/** + * @swagger + * /api/time/week-number: + * post: + * summary: Get week number from date + * description: Returns the ISO week number for the provided date string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * date: + * type: string + * format: date + * description: Date string in ISO format + * example: "2024-03-15" + * responses: + * 200: + * description: Week number calculated successfully + * 400: + * description: Invalid date format + */ +app.post('/api/time/week-number', (req, res) => { + try { + const { date } = req.body; + const result = analytics.getWeekNumber(date); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/time/same-day-last-year: + * post: + * summary: Get same day of week from last year + * description: Returns the date string for the same day of the week from the previous year + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * date: + * type: string + * format: date + * description: Date string in ISO format + * example: "2024-03-15" + * responses: + * 200: + * description: Same day last year calculated successfully + * 400: + * description: Invalid date format + */ +app.post('/api/time/same-day-last-year', (req, res) => { + try { + const { date } = req.body; + const result = analytics.getSameWeekDayLastYear(date); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/purchase-rate: + * post: + * summary: Calculate purchase rate + * description: Calculates the purchase rate as a percentage of product purchases over total transactions + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * productPurchases: + * type: number + * description: Number of product purchases + * example: 150 + * totalTransactions: + * type: number + * description: Total number of transactions + * example: 1000 + * responses: + * 200: + * description: Purchase rate calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/purchase-rate', (req, res) => { + try { + const result = analytics.purchaseRate(req.body.productPurchases, req.body.totalTransactions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/lift-value: + * post: + * summary: Calculate lift value + * description: Calculates the lift value for market basket analysis + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * jointPurchaseRate: + * type: number + * description: Joint purchase rate of both products + * example: 0.05 + * productAPurchaseRate: + * type: number + * description: Purchase rate of product A + * example: 0.2 + * productBPurchaseRate: + * type: number + * description: Purchase rate of product B + * example: 0.3 + * responses: + * 200: + * description: Lift value calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/lift-value', (req, res) => { + try { + const result = analytics.liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/cost-ratio: + * post: + * summary: Calculate cost ratio + * description: Calculates the cost ratio (cost divided by sale price) + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * cost: + * type: number + * description: Cost of the product + * example: 50 + * salePrice: + * type: number + * description: Sale price of the product + * example: 100 + * responses: + * 200: + * description: Cost ratio calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/cost-ratio', (req, res) => { + try { + const result = analytics.costRatio(req.body.cost, req.body.salePrice); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/gross-margin: + * post: + * summary: Calculate gross margin rate + * description: Calculates the gross margin rate as a percentage + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * salePrice: + * type: number + * description: Sale price of the product + * example: 100 + * cost: + * type: number + * description: Cost of the product + * example: 60 + * responses: + * 200: + * description: Gross margin rate calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/gross-margin', (req, res) => { + try { + const result = analytics.grossMarginRate(req.body.salePrice, req.body.cost); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/average-spend: + * post: + * summary: Calculate average spend per customer + * description: Calculates the average amount spent per customer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * totalRevenue: + * type: number + * description: Total revenue + * example: 50000 + * numberOfCustomers: + * type: number + * description: Number of customers + * example: 500 + * responses: + * 200: + * description: Average spend calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/average-spend', (req, res) => { + try { + const { totalRevenue, numberOfCustomers } = req.body; + const result = analytics.averageSpendPerCustomer(totalRevenue, numberOfCustomers); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/purchase-index: + * post: + * summary: Calculate purchase index + * description: Calculates the purchase index (items per 1000 customers) + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * totalItemsSold: + * type: number + * description: Total number of items sold + * example: 2500 + * numberOfCustomers: + * type: number + * description: Number of customers + * example: 1000 + * responses: + * 200: + * description: Purchase index calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/purchase-index', (req, res) => { + try { + const { totalItemsSold, numberOfCustomers } = req.body; + const result = analytics.purchaseIndex(totalItemsSold, numberOfCustomers); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/predict/forecast: + * post: + * summary: Generate time series forecast + * description: Generates a forecast for time series data using linear regression + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * forecastPeriods: + * type: integer + * description: Number of periods to forecast + * minimum: 1 + * example: 10 + * responses: + * 200: + * description: Forecast generated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/predict/forecast', (req, res) => { + try { + const { series, forecastPeriods } = req.body; + const result = analytics.timeSeriesForecast(series, forecastPeriods); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +// ======================================== +// SWAGGER COMPONENTS +// ======================================== + +/** + * @swagger + * components: + * schemas: + * DataSeries: + * type: object + * required: + * - values + * properties: + * values: + * type: array + * items: + * type: number + * description: Array of numerical values + * example: [1, 2, 3, 4, 5] + * labels: + * type: array + * items: + * type: string + * description: Optional labels for the values + * example: ["Jan", "Feb", "Mar", "Apr", "May"] + * + * DataMatrix: + * type: object + * required: + * - data + * properties: + * data: + * type: array + * items: + * type: array + * items: + * type: number + * description: 2D array of numerical values + * example: [[1, 2], [3, 4], [5, 6]] + * columns: + * type: array + * items: + * type: string + * description: Optional column names + * example: ["x", "y"] + * rows: + * type: array + * items: + * type: string + * description: Optional row names + * example: ["row1", "row2", "row3"] + * + * Condition: + * type: object + * required: + * - field + * - operator + * - value + * properties: + * field: + * type: string + * description: Field name to apply condition on + * example: "value" + * operator: + * type: string + * enum: [">", "<", "=", ">=", "<=", "!="] + * description: Comparison operator + * example: ">" + * value: + * oneOf: + * - type: number + * - type: string + * description: Value to compare against + * example: 10 + * + * ApiResponse: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the request was successful + * data: + * description: Response data (varies by endpoint) + * error: + * type: string + * description: Error message if success is false + */ + +/** + * @swagger + * /api/docs/export/json: + * get: + * summary: Export API documentation as JSON + * description: Returns the complete OpenAPI specification in JSON format + * responses: + * 200: + * description: OpenAPI specification in JSON format + * content: + * application/json: + * schema: + * type: object + */ +app.get('/api/docs/export/json', (req, res) => { + res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.json"'); + res.setHeader('Content-Type', 'application/json'); + res.json(swaggerSpec); +}); + +/** + * @swagger + * /api/docs/export/yaml: + * get: + * summary: Export API documentation as YAML + * description: Returns the complete OpenAPI specification in YAML format + * responses: + * 200: + * description: OpenAPI specification in YAML format + * content: + * text/yaml: + * schema: + * type: string + */ +app.get('/api/docs/export/yaml', (req, res) => { + const yaml = require('js-yaml'); + const yamlString = yaml.dump(swaggerSpec, { indent: 2 }); + + res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.yaml"'); + res.setHeader('Content-Type', 'text/yaml'); + res.send(yamlString); +}); + +/** + * @swagger + * /api/docs/export/html: + * get: + * summary: Export API documentation as HTML + * description: Returns a standalone HTML file with the complete API documentation + * responses: + * 200: + * description: Standalone HTML documentation + * content: + * text/html: + * schema: + * type: string + */ +app.get('/api/docs/export/html', (req, res) => { + const htmlTemplate = ` + + + + + + API Documentation + + + + +
+ + + + +`; + + res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.html"'); + res.setHeader('Content-Type', 'text/html'); + res.send(htmlTemplate); +}); + + +// ======================================== +// ERROR HANDLING +// ======================================== + +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error(err.stack); + res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse); +}); + +app.use('*', (req, res) => { + res.status(404).json({ success: false, error: 'Endpoint not found' }); +}); + +// ======================================== +// SERVER STARTUP +// ======================================== + +app.listen(PORT, () => { + console.log(`Analytics API server running on port ${PORT}`); + console.log(`Health check: http://localhost:${PORT}/api/health`); + console.log(`API Documentation: http://localhost:${PORT}/api-docs`); +}); + export default app; \ No newline at end of file