Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mintlify-codegen/lib/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Blueprint } from '@seamapi/blueprint'
import * as types from '@seamapi/types/connect'
import type Metalsmith from 'metalsmith'

import { ObjectPageMetadataSchema } from './object-page-metadata.js'
import {
type PathMetadata,
transformSpec,
Expand All @@ -27,6 +28,7 @@ export const openapi: Metalsmith.Plugin = (files, metalsmith) => {
const metadata = metalsmith.metadata() as {
blueprint: Blueprint
pathMetadata: PathMetadata
objectPages: unknown
mintlify?: MintlifyMetadata
}

Expand All @@ -36,6 +38,7 @@ export const openapi: Metalsmith.Plugin = (files, metalsmith) => {
rawSpec,
metadata.blueprint,
metadata.pathMetadata,
ObjectPageMetadataSchema.parse(metadata.objectPages),
)

const orderedSpec = {
Expand Down
96 changes: 90 additions & 6 deletions mintlify-codegen/lib/transform-spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Blueprint, Endpoint, SdkName } from '@seamapi/blueprint'

import { supportedSdkOrder } from '../../codegen/lib/code-sample-tab-order.js'
import type { ObjectPageMetadata } from './object-page-metadata.js'

export interface PathMetadataEntry {
title: string
Expand Down Expand Up @@ -137,13 +138,78 @@ function buildEndpointMap(blueprint: Blueprint): Map<string, Endpoint> {
return map
}

/** A resource's object page: its href and the resource type it renders as
* (the noun in the page's "The <type> Object" heading). */
interface ObjectPageLink {
href: string
resourceType: string
}

/**
* Map each blueprint resource type to its object page, so endpoint responses
* that reference a shared component schema can link to the resource's object
* page. Endpoint-listing and namespace pages are not object pages, and only
* resources with samples render on a default object page (mirroring
* reference.ts). Filtered views of a shared resource (e.g. /locks over
* device) don't claim the type because resources documented at their own
* route map first.
*/
function buildObjectPageLinks(
blueprint: Blueprint,
objectPages: ObjectPageMetadata,
): Map<string, ObjectPageLink> {
const links = new Map<string, ObjectPageLink>()
const add = (resourceType: string, routePath: string): void => {
if (links.has(resourceType)) return
links.set(resourceType, {
href: `/api${routePath}/object`,
resourceType,
})
}

for (const resource of blueprint.resources) {
const meta = objectPages[resource.routePath]
if (meta == null || meta.endpoints || meta.namespace) continue
if (meta.resource_type == null && resource.resourceSamples.length === 0) {
continue
}
if (
meta.resource_type != null &&
meta.resource_type !== resource.resourceType
) {
continue
}
add(resource.resourceType, resource.routePath)
}

// Pages backed by an explicit resource_type with no blueprint resource at
// their own route (e.g. /action_attempts).
for (const [routePath, meta] of Object.entries(objectPages)) {
if (meta.endpoints || meta.namespace || meta.resource_type == null) continue
add(meta.resource_type, routePath)
}

return links
}

/** The "See [the \`device\` object](/api/devices/object)." sentence appended
* to schema descriptions. */
function objectPageSentence(link: ObjectPageLink): string {
return `See [the \`${link.resourceType}\` object](${link.href}).`
}

/**
* Create a scoped action_attempt inline schema for an endpoint.
*/
function createScopedActionAttempt(actionType: string): object {
function createScopedActionAttempt(
actionType: string,
objectPageLink?: ObjectPageLink,
): object {
return {
type: 'object',
description: `Tracks the progress of this operation. Poll using the action_attempt_id.`,
description:
`Tracks the progress of this operation. Poll using the action_attempt_id.` +
(objectPageLink ? ` ${objectPageSentence(objectPageLink)}` : ''),
properties: {
action_attempt_id: {
type: 'string',
Expand Down Expand Up @@ -345,8 +411,10 @@ export function transformSpec(
spec: any,
blueprint: Blueprint,
_pathMetadata: PathMetadata,
objectPages: ObjectPageMetadata = {},
): { spec: any; stats: TransformStats } {
const endpointMap = buildEndpointMap(blueprint)
const objectPageLinks = buildObjectPageLinks(blueprint, objectPages)
const stats: TransformStats = {
totalEndpoints: 0,
withCodeSamples: 0,
Expand Down Expand Up @@ -435,8 +503,10 @@ export function transformSpec(
(actionAttemptProp.$ref ||
(actionAttemptProp.oneOf && actionAttemptProp.oneOf.length > 3))
) {
responseSchema.properties.action_attempt =
createScopedActionAttempt(actionType)
responseSchema.properties.action_attempt = createScopedActionAttempt(
actionType,
objectPageLinks.get('action_attempt'),
)
stats.withActionAttempts++
}
}
Expand Down Expand Up @@ -483,7 +553,7 @@ export function transformSpec(
}

// Transform component schemas
transformComponents(spec)
transformComponents(spec, objectPageLinks)

// Recursively rewrite all description fields in the entire spec.
// This catches deeply nested descriptions that the per-operation and
Expand All @@ -497,7 +567,10 @@ export function transformSpec(
/**
* Clean up component schemas for Mintlify rendering.
*/
function transformComponents(spec: any): void {
function transformComponents(
spec: any,
objectPageLinks: Map<string, ObjectPageLink>,
): void {
const schemas = spec.components?.schemas
if (!schemas) return

Expand Down Expand Up @@ -562,6 +635,17 @@ function transformComponents(spec: any): void {
}
schema.required = ['action_attempt_id', 'action_type', 'status']
}

// Link every schema that backs an object page to that page, so endpoint
// responses that reference the schema point readers at the full resource
// reference. Appended after truncation so the link is never cut off.
const link = objectPageLinks.get(name)
if (link != null) {
const sentence = objectPageSentence(link)
schema.description = schema.description
? `${schema.description}\n\n${sentence}`
: sentence
}
}
}

Expand Down
Loading
Loading