feat: Adds auto openapi doc generation + swagger UI for API (#779)

* Adds support for auto-openapi doc generation
* Adds a swagger route for local development
* Adds a script to manually generate the openapi doc (to be used later if we publish to our site, etc...)

Ref: HDX-1661
This commit is contained in:
Tom Alexander 2025-04-29 13:54:34 -04:00 committed by GitHub
parent 7de8916074
commit 293a2affc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1241 additions and 37 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/api": patch
---
Adds openapidoc annotations for spec generation and swagger route for development

3
.gitignore vendored
View file

@ -35,6 +35,9 @@ packages/app/.vercel
packages/app/coverage
packages/app/out
# OpenAPI spec
packages/public/openapi.json
# optional npm cache directory
**/.npm

View file

@ -18,3 +18,4 @@ PORT=${HYPERDX_API_PORT}
REDIS_URL=redis://localhost:6379
USAGE_STATS_ENABLED=false
NODE_OPTIONS="--max-http-header-size=131072"
ENABLE_SWAGGER=true

View file

@ -73,12 +73,15 @@
"@types/semver": "^7.3.12",
"@types/sqlstring": "^2.3.0",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6",
"@types/uuid": "^8.3.4",
"jest": "^28.1.3",
"migrate-mongo": "^11.0.0",
"nodemon": "^2.0.20",
"rimraf": "^4.4.1",
"supertest": "^6.3.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"ts-jest": "^28.0.5",
"ts-node": "^10.8.1",
"tsc-alias": "^1.8.8",
@ -98,6 +101,7 @@
"dev:migrate-db-create": "ts-node node_modules/.bin/migrate-mongo create -f migrate-mongo-config.ts",
"dev:migrate-db": "ts-node node_modules/.bin/migrate-mongo up -f migrate-mongo-config.ts",
"dev:migrate-ch-create": "migrate create -ext sql -dir ./migrations/ch -seq",
"dev:migrate-ch": "migrate -database 'clickhouse://localhost:9000?database=default&x-multi-statement=true' -path ./migrations/ch up"
"dev:migrate-ch": "migrate -database 'clickhouse://localhost:9000?database=default&x-multi-statement=true' -path ./migrations/ch up",
"docgen": "ts-node scripts/generate-api-docs.ts"
}
}

View file

@ -0,0 +1,12 @@
import fs from 'fs';
import path from 'path';
import swaggerJsdoc from 'swagger-jsdoc';
import { swaggerOptions } from '../src/utils/swagger';
const specs = swaggerJsdoc(swaggerOptions);
const outputPath = path.resolve(__dirname, '../../public/openapi.json');
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(specs, null, 2));
console.log(`OpenAPI specification written to ${outputPath}`);

View file

@ -18,6 +18,7 @@ import externalRoutersV2 from './routers/external-api/v2';
import usageStats from './tasks/usageStats';
import { expressLogger } from './utils/logger';
import passport from './utils/passport';
import { setupSwagger } from './utils/swagger';
const app: express.Application = express();
@ -102,7 +103,15 @@ app.use('/clickhouse-proxy', isUserAuthenticated, clickhouseProxyRouter);
// ---------------------------------------------------------------------
// ----------------------- External Routers ----------------------------
// ---------------------------------------------------------------------
// API v1
// API v2
// Only initialize Swagger in development or if explicitly enabled
if (
process.env.NODE_ENV !== 'production' ||
process.env.ENABLE_SWAGGER === 'true'
) {
setupSwagger(app);
}
app.use('/api/v2', externalRoutersV2);
// error handling

View file

@ -13,8 +13,241 @@ import {
import { translateAlertDocumentToExternalAlert } from '@/utils/externalApi';
import { alertSchema, objectIdSchema } from '@/utils/zod';
const router = express.Router();
/**
* @openapi
* components:
* schemas:
* Error:
* type: object
* properties:
* message:
* type: string
* Alert:
* type: object
* properties:
* id:
* type: string
* example: "65f5e4a3b9e77c001a123456"
* name:
* type: string
* example: "High Error Rate"
* message:
* type: string
* example: "Error rate exceeds threshold"
* threshold:
* type: number
* example: 100
* interval:
* type: string
* example: "15m"
* thresholdType:
* type: string
* enum: [above, below]
* example: "above"
* source:
* type: string
* enum: [tile, search]
* example: "tile"
* state:
* type: string
* example: "inactive"
* channel:
* type: object
* properties:
* type:
* type: string
* example: "webhook"
* webhookId:
* type: string
* example: "65f5e4a3b9e77c001a789012"
* team:
* type: string
* example: "65f5e4a3b9e77c001a345678"
* tileId:
* type: string
* example: "65f5e4a3b9e77c001a901234"
* dashboard:
* type: string
* example: "65f5e4a3b9e77c001a567890"
* savedSearch:
* type: string
* nullable: true
* groupBy:
* type: string
* nullable: true
* silenced:
* type: boolean
* nullable: true
* createdAt:
* type: string
* format: date-time
* example: "2023-01-01T00:00:00.000Z"
* updatedAt:
* type: string
* format: date-time
* example: "2023-01-01T00:00:00.000Z"
*
* CreateAlertRequest:
* type: object
* required:
* - threshold
* - interval
* - source
* - thresholdType
* - channel
* properties:
* dashboardId:
* type: string
* example: "65f5e4a3b9e77c001a567890"
* tileId:
* type: string
* example: "65f5e4a3b9e77c001a901234"
* threshold:
* type: number
* example: 100
* interval:
* type: string
* example: "1h"
* source:
* type: string
* enum: [tile, search]
* example: "tile"
* thresholdType:
* type: string
* enum: [above, below]
* example: "above"
* channel:
* type: object
* properties:
* type:
* type: string
* example: "webhook"
* webhookId:
* type: string
* example: "65f5e4a3b9e77c001a789012"
* name:
* type: string
* example: "Test Alert"
* message:
* type: string
* example: "Test Alert Message"
*
* UpdateAlertRequest:
* type: object
* properties:
* threshold:
* type: number
* example: 500
* interval:
* type: string
* example: "1h"
* thresholdType:
* type: string
* enum: [above, below]
* example: "above"
* source:
* type: string
* enum: [tile, search]
* example: "tile"
* dashboardId:
* type: string
* example: "65f5e4a3b9e77c001a567890"
* tileId:
* type: string
* example: "65f5e4a3b9e77c001a901234"
* channel:
* type: object
* properties:
* type:
* type: string
* example: "webhook"
* webhookId:
* type: string
* example: "65f5e4a3b9e77c001a789012"
* name:
* type: string
* example: "Updated Alert Name"
* message:
* type: string
* example: "Updated message"
*
* AlertResponse:
* type: object
* properties:
* data:
* $ref: '#/components/schemas/Alert'
*
* AlertsListResponse:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/Alert'
*
* EmptyResponse:
* type: object
* properties: {}
*/
const router = express.Router();
/**
* @openapi
* /api/v2/alerts/{id}:
* get:
* summary: Get Alert
* description: Retrieves a specific alert by ID
* operationId: getAlert
* tags: [Alerts]
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* description: Alert ID
* example: "65f5e4a3b9e77c001a123456"
* responses:
* '200':
* description: Successfully retrieved alert
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AlertResponse'
* examples:
* alertResponse:
* summary: Single alert response
* value:
* data:
* id: "65f5e4a3b9e77c001a123456"
* name: "CPU Usage Alert"
* message: "CPU usage is above 80%"
* threshold: 80
* interval: "5m"
* thresholdType: "above"
* source: "tile"
* state: "active"
* channel:
* type: "webhook"
* webhookId: "65f5e4a3b9e77c001a789012"
* team: "65f5e4a3b9e77c001a345678"
* tileId: "65f5e4a3b9e77c001a901234"
* dashboard: "65f5e4a3b9e77c001a567890"
* createdAt: "2023-03-15T10:20:30.000Z"
* updatedAt: "2023-03-15T14:25:10.000Z"
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* '404':
* description: Alert not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.get(
'/:id',
validateRequest({
@ -44,6 +277,51 @@ router.get(
},
);
/**
* @openapi
* /api/v2/alerts:
* get:
* summary: List Alerts
* description: Retrieves a list of all alerts for the authenticated team
* operationId: listAlerts
* tags: [Alerts]
* responses:
* '200':
* description: Successfully retrieved alerts
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AlertsListResponse'
* examples:
* alertsList:
* summary: List of alerts
* value:
* data:
* - id: "65f5e4a3b9e77c001a123456"
* name: "High Error Rate"
* message: "Error rate exceeds threshold"
* threshold: 100
* interval: "15m"
* thresholdType: "above"
* source: "tile"
* state: "inactive"
* channel:
* type: "webhook"
* webhookId: "65f5e4a3b9e77c001a789012"
* team: "65f5e4a3b9e77c001a345678"
* tileId: "65f5e4a3b9e77c001a901234"
* dashboard: "65f5e4a3b9e77c001a567890"
* createdAt: "2023-01-01T00:00:00.000Z"
* updatedAt: "2023-01-01T00:00:00.000Z"
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Unauthorized access. API key is missing or invalid."
*/
router.get('/', async (req, res, next) => {
try {
const teamId = req.user?.team;
@ -61,6 +339,55 @@ router.get('/', async (req, res, next) => {
}
});
/**
* @openapi
* /api/v2/alerts:
* post:
* summary: Create Alert
* description: Creates a new alert
* operationId: createAlert
* tags: [Alerts]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateAlertRequest'
* examples:
* tileAlert:
* summary: Create a tile-based alert
* value:
* dashboardId: "65f5e4a3b9e77c001a567890"
* tileId: "65f5e4a3b9e77c001a901234"
* threshold: 100
* interval: "1h"
* source: "tile"
* thresholdType: "above"
* channel:
* type: "webhook"
* webhookId: "65f5e4a3b9e77c001a789012"
* name: "Error Spike Alert"
* message: "Error rate has exceeded 100 in the last hour"
* responses:
* '200':
* description: Successfully created alert
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AlertResponse'
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* '500':
* description: Server error or validation failure
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.post('/', async (req, res, next) => {
const teamId = req.user?.team;
if (teamId == null) {
@ -78,6 +405,69 @@ router.post('/', async (req, res, next) => {
}
});
/**
* @openapi
* /api/v2/alerts/{id}:
* put:
* summary: Update Alert
* description: Updates an existing alert
* operationId: updateAlert
* tags: [Alerts]
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* description: Alert ID
* example: "65f5e4a3b9e77c001a123456"
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UpdateAlertRequest'
* examples:
* updateAlert:
* summary: Update alert properties
* value:
* threshold: 500
* interval: "1h"
* thresholdType: "above"
* source: "tile"
* dashboardId: "65f5e4a3b9e77c001a567890"
* tileId: "65f5e4a3b9e77c001a901234"
* channel:
* type: "webhook"
* webhookId: "65f5e4a3b9e77c001a789012"
* name: "Updated Alert Name"
* message: "Updated threshold and interval"
* responses:
* '200':
* description: Successfully updated alert
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AlertResponse'
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* '404':
* description: Alert not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* '500':
* description: Server error or validation failure
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.put(
'/:id',
validateRequest({
@ -111,6 +501,43 @@ router.put(
},
);
/**
* @openapi
* /api/v2/alerts/{id}:
* delete:
* summary: Delete Alert
* description: Deletes an alert
* operationId: deleteAlert
* tags: [Alerts]
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* description: Alert ID
* example: "65f5e4a3b9e77c001a123456"
* responses:
* '200':
* description: Successfully deleted alert
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/EmptyResponse'
* example: {}
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* '404':
* description: Alert not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.delete(
'/:id',
validateRequest({

View file

@ -22,8 +22,249 @@ import {
tagsSchema,
} from '@/utils/zod';
/**
* @openapi
* components:
* schemas:
* ChartSeries:
* type: object
* properties:
* type:
* type: string
* enum: [time, table, number, histogram, search, markdown]
* example: "time"
* dataSource:
* type: string
* enum: [events, metrics]
* example: "events"
* aggFn:
* type: string
* example: "count"
* where:
* type: string
* example: "level:error"
* groupBy:
* type: array
* items:
* type: string
* example: []
*
* Tile:
* type: object
* properties:
* id:
* type: string
* example: "65f5e4a3b9e77c001a901234"
* name:
* type: string
* example: "Error Rate"
* x:
* type: integer
* example: 0
* y:
* type: integer
* example: 0
* w:
* type: integer
* example: 6
* h:
* type: integer
* example: 3
* asRatio:
* type: boolean
* example: false
* series:
* type: array
* items:
* $ref: '#/components/schemas/ChartSeries'
*
* Dashboard:
* type: object
* properties:
* id:
* type: string
* example: "65f5e4a3b9e77c001a567890"
* name:
* type: string
* example: "Service Overview"
* tiles:
* type: array
* items:
* $ref: '#/components/schemas/Tile'
* tags:
* type: array
* items:
* type: string
* example: ["production", "monitoring"]
*
* CreateDashboardRequest:
* type: object
* required:
* - name
* properties:
* name:
* type: string
* example: "New Dashboard"
* tiles:
* type: array
* items:
* $ref: '#/components/schemas/Tile'
* tags:
* type: array
* items:
* type: string
* example: ["development"]
*
* UpdateDashboardRequest:
* type: object
* properties:
* name:
* type: string
* example: "Updated Dashboard Name"
* tiles:
* type: array
* items:
* $ref: '#/components/schemas/Tile'
* tags:
* type: array
* items:
* type: string
* example: ["production", "updated"]
*
* DashboardResponse:
* type: object
* properties:
* data:
* $ref: '#/components/schemas/Dashboard'
*
* DashboardsListResponse:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/Dashboard'
*/
const router = express.Router();
/**
* @openapi
* /api/v2/dashboards:
* get:
* summary: List Dashboards
* description: Retrieves a list of all dashboards for the authenticated team
* operationId: listDashboards
* tags: [Dashboards]
* responses:
* '200':
* description: Successfully retrieved dashboards
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/DashboardsListResponse'
* '401':
* description: Unauthorized
*/
router.get('/', async (req, res, next) => {
try {
const teamId = req.user?.team;
if (teamId == null) {
return res.sendStatus(403);
}
const dashboards = await Dashboard.find(
{ team: teamId },
{ _id: 1, name: 1, tiles: 1, tags: 1 },
).sort({ name: -1 });
res.json({
data: dashboards.map(d =>
translateDashboardDocumentToExternalDashboard(d),
),
});
} catch (e) {
next(e);
}
});
/**
* @openapi
* /api/v2/dashboards/{id}:
* get:
* summary: Get Dashboard
* description: Retrieves a specific dashboard by ID
* operationId: getDashboard
* tags: [Dashboards]
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* description: Dashboard ID
* example: "65f5e4a3b9e77c001a567890"
* responses:
* '200':
* description: Successfully retrieved dashboard
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/DashboardResponse'
* examples:
* dashboard:
* summary: Single dashboard response
* value:
* data:
* id: "65f5e4a3b9e77c001a567890"
* name: "Infrastructure Monitoring"
* tiles:
* - id: "65f5e4a3b9e77c001a901234"
* name: "Server CPU"
* x: 0
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "time"
* dataSource: "metrics"
* aggFn: "avg"
* field: "cpu.usage"
* where: "host:server-01"
* groupBy: []
* - id: "65f5e4a3b9e77c001a901235"
* name: "Memory Usage"
* x: 6
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "time"
* dataSource: "metrics"
* aggFn: "avg"
* field: "memory.usage"
* where: "host:server-01"
* groupBy: []
* tags: ["infrastructure", "monitoring"]
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Unauthorized access. API key is missing or invalid."
* '404':
* description: Dashboard not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Dashboard not found"
*/
router.get(
'/:id',
validateRequest({
@ -56,28 +297,116 @@ router.get(
},
);
router.get('/', async (req, res, next) => {
try {
const teamId = req.user?.team;
if (teamId == null) {
return res.sendStatus(403);
}
const dashboards = await Dashboard.find(
{ team: teamId },
{ _id: 1, name: 1, tiles: 1, tags: 1 },
).sort({ name: -1 });
res.json({
data: dashboards.map(d =>
translateDashboardDocumentToExternalDashboard(d),
),
});
} catch (e) {
next(e);
}
});
/**
* @openapi
* /api/v2/dashboards:
* post:
* summary: Create Dashboard
* description: Creates a new dashboard
* operationId: createDashboard
* tags: [Dashboards]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateDashboardRequest'
* examples:
* simpleTimeSeriesDashboard:
* summary: Dashboard with time series chart
* value:
* name: "API Monitoring Dashboard"
* tiles:
* - name: "API Request Volume"
* x: 0
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "time"
* dataSource: "events"
* aggFn: "count"
* where: "service:api"
* groupBy: []
* tags: ["api", "monitoring"]
* complexDashboard:
* summary: Dashboard with multiple chart types
* value:
* name: "Service Health Overview"
* tiles:
* - name: "Request Count"
* x: 0
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "time"
* dataSource: "events"
* aggFn: "count"
* where: "service:backend"
* groupBy: []
* - name: "Error Distribution"
* x: 6
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "table"
* dataSource: "events"
* aggFn: "count"
* where: "level:error"
* groupBy: ["errorType"]
* sortOrder: "desc"
* tags: ["service-health", "production"]
* responses:
* '200':
* description: Successfully created dashboard
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/DashboardResponse'
* examples:
* createdDashboard:
* summary: Created dashboard response
* value:
* data:
* id: "65f5e4a3b9e77c001a567890"
* name: "API Monitoring Dashboard"
* tiles:
* - id: "65f5e4a3b9e77c001a901234"
* name: "API Request Volume"
* x: 0
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "time"
* dataSource: "events"
* aggFn: "count"
* where: "service:api"
* groupBy: []
* tags: ["api", "monitoring"]
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Unauthorized access. API key is missing or invalid."
* '500':
* description: Server error or validation failure
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Dashboard validation failed: name is required"
*/
router.post(
'/',
validateRequest({
@ -121,6 +450,125 @@ router.post(
},
);
/**
* @openapi
* /api/v2/dashboards/{id}:
* put:
* summary: Update Dashboard
* description: Updates an existing dashboard
* operationId: updateDashboard
* tags: [Dashboards]
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* description: Dashboard ID
* example: "65f5e4a3b9e77c001a567890"
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UpdateDashboardRequest'
* examples:
* updateDashboard:
* summary: Update dashboard properties and tiles
* value:
* name: "Updated Dashboard Name"
* tiles:
* - id: "65f5e4a3b9e77c001a901234"
* name: "Updated Time Series Chart"
* x: 0
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "time"
* dataSource: "events"
* aggFn: "count"
* where: "level:error"
* groupBy: []
* - name: "New Number Chart"
* x: 6
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "number"
* dataSource: "events"
* aggFn: "count"
* where: "level:info"
* tags: ["production", "updated"]
* responses:
* '200':
* description: Successfully updated dashboard
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/DashboardResponse'
* examples:
* updatedDashboard:
* summary: Updated dashboard response
* value:
* data:
* id: "65f5e4a3b9e77c001a567890"
* name: "Updated Dashboard Name"
* tiles:
* - id: "65f5e4a3b9e77c001a901234"
* name: "Updated Time Series Chart"
* x: 0
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "time"
* dataSource: "events"
* aggFn: "count"
* where: "level:error"
* groupBy: []
* - id: "65f5e4a3b9e77c001a901236"
* name: "New Number Chart"
* x: 6
* y: 0
* w: 6
* h: 3
* asRatio: false
* series:
* - type: "number"
* dataSource: "events"
* aggFn: "count"
* where: "level:info"
* tags: ["production", "updated"]
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Unauthorized access. API key is missing or invalid."
* '404':
* description: Dashboard not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Dashboard not found"
* '500':
* description: Server error or validation failure
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Invalid dashboard configuration"
*/
router.put(
'/:id',
validateRequest({
@ -183,6 +631,47 @@ router.put(
},
);
/**
* @openapi
* /api/v2/dashboards/{id}:
* delete:
* summary: Delete Dashboard
* description: Deletes a dashboard
* operationId: deleteDashboard
* tags: [Dashboards]
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* description: Dashboard ID
* example: "65f5e4a3b9e77c001a567890"
* responses:
* '200':
* description: Successfully deleted dashboard
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/EmptyResponse'
* example: {}
* '401':
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Unauthorized access. API key is missing or invalid."
* '404':
* description: Dashboard not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* message: "Dashboard not found"
*/
router.delete(
'/:id',
validateRequest({

View file

@ -0,0 +1,70 @@
import { Application, Express } from 'express';
import fs from 'fs';
import path from 'path';
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
export const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'HyperDX External API',
description: 'API for managing HyperDX alerts and dashboards',
version: '2.0.0',
},
servers: [
{
url: 'https://api.hyperdx.io',
description: 'Production API server',
},
{
url: '/',
description: 'Current server',
},
],
tags: [
{
name: 'Dashboards',
description:
'Endpoints for managing dashboards and their visualizations',
},
{
name: 'Alerts',
description: 'Endpoints for managing monitoring alerts',
},
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'API Key',
},
},
},
security: [
{
BearerAuth: [],
},
],
},
apis: ['./src/routers/external-api/**/*.ts'], // Path to the API routes files
};
export function setupSwagger(app: Application) {
const specs = swaggerJsdoc(swaggerOptions);
// Serve swagger docs
app.use('/api/v2/docs', swaggerUi.serve, swaggerUi.setup(specs));
// Serve OpenAPI spec as JSON (needed for ReDoc)
app.get('/api/v2/docs.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(specs);
});
// Optionally save the spec to a file
const outputPath = path.resolve(__dirname, '../../../public/openapi.json');
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(specs, null, 2));
}

View file

@ -24,6 +24,6 @@
"strict": true,
"target": "ES2022"
},
"include": ["src", "migrations"],
"include": ["src", "migrations", "scripts"],
"exclude": ["node_modules", "**/*.test.ts"]
}

206
yarn.lock
View file

@ -29,6 +29,48 @@ __metadata:
languageName: node
linkType: hard
"@apidevtools/json-schema-ref-parser@npm:^9.0.6":
version: 9.1.2
resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.2"
dependencies:
"@jsdevtools/ono": "npm:^7.1.3"
"@types/json-schema": "npm:^7.0.6"
call-me-maybe: "npm:^1.0.1"
js-yaml: "npm:^4.1.0"
checksum: 10c0/ebf952eb2e00bf0919f024e72897e047fd5012f0a9e47ac361873f6de0a733b9334513cdbc73205a6b43ac4a652b8c87f55e489c39b2d60bd0bc1cb2b411e218
languageName: node
linkType: hard
"@apidevtools/openapi-schemas@npm:^2.0.4":
version: 2.1.0
resolution: "@apidevtools/openapi-schemas@npm:2.1.0"
checksum: 10c0/f4aa0f9df32e474d166c84ef91bceb18fa1c4f44b5593879529154ef340846811ea57dc2921560f157f692262827d28d988dd6e19fb21f00320e9961964176b4
languageName: node
linkType: hard
"@apidevtools/swagger-methods@npm:^3.0.2":
version: 3.0.2
resolution: "@apidevtools/swagger-methods@npm:3.0.2"
checksum: 10c0/8c390e8e50c0be7787ba0ba4c3758488bde7c66c2d995209b4b48c1f8bc988faf393cbb24a4bd1cd2d42ce5167c26538e8adea5c85eb922761b927e4dab9fa1c
languageName: node
linkType: hard
"@apidevtools/swagger-parser@npm:10.0.3":
version: 10.0.3
resolution: "@apidevtools/swagger-parser@npm:10.0.3"
dependencies:
"@apidevtools/json-schema-ref-parser": "npm:^9.0.6"
"@apidevtools/openapi-schemas": "npm:^2.0.4"
"@apidevtools/swagger-methods": "npm:^3.0.2"
"@jsdevtools/ono": "npm:^7.1.3"
call-me-maybe: "npm:^1.0.1"
z-schema: "npm:^5.0.1"
peerDependencies:
openapi-types: ">=7"
checksum: 10c0/3b43f719c2d647ac8dcf30f132834d413ce21cbf7a8d9c3b35ec91149dd25d608c8fd892358fcd61a8edd8c5140a7fb13676f948e2d87067d081a47b8c7107e9
languageName: node
linkType: hard
"@aw-web-design/x-default-browser@npm:1.4.126":
version: 1.4.126
resolution: "@aw-web-design/x-default-browser@npm:1.4.126"
@ -4242,6 +4284,7 @@ __metadata:
"@types/semver": "npm:^7.3.12"
"@types/sqlstring": "npm:^2.3.0"
"@types/supertest": "npm:^2.0.12"
"@types/swagger-jsdoc": "npm:^6"
"@types/uuid": "npm:^8.3.4"
axios: "npm:^1.6.2"
compression: "npm:^1.7.4"
@ -4281,6 +4324,8 @@ __metadata:
serialize-error: "npm:^8.1.0"
sqlstring: "npm:^2.3.3"
supertest: "npm:^6.3.1"
swagger-jsdoc: "npm:^6.2.8"
swagger-ui-express: "npm:^5.0.1"
ts-jest: "npm:^28.0.5"
ts-node: "npm:^10.8.1"
tsc-alias: "npm:^1.8.8"
@ -5264,6 +5309,13 @@ __metadata:
languageName: node
linkType: hard
"@jsdevtools/ono@npm:^7.1.3":
version: 7.1.3
resolution: "@jsdevtools/ono@npm:7.1.3"
checksum: 10c0/a9f7e3e8e3bc315a34959934a5e2f874c423cf4eae64377d3fc9de0400ed9f36cb5fd5ebce3300d2e8f4085f557c4a8b591427a583729a87841fda46e6c216b9
languageName: node
linkType: hard
"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0":
version: 1.2.1
resolution: "@lezer/common@npm:1.2.1"
@ -7982,6 +8034,13 @@ __metadata:
languageName: node
linkType: hard
"@scarf/scarf@npm:=1.4.0":
version: 1.4.0
resolution: "@scarf/scarf@npm:1.4.0"
checksum: 10c0/332118bb488e7a70eaad068fb1a33f016d30442fb0498b37a80cb425c1e741853a5de1a04dce03526ed6265481ecf744aa6e13f072178d19e6b94b19f623ae1c
languageName: node
linkType: hard
"@sentry/core@npm:^8.7.0":
version: 8.13.0
resolution: "@sentry/core@npm:8.13.0"
@ -9823,7 +9882,7 @@ __metadata:
languageName: node
linkType: hard
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8":
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.8":
version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15"
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
@ -10382,6 +10441,13 @@ __metadata:
languageName: node
linkType: hard
"@types/swagger-jsdoc@npm:^6":
version: 6.0.4
resolution: "@types/swagger-jsdoc@npm:6.0.4"
checksum: 10c0/fbe17d91a12e1e60a255b02e6def6877c81b356c75ffcd0e5167fbaf1476e2d6600cd7eea79e6b3e0ff7929dec33ade345147509ed3b98026f63c782b74514f6
languageName: node
linkType: hard
"@types/tedious@npm:^4.0.10":
version: 4.0.14
resolution: "@types/tedious@npm:4.0.14"
@ -12453,6 +12519,13 @@ __metadata:
languageName: node
linkType: hard
"call-me-maybe@npm:^1.0.1":
version: 1.0.2
resolution: "call-me-maybe@npm:1.0.2"
checksum: 10c0/8eff5dbb61141ebb236ed71b4e9549e488bcb5451c48c11e5667d5c75b0532303788a1101e6978cafa2d0c8c1a727805599c2741e3e0982855c9f1d78cd06c9f
languageName: node
linkType: hard
"callsites@npm:^3.0.0":
version: 3.1.0
resolution: "callsites@npm:3.1.0"
@ -13101,6 +13174,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:6.2.0":
version: 6.2.0
resolution: "commander@npm:6.2.0"
checksum: 10c0/1b701c6726fc2b6c6a7d9ab017be9465153546a05767cdd0e15e9f9a11c07f88f64d47684b90b07e5fb103d173efb6afdf4a21f6d6c4c25f7376bd027d21062c
languageName: node
linkType: hard
"commander@npm:^2.19.0, commander@npm:^2.20.0":
version: 2.20.3
resolution: "commander@npm:2.20.3"
@ -14201,6 +14281,15 @@ __metadata:
languageName: node
linkType: hard
"doctrine@npm:3.0.0, doctrine@npm:^3.0.0":
version: 3.0.0
resolution: "doctrine@npm:3.0.0"
dependencies:
esutils: "npm:^2.0.2"
checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520
languageName: node
linkType: hard
"doctrine@npm:^2.1.0":
version: 2.1.0
resolution: "doctrine@npm:2.1.0"
@ -14210,15 +14299,6 @@ __metadata:
languageName: node
linkType: hard
"doctrine@npm:^3.0.0":
version: 3.0.0
resolution: "doctrine@npm:3.0.0"
dependencies:
esutils: "npm:^2.0.2"
checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.5.9":
version: 0.5.16
resolution: "dom-accessibility-api@npm:0.5.16"
@ -16784,6 +16864,20 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:7.1.6":
version: 7.1.6
resolution: "glob@npm:7.1.6"
dependencies:
fs.realpath: "npm:^1.0.0"
inflight: "npm:^1.0.4"
inherits: "npm:2"
minimatch: "npm:^3.0.4"
once: "npm:^1.3.0"
path-is-absolute: "npm:^1.0.0"
checksum: 10c0/2575cce9306ac534388db751f0aa3e78afedb6af8f3b529ac6b2354f66765545145dba8530abf7bff49fb399a047d3f9b6901c38ee4c9503f592960d9af67763
languageName: node
linkType: hard
"glob@npm:7.1.7":
version: 7.1.7
resolution: "glob@npm:7.1.7"
@ -19678,7 +19772,14 @@ __metadata:
languageName: node
linkType: hard
"lodash.isequal@npm:^4.0.0":
"lodash.get@npm:^4.4.2":
version: 4.4.2
resolution: "lodash.get@npm:4.4.2"
checksum: 10c0/48f40d471a1654397ed41685495acb31498d5ed696185ac8973daef424a749ca0c7871bf7b665d5c14f5cc479394479e0307e781f61d5573831769593411be6e
languageName: node
linkType: hard
"lodash.isequal@npm:^4.0.0, lodash.isequal@npm:^4.5.0":
version: 4.5.0
resolution: "lodash.isequal@npm:4.5.0"
checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f
@ -19720,6 +19821,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.mergewith@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.mergewith@npm:4.6.2"
checksum: 10c0/4adbed65ff96fd65b0b3861f6899f98304f90fd71e7f1eb36c1270e05d500ee7f5ec44c02ef979b5ddbf75c0a0b9b99c35f0ad58f4011934c4d4e99e5200b3b5
languageName: node
linkType: hard
"lodash.sortby@npm:^4.7.0":
version: 4.7.0
resolution: "lodash.sortby@npm:4.7.0"
@ -26638,6 +26746,51 @@ __metadata:
languageName: node
linkType: hard
"swagger-jsdoc@npm:^6.2.8":
version: 6.2.8
resolution: "swagger-jsdoc@npm:6.2.8"
dependencies:
commander: "npm:6.2.0"
doctrine: "npm:3.0.0"
glob: "npm:7.1.6"
lodash.mergewith: "npm:^4.6.2"
swagger-parser: "npm:^10.0.3"
yaml: "npm:2.0.0-1"
bin:
swagger-jsdoc: bin/swagger-jsdoc.js
checksum: 10c0/7e20f08e8d90cc1e787cd82c096291cf12533359f89c70fbe4295a01f7c4734f2e82a03ba94027127bcd3da04b817abfe979f00d00ef0cd8283e449250a66215
languageName: node
linkType: hard
"swagger-parser@npm:^10.0.3":
version: 10.0.3
resolution: "swagger-parser@npm:10.0.3"
dependencies:
"@apidevtools/swagger-parser": "npm:10.0.3"
checksum: 10c0/d1a5c05f651f21a23508a36416071630b83e91dfffd52a6d44b06ca2cd1b86304c0dd2f4c04526c999b70062fa89bde3f5d54a1436626f4350590b6c6265a098
languageName: node
linkType: hard
"swagger-ui-dist@npm:>=5.0.0":
version: 5.21.0
resolution: "swagger-ui-dist@npm:5.21.0"
dependencies:
"@scarf/scarf": "npm:=1.4.0"
checksum: 10c0/fed58b709cc956f5965cc3e2c522898365ecd43f5bb942252d7352e52039ef7b2106b915165295114009ce7e1b64c2b2cb97edf93e3f2a44adfed70bb8a4fac8
languageName: node
linkType: hard
"swagger-ui-express@npm:^5.0.1":
version: 5.0.1
resolution: "swagger-ui-express@npm:5.0.1"
dependencies:
swagger-ui-dist: "npm:>=5.0.0"
peerDependencies:
express: ">=4.0.0 || >=5.0.0-beta"
checksum: 10c0/dbe9830caef7fe455241e44e74958bac62642997e4341c1b0f38a3d684d19a4a81b431217c656792d99f046a1b5f261abf7783ede0afe41098cd4450401f6fd1
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.4":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
@ -28223,6 +28376,13 @@ __metadata:
languageName: node
linkType: hard
"validator@npm:^13.7.0":
version: 13.15.0
resolution: "validator@npm:13.15.0"
checksum: 10c0/0f13fd7031ac575e8d7828431da8ef5859bac6a38ee65e1d7fdd367dbf1c3d94d95182aecc3183f7fa7a30ff4474bf864d1aff54707620227a2cdbfd36d894c2
languageName: node
linkType: hard
"vary@npm:^1, vary@npm:~1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"
@ -28879,6 +29039,13 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:2.0.0-1":
version: 2.0.0-1
resolution: "yaml@npm:2.0.0-1"
checksum: 10c0/e76eba2fbae37cd3e5bff057184be7cdca849895149d2f5660386871a501d76d2e1ec5906c48269a9fe798f214df31d342675b37bcd9d09af7c12eb6fb46a740
languageName: node
linkType: hard
"yaml@npm:^1.10.0":
version: 1.10.2
resolution: "yaml@npm:1.10.2"
@ -28980,6 +29147,23 @@ __metadata:
languageName: node
linkType: hard
"z-schema@npm:^5.0.1":
version: 5.0.5
resolution: "z-schema@npm:5.0.5"
dependencies:
commander: "npm:^9.4.1"
lodash.get: "npm:^4.4.2"
lodash.isequal: "npm:^4.5.0"
validator: "npm:^13.7.0"
dependenciesMeta:
commander:
optional: true
bin:
z-schema: bin/z-schema
checksum: 10c0/e4c812cfe6468c19b2a21d07d4ff8fb70359062d33400b45f89017eaa3efe9d51e85963f2b115eaaa99a16b451782249bf9b1fa8b31d35cc473e7becb3e44264
languageName: node
linkType: hard
"zod-express-middleware@npm:^1.4.0":
version: 1.4.0
resolution: "zod-express-middleware@npm:1.4.0"