From 59b1ecc76ce847eb20f865a473c44d467db984ab Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Fri, 6 Mar 2026 16:50:44 +0000 Subject: [PATCH 1/6] feat(poc): generate tsoa openapi spec --- .github/workflows/build.yml | 7 +++- docs/APIs-and-SDKs/Office-API/_category_.json | 6 +++ docusaurus.config.js | 8 ++++ netlify.toml | 6 +++ office-api-spec.json | 9 ++++ scripts/generate-office-api.sh | 42 +++++++++++++++++++ 6 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 docs/APIs-and-SDKs/Office-API/_category_.json create mode 100644 netlify.toml create mode 100644 office-api-spec.json create mode 100755 scripts/generate-office-api.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b78388b6..e4692dc9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,12 @@ jobs: - name: Install Deps run: yarn install --frozen-lockfile - + + - name: Generate Office API spec + run: bash scripts/generate-office-api.sh + env: + ABS_REPO_ACCESS_TOKEN: ${{ secrets.ABS_REPO_ACCESS_TOKEN }} + - name: Clean API run: yarn clean:api diff --git a/docs/APIs-and-SDKs/Office-API/_category_.json b/docs/APIs-and-SDKs/Office-API/_category_.json new file mode 100644 index 00000000..80359bde --- /dev/null +++ b/docs/APIs-and-SDKs/Office-API/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 5, + "collapsed": true, + "collapsible": true, + "label": "Office API" +} diff --git a/docusaurus.config.js b/docusaurus.config.js index 5d753f2a..408a6cd4 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -102,6 +102,14 @@ const config = { sidebarCollapsed: false, }, }, + office: { + specPath: "office-api-spec.json", + outputDir: "docs/APIs-and-SDKs/Office-API", + sidebarOptions: { + sidebarCollapsible: false, + sidebarCollapsed: false, + }, + }, }, }, ], diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..dea80940 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,6 @@ +[build] + command = "bash scripts/generate-office-api.sh && yarn clean:api && yarn gen:api && yarn build" + publish = "build" + +[build.environment] + NODE_VERSION = "24" diff --git a/office-api-spec.json b/office-api-spec.json new file mode 100644 index 00000000..01897912 --- /dev/null +++ b/office-api-spec.json @@ -0,0 +1,9 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "ABsmartly Office API", + "version": "0.0.0", + "description": "Auto-generated from the latest release branch. Run scripts/generate-office-api.sh with ABS_REPO_ACCESS_TOKEN to update locally." + }, + "paths": {} +} diff --git a/scripts/generate-office-api.sh b/scripts/generate-office-api.sh new file mode 100755 index 00000000..8117a62f --- /dev/null +++ b/scripts/generate-office-api.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOCS_DIR="$(pwd)" + +if [ -z "${ABS_REPO_ACCESS_TOKEN:-}" ]; then + echo "No ABS_REPO_ACCESS_TOKEN set, skipping Office API spec generation" + exit 0 +fi + +REPO_DIR=$(mktemp -d) +trap "rm -rf $REPO_DIR" EXIT + +echo "Cloning absmartly/abs..." +git clone --no-checkout \ + "https://x-access-token:${ABS_REPO_ACCESS_TOKEN}@github.com/absmartly/abs.git" \ + "$REPO_DIR" + +cd "$REPO_DIR" + +# TODO: Once tsoa is in a release branch, switch to auto-detecting latest release: +# LATEST=$(git branch -r \ +# | sed 's|origin/||; s/^[[:space:]]*//' \ +# | grep -E '^release/[0-9]+-[0-9]+$' \ +# | sed 's|release/||' \ +# | sort -t'-' -k1,1rn -k2,2rn \ +# | head -1) +# BRANCH="release/$LATEST" +BRANCH="vk/d96f-migrate-one-set" +echo "Using branch: $BRANCH" + +git sparse-checkout set office/backend office/shared +git checkout "$BRANCH" + +cd office/shared/lib +npm ci +cd ../../backend +npm ci --ignore-scripts +npx tsoa spec + +cp src/generated/openapi.json "${DOCS_DIR}/office-api-spec.json" +echo "Office API spec generated successfully from $BRANCH" From aa6d300054b6b5d9a68329f8ba0102602fd4ea43 Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Tue, 10 Mar 2026 08:52:32 +0000 Subject: [PATCH 2/6] feat(poc): combine openapi specs with redocly cli --- docusaurus.config.js | 10 +--------- scripts/generate-office-api.sh | 13 +++++++++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index 408a6cd4..2943a6c3 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -94,17 +94,9 @@ const config = { sidebarCollapsed: false, }, }, - nodeapi: { - specPath: "nodeapi-spec.yaml", - outputDir: "docs/APIs-and-SDKs/Web-Console-API", // Output directory for generated .mdx docs - sidebarOptions: { - sidebarCollapsible: false, - sidebarCollapsed: false, - }, - }, office: { specPath: "office-api-spec.json", - outputDir: "docs/APIs-and-SDKs/Office-API", + outputDir: "docs/APIs-and-SDKs/Web-Console-API", sidebarOptions: { sidebarCollapsible: false, sidebarCollapsed: false, diff --git a/scripts/generate-office-api.sh b/scripts/generate-office-api.sh index 8117a62f..008b3b4f 100755 --- a/scripts/generate-office-api.sh +++ b/scripts/generate-office-api.sh @@ -38,5 +38,14 @@ cd ../../backend npm ci --ignore-scripts npx tsoa spec -cp src/generated/openapi.json "${DOCS_DIR}/office-api-spec.json" -echo "Office API spec generated successfully from $BRANCH" +TSOA_SPEC="src/generated/openapi.json" + +echo "Merging tsoa spec with existing nodeapi spec..." +echo " - tsoa spec (takes priority for duplicate paths)" +echo " - nodeapi-spec.yaml (fills in remaining endpoints)" +npx -y @redocly/cli join \ + "$TSOA_SPEC" \ + "${DOCS_DIR}/nodeapi-spec.yaml" \ + -o "${DOCS_DIR}/office-api-spec.json" + +echo "Merged Office API spec generated successfully from $BRANCH" From 91a5474574e97d6aac8b51198dc61a4d4ccd6056 Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Tue, 10 Mar 2026 09:12:58 +0000 Subject: [PATCH 3/6] feat(poc): custom merge script --- scripts/generate-office-api.sh | 4 +-- scripts/merge-specs.mjs | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 scripts/merge-specs.mjs diff --git a/scripts/generate-office-api.sh b/scripts/generate-office-api.sh index 008b3b4f..87dd3581 100755 --- a/scripts/generate-office-api.sh +++ b/scripts/generate-office-api.sh @@ -43,9 +43,9 @@ TSOA_SPEC="src/generated/openapi.json" echo "Merging tsoa spec with existing nodeapi spec..." echo " - tsoa spec (takes priority for duplicate paths)" echo " - nodeapi-spec.yaml (fills in remaining endpoints)" -npx -y @redocly/cli join \ +node "${DOCS_DIR}/scripts/merge-specs.mjs" \ "$TSOA_SPEC" \ "${DOCS_DIR}/nodeapi-spec.yaml" \ - -o "${DOCS_DIR}/office-api-spec.json" + "${DOCS_DIR}/office-api-spec.json" echo "Merged Office API spec generated successfully from $BRANCH" diff --git a/scripts/merge-specs.mjs b/scripts/merge-specs.mjs new file mode 100644 index 00000000..8317e55e --- /dev/null +++ b/scripts/merge-specs.mjs @@ -0,0 +1,64 @@ +import { readFileSync, writeFileSync } from "fs"; +import yaml from "js-yaml"; + +const [primaryPath, fallbackPath, outputPath] = process.argv.slice(2); + +if (!primaryPath || !fallbackPath || !outputPath) { + console.error( + "Usage: node merge-specs.mjs " + ); + process.exit(1); +} + +function loadSpec(filePath) { + const content = readFileSync(filePath, "utf8"); + return filePath.endsWith(".yaml") || filePath.endsWith(".yml") + ? yaml.load(content) + : JSON.parse(content); +} + +const primary = loadSpec(primaryPath); +const fallback = loadSpec(fallbackPath); + +const merged = { + openapi: primary.openapi || fallback.openapi, + info: { ...fallback.info, ...primary.info }, + servers: primary.servers || fallback.servers, + security: primary.security || fallback.security, + tags: mergeTags(primary.tags, fallback.tags), + paths: { ...fallback.paths, ...primary.paths }, + components: mergeComponents(primary.components, fallback.components), +}; + +if (fallback.externalDocs || primary.externalDocs) { + merged.externalDocs = primary.externalDocs || fallback.externalDocs; +} + +function mergeTags(primaryTags = [], fallbackTags = []) { + const seen = new Set(primaryTags.map((t) => t.name)); + return [...primaryTags, ...fallbackTags.filter((t) => !seen.has(t.name))]; +} + +function mergeComponents(primary = {}, fallback = {}) { + const allKeys = new Set([ + ...Object.keys(primary), + ...Object.keys(fallback), + ]); + const merged = {}; + for (const key of allKeys) { + merged[key] = { ...fallback[key], ...primary[key] }; + } + return merged; +} + +writeFileSync(outputPath, JSON.stringify(merged, null, 2)); + +const primaryPathCount = Object.keys(primary.paths || {}).length; +const fallbackPathCount = Object.keys(fallback.paths || {}).length; +const mergedPathCount = Object.keys(merged.paths).length; +const overwritten = primaryPathCount + fallbackPathCount - mergedPathCount; + +console.log(` Primary spec: ${primaryPathCount} paths (from ${primaryPath})`); +console.log(` Fallback spec: ${fallbackPathCount} paths (from ${fallbackPath})`); +console.log(` Merged result: ${mergedPathCount} paths (${overwritten} overwritten)`); +console.log(` Output: ${outputPath}`); From 6dad576622ce865ae9b89bad45943ba66853cf3c Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Tue, 10 Mar 2026 09:33:46 +0000 Subject: [PATCH 4/6] feat(poc): update old spec to use `id` on single id paths --- nodeapi-spec.yaml | 110 +++++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/nodeapi-spec.yaml b/nodeapi-spec.yaml index 557ea8a4..1ff02e67 100644 --- a/nodeapi-spec.yaml +++ b/nodeapi-spec.yaml @@ -186,12 +186,12 @@ paths: items: $ref: "#/components/schemas/Experiment" - /experiments/{experimentId}: + /experiments/{id}: get: summary: Experiment GET description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment which you want data from. @@ -210,12 +210,12 @@ paths: experiment: $ref: "#/components/schemas/Experiment" - /experiments/{experimentId}/start: + /experiments/{id}/start: put: summary: Experiment START description: "**Required Permissions**: `Experiment Start` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment which you want to start. @@ -257,12 +257,12 @@ paths: ok: type: boolean - /experiments/{experimentId}/stop: + /experiments/{id}/stop: put: summary: Experiment STOP description: "**Required Permissions**: `Experiment Stop` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment which you want to stop. @@ -304,12 +304,12 @@ paths: ok: type: boolean - /experiments/{experimentId}/full_on: + /experiments/{id}/full_on: put: summary: Experiment FULL ON description: "**Required Permissions**: `Experiment Full On` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment which you want to put full on. @@ -351,12 +351,12 @@ paths: ok: type: boolean - /experiments/{experimentId}/archive: + /experiments/{id}/archive: put: summary: Experiment ARCHIVE description: "**Required Permissions**: `Experiment Archive` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment which you want to archive. @@ -404,12 +404,12 @@ paths: ok: type: boolean - /experiments/{experimentId}/participants/history: + /experiments/{id}/participants/history: post: summary: Experiment Participants History DATA description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment that you want the history of. @@ -492,12 +492,12 @@ paths: items: type: number - /experiments/{experimentId}/metrics/main/history: + /experiments/{id}/metrics/main/history: post: summary: Experiment Main Metrics History DATA description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment that you want the history of. @@ -593,12 +593,12 @@ paths: items: type: number - /experiments/{experimentId}/metrics/main: + /experiments/{id}/metrics/main: post: summary: Experiment Main Metrics DATA description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true description: The `id` of the experiment that you want the main metrics of. @@ -685,12 +685,12 @@ paths: items: type: number - /experiments/{experimentId}/activity: + /experiments/{id}/activity: get: summary: Experiment Activity GET description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true schema: @@ -718,7 +718,7 @@ paths: summary: Experiment Comment CREATE description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true schema: @@ -765,14 +765,14 @@ paths: summary: Experiment Comment Reply CREATE description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentId + - name: id in: path required: true schema: type: integer format: int64 minimum: 1 - - name: commentId + - name: id in: path required: true description: The `id` of the comment where the reply will be created. @@ -860,12 +860,12 @@ paths: items: $ref: "#/components/schemas/ApiKey" - /api_keys/{apiKeyId}: + /api_keys/{id}: get: summary: API Key GET description: "**Required Permissions**: `ApiKey Get` or `ApiKey Admin`" parameters: - - name: apiKeyId + - name: id in: path required: true schema: @@ -928,12 +928,12 @@ paths: items: $ref: "#/components/schemas/Application" - /applications/{applicationId}: + /applications/{id}: get: summary: Application GET description: "**Required Permissions**: `Application Get` or `Application Admin`" parameters: - - name: applicationId + - name: id in: path required: true schema: @@ -996,12 +996,12 @@ paths: items: $ref: "#/components/schemas/CORS" - /cors/{corsId}: + /cors/{id}: get: summary: CORS Allowed Origin GET description: "**Required Permissions**: `CorsAllowedOrigin Get` or `CorsAllowedOrigin Admin`" parameters: - - name: applicationId + - name: id in: path required: true schema: @@ -1064,12 +1064,12 @@ paths: items: $ref: "#/components/schemas/Environment" - /environments/{environmentId}: + /environments/{id}: get: summary: Environment GET description: "**Required Permissions**: `Environment Get` or `Environment Admin`" parameters: - - name: environmentId + - name: id in: path required: true schema: @@ -1138,12 +1138,12 @@ paths: items: $ref: "#/components/schemas/ExperimentAnnotation" - /experiment_annotations/{experimentAnnotationId}: + /experiment_annotations/{id}: get: summary: Experiment Annotation GET description: "**Required Permissions**: `Annotation Get` or `Annotation Admin`" parameters: - - name: experimentAnnotationId + - name: id in: path required: true schema: @@ -1206,12 +1206,12 @@ paths: items: $ref: "#/components/schemas/ExperimentTag" - /experiment_tags/{experimentTagId}: + /experiment_tags/{id}: get: summary: Experiment Tag GET description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" parameters: - - name: experimentTagId + - name: id in: path required: true schema: @@ -1274,12 +1274,12 @@ paths: items: $ref: "#/components/schemas/GoalTag" - /goal_tags/{goalTagId}: + /goal_tags/{id}: get: summary: Goal Tag GET description: "**Required Permissions**: `Goal Get` or `Goal Admin`" parameters: - - name: goalTagId + - name: id in: path required: true schema: @@ -1342,12 +1342,12 @@ paths: items: $ref: "#/components/schemas/Goal" - /goals/{goalId}: + /goals/{id}: get: summary: Goal GET description: "**Required Permissions**: `Goal Get` or `Goal Admin`" parameters: - - name: goalId + - name: id in: path required: true schema: @@ -1410,12 +1410,12 @@ paths: items: $ref: "#/components/schemas/MetricTag" - /metric_tags/{metricTagId}: + /metric_tags/{id}: get: summary: Metric Tag GET description: "**Required Permissions**: `Metric Get` or `Metric Admin`" parameters: - - name: metricTagId + - name: id in: path required: true schema: @@ -1496,12 +1496,12 @@ paths: items: $ref: "#/components/schemas/Metric" - /metrics/{metricId}: + /metrics/{id}: get: summary: Metric GET description: "**Required Permissions**: `Metric Get` or `Metric Admin`" parameters: - - name: metricId + - name: id in: path required: true schema: @@ -1640,12 +1640,12 @@ paths: items: $ref: "#/components/schemas/Role" - /roles/{roleId}: + /roles/{id}: get: summary: Role GET description: "**Required Permissions**: `Role Get` or `Role Admin`" parameters: - - name: roleId + - name: id in: path required: true schema: @@ -1708,12 +1708,12 @@ paths: items: $ref: "#/components/schemas/Segment" - /segments/{segmentId}: + /segments/{id}: get: summary: Segment GET description: "**Required Permissions**: `Segment Get` or `Segment Admin`" parameters: - - name: segmentId + - name: id in: path required: true schema: @@ -1776,12 +1776,12 @@ paths: items: $ref: "#/components/schemas/Team" - /teams/{teamId}: + /teams/{id}: get: summary: Team GET description: "**Required Permissions**: `Team Get` or `Team Admin`" parameters: - - name: teamId + - name: id in: path required: true schema: @@ -1844,12 +1844,12 @@ paths: items: $ref: "#/components/schemas/UnitType" - /unit_types/{unitTypeId}: + /unit_types/{id}: get: summary: Unit GET description: "**Required Permissions**: `UnitType Get` or `UnitType Admin`" parameters: - - name: unitTypeId + - name: id in: path required: true schema: @@ -1912,12 +1912,12 @@ paths: items: $ref: "#/components/schemas/User" - /users/{userId}: + /users/{id}: get: summary: User GET description: "**Required Permissions**: `User Get` or `User Admin`" parameters: - - name: userId + - name: id in: path required: true schema: @@ -1934,12 +1934,12 @@ paths: user: $ref: "#/components/schemas/User" - /users/{userId}/api_keys: + /users/{id}/api_keys: get: summary: User API keys GET description: "**Required Permissions**: `User Get` or `User Admin`" parameters: - - name: userId + - name: id in: path required: true schema: @@ -2057,12 +2057,12 @@ paths: items: $ref: "#/components/schemas/Webhook" - /webhooks/{webhookId}: + /webhooks/{id}: get: summary: Webhook GET description: "**Required Permissions**: `Webhook Get` or `Webhook Admin`" parameters: - - name: webhookId + - name: id in: path required: true schema: From 27f4a341b809d2c74b119fed8f19b8332d03fc5a Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Tue, 10 Mar 2026 10:06:28 +0000 Subject: [PATCH 5/6] fix(poc): move /v1 prefix to path level --- nodeapi-spec.yaml | 96 +++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/nodeapi-spec.yaml b/nodeapi-spec.yaml index 1ff02e67..7f60461f 100644 --- a/nodeapi-spec.yaml +++ b/nodeapi-spec.yaml @@ -12,12 +12,12 @@ externalDocs: description: Find out more about ABsmartly url: "https://www.absmartly.com" servers: - - url: https://sandbox.absmartly.com/v1 + - url: https://sandbox.absmartly.com security: - UserApiKey: [] paths: - /experiments: + /v1/experiments: get: summary: Experiments LIST description: "**Required Permissions**: `Experiment List` or `Experiment Admin`" @@ -186,7 +186,7 @@ paths: items: $ref: "#/components/schemas/Experiment" - /experiments/{id}: + /v1/experiments/{id}: get: summary: Experiment GET description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" @@ -210,7 +210,7 @@ paths: experiment: $ref: "#/components/schemas/Experiment" - /experiments/{id}/start: + /v1/experiments/{id}/start: put: summary: Experiment START description: "**Required Permissions**: `Experiment Start` or `Experiment Admin`" @@ -257,7 +257,7 @@ paths: ok: type: boolean - /experiments/{id}/stop: + /v1/experiments/{id}/stop: put: summary: Experiment STOP description: "**Required Permissions**: `Experiment Stop` or `Experiment Admin`" @@ -304,7 +304,7 @@ paths: ok: type: boolean - /experiments/{id}/full_on: + /v1/experiments/{id}/full_on: put: summary: Experiment FULL ON description: "**Required Permissions**: `Experiment Full On` or `Experiment Admin`" @@ -351,7 +351,7 @@ paths: ok: type: boolean - /experiments/{id}/archive: + /v1/experiments/{id}/archive: put: summary: Experiment ARCHIVE description: "**Required Permissions**: `Experiment Archive` or `Experiment Admin`" @@ -404,7 +404,7 @@ paths: ok: type: boolean - /experiments/{id}/participants/history: + /v1/experiments/{id}/participants/history: post: summary: Experiment Participants History DATA description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" @@ -492,7 +492,7 @@ paths: items: type: number - /experiments/{id}/metrics/main/history: + /v1/experiments/{id}/metrics/main/history: post: summary: Experiment Main Metrics History DATA description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" @@ -593,7 +593,7 @@ paths: items: type: number - /experiments/{id}/metrics/main: + /v1/experiments/{id}/metrics/main: post: summary: Experiment Main Metrics DATA description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" @@ -685,7 +685,7 @@ paths: items: type: number - /experiments/{id}/activity: + /v1/experiments/{id}/activity: get: summary: Experiment Activity GET description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" @@ -760,7 +760,7 @@ paths: items: type: string - /experiments/{experimentId}/activity/{commentId}/reply: + /v1/experiments/{experimentId}/activity/{commentId}/reply: post: summary: Experiment Comment Reply CREATE description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" @@ -814,7 +814,7 @@ paths: type: array items: type: string - /api_keys: + /v1/api_keys: get: summary: API Keys LIST description: "**Required Permissions**: `ApiKey List` or `ApiKey Admin`" @@ -860,7 +860,7 @@ paths: items: $ref: "#/components/schemas/ApiKey" - /api_keys/{id}: + /v1/api_keys/{id}: get: summary: API Key GET description: "**Required Permissions**: `ApiKey Get` or `ApiKey Admin`" @@ -882,7 +882,7 @@ paths: api_key: $ref: "#/components/schemas/ApiKey" - /applications: + /v1/applications: get: summary: Applications LIST description: "**Required Permissions**: `Application List` or `Application Admin`" @@ -928,7 +928,7 @@ paths: items: $ref: "#/components/schemas/Application" - /applications/{id}: + /v1/applications/{id}: get: summary: Application GET description: "**Required Permissions**: `Application Get` or `Application Admin`" @@ -950,7 +950,7 @@ paths: application: $ref: "#/components/schemas/Application" - /cors: + /v1/cors: get: summary: CORS Allowed Origins LIST description: "**Required Permissions**: `CorsAllowedOrigin List` or `CorsAllowedOrigin Admin`" @@ -996,7 +996,7 @@ paths: items: $ref: "#/components/schemas/CORS" - /cors/{id}: + /v1/cors/{id}: get: summary: CORS Allowed Origin GET description: "**Required Permissions**: `CorsAllowedOrigin Get` or `CorsAllowedOrigin Admin`" @@ -1018,7 +1018,7 @@ paths: cors_allowed_origin: $ref: "#/components/schemas/CORS" - /environments: + /v1/environments: get: description: "**Required Permissions**: `Environment List` or `Environment Admin`" summary: Environments LIST @@ -1064,7 +1064,7 @@ paths: items: $ref: "#/components/schemas/Environment" - /environments/{id}: + /v1/environments/{id}: get: summary: Environment GET description: "**Required Permissions**: `Environment Get` or `Environment Admin`" @@ -1086,7 +1086,7 @@ paths: environment: $ref: "#/components/schemas/Environment" - /experiment_annotations: + /v1/experiment_annotations: get: summary: Experiment Annotations LIST description: "**Required Permissions**: `Annotation List` or `Annotation Admin`" @@ -1138,7 +1138,7 @@ paths: items: $ref: "#/components/schemas/ExperimentAnnotation" - /experiment_annotations/{id}: + /v1/experiment_annotations/{id}: get: summary: Experiment Annotation GET description: "**Required Permissions**: `Annotation Get` or `Annotation Admin`" @@ -1160,7 +1160,7 @@ paths: experiment_annotation: $ref: "#/components/schemas/ExperimentAnnotation" - /experiment_tags: + /v1/experiment_tags: get: summary: Experiment Tags LIST description: "**Required Permissions**: `Experiment List` or `Experiment Admin`" @@ -1206,7 +1206,7 @@ paths: items: $ref: "#/components/schemas/ExperimentTag" - /experiment_tags/{id}: + /v1/experiment_tags/{id}: get: summary: Experiment Tag GET description: "**Required Permissions**: `Experiment Get` or `Experiment Admin`" @@ -1228,7 +1228,7 @@ paths: experiment_tag: $ref: "#/components/schemas/ExperimentTag" - /goal_tags: + /v1/goal_tags: get: summary: Goal Tags LIST description: "**Required Permissions**: `Goal List` or `Goal Admin`" @@ -1274,7 +1274,7 @@ paths: items: $ref: "#/components/schemas/GoalTag" - /goal_tags/{id}: + /v1/goal_tags/{id}: get: summary: Goal Tag GET description: "**Required Permissions**: `Goal Get` or `Goal Admin`" @@ -1296,7 +1296,7 @@ paths: goal_tag: $ref: "#/components/schemas/GoalTag" - /goals: + /v1/goals: get: summary: Goals LIST description: "**Required Permissions**: `Goal List` or `Goal Admin`" @@ -1342,7 +1342,7 @@ paths: items: $ref: "#/components/schemas/Goal" - /goals/{id}: + /v1/goals/{id}: get: summary: Goal GET description: "**Required Permissions**: `Goal Get` or `Goal Admin`" @@ -1364,7 +1364,7 @@ paths: goal: $ref: "#/components/schemas/Goal" - /metric_tags: + /v1/metric_tags: get: summary: Metric Tags LIST description: "**Required Permissions**: `Metric List` or `Metric Admin`" @@ -1410,7 +1410,7 @@ paths: items: $ref: "#/components/schemas/MetricTag" - /metric_tags/{id}: + /v1/metric_tags/{id}: get: summary: Metric Tag GET description: "**Required Permissions**: `Metric Get` or `Metric Admin`" @@ -1432,7 +1432,7 @@ paths: metric_tag: $ref: "#/components/schemas/MetricTag" - /metrics: + /v1/metrics: get: summary: Metrics LIST description: "**Required Permissions**: `Metric List` or `Metric Admin`" @@ -1496,7 +1496,7 @@ paths: items: $ref: "#/components/schemas/Metric" - /metrics/{id}: + /v1/metrics/{id}: get: summary: Metric GET description: "**Required Permissions**: `Metric Get` or `Metric Admin`" @@ -1518,7 +1518,7 @@ paths: metric: $ref: "#/components/schemas/Metric" - /permission_categories: + /v1/permission_categories: get: summary: Permission Categories LIST parameters: @@ -1556,7 +1556,7 @@ paths: items: $ref: "#/components/schemas/PermissionCategories" - /permissions: + /v1/permissions: get: summary: Permissions LIST parameters: @@ -1594,7 +1594,7 @@ paths: items: $ref: "#/components/schemas/Permission" - /roles: + /v1/roles: get: summary: Roles LIST description: "**Required Permissions**: `Role List` or `Role Admin`" @@ -1640,7 +1640,7 @@ paths: items: $ref: "#/components/schemas/Role" - /roles/{id}: + /v1/roles/{id}: get: summary: Role GET description: "**Required Permissions**: `Role Get` or `Role Admin`" @@ -1662,7 +1662,7 @@ paths: role: $ref: "#/components/schemas/Role" - /segments: + /v1/segments: get: summary: Segments LIST description: "**Required Permissions**: `Segment List` or `Segment Admin`" @@ -1708,7 +1708,7 @@ paths: items: $ref: "#/components/schemas/Segment" - /segments/{id}: + /v1/segments/{id}: get: summary: Segment GET description: "**Required Permissions**: `Segment Get` or `Segment Admin`" @@ -1730,7 +1730,7 @@ paths: segment: $ref: "#/components/schemas/Segment" - /teams: + /v1/teams: get: summary: Teams LIST description: "**Required Permissions**: `Team List` or `Team Admin`" @@ -1776,7 +1776,7 @@ paths: items: $ref: "#/components/schemas/Team" - /teams/{id}: + /v1/teams/{id}: get: summary: Team GET description: "**Required Permissions**: `Team Get` or `Team Admin`" @@ -1798,7 +1798,7 @@ paths: team: $ref: "#/components/schemas/Team" - /unit_types: + /v1/unit_types: get: summary: Units LIST description: "**Required Permissions**: `UnitType List` or `UnitType Admin`" @@ -1844,7 +1844,7 @@ paths: items: $ref: "#/components/schemas/UnitType" - /unit_types/{id}: + /v1/unit_types/{id}: get: summary: Unit GET description: "**Required Permissions**: `UnitType Get` or `UnitType Admin`" @@ -1866,7 +1866,7 @@ paths: unit_type: $ref: "#/components/schemas/UnitType" - /users: + /v1/users: get: summary: Users LIST description: "**Required Permissions**: `User List` or `User Admin`" @@ -1912,7 +1912,7 @@ paths: items: $ref: "#/components/schemas/User" - /users/{id}: + /v1/users/{id}: get: summary: User GET description: "**Required Permissions**: `User Get` or `User Admin`" @@ -1934,7 +1934,7 @@ paths: user: $ref: "#/components/schemas/User" - /users/{id}/api_keys: + /v1/users/{id}/api_keys: get: summary: User API keys GET description: "**Required Permissions**: `User Get` or `User Admin`" @@ -1965,7 +1965,7 @@ paths: items: $ref: "#/components/schemas/ApiKey" - /webhook_events: + /v1/webhook_events: get: summary: Webhook Events LIST description: "**Required Permissions**: `Webhook List` or `Webhook Admin`" @@ -2011,7 +2011,7 @@ paths: items: $ref: "#/components/schemas/WebhookEvent" - /webhooks: + /v1/webhooks: get: summary: Webhooks LIST description: "**Required Permissions**: `Webhook List` or `Webhook Admin`" @@ -2057,7 +2057,7 @@ paths: items: $ref: "#/components/schemas/Webhook" - /webhooks/{id}: + /v1/webhooks/{id}: get: summary: Webhook GET description: "**Required Permissions**: `Webhook Get` or `Webhook Admin`" From 29fcea61dd2ed90ad6da2e79369b3b95753fc24a Mon Sep 17 00:00:00 2001 From: Cal Courtney Date: Tue, 10 Mar 2026 10:13:41 +0000 Subject: [PATCH 6/6] fix(poc): attempt to fix double slashes --- scripts/merge-specs.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/merge-specs.mjs b/scripts/merge-specs.mjs index 8317e55e..a6512b78 100644 --- a/scripts/merge-specs.mjs +++ b/scripts/merge-specs.mjs @@ -23,7 +23,7 @@ const fallback = loadSpec(fallbackPath); const merged = { openapi: primary.openapi || fallback.openapi, info: { ...fallback.info, ...primary.info }, - servers: primary.servers || fallback.servers, + servers: stripTrailingSlashes(primary.servers || fallback.servers), security: primary.security || fallback.security, tags: mergeTags(primary.tags, fallback.tags), paths: { ...fallback.paths, ...primary.paths }, @@ -34,6 +34,11 @@ if (fallback.externalDocs || primary.externalDocs) { merged.externalDocs = primary.externalDocs || fallback.externalDocs; } +function stripTrailingSlashes(servers) { + if (!servers) return servers; + return servers.map((s) => ({ ...s, url: s.url.replace(/\/+$/, "") })); +} + function mergeTags(primaryTags = [], fallbackTags = []) { const seen = new Set(primaryTags.map((t) => t.name)); return [...primaryTags, ...fallbackTags.filter((t) => !seen.has(t.name))];