Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add pagination support #162

Merged
merged 17 commits into from
Feb 27, 2025
Merged
7 changes: 5 additions & 2 deletions src/lib/blueprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
getSemanticMethod,
getWorkspaceScope,
type Method,
type OpenapiAuthMethod,
} from 'lib/blueprint.js'
import type { OpenapiOperation, OpenapiSchema } from 'lib/openapi/types.js'
import type {
OpenapiAuthMethod,
OpenapiOperation,
OpenapiSchema,
} from 'lib/openapi/types.js'

test('createProperties: assigns appropriate default values', (t) => {
const minimalProperties = {
Expand Down
99 changes: 64 additions & 35 deletions src/lib/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,32 @@ import type {
import { findCommonOpenapiSchemaProperties } from './openapi/find-common-openapi-schema-properties.js'
import { flattenOpenapiSchema } from './openapi/flatten-openapi-schema.js'
import {
type AuthMethodSchema,
EventResourceSchema,
OpenapiOperationSchema,
PropertySchema,
ResourceSchema,
} from './openapi/schemas.js'
import type {
Openapi,
OpenapiAuthMethod,
OpenapiOperation,
OpenapiPathItem,
OpenapiPaths,
OpenapiSchema,
} from './openapi/types.js'
import {
mapOpenapiToSeamAuthMethod,
type SeamAuthMethod,
type SeamWorkspaceScope,
} from './seam.js'

const paginationResponseKey = 'pagination'

export interface Blueprint {
title: string
routes: Route[]
resources: Record<string, Resource>
pagination: Pagination | null
events: EventResource[]
actionAttempts: ActionAttempt[]
}
Expand Down Expand Up @@ -58,6 +66,12 @@ export interface Resource {
draftMessage: string
}

export interface Pagination {
properties: Property[]
description: string
responseKey: string
}

interface EventResource extends Resource {
resourceType: 'event'
eventType: string
Expand Down Expand Up @@ -89,20 +103,12 @@ export interface Endpoint {
draftMessage: string
request: Request
response: Response
hasPagination: boolean
codeSamples: CodeSample[]
authMethods: SeamAuthMethod[]
workspaceScope: SeamWorkspaceScope
}

export type SeamAuthMethod =
| 'api_key'
| 'personal_access_token'
| 'console_session_token'
| 'client_session_token'
| 'publishable_key'

export type SeamWorkspaceScope = 'none' | 'optional' | 'required'

interface BaseParameter {
name: string
description: string
Expand Down Expand Up @@ -315,7 +321,15 @@ export const createBlueprint = async (
}

const routes = await createRoutes(openapi.paths, context)
const resources = createResources(openapi.components.schemas, routes)

const pagination = openapi.components.schemas[paginationResponseKey]
const openapiSchemas = Object.fromEntries(
Object.entries(openapi.components.schemas).filter(
([k]) => k !== paginationResponseKey,
),
)

const resources = createResources(openapiSchemas, routes)
const actionAttempts = createActionAttempts(
openapi.components.schemas,
routes,
Expand All @@ -325,7 +339,8 @@ export const createBlueprint = async (
title: openapi.info.title,
routes,
resources,
events: createEvents(openapi.components.schemas, resources, routes),
pagination: createPagination(pagination),
events: createEvents(openapiSchemas, resources, routes),
actionAttempts,
}
}
Expand Down Expand Up @@ -560,7 +575,11 @@ const createEndpointFromOperation = async (
const draftMessage = parsedOperation['x-draft']

const request = createRequest(methods, operation, path)
const response = createResponse(operation, path, context)
const { hasPagination, ...response } = createResponse(
operation,
path,
context,
)

const operationAuthMethods = parsedOperation.security.map(
(securitySchema) => {
Expand All @@ -571,6 +590,7 @@ const createEndpointFromOperation = async (
const endpointAuthMethods = operationAuthMethods
.map(mapOpenapiToSeamAuthMethod)
.filter((authMethod): authMethod is SeamAuthMethod => authMethod != null)

const workspaceScope = getWorkspaceScope(operationAuthMethods)

const endpoint: Omit<Endpoint, 'codeSamples'> = {
Expand All @@ -586,6 +606,7 @@ const createEndpointFromOperation = async (
draftMessage,
response,
request,
hasPagination,
authMethods: endpointAuthMethods,
workspaceScope,
}
Expand All @@ -606,8 +627,6 @@ const createEndpointFromOperation = async (
}
}

export type OpenapiAuthMethod = z.infer<typeof AuthMethodSchema>

export const getWorkspaceScope = (
authMethods: OpenapiAuthMethod[],
): SeamWorkspaceScope => {
Expand Down Expand Up @@ -641,24 +660,6 @@ export const getWorkspaceScope = (
return 'none'
}

type KnownOpenapiAuthMethod = Exclude<OpenapiAuthMethod, 'unknown'>

const mapOpenapiToSeamAuthMethod = (
method: string,
): SeamAuthMethod | undefined => {
const AUTH_METHOD_MAPPING: Record<KnownOpenapiAuthMethod, SeamAuthMethod> = {
api_key: 'api_key',
pat_with_workspace: 'personal_access_token',
pat_without_workspace: 'personal_access_token',
console_session_token_with_workspace: 'console_session_token',
console_session_token_without_workspace: 'console_session_token',
client_session: 'client_session_token',
publishable_key: 'publishable_key',
} as const

return AUTH_METHOD_MAPPING[method as KnownOpenapiAuthMethod]
}

export const createRequest = (
methods: Method[],
operation: OpenapiOperation,
Expand Down Expand Up @@ -823,6 +824,19 @@ const createParameter = (
}
}

const createPagination = (
schema: Openapi['components']['schemas'][number] | undefined,
): Pagination | null => {
if (schema == null) return null
return {
responseKey: paginationResponseKey,
description: schema.description ?? '',
properties: createProperties(schema.properties ?? {}, [
paginationResponseKey,
]),
}
}

export const createResources = (
schemas: Openapi['components']['schemas'],
routes: Route[],
Expand Down Expand Up @@ -905,7 +919,7 @@ const createResponse = (
operation: OpenapiOperation,
path: string,
context: Context,
): Response => {
): Response & { hasPagination: boolean } => {
if (!('responses' in operation) || operation.responses == null) {
throw new Error(
`Missing responses in operation for ${operation.operationId}`,
Expand All @@ -920,7 +934,11 @@ const createResponse = (
const okResponse = responses['200']

if (typeof okResponse !== 'object' || okResponse == null) {
return { responseType: 'void', description: 'Unknown' }
return {
responseType: 'void',
description: 'Unknown',
hasPagination: false,
}
}

const description = okResponse.description ?? ''
Expand All @@ -935,6 +953,7 @@ const createResponse = (
return {
responseType: 'void',
description,
hasPagination: false,
}
}

Expand All @@ -947,6 +966,7 @@ const createResponse = (
return {
responseType: 'void',
description,
hasPagination: false,
}
}

Expand All @@ -956,6 +976,7 @@ const createResponse = (
return {
responseType: 'void',
description,
hasPagination: false,
}
}

Expand All @@ -964,6 +985,7 @@ const createResponse = (
return {
responseType: 'void',
description,
hasPagination: false,
}
}

Expand Down Expand Up @@ -999,6 +1021,12 @@ const createResponse = (
responseKey: refKey,
resourceType: refString?.split('/').at(-1) ?? 'unknown',
description,
hasPagination:
(paginationResponseKey in properties &&
properties[paginationResponseKey]?.$ref?.endsWith(
`/${paginationResponseKey}`,
)) ??
false,
...(actionAttemptType != null && { actionAttemptType }),
}
}
Expand All @@ -1007,6 +1035,7 @@ const createResponse = (
return {
responseType: 'void',
description: 'Unknown',
hasPagination: false,
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ export {
createBlueprint,
type Endpoint,
type Namespace,
type Pagination,
type Parameter,
type Property,
type Request,
type Resource,
type Response,
type Route,
type SeamAuthMethod,
type SeamWorkspaceScope,
type TypesModule,
type TypesModuleInput,
TypesModuleSchema,
Expand All @@ -24,3 +23,4 @@ export {
type CodeSampleSdk,
type CodeSampleSyntax,
} from './code-sample/index.js'
export { type SeamAuthMethod, type SeamWorkspaceScope } from './seam.js'
8 changes: 8 additions & 0 deletions src/lib/openapi/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { z } from 'zod'

import type { AuthMethodSchema } from './schemas.js'

export interface Openapi {
openapi: string
info: OpenapiInfo
Expand Down Expand Up @@ -98,3 +102,7 @@ export interface OpenapiComponents {
}

export type OpenapiSecurity = Record<string, string[]>

export type KnownOpenapiAuthMethod = Exclude<OpenapiAuthMethod, 'unknown'>

export type OpenapiAuthMethod = z.infer<typeof AuthMethodSchema>
26 changes: 26 additions & 0 deletions src/lib/seam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { KnownOpenapiAuthMethod } from './openapi/types.js'

export type SeamWorkspaceScope = 'none' | 'optional' | 'required'

export type SeamAuthMethod =
| 'api_key'
| 'personal_access_token'
| 'console_session_token'
| 'client_session_token'
| 'publishable_key'

export const mapOpenapiToSeamAuthMethod = (
method: string,
): SeamAuthMethod | undefined => {
const AUTH_METHOD_MAPPING: Record<KnownOpenapiAuthMethod, SeamAuthMethod> = {
api_key: 'api_key',
pat_with_workspace: 'personal_access_token',
pat_without_workspace: 'personal_access_token',
console_session_token_with_workspace: 'console_session_token',
console_session_token_without_workspace: 'console_session_token',
client_session: 'client_session_token',
publishable_key: 'publishable_key',
} as const

return AUTH_METHOD_MAPPING[method as KnownOpenapiAuthMethod]
}
4 changes: 3 additions & 1 deletion test/fixtures/types/model-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { z } from 'zod'

import type { foo } from './schemas.js'
import type { foo, pagination } from './schemas.js'

export type Foo = z.infer<typeof foo>

export type Pagination = z.infer<typeof pagination>
13 changes: 13 additions & 0 deletions test/fixtures/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export default {
tags: [{ description: 'foos', name: '/foos' }],
components: {
schemas: {
pagination: {
type: 'object',
description: 'A pagination resource.',
properties: {
has_next_page: {
description: 'If there is a next page',
type: 'boolean',
},
},
},
foo: {
type: 'object',
description: 'A foo resource.',
Expand Down Expand Up @@ -366,6 +376,9 @@ export default {
items: { $ref: '#/components/schemas/foo' },
type: 'array',
},
pagination: {
$ref: '#/components/schemas/pagination',
},
},
required: ['foos', 'ok'],
type: 'object',
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/types/route-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const routes = {
methods: ['GET', 'POST'],
jsonResponse: z.object({
foos: z.array(schemas.foo),
pagination: schemas.pagination,
}),
'/transport/air/planes/list': {
auth: 'none',
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/types/route-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export interface Routes {
object_prop?: Record<string, string>
array_prop?: string[]
}
pagination: {
has_next_page: boolean
}
}
}
'/transport/air/planes/list': {
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/types/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ export const plane = z.object({
plane_id: z.string().uuid(),
name: z.string(),
})

export const pagination = z.object({
has_next_page: z.boolean(),
})
Loading