// 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 cors from 'cors'; // Assuming these 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 './services/signal_processing_convolution'; import { TimeSeriesAnalyzer, ARIMAOptions } from './services/timeseries'; import { AnalysisPipelines } from './services/analysis_pipelines'; import { convolve1D, convolve2D, ConvolutionKernels } from './services/convolution'; import { DataSeries, DataMatrix, Condition, ApiResponse } from './types/index'; import { handleError, validateSeries, validateMatrix } from './services/analytics_engine'; import { ForecastResult } from './services/prediction'; import { analytics } from './services/analytics_engine'; import { purchaseRate, liftValue, costRatio, grossMarginRate, averageSpendPerCustomer, purchaseIndex } from './services/retail_metrics'; import { RollingWindow } from './services/rolling_window'; import { pivotTable, PivotOptions } from './services/pivot_table'; // Initialize Express app const app = express(); app.use(express.json()); app.use(cors()); // <-- 2. ENABLE CORS FOR ALL ROUTES 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 the correct, renamed file }; const swaggerSpec = swaggerJsdoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // ======================================== // 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/pivot-table: * post: * summary: Generate a pivot table from records * description: Returns a pivot table based on the provided data and options * tags: [Data Transformation] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * type: object * description: Array of records to pivot * options: * $ref: '#/components/schemas/PivotOptions' * responses: * '200': * description: Pivot table generated successfully * '400': * description: Invalid input data */ app.post('/api/pivot-table', (req, res) => { try { const { data, options } = req.body; // You can pass analytics.mean, analytics.count, etc. as options.aggFunc if needed const result = pivotTable(data, options); res.status(200).json({ success: true, data: result }); } catch (error) { const errorMessage = handleError(error); res.status(400).json({ success: false, error: errorMessage }); } }); /** * @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/series/auto-arima-find: * post: * summary: (EXPERIMENTAL) Automatically find best SARIMA parameters * description: Performs a grid search to find the best SARIMA parameters based on AIC. NOTE - This is a simplified estimation and may not find the true optimal model. For best results, use the identification tools and the 'manual-forecast' endpoint. * tags: [Series Operations] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * series: * $ref: '#/components/schemas/DataSeries' * seasonalPeriod: * type: integer * description: The seasonal period of the data (e.g., 7 for weekly). * example: 7 * responses: * '200': * description: The best model found and the search log. * '400': * description: Invalid input data. */ app.post('/api/series/auto-arima-find', (req, res) => { try { const { series, seasonalPeriod } = req.body; validateSeries(series); const result = AnalysisPipelines.findBestArimaParameters(series.values, seasonalPeriod); res.status(200).json({ success: true, data: result }); } catch (error) { const errorMessage = handleError(error); res.status(400).json({ success: false, error: errorMessage }); } }); /** * @swagger * /api/series/manual-forecast: * post: * summary: Generate a forecast with manually specified SARIMA parameters * description: This is the primary forecasting tool. It allows an expert user (who has analyzed ACF/PACF plots) to apply a specific SARIMA model to a time series and generate a forecast. * tags: [Series Operations] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * series: * $ref: '#/components/schemas/DataSeries' * options: * $ref: '#/components/schemas/ARIMAOptions' * forecastSteps: * type: integer * description: The number of future time steps to predict. * example: 7 * responses: * '200': * description: The forecast results. * '400': * description: Invalid input data */ app.post('/api/series/manual-forecast', (req, res) => { try { const { series, options, forecastSteps } = req.body; validateSeries(series); const result = TimeSeriesAnalyzer.arimaForecast(series.values, options, forecastSteps); res.status(200).json({ success: true, data: result }); } catch (error) { const errorMessage = handleError(error); res.status(400).json({ success: false, error: errorMessage }); } }); /** * @swagger * /api/series/identify-correlations: * post: * summary: Calculate ACF and PACF for a time series * description: Returns the Autocorrelation and Partial Autocorrelation function values, which are essential for identifying SARIMA model parameters. * tags: [Series Operations] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * series: * $ref: '#/components/schemas/DataSeries' * maxLag: * type: integer * description: The maximum number of lags to calculate. * example: 40 * responses: * '200': * description: The calculated ACF and PACF values. * '400': * description: Invalid input data. */ app.post('/api/series/identify-correlations', (req, res) => { try { const { series, maxLag } = req.body; validateSeries(series); const acf = TimeSeriesAnalyzer.calculateACF(series.values, maxLag); const pacf = TimeSeriesAnalyzer.calculatePACF(series.values, maxLag); res.status(200).json({ success: true, data: { acf, pacf } }); } catch (error) { res.status(400).json({ success: false, error: handleError(error) }); } }); /** * @swagger * /api/series/decompose-stl: * post: * summary: Decompose a time series into components * description: Applies Seasonal-Trend-Loess (STL) decomposition to separate the series into trend, seasonal, and residual components. * tags: [Series Operations] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * series: * $ref: '#/components/schemas/DataSeries' * period: * type: integer * description: The seasonal period of the data (e.g., 7 for weekly). * example: 7 * responses: * '200': * description: The decomposed components of the time series. * '400': * description: Invalid input data. */ app.post('/api/series/decompose-stl', (req, res) => { try { const { series, period } = req.body; validateSeries(series); const result = TimeSeriesAnalyzer.stlDecomposition(series.values, period); res.status(200).json({ success: true, data: result }); } catch (error) { res.status(400).json({ success: false, error: handleError(error) }); } }); /** * @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 = 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 = 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 = 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 = 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 = 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 = 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/series/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: [Series Operations] * 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/series/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/series/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: [Series Operations] * 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/series/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/series/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: [Series Operations] * 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/series/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/series/detect-outliers: * post: * summary: Detect outliers in a 1D data series * description: Identifies outliers in a 1D data series using statistically sound methods. * tags: [Series Operations] * 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/series/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/series/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: [Series Operations] * 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/series/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. * ARIMAOptions: * type: object * properties: * p: * type: integer * description: Non-seasonal AutoRegressive (AR) order. * d: * type: integer * description: Non-seasonal Differencing (I) order. * q: * type: integer * description: Non-seasonal Moving Average (MA) order. * P: * type: integer * description: Seasonal AR order. * D: * type: integer * description: Seasonal Differencing order. * Q: * type: integer * description: Seasonal MA order. * s: * type: integer * description: The seasonal period length (e.g., 7 for weekly). * PivotOptions: * type: object * required: * - index * - columns * - values * properties: * index: * type: array * items: * type: string * description: Keys to use as row labels * columns: * type: array * items: * type: string * description: Keys to use as column labels * values: * type: string * description: Key to aggregate * aggFunc: * type: string * description: Aggregation function name (e.g., "sum", "mean", "count") * 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' }); }); 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(`API Documentation: http://localhost:${PORT}/api-docs`); }); export default app;