Skip to content
Merged
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@seamapi/types": "1.345.0",
"@seamapi/types": "1.345.1",
"@types/node": "^20.8.10",
"ava": "^6.0.1",
"c8": "^10.1.2",
Expand Down
90 changes: 74 additions & 16 deletions src/lib/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'

interface Context extends Required<BlueprintOptions> {
codeSampleDefinitions: CodeSampleDefinition[]
actionAttempts: ActionAttempt[]
validActionAttemptTypes: string[]
}

export const TypesModuleSchema = z.object({
Expand All @@ -303,24 +303,56 @@ export const createBlueprint = async (
// TODO: Move openapi to TypesModuleSchema
const openapi = typesModule.openapi as Openapi

const resources = createResources(openapi.components.schemas)
const actionAttempts = createActionAttempts(openapi.components.schemas)
const validActionAttemptTypes = extractValidActionAttemptTypes(
openapi.components.schemas,
)

const context = {
const context: Context = {
codeSampleDefinitions,
formatCode,
actionAttempts,
validActionAttemptTypes,
}

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

return {
title: openapi.info.title,
routes: await createRoutes(openapi.paths, context),
routes,
resources,
events: createEvents(openapi.components.schemas, resources),
events: createEvents(openapi.components.schemas, resources, routes),
actionAttempts,
}
}

const extractValidActionAttemptTypes = (
schemas: Openapi['components']['schemas'],
): string[] => {
const actionAttemptSchema = schemas['action_attempt']
if (
actionAttemptSchema == null ||
typeof actionAttemptSchema !== 'object' ||
!('oneOf' in actionAttemptSchema) ||
!Array.isArray(actionAttemptSchema.oneOf)
) {
return []
}

const processedActionAttemptTypes = new Set<string>()
actionAttemptSchema.oneOf.forEach((schema) => {
const actionType = schema.properties?.action_type?.enum?.[0]
if (typeof actionType === 'string') {
processedActionAttemptTypes.add(actionType)
}
})

return Array.from(processedActionAttemptTypes)
}

const createRoutes = async (
paths: OpenapiPaths,
context: Context,
Expand Down Expand Up @@ -792,6 +824,7 @@ const createParameter = (

export const createResources = (
schemas: Openapi['components']['schemas'],
routes: Route[],
): Record<string, Resource> => {
return Object.entries(schemas).reduce<Record<string, Resource>>(
(resources, [schemaName, schema]) => {
Expand All @@ -802,12 +835,13 @@ export const createResources = (
parsedEvent.oneOf,
)
const eventSchema: OpenapiSchema = {
'x-route-path': parsedEvent['x-route-path'],
properties: commonProperties,
type: 'object',
}
return {
...resources,
[schemaName]: createResource(schemaName, eventSchema),
[schemaName]: createResource(schemaName, eventSchema, routes),
}
}

Expand All @@ -816,7 +850,7 @@ export const createResources = (
if (isValidResourceSchema) {
return {
...resources,
[schemaName]: createResource(schemaName, schema),
[schemaName]: createResource(schemaName, schema, routes),
}
}

Expand All @@ -829,13 +863,20 @@ export const createResources = (
const createResource = (
schemaName: string,
schema: OpenapiSchema,
routes: Route[],
): Resource => {
const routePath = validateRoutePath(
schemaName,
schema['x-route-path'],
routes,
)

return {
resourceType: schemaName,
properties: createProperties(schema.properties ?? {}, [schemaName]),
description: schema.description ?? '',
isDeprecated: schema.deprecated ?? false,
routePath: schema['x-route-path'] ?? '',
routePath,
deprecationMessage: schema['x-deprecated'] ?? '',
isUndocumented: (schema['x-undocumented'] ?? '').length > 0,
undocumentedMessage: schema['x-undocumented'] ?? '',
Expand All @@ -844,6 +885,21 @@ const createResource = (
}
}

const validateRoutePath = (
resourceName: string,
routePath: string | undefined,
routes: Route[],
): string => {
if (routePath == null || routePath.length === 0) {
throw new Error(`Resource ${resourceName} is missing a route path`)
}
if (!routes.some((r) => r.path === routePath)) {
throw new Error(`Route path ${routePath} not found in routes`)
}

return routePath
}

const createResponse = (
operation: OpenapiOperation,
path: string,
Expand Down Expand Up @@ -929,7 +985,7 @@ const createResponse = (
parsedOperation['x-action-attempt-type'],
responseKey,
path,
context,
context.validActionAttemptTypes,
)
const refKey = responseKey

Expand Down Expand Up @@ -957,7 +1013,7 @@ const validateActionAttemptType = (
actionAttemptType: string | undefined,
responseKey: string,
path: string,
context: Context,
validActionAttemptTypes: string[],
): string | undefined => {
const excludedPaths = ['/action_attempts']
const isPathExcluded = excludedPaths.some((p) => path.startsWith(p))
Expand All @@ -972,9 +1028,7 @@ const validateActionAttemptType = (

if (
actionAttemptType != null &&
!context.actionAttempts.some(
(attempt) => attempt.actionAttemptType === actionAttemptType,
)
!validActionAttemptTypes.includes(actionAttemptType)
) {
throw new Error(
`Invalid action_attempt_type '${actionAttemptType}' for path ${path}`,
Expand Down Expand Up @@ -1132,6 +1186,7 @@ export const getPreferredMethod = (
const createEvents = (
schemas: Openapi['components']['schemas'],
resources: Record<string, Resource>,
routes: Route[],
): EventResource[] => {
const eventSchema = schemas['event']
if (
Expand All @@ -1158,7 +1213,7 @@ const createEvents = (
)

return {
...createResource('event', schema as OpenapiSchema),
...createResource('event', schema as OpenapiSchema, routes),
eventType,
targetResourceType: targetResourceType ?? null,
}
Expand All @@ -1168,6 +1223,7 @@ const createEvents = (

const createActionAttempts = (
schemas: Openapi['components']['schemas'],
routes: Route[],
): ActionAttempt[] => {
const actionAttemptSchema = schemas['action_attempt']
if (
Expand Down Expand Up @@ -1198,6 +1254,7 @@ const createActionAttempts = (
processedActionTypes.add(actionType)

const schemaWithStandardStatus: OpenapiSchema = {
'x-route-path': actionAttemptSchema['x-route-path'],
...schema,
properties: {
...schema.properties,
Expand All @@ -1212,6 +1269,7 @@ const createActionAttempts = (
const resource = createResource(
'action_attempt',
schemaWithStandardStatus,
routes,
)

return {
Expand Down
1 change: 1 addition & 0 deletions src/lib/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const ResourceSchema = z.object({
})

export const EventResourceSchema = z.object({
'x-route-path': z.string().default(''),
discriminator: z.object({ propertyName: z.string() }),
oneOf: z.array(ResourceSchema),
})
45 changes: 41 additions & 4 deletions test/fixtures/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default {
},
},
required: ['plane_id', 'name'],
'x-route-path': '/planes',
'x-route-path': '/transport/air/planes',
},
deprecated_resource: {
type: 'object',
Expand All @@ -99,7 +99,7 @@ export default {
required: ['deprecated_resource_id'],
deprecated: true,
'x-deprecated': 'This resource is deprecated',
'x-route-path': '/deprecated/resources',
'x-route-path': '/deprecated/undocumented',
},
draft_resource: {
type: 'object',
Expand All @@ -113,7 +113,7 @@ export default {
},
required: ['draft_resource_id'],
'x-draft': 'This resource is draft',
'x-route-path': '/draft/resources',
'x-route-path': '/draft',
},
undocumented_resource: {
type: 'object',
Expand All @@ -127,9 +127,10 @@ export default {
},
required: ['undocumented_resource_id'],
'x-undocumented': 'This resource is undocumented',
'x-route-path': '/undocumented/resources',
'x-route-path': '/deprecated/undocumented',
},
event: {
'x-route-path': '/events',
oneOf: [
{
type: 'object',
Expand Down Expand Up @@ -162,6 +163,7 @@ export default {
],
},
action_attempt: {
'x-route-path': '/action_attempts',
oneOf: [
{
type: 'object',
Expand Down Expand Up @@ -506,5 +508,40 @@ export default {
'x-title': 'Draft endpoint',
},
},
'/action_attempts/get': {
post: {
operationId: 'actionAttemptsGetPost',
responses: {
200: {
content: {
'application/json': {
schema: {
properties: {
ok: { type: 'boolean' },
action_attempt: {
$ref: '#/components/schemas/action_attempt',
},
},
required: ['action_attempt', 'ok'],
type: 'object',
},
},
},
description: 'Get an action attempt.',
},
400: { description: 'Bad Request' },
401: { description: 'Unauthorized' },
},
security: [
{
api_key: [],
},
],
summary: '/action_attempts/get',
tags: ['/action_attempts'],
'x-response-key': 'action_attempt',
'x-title': 'Get an action attempt',
},
},
},
}
Loading