diff --git a/Makefile b/Makefile
index 42d87c2..8d89e64 100644
--- a/Makefile
+++ b/Makefile
@@ -8,13 +8,17 @@ help: ## help message, list all command
.PHONY: up docker-run
up docker-run: docker-build ## docker-compose up
touch dbinit.sql
- docker compose up --build
+ docker compose -d up --build
.PHONY: up-db
up-db: ## docker-compose up db
touch dbinit.sql
docker compose up db
+.PHONY: logs
+logs: ## docker-compose logs -f
+ docker compose logs -f
+
.PHONY: down-db
down-db: ## docker-compose down db and remove volumes
docker compose down -v
diff --git a/db_schema/queries/v1/appleEpisodesLTR.sql b/db_schema/queries/v1/appleEpisodesLTR.sql
index 58c2cee..16081e1 100644
--- a/db_schema/queries/v1/appleEpisodesLTR.sql
+++ b/db_schema/queries/v1/appleEpisodesLTR.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Apple Podcasts episode listener-through-rate (LTR) data showing retention at 25%, 50%, 75%, and 100% completion points.
+-- Fields: Episode Name, GUID, Date, Quarter 1-4 LTR percentages, Apple Listeners count
+
WITH
apple as (
SELECT
diff --git a/db_schema/queries/v1/appleEpisodesPlays.sql b/db_schema/queries/v1/appleEpisodesPlays.sql
index ae52729..9e5f21b 100644
--- a/db_schema/queries/v1/appleEpisodesPlays.sql
+++ b/db_schema/queries/v1/appleEpisodesPlays.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Apple Podcasts episode performance metrics including plays, listening time, and engagement data.
+-- Fields: Date, Episode Name, GUID, Plays Count, Total Time Listened, Unique Engaged Listeners, Unique Listeners
+
WITH
apple as (
SELECT
diff --git a/db_schema/queries/v1/applePodcastFollowers.sql b/db_schema/queries/v1/applePodcastFollowers.sql
index b5ea85a..bdb3413 100644
--- a/db_schema/queries/v1/applePodcastFollowers.sql
+++ b/db_schema/queries/v1/applePodcastFollowers.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Apple Podcasts follower metrics showing daily follower growth and churn.
+-- Fields: Date, Total Followers, Total Unfollowers, Gained Followers, Lost Followers
+
SELECT
atf_date as `date`,
atf_totalfollowers as total_followers,
diff --git a/db_schema/queries/v1/chartsRankings.sql b/db_schema/queries/v1/chartsRankings.sql
index 9a8e5c7..6a7a90b 100644
--- a/db_schema/queries/v1/chartsRankings.sql
+++ b/db_schema/queries/v1/chartsRankings.sql
@@ -1,5 +1,6 @@
--- Chart rankings query using provider IDs from podcasts table
--- Requires @account_id parameter to be set
+-- @doc
+-- Returns podcast and episode chart rankings from Spotify and Apple Podcasts across different markets and categories.
+-- Fields: Platform, Item Type, Show ID, Episode ID, Episode Name, Market, Chart Name, Position, Chart Date
WITH podcast_ids AS (
SELECT
diff --git a/db_schema/queries/v1/episodesAge.sql b/db_schema/queries/v1/episodesAge.sql
index 6514e03..dad2148 100644
--- a/db_schema/queries/v1/episodesAge.sql
+++ b/db_schema/queries/v1/episodesAge.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Spotify episode listener demographics by age group with listener counts and percentages.
+-- Fields: Date, Episode GUID, Age Group, Listeners Count, Percentage of Total
+
WITH data as
(
SELECT spa_date,episode_id,spa_facet,spa_gender_female+spa_gender_male+spa_gender_non_binary+spa_gender_not_specified as listeners
diff --git a/db_schema/queries/v1/episodesDailyMetrics.sql b/db_schema/queries/v1/episodesDailyMetrics.sql
index 59bd1bd..b450951 100644
--- a/db_schema/queries/v1/episodesDailyMetrics.sql
+++ b/db_schema/queries/v1/episodesDailyMetrics.sql
@@ -1,5 +1,6 @@
--- Daily episodes metrics from Apple and Spotify platforms using episodeMapping view
--- Returns daily performance metrics for plays, listeners, streams, and engagement data
+-- @doc
+-- Returns daily performance metrics for plays, listeners, streams, and engagement data of episodes from Apple and Spotify.
+-- Fields: Podcast ID, Spotify Episode ID, Apple Episode ID, GUID, Date, Spotify Starts, Spotify Streams, Spotify Listeners, Apple Plays, Apple Unique Listeners, Apple Engaged Listeners, Apple Total Time Listened
SELECT
-- Episode identifiers from mapping view
diff --git a/db_schema/queries/v1/episodesGender.sql b/db_schema/queries/v1/episodesGender.sql
index 93947fe..844f3ec 100644
--- a/db_schema/queries/v1/episodesGender.sql
+++ b/db_schema/queries/v1/episodesGender.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Spotify episode listener demographics broken down by gender and age group.
+-- Fields: Date, Episode GUID, Gender, Listeners Count, Age Group
+
WITH data as
(
SELECT spa_date,ep_guid,spa_facet,spa_gender_female,spa_gender_male,spa_gender_non_binary,spa_gender_not_specified
diff --git a/db_schema/queries/v1/episodesLTR.sql b/db_schema/queries/v1/episodesLTR.sql
index a2a0e03..9b1d443 100644
--- a/db_schema/queries/v1/episodesLTR.sql
+++ b/db_schema/queries/v1/episodesLTR.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns combined episode listener-through-rate (LTR) data from both Spotify and Apple Podcasts showing retention at different completion points.
+-- Fields: Episode Name, GUID, Date, Combined Quarter 1-4 LTR, Individual Platform LTR values, Listener Counts
+
WITH
spotify as (
SELECT
diff --git a/db_schema/queries/v1/episodesLTRHistogram.sql b/db_schema/queries/v1/episodesLTRHistogram.sql
index 4bab796..1ecf45e 100644
--- a/db_schema/queries/v1/episodesLTRHistogram.sql
+++ b/db_schema/queries/v1/episodesLTRHistogram.sql
@@ -1,4 +1,8 @@
+-- @doc
+-- Returns detailed listener retention histogram data for episodes from both Spotify and Apple Podcasts at 15-second intervals.
+-- Fields: Episode GUID, Date, Max Listeners (Spotify/Apple), Histogram Data Arrays
+
WITH spotify as (
SELECT JSON_ARRAYAGG(JSON_OBJECT(sample_id-1,listeners)) as histogram,episode_id,account_id,spp_date,spp_sample_max FROM
spotifyEpisodePerformance CROSS JOIN
diff --git a/db_schema/queries/v1/episodesMetadata.sql b/db_schema/queries/v1/episodesMetadata.sql
index 1cbe05a..bcaab45 100644
--- a/db_schema/queries/v1/episodesMetadata.sql
+++ b/db_schema/queries/v1/episodesMetadata.sql
@@ -1,5 +1,6 @@
--- Combined episodes metadata from Apple and Spotify platforms using episodeMapping view
--- Returns metadata for all episodes with their basic information
+-- @doc
+-- Returns comprehensive episode metadata from both Spotify and Apple Podcasts including URLs, descriptions, release dates, and technical details.
+-- Fields: Account ID, Episode IDs, Name, GUID, URLs, Release Dates, Descriptions, Duration, Language, Content Flags
SELECT
-- Common episode identifiers from mapping view
diff --git a/db_schema/queries/v1/episodesTotalMetrics.sql b/db_schema/queries/v1/episodesTotalMetrics.sql
index 45025aa..4f2cbb8 100644
--- a/db_schema/queries/v1/episodesTotalMetrics.sql
+++ b/db_schema/queries/v1/episodesTotalMetrics.sql
@@ -1,6 +1,10 @@
-- Total episodes metrics from Apple and Spotify platforms using episodeMapping view
-- Returns cumulative/total performance metrics for plays, listeners, and engagement data
+-- @doc
+-- Returns aggregated total metrics for episodes combining data from Spotify, Apple Podcasts, and hosting providers over the specified date range.
+-- Fields: Episode identifiers, Total Streams, Plays, Listeners, Downloads, Time Listened across all platforms
+
SELECT
-- Episode identifiers from mapping view
em.account_id,
diff --git a/db_schema/queries/v1/ping.sql b/db_schema/queries/v1/ping.sql
index e901a28..fc43932 100644
--- a/db_schema/queries/v1/ping.sql
+++ b/db_schema/queries/v1/ping.sql
@@ -1 +1,5 @@
+-- @doc
+-- Simple health check endpoint that echoes back the provided date parameters.
+-- Fields: Start Date, End Date, Result Status
+
SELECT @start as start, @end as end, "pong" as result
\ No newline at end of file
diff --git a/db_schema/queries/v1/podcastAge.sql b/db_schema/queries/v1/podcastAge.sql
index 251bab8..c0da0f2 100644
--- a/db_schema/queries/v1/podcastAge.sql
+++ b/db_schema/queries/v1/podcastAge.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Spotify podcast listener demographics by age group with listener counts and percentages of total audience.
+-- Fields: Date, Age Group, Listeners Count, Percentage of Total
+
WITH data as
(
SELECT
diff --git a/db_schema/queries/v1/podcastFollowers.sql b/db_schema/queries/v1/podcastFollowers.sql
index 5e16507..c8e714e 100644
--- a/db_schema/queries/v1/podcastFollowers.sql
+++ b/db_schema/queries/v1/podcastFollowers.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns podcast follower metrics across platforms showing daily follower counts and growth trends.
+-- Fields: Date, Platform, Followers Count, Growth metrics
+
SELECT * FROM podcastFollowers
WHERE
account_id = @podcast_id
diff --git a/db_schema/queries/v1/podcastGender.sql b/db_schema/queries/v1/podcastGender.sql
index 175d5c1..8821c9c 100644
--- a/db_schema/queries/v1/podcastGender.sql
+++ b/db_schema/queries/v1/podcastGender.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Spotify podcast listener demographics broken down by gender and age group.
+-- Fields: Date, Gender, Listeners Count, Age Group
+
WITH data as (
SELECT *
FROM spotifyPodcastAggregate
diff --git a/db_schema/queries/v1/podcastMetadata.sql b/db_schema/queries/v1/podcastMetadata.sql
index e3142eb..b0d39bc 100644
--- a/db_schema/queries/v1/podcastMetadata.sql
+++ b/db_schema/queries/v1/podcastMetadata.sql
@@ -1,5 +1,6 @@
--- Combined podcast metadata from Apple and Spotify platforms
--- Returns the latest metadata information for the podcast
+-- @doc
+-- Returns comprehensive podcast metadata including basic information, Spotify statistics, and Apple follower data.
+-- Fields: Account ID, Podcast Name, Artwork URL, Release Date, Publisher, Latest Platform Statistics
SELECT
pm.account_id,
diff --git a/db_schema/queries/v1/spotifyCountries.sql b/db_schema/queries/v1/spotifyCountries.sql
index bf533b4..61bcbcd 100644
--- a/db_schema/queries/v1/spotifyCountries.sql
+++ b/db_schema/queries/v1/spotifyCountries.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns Spotify podcast listener geographic distribution by country with listener counts and percentages.
+-- Fields: Country Short Code, Listeners Count, Percentage of Total
+
WITH
data as (
SELECT
diff --git a/db_schema/queries/v1/spotifyImpressions.sql b/db_schema/queries/v1/spotifyImpressions.sql
index 81e731a..ab9fe9c 100644
--- a/db_schema/queries/v1/spotifyImpressions.sql
+++ b/db_schema/queries/v1/spotifyImpressions.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns daily Spotify impression metrics showing how many times the podcast appeared in user interfaces.
+-- Fields: Account ID, Date, Impressions Count
+
SELECT
account_id,
date,
diff --git a/db_schema/queries/v1/spotifyImpressionsFunnel.sql b/db_schema/queries/v1/spotifyImpressionsFunnel.sql
index 1e9f70c..8b37270 100644
--- a/db_schema/queries/v1/spotifyImpressionsFunnel.sql
+++ b/db_schema/queries/v1/spotifyImpressionsFunnel.sql
@@ -1,5 +1,6 @@
--- Gets the latest 30-day funnel data (impressions -> considerations -> streams)
--- Returns data for the most recent day within the requested date range
+-- @doc
+-- Returns Spotify conversion funnel data showing the progression from impressions to considerations to streams with conversion rates.
+-- Fields: Account ID, Date, Step ID, Step Count, Conversion Percentage
SELECT DISTINCT
account_id,
diff --git a/db_schema/queries/v1/spotifyImpressionsSources.sql b/db_schema/queries/v1/spotifyImpressionsSources.sql
index 498497b..5bbdf6e 100644
--- a/db_schema/queries/v1/spotifyImpressionsSources.sql
+++ b/db_schema/queries/v1/spotifyImpressionsSources.sql
@@ -1,4 +1,6 @@
--- Gets impressions breakdown by source (HOME, SEARCH, LIBRARY, OTHER)
+-- @doc
+-- Returns Spotify impressions broken down by source location (HOME, SEARCH, LIBRARY, OTHER) showing where users discover the podcast.
+-- Fields: Account ID, Date Start, Date End, Source ID, Impression Count
SELECT DISTINCT
account_id,
diff --git a/db_schema/queries/v1/spotifyPlaysSum.sql b/db_schema/queries/v1/spotifyPlaysSum.sql
index e68317c..da1fa66 100644
--- a/db_schema/queries/v1/spotifyPlaysSum.sql
+++ b/db_schema/queries/v1/spotifyPlaysSum.sql
@@ -1,3 +1,7 @@
+-- @doc
+-- Returns aggregated Spotify podcast metrics totals including episodes, starts, streams, listeners, and followers over the date range.
+-- Fields: Podcast ID, Total Episodes, Starts, Streams, Listeners, Followers
+
SELECT
account_id as podcast_id,
SUM(spm_total_episodes) as spotify_total_episodes,
diff --git a/package-lock.json b/package-lock.json
index bebf9dc..b1c2acc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,16 +9,20 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+ "@types/cors": "^2.8.19",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"body-parser": "^1.20.1",
+ "cors": "^2.8.5",
"dotenv": "^16.0.2",
"express": "^4.18.1",
"express-validator": "^6.14.2",
"hbs": "^4.2.0",
"mathjs": "^11.4.0",
"moment": "^2.29.4",
- "mysql2": "^2.3.3"
+ "mysql2": "^2.3.3",
+ "swagger-jsdoc": "^6.2.8",
+ "swagger-ui-express": "^5.0.1"
},
"devDependencies": {
"@types/body-parser": "^1.19.2",
@@ -26,6 +30,8 @@
"@types/jest": "^30.0.0",
"@types/mysql2": "types/mysql2",
"@types/node": "^18.7.18",
+ "@types/swagger-jsdoc": "^6.0.4",
+ "@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"concurrently": "^7.4.0",
@@ -60,6 +66,50 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@apidevtools/json-schema-ref-parser": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
+ "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jsdevtools/ono": "^7.1.3",
+ "@types/json-schema": "^7.0.6",
+ "call-me-maybe": "^1.0.1",
+ "js-yaml": "^4.1.0"
+ }
+ },
+ "node_modules/@apidevtools/openapi-schemas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
+ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@apidevtools/swagger-methods": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
+ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
+ "license": "MIT"
+ },
+ "node_modules/@apidevtools/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "^9.0.6",
+ "@apidevtools/openapi-schemas": "^2.0.4",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "@jsdevtools/ono": "^7.1.3",
+ "call-me-maybe": "^1.0.1",
+ "z-schema": "^5.0.1"
+ },
+ "peerDependencies": {
+ "openapi-types": ">=7"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -1191,6 +1241,12 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
+ "node_modules/@jsdevtools/ono": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1226,6 +1282,13 @@
"node": ">= 8"
}
},
+ "node_modules/@scarf/scarf": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
+ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0"
+ },
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -1334,6 +1397,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/eslint": {
"version": "8.4.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
@@ -1651,8 +1723,7 @@
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
+ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
},
"node_modules/@types/json5": {
"version": "0.0.29",
@@ -1684,8 +1755,7 @@
"node_modules/@types/node": {
"version": "18.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz",
- "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
- "dev": true
+ "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng=="
},
"node_modules/@types/prettier": {
"version": "2.7.2",
@@ -1748,6 +1818,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/swagger-jsdoc": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
+ "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/swagger-ui-express": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
+ "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*",
+ "@types/serve-static": "*"
+ }
+ },
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -2288,8 +2376,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-flatten": {
"version": "1.1.1",
@@ -2448,8 +2535,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/binary-extensions": {
"version": "2.2.0",
@@ -2500,7 +2586,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -2603,6 +2688,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/call-me-maybe": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
+ "license": "MIT"
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2828,8 +2919,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/concurrently": {
"version": "7.6.0",
@@ -2902,6 +2992,19 @@
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"dev": true
},
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -3081,7 +3184,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
"dependencies": {
"esutils": "^2.0.2"
},
@@ -3774,7 +3876,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4141,8 +4242,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.2",
@@ -4578,7 +4678,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -5530,7 +5629,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -5646,6 +5744,20 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "license": "MIT"
+ },
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -5658,6 +5770,12 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+ "license": "MIT"
+ },
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -5828,7 +5946,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -6052,6 +6169,15 @@
"node": ">=8"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -6119,7 +6245,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -6139,6 +6264,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/openapi-types": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -6246,7 +6378,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7241,6 +7372,92 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swagger-jsdoc": {
+ "version": "6.2.8",
+ "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
+ "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
+ "license": "MIT",
+ "dependencies": {
+ "commander": "6.2.0",
+ "doctrine": "3.0.0",
+ "glob": "7.1.6",
+ "lodash.mergewith": "^4.6.2",
+ "swagger-parser": "^10.0.3",
+ "yaml": "2.0.0-1"
+ },
+ "bin": {
+ "swagger-jsdoc": "bin/swagger-jsdoc.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/swagger-jsdoc/node_modules/commander": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
+ "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/swagger-jsdoc/node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/swagger-parser": "10.0.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/swagger-ui-dist": {
+ "version": "5.29.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.1.tgz",
+ "integrity": "sha512-qyjpz0qgcomRr41a5Aye42o69TKwCeHM9F8htLGVeUMKekNS6qAqz9oS7CtSvgGJSppSNAYAIh7vrfrSdHj9zw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@scarf/scarf": "=1.4.0"
+ }
+ },
+ "node_modules/swagger-ui-express": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
+ "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
+ "license": "MIT",
+ "dependencies": {
+ "swagger-ui-dist": ">=5.0.0"
+ },
+ "engines": {
+ "node": ">= v0.10.32"
+ },
+ "peerDependencies": {
+ "express": ">=4.0.0 || >=5.0.0-beta"
+ }
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -7929,8 +8146,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/write-file-atomic": {
"version": "4.0.2",
@@ -7959,6 +8175,15 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
+ "node_modules/yaml": {
+ "version": "2.0.0-1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
+ "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/yargs": {
"version": "17.6.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz",
@@ -8006,6 +8231,36 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/z-schema": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
+ "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "validator": "^13.7.0"
+ },
+ "bin": {
+ "z-schema": "bin/z-schema"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "commander": "^9.4.1"
+ }
+ },
+ "node_modules/z-schema/node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
}
},
"dependencies": {
@@ -8019,6 +8274,40 @@
"@jridgewell/trace-mapping": "^0.3.9"
}
},
+ "@apidevtools/json-schema-ref-parser": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
+ "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
+ "requires": {
+ "@jsdevtools/ono": "^7.1.3",
+ "@types/json-schema": "^7.0.6",
+ "call-me-maybe": "^1.0.1",
+ "js-yaml": "^4.1.0"
+ }
+ },
+ "@apidevtools/openapi-schemas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
+ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ=="
+ },
+ "@apidevtools/swagger-methods": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
+ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
+ },
+ "@apidevtools/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
+ "requires": {
+ "@apidevtools/json-schema-ref-parser": "^9.0.6",
+ "@apidevtools/openapi-schemas": "^2.0.4",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "@jsdevtools/ono": "^7.1.3",
+ "call-me-maybe": "^1.0.1",
+ "z-schema": "^5.0.1"
+ }
+ },
"@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -8891,6 +9180,11 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
+ "@jsdevtools/ono": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -8917,6 +9211,11 @@
"fastq": "^1.6.0"
}
},
+ "@scarf/scarf": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
+ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="
+ },
"@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -9025,6 +9324,14 @@
"@types/node": "*"
}
},
+ "@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/eslint": {
"version": "8.4.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz",
@@ -9274,8 +9581,7 @@
"@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
- "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
- "dev": true
+ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
},
"@types/json5": {
"version": "0.0.29",
@@ -9305,8 +9611,7 @@
"@types/node": {
"version": "18.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz",
- "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==",
- "dev": true
+ "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng=="
},
"@types/prettier": {
"version": "2.7.2",
@@ -9366,6 +9671,22 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true
},
+ "@types/swagger-jsdoc": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
+ "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
+ "dev": true
+ },
+ "@types/swagger-ui-express": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
+ "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*",
+ "@types/serve-static": "*"
+ }
+ },
"@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -9766,8 +10087,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"array-flatten": {
"version": "1.1.1",
@@ -9890,8 +10210,7 @@
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"binary-extensions": {
"version": "2.2.0",
@@ -9937,7 +10256,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -10011,6 +10329,11 @@
"get-intrinsic": "^1.0.2"
}
},
+ "call-me-maybe": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
+ },
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -10169,8 +10492,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"concurrently": {
"version": "7.6.0",
@@ -10224,6 +10546,15 @@
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"dev": true
},
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -10351,7 +10682,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
"requires": {
"esutils": "^2.0.2"
}
@@ -10858,8 +11188,7 @@
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
},
"etag": {
"version": "1.8.1",
@@ -11161,8 +11490,7 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"fsevents": {
"version": "2.3.2",
@@ -11464,7 +11792,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@@ -12175,7 +12502,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
"requires": {
"argparse": "^2.0.1"
}
@@ -12258,6 +12584,16 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
+ },
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
+ },
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -12270,6 +12606,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
+ },
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -12396,7 +12737,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -12574,6 +12914,11 @@
"path-key": "^3.0.0"
}
},
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
+ },
"object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -12620,7 +12965,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
"requires": {
"wrappy": "1"
}
@@ -12634,6 +12978,12 @@
"mimic-fn": "^2.1.0"
}
},
+ "openapi-types": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+ "peer": true
+ },
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -12707,8 +13057,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
},
"path-key": {
"version": "3.1.1",
@@ -13432,6 +13781,63 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
+ "swagger-jsdoc": {
+ "version": "6.2.8",
+ "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
+ "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
+ "requires": {
+ "commander": "6.2.0",
+ "doctrine": "3.0.0",
+ "glob": "7.1.6",
+ "lodash.mergewith": "^4.6.2",
+ "swagger-parser": "^10.0.3",
+ "yaml": "2.0.0-1"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
+ "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q=="
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
+ "requires": {
+ "@apidevtools/swagger-parser": "10.0.3"
+ }
+ },
+ "swagger-ui-dist": {
+ "version": "5.29.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.1.tgz",
+ "integrity": "sha512-qyjpz0qgcomRr41a5Aye42o69TKwCeHM9F8htLGVeUMKekNS6qAqz9oS7CtSvgGJSppSNAYAIh7vrfrSdHj9zw==",
+ "requires": {
+ "@scarf/scarf": "=1.4.0"
+ }
+ },
+ "swagger-ui-express": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
+ "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
+ "requires": {
+ "swagger-ui-dist": ">=5.0.0"
+ }
+ },
"tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -13906,8 +14312,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"write-file-atomic": {
"version": "4.0.2",
@@ -13930,6 +14335,11 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
+ "yaml": {
+ "version": "2.0.0-1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
+ "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ=="
+ },
"yargs": {
"version": "17.6.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz",
@@ -13962,6 +14372,25 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
+ },
+ "z-schema": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
+ "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
+ "requires": {
+ "commander": "^9.4.1",
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "validator": "^13.7.0"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index b3aaf87..dbd0e0d 100644
--- a/package.json
+++ b/package.json
@@ -11,16 +11,20 @@
"license": "MIT",
"private": true,
"dependencies": {
+ "@types/cors": "^2.8.19",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"body-parser": "^1.20.1",
+ "cors": "^2.8.5",
"dotenv": "^16.0.2",
"express": "^4.18.1",
"express-validator": "^6.14.2",
"hbs": "^4.2.0",
"mathjs": "^11.4.0",
"moment": "^2.29.4",
- "mysql2": "^2.3.3"
+ "mysql2": "^2.3.3",
+ "swagger-jsdoc": "^6.2.8",
+ "swagger-ui-express": "^5.0.1"
},
"devDependencies": {
"@types/body-parser": "^1.19.2",
@@ -28,6 +32,8 @@
"@types/jest": "^30.0.0",
"@types/mysql2": "types/mysql2",
"@types/node": "^18.7.18",
+ "@types/swagger-jsdoc": "^6.0.4",
+ "@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"concurrently": "^7.4.0",
diff --git a/src/config/generate-query-docs.ts b/src/config/generate-query-docs.ts
new file mode 100644
index 0000000..fddf72d
--- /dev/null
+++ b/src/config/generate-query-docs.ts
@@ -0,0 +1,173 @@
+import fs from 'fs'
+import path from 'path'
+import { getTwoDaysAgo } from './swagger'
+
+/**
+ * Generates OpenAPI documentation for SQL query endpoints
+ * Reads SQL files from db_schema/queries/v1/ and creates endpoint specs
+ */
+
+const QUERIES_DIR = path.join(__dirname, '../../db_schema/queries/v1')
+
+// Category mappings based on query name patterns
+const categorizeQuery = (queryName: string): string => {
+ if (queryName.includes('Spotify') || queryName.includes('spotify'))
+ return 'Spotify'
+ if (queryName.includes('Apple') || queryName.includes('apple'))
+ return 'Apple Podcasts'
+ if (queryName.includes('Hoster') || queryName.includes('hoster'))
+ return 'Hoster'
+ if (queryName.includes('episodes') || queryName.includes('Episodes'))
+ return 'Episodes'
+ if (queryName.includes('podcast') || queryName.includes('Podcast'))
+ return 'Podcast'
+ if (queryName.includes('charts') || queryName.includes('Charts'))
+ return 'Charts'
+ if (queryName.includes('Anchor') || queryName.includes('anchor'))
+ return 'Anchor (Legacy)'
+ return 'Other'
+}
+
+// Extract description from SQL comment at the top of file
+const extractDescription = (sqlContent: string): string => {
+ const lines = sqlContent.split('\n')
+ const commentLines: string[] = []
+
+ for (const line of lines) {
+ const trimmed = line.trim()
+ if (trimmed.startsWith('--')) {
+ commentLines.push(trimmed.substring(2).trim())
+ } else if (trimmed && !trimmed.startsWith('--')) {
+ break // Stop at first non-comment, non-empty line
+ }
+ }
+
+ return commentLines.join('
') || 'Analytics query endpoint'
+}
+
+export const generateQueryDocs = () => {
+ const queries: any[] = []
+
+ if (!fs.existsSync(QUERIES_DIR)) {
+ console.warn(`Queries directory not found: ${QUERIES_DIR}`)
+ return queries
+ }
+
+ const files = fs.readdirSync(QUERIES_DIR).filter((f) => f.endsWith('.sql'))
+
+ for (const file of files) {
+ const queryName = file.replace('.sql', '')
+ const filePath = path.join(QUERIES_DIR, file)
+ const sqlContent = fs.readFileSync(filePath, 'utf-8')
+
+ let description = extractDescription(sqlContent)
+ const category = categorizeQuery(queryName)
+
+ // First line has to start with `-- @doc`. If not, ignore the file
+ if (!description.startsWith('@doc')) {
+ continue
+ } else {
+ // Remove @doc from description
+ description = description.replace('@doc', '').trim()
+ }
+
+ queries.push({
+ name: queryName,
+ category,
+ description,
+ path: `/analytics/v1/{podcastId}/${queryName}`,
+ })
+ }
+
+ return queries
+}
+
+// Generate OpenAPI paths object for all queries
+export const generateQueryPaths = () => {
+ const queries = generateQueryDocs()
+ const paths: any = {}
+
+ // Group by category for better organization
+ const byCategory = queries.reduce((acc, q) => {
+ if (!acc[q.category]) acc[q.category] = []
+ acc[q.category].push(q)
+ return acc
+ }, {} as Record)
+
+ for (const query of queries) {
+ paths[query.path] = {
+ get: {
+ summary: query.name,
+ description: query.description,
+ tags: [query.category],
+ parameters: [
+ {
+ name: 'podcastId',
+ in: 'path',
+ required: true,
+ schema: { type: 'integer' },
+ description: 'Podcast ID',
+ },
+ {
+ name: 'start',
+ in: 'query',
+ required: false,
+ schema: {
+ type: 'string',
+ format: 'date',
+ example: getTwoDaysAgo(),
+ },
+ description: 'Start date (YYYY-MM-DD)',
+ },
+ {
+ name: 'end',
+ in: 'query',
+ required: false,
+ schema: {
+ type: 'string',
+ format: 'date',
+ example: getTwoDaysAgo(),
+ },
+ description: 'End date (YYYY-MM-DD)',
+ },
+ ],
+ security: [{ bearerAuth: [] }],
+ responses: {
+ 200: {
+ description: 'Query results',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ meta: {
+ type: 'object',
+ properties: {
+ query: { type: 'string' },
+ podcastId: { type: 'string' },
+ startDate: {
+ type: 'string',
+ format: 'date',
+ },
+ endDate: {
+ type: 'string',
+ format: 'date',
+ },
+ },
+ },
+ data: {
+ type: 'array',
+ items: { type: 'object' },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ }
+
+ return { paths, categories: Object.keys(byCategory) }
+}
diff --git a/src/config/swagger.ts b/src/config/swagger.ts
new file mode 100644
index 0000000..fe0dad2
--- /dev/null
+++ b/src/config/swagger.ts
@@ -0,0 +1,108 @@
+import swaggerJsdoc from 'swagger-jsdoc'
+import { generateQueryPaths } from './generate-query-docs'
+
+// Use hardcoded version to avoid rootDir issues with package.json import
+const version = '1.0.0'
+
+// Calculate dynamic date (2 days ago)
+export const getTwoDaysAgo = (): string => {
+ const date = new Date()
+ date.setDate(date.getDate() - 2)
+ return date.toISOString().split('T')[0] // YYYY-MM-DD format
+}
+
+const { paths: queryPaths, categories } = generateQueryPaths()
+
+const servers = [
+ {
+ url: 'https://api.openpodcast.dev',
+ description: 'OpenPodcast API',
+ },
+]
+
+if (process.env.NODE_ENV !== 'production') {
+ servers.push({
+ url: 'http://localhost:8080',
+ description: 'Local development server',
+ })
+}
+
+const options: swaggerJsdoc.Options = {
+ definition: {
+ openapi: '3.0.0',
+ info: {
+ title: 'Open Podcast Analytics API',
+ version: version,
+ description:
+ 'Podcast analytics API providing metrics from Spotify, Apple Podcasts, and custom hosting providers.',
+ contact: {
+ name: 'Open Podcast Developer Documentation',
+ url: 'https://openpodcast.dev',
+ email: 'echo@openpodcast.dev',
+ },
+ license: {
+ name: 'MIT',
+ url: 'https://github.com/openpodcast/api/blob/main/LICENSE',
+ },
+ },
+ servers,
+ components: {
+ securitySchemes: {
+ bearerAuth: {
+ type: 'http',
+ scheme: 'bearer',
+ bearerFormat: 'JWT',
+ description: 'Enter your API token',
+ },
+ },
+ parameters: {
+ podcastId: {
+ name: 'podcast_id',
+ in: 'path',
+ required: true,
+ schema: {
+ type: 'integer',
+ },
+ description: 'Podcast ID',
+ },
+ startDate: {
+ name: 'start',
+ in: 'query',
+ required: false,
+ schema: {
+ type: 'string',
+ format: 'date',
+ example: getTwoDaysAgo(),
+ },
+ description: 'Start date (YYYY-MM-DD)',
+ },
+ endDate: {
+ name: 'end',
+ in: 'query',
+ required: false,
+ schema: {
+ type: 'string',
+ format: 'date',
+ example: getTwoDaysAgo(),
+ },
+ description: 'End date (YYYY-MM-DD)',
+ },
+ },
+ },
+ security: [
+ {
+ bearerAuth: [],
+ },
+ ],
+ tags: [
+ ...categories.map((cat) => ({
+ name: cat,
+ description: `${cat} analytics endpoints`,
+ })),
+ ],
+ paths: queryPaths,
+ },
+ apis: ['./src/api/*.ts', './src/index.ts'],
+}
+
+export const swaggerSpec = swaggerJsdoc(options)
diff --git a/src/index.ts b/src/index.ts
index ce1784d..9a82a65 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,6 +5,7 @@ import express, {
RequestHandler,
NextFunction,
} from 'express'
+import cors from 'cors'
import bodyParser from 'body-parser'
import { EventsApi, ConnectorApi } from './api'
import { EventRepository } from './db/EventRepository'
@@ -37,6 +38,8 @@ import { formatDate, nowString } from './utils/dateHelpers'
import { AccountKeyRepository } from './db/AccountKeyRepository'
import fs from 'fs'
import path from 'path'
+import swaggerUi from 'swagger-ui-express'
+import { swaggerSpec } from './config/swagger'
const config = new Config()
@@ -168,6 +171,8 @@ const publicEndpoints = [
'^/status',
'^/feedback/*',
'^/comments/*',
+ '^/api-docs',
+ '^/api-docs.json',
]
const authController = new AuthController(accountKeyRepo)
@@ -175,6 +180,18 @@ const authController = new AuthController(accountKeyRepo)
const app: Express = express()
const port = config.getExpressPort()
+// Configure CORS to allow Swagger UI to make API calls
+app.use(
+ cors({
+ origin: ['http://localhost:8080', 'https://api.openpodcast.dev'],
+ methods: ['GET', 'POST', 'OPTIONS'],
+ allowedHeaders: ['Content-Type', 'Authorization'],
+ })
+)
+
+// Remove X-Powered-By header for security
+app.disable('x-powered-by')
+
// extract json payload from body automatically
app.use(bodyParser.json({ limit: '5mb' }))
@@ -270,12 +287,6 @@ app.get(
}
)
-// Analytics endpoint, which returns a JSON of the query results.
-// Check that the user is allowed to access the endpoint
-// and then run the query
-// The endpoint must contain a version number and a query name
-// e.g. /analytics/v1/1234/someQuery or /analytics/v1/1234/someQuery/csv
-// where 1234 is the podcast id and someQuery is the name of the query
app.get(
'/analytics/:version/:podcastId/:query/:format?',
async (req: Request, res: Response, next: NextFunction) => {
@@ -510,6 +521,21 @@ app.get(
})
)
+app.use(
+ '/api-docs',
+ swaggerUi.serve,
+ swaggerUi.setup(swaggerSpec, {
+ customCss: '.swagger-ui .topbar { display: none }',
+ customSiteTitle: 'Open Podcast API Documentation',
+ })
+)
+
+// Serve OpenAPI spec as JSON
+app.get('/api-docs.json', (req: Request, res: Response) => {
+ res.setHeader('Content-Type', 'application/json')
+ res.send(swaggerSpec)
+})
+
// catch 404 and forward to error handler
app.use(function (req: Request, res: Response, next: NextFunction) {
const err = new HttpError(`Not Found: ${req.originalUrl}`)
@@ -537,10 +563,21 @@ app.use(function (
// if it is not a known http error, print it for debugging purposes
console.log(err)
}
- res.status(
+
+ const status =
err instanceof HttpError || err instanceof AuthError ? err.status : 500
- )
- res.send(`Something's wrong. We're looking into it. (${tracingId})`)
+ res.status(status)
+
+ // Provide more specific error messages for common HTTP status codes
+ if (status === 401) {
+ res.send(
+ `Authentication required. Please provide a valid API token. (${tracingId})`
+ )
+ } else if (status === 404) {
+ res.send(`Resource not found. (${tracingId})`)
+ } else {
+ res.send(`Something's wrong. We're looking into it. (${tracingId})`)
+ }
})
dbInit.init().then(() => {