[Proposal] Publishing APIs from Platform API to API Portal #3242
Pranavan-S
started this conversation in
Ideas
Replies: 2 comments 2 replies
UpdateAdding REST Design19 endpoints in total(16 new, 3 endpoints coming from documentation feature):
Refer the trimmed version of the API contract below (tags, long descriptions, detailed component schema are stripped out to keep it short): paths:
/apis/{apiType}/{apiId}/definition:
parameters:
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiDefinition
summary: Get API definition
security:
- OAuth2Security: [ap:api:read]
responses:
'200': { $ref: '#/components/responses/DefinitionResponse' }
'404': { $ref: '#/components/responses/NotFound' }
/apis/{apiType}/{apiId}/thumbnail:
parameters:
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiThumbnail
summary: Get API thumbnail
security:
- OAuth2Security: [ap:api:read]
responses:
'200': { $ref: '#/components/responses/ThumbnailImageResponse' }
'404': { $ref: '#/components/responses/NotFound' }
/apis/{apiType}/{apiId}/docs:
parameters:
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: listApiDocs
summary: List API documents
security:
- OAuth2Security: [ap:api:read]
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/DocListResponse' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'404': { $ref: '#/components/responses/NotFound' }
# --- Publications — standalone, not nested under /apis or /api-portals -----
/api-publications:
get:
operationId: listApiPublications
summary: List API publications across API Portals
parameters:
- $ref: '#/components/parameters/apiType-Q'
- $ref: '#/components/parameters/apiHandle-Q'
security:
- OAuth2Security: [ap:api_publication:read]
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/PublicationSummaryResponse' } } } }
'400': { $ref: '#/components/responses/BadRequest' }
'404': { $ref: '#/components/responses/NotFound' }
# --- The live publication -----------------------------------------------------
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/publication:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublication
summary: Get API publication
security:
- OAuth2Security: [ap:api_portal:publication:read]
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/Publication' } } } }
'404': { $ref: '#/components/responses/NotFound' }
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/publication/definition:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublicationDefinition
summary: Get published definition
security:
- OAuth2Security: [ap:api_portal:publication:read]
responses:
'200': { $ref: '#/components/responses/DefinitionResponse' }
'404': { $ref: '#/components/responses/NotFound' }
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/publication/landing-page:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublicationLandingPage
summary: Get published landing page
security:
- OAuth2Security: [ap:api_portal:publication:read]
responses:
'200': { $ref: '#/components/responses/LandingPageResponse' }
'404': { $ref: '#/components/responses/NotFound' }
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/publication/thumbnail:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublicationThumbnail
summary: Get published thumbnail
security:
- OAuth2Security: [ap:api_portal:publication:read]
responses:
'200': { $ref: '#/components/responses/ThumbnailImageResponse' }
'404': { $ref: '#/components/responses/NotFound' }
# --- The per-portal draft ------------------------------------------------------
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/draft:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublicationDraft
summary: Get publication draft
security:
- OAuth2Security: [ap:api_portal:draft:read]
- OAuth2Security: [ap:api_portal:draft:manage]
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/PublicationDraftDetails' } } } }
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: saveApiPublicationDraft
summary: Save publication draft (creates the draft on first save)
security:
- OAuth2Security: [ap:api_portal:draft:update]
- OAuth2Security: [ap:api_portal:draft:manage]
requestBody:
content: { application/json: { schema: { $ref: '#/components/schemas/PublicationDraftDetailsInput' } } }
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/PublicationDraftDetails' } } } }
'400': { $ref: '#/components/responses/PublicationBadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/draft/definition:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublicationDraftDefinition
summary: Get draft definition
security:
- OAuth2Security: [ap:api_portal:draft:read]
- OAuth2Security: [ap:api_portal:draft:manage]
responses:
'200': { $ref: '#/components/responses/DefinitionResponse' }
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: saveApiPublicationDraftDefinition
summary: Save draft definition (requires the draft to exist)
security:
- OAuth2Security: [ap:api_portal:draft:update]
- OAuth2Security: [ap:api_portal:draft:manage]
requestBody: { $ref: '#/components/requestBodies/DefinitionUpload' }
responses:
'204': { description: Definition saved }
'400': { $ref: '#/components/responses/PublicationBadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/draft/landing-page:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublicationDraftLandingPage
summary: Get draft landing page
security:
- OAuth2Security: [ap:api_portal:draft:read]
- OAuth2Security: [ap:api_portal:draft:manage]
responses:
'200': { $ref: '#/components/responses/LandingPageResponse' }
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: saveApiPublicationDraftLandingPage
summary: Save draft landing page (Markdown; embedded raw HTML is stripped)
security:
- OAuth2Security: [ap:api_portal:draft:update]
- OAuth2Security: [ap:api_portal:draft:manage]
requestBody: { $ref: '#/components/requestBodies/LandingPageUpload' }
responses:
'204': { description: Landing page saved }
'400': { $ref: '#/components/responses/PublicationBadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
/api-portals/{apiPortalId}/apis/{apiType}/{apiId}/draft/thumbnail:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiType'
- $ref: '#/components/parameters/apiHandle'
get:
operationId: getApiPublicationDraftThumbnail
summary: Get draft thumbnail
security:
- OAuth2Security: [ap:api_portal:draft:read]
- OAuth2Security: [ap:api_portal:draft:manage]
responses:
'200': { $ref: '#/components/responses/ThumbnailImageResponse' }
'404': { $ref: '#/components/responses/NotFound' }
put:
operationId: saveApiPublicationDraftThumbnail
summary: Upload draft thumbnail (PNG/JPEG, sniffed from bytes — not the declared type or file name)
security:
- OAuth2Security: [ap:api_portal:draft:update]
- OAuth2Security: [ap:api_portal:draft:manage]
requestBody:
content: { multipart/form-data: { schema: { type: object, required: [file], properties: { file: { type: string, format: binary } } } } }
responses:
'204': { description: Thumbnail saved }
'400': { $ref: '#/components/responses/PublicationBadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
# --- Lifecycle actions: per-type, portal-first (type pinned as a literal apiType value) --
/api-portals/{apiPortalId}/apis/rest-api/{apiId}/publish:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiHandle'
post:
operationId: publishRestApiToApiPortal
summary: Publish the current draft (content is pushed to the portal before anything is written locally)
security:
- OAuth2Security: [ap:api_portal:rest_api:publish]
- OAuth2Security: [ap:api_portal:rest_api:manage]
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/Publication' } } } }
'201':
description: Listing created
headers: { Location: { $ref: '#/components/headers/Location' } }
content: { application/json: { schema: { $ref: '#/components/schemas/Publication' } } }
'400': { $ref: '#/components/responses/PublicationTypeUnsupported' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { $ref: '#/components/responses/PublicationConflict' }
'503': { $ref: '#/components/responses/PortalUnavailable' }
/api-portals/{apiPortalId}/apis/rest-api/{apiId}/unpublish:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiHandle'
post:
operationId: unpublishRestApiFromApiPortal
summary: Remove the listing, then delete the publication and any draft (valid only when published or deprecated)
security:
- OAuth2Security: [ap:api_portal:rest_api:unpublish]
- OAuth2Security: [ap:api_portal:rest_api:manage]
responses:
'204': { description: Listing removed }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { $ref: '#/components/responses/PublicationConflict' }
'503': { $ref: '#/components/responses/PortalUnavailable' }
/api-portals/{apiPortalId}/apis/rest-api/{apiId}/deprecate:
parameters:
- $ref: '#/components/parameters/apiPortalId'
- $ref: '#/components/parameters/apiHandle'
post:
operationId: deprecateRestApiOnApiPortal
summary: Mark the live listing deprecated — still visible, closed to new subscriptions (valid only when published)
security:
- OAuth2Security: [ap:api_portal:rest_api:deprecate]
- OAuth2Security: [ap:api_portal:rest_api:manage]
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/Publication' } } } }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { $ref: '#/components/responses/PublicationConflict' }
'503': { $ref: '#/components/responses/PortalUnavailable' } |
0 replies
UpdateAdding DB Design9 Tables in total (8 new tables, Table 0 is from documentation feature):
-- 0. PLACEHOLDER — pre-existing, out of scope for this design. Stores each
-- API's own content (definition, thumbnail, uploaded documents), one row
-- per content item. Shown only because tables 5/6 below take an FK
-- against it: a publication/draft doesn't copy a document, it points at
-- the row already living here.
CREATE TABLE artifact_documents ( uuid VARCHAR(40) PRIMARY KEY, organization_uuid VARCHAR(40) NOT NULL, /* ... */ );
-- 1. The live listing for one (API, portal) pairing
CREATE TABLE IF NOT EXISTS api_publications (
uuid VARCHAR(40) PRIMARY KEY,
organization_uuid VARCHAR(40) NOT NULL,
artifact_uuid VARCHAR(40) NOT NULL,
api_portal_uuid VARCHAR(40) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED', -- PUBLISHED | DEPRECATED
display_name VARCHAR(255) NOT NULL,
version VARCHAR(30) NOT NULL DEFAULT 'v1.0',
description VARCHAR(1023),
tags BYTEA, -- serialized string array; no tag catalog exists
labels BYTEA, -- serialized array of API Portal label handles
agent_visibility VARCHAR(20) NOT NULL DEFAULT 'VISIBLE', -- VISIBLE | HIDDEN
production_url VARCHAR(255),
sandbox_url VARCHAR(255),
business_owner VARCHAR(255), -- author-entered; omitting these nulls the portal's own columns
business_owner_email VARCHAR(255),
technical_owner VARCHAR(255),
technical_owner_email VARCHAR(255),
data_version VARCHAR(20) NOT NULL DEFAULT '1.0',
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by VARCHAR(200) NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (organization_uuid, artifact_uuid, api_portal_uuid),
UNIQUE (organization_uuid, uuid),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (artifact_uuid, organization_uuid)
REFERENCES artifacts(uuid, organization_uuid) ON DELETE CASCADE,
FOREIGN KEY (api_portal_uuid, organization_uuid)
REFERENCES api_portals(uuid, organization_uuid) ON DELETE CASCADE
);
-- 2. Draft counterpart of #1 (same shape, no status column)
CREATE TABLE IF NOT EXISTS api_publication_drafts (
uuid VARCHAR(40) PRIMARY KEY,
organization_uuid VARCHAR(40) NOT NULL,
artifact_uuid VARCHAR(40) NOT NULL,
api_portal_uuid VARCHAR(40) NOT NULL,
display_name VARCHAR(255) NOT NULL,
version VARCHAR(30) NOT NULL DEFAULT 'v1.0',
description VARCHAR(1023),
tags BYTEA,
labels BYTEA,
agent_visibility VARCHAR(20) NOT NULL DEFAULT 'VISIBLE',
production_url VARCHAR(255),
sandbox_url VARCHAR(255),
business_owner VARCHAR(255),
business_owner_email VARCHAR(255),
technical_owner VARCHAR(255),
technical_owner_email VARCHAR(255),
data_version VARCHAR(20) NOT NULL DEFAULT '1.0',
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by VARCHAR(200) NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (organization_uuid, artifact_uuid, api_portal_uuid),
UNIQUE (organization_uuid, uuid),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (artifact_uuid, organization_uuid)
REFERENCES artifacts(uuid, organization_uuid) ON DELETE CASCADE,
FOREIGN KEY (api_portal_uuid, organization_uuid)
REFERENCES api_portals(uuid, organization_uuid) ON DELETE CASCADE
);
-- 3. Published definition / landing page / thumbnail. Written in the same
-- transaction as the publication row.
CREATE TABLE IF NOT EXISTS api_publication_contents (
uuid VARCHAR(40) PRIMARY KEY,
organization_uuid VARCHAR(40) NOT NULL,
publication_uuid VARCHAR(40) NOT NULL,
type VARCHAR(20) NOT NULL, -- IMAGE | API_DEFINITION | MARKETING
file_name VARCHAR(255), -- API_DEFINITION: canonical name recording the serialization.
-- IMAGE: the uploader's own file name. Null for MARKETING.
content BYTEA NOT NULL,
data_version VARCHAR(20) NOT NULL DEFAULT '1.0',
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by VARCHAR(200) NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (organization_uuid, publication_uuid, type),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (publication_uuid, organization_uuid)
REFERENCES api_publications(uuid, organization_uuid) ON DELETE CASCADE
);
-- 4. Draft counterpart of #3
CREATE TABLE IF NOT EXISTS api_publication_draft_contents (
uuid VARCHAR(40) PRIMARY KEY,
organization_uuid VARCHAR(40) NOT NULL,
draft_uuid VARCHAR(40) NOT NULL,
type VARCHAR(20) NOT NULL, -- IMAGE | API_DEFINITION | MARKETING
file_name VARCHAR(255),
content BYTEA NOT NULL,
data_version VARCHAR(20) NOT NULL DEFAULT '1.0',
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by VARCHAR(200) NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (organization_uuid, draft_uuid, type),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (draft_uuid, organization_uuid)
REFERENCES api_publication_drafts(uuid, organization_uuid) ON DELETE CASCADE
);
-- 5. Documents selected in the live listing (pure junction table)
CREATE TABLE IF NOT EXISTS api_publication_doc_mappings (
organization_uuid VARCHAR(40) NOT NULL,
publication_uuid VARCHAR(40) NOT NULL,
doc_uuid VARCHAR(40) NOT NULL,
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (organization_uuid, publication_uuid, doc_uuid),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (publication_uuid, organization_uuid)
REFERENCES api_publications(uuid, organization_uuid) ON DELETE CASCADE,
FOREIGN KEY (doc_uuid, organization_uuid) -- CASCADE: a document deleted at its source is
REFERENCES artifact_documents(uuid, organization_uuid) ON DELETE CASCADE -- withdrawn here, never blocked
);
-- 6. Draft counterpart of #5
CREATE TABLE IF NOT EXISTS api_publication_draft_doc_mappings (
organization_uuid VARCHAR(40) NOT NULL,
draft_uuid VARCHAR(40) NOT NULL,
doc_uuid VARCHAR(40) NOT NULL,
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (organization_uuid, draft_uuid, doc_uuid),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (draft_uuid, organization_uuid)
REFERENCES api_publication_drafts(uuid, organization_uuid) ON DELETE CASCADE,
FOREIGN KEY (doc_uuid, organization_uuid)
REFERENCES artifact_documents(uuid, organization_uuid) ON DELETE CASCADE
);
-- 7. Plans selected in the live listing. subscription_plans has no
-- UNIQUE(organization_uuid, uuid), so this FK is single-column; the org
-- match is checked in the service layer.
CREATE TABLE IF NOT EXISTS api_publication_plan_mappings (
organization_uuid VARCHAR(40) NOT NULL,
publication_uuid VARCHAR(40) NOT NULL,
subscription_plan_uuid VARCHAR(40) NOT NULL,
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (organization_uuid, publication_uuid, subscription_plan_uuid),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (publication_uuid, organization_uuid)
REFERENCES api_publications(uuid, organization_uuid) ON DELETE CASCADE,
FOREIGN KEY (subscription_plan_uuid) -- RESTRICT: a plan with live subscribers can't
REFERENCES subscription_plans(uuid) ON DELETE RESTRICT -- silently vanish from a published listing
);
-- 8. Draft counterpart of #7
CREATE TABLE IF NOT EXISTS api_publication_draft_plan_mappings (
organization_uuid VARCHAR(40) NOT NULL,
draft_uuid VARCHAR(40) NOT NULL,
subscription_plan_uuid VARCHAR(40) NOT NULL,
created_by VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (organization_uuid, draft_uuid, subscription_plan_uuid),
FOREIGN KEY (organization_uuid) REFERENCES organizations(uuid) ON DELETE CASCADE,
FOREIGN KEY (draft_uuid, organization_uuid)
REFERENCES api_publication_drafts(uuid, organization_uuid) ON DELETE CASCADE,
FOREIGN KEY (subscription_plan_uuid)
REFERENCES subscription_plans(uuid) ON DELETE RESTRICT
); |
2 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Problem Overview
Feature Ticket: #429
Design Document: Doc Link
What is the problem?
An organization builds and manages its APIs in the Management Console (backed by platform-api), but currently there is no functionality provided from the platform-api to publish the API to the API Portal.
Why now?
This is a foundational gap, as every other portal capability (search, docs, try out, subscriptions) depends on APIs actually getting published first, so api-portal can't fully serve its purpose until this exists.
Audience
We're primarily solving this for API providers who want to publish their APIs to one or more API Portals and manage that from one place seamlessly.
Problem Breakdown
Proposed Solution
Overview
From the high-level view, this solution packs the following:
This design lets an API Provider/Publisher to:
Open for discussion
All reactions