Skip to content

Commit

Permalink
fix: Throw if resource's route path is not defined (#150)
Browse files Browse the repository at this point in the history
* Throw if resource's route path is not defined

* ci: Format code

* Bump seam types to 1.338.1

* Make sure route path is defined where needed, update types to 1.340.1

* ci: Generate code

* Validate that x-route-path actually exists in routes

* Include resource name in missing route path error

* Bump types to 1.345.1

* ci: Generate code

* ci: Generate code

* Extract valid action attempt types into the context instead of the whole ActionAttempt array

---------

Co-authored-by: Seam Bot <[email protected]>
  • Loading branch information
andrii-balitskyi and seambot authored Feb 7, 2025
1 parent 3011d95 commit f0dbd17
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 132 deletions.
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

0 comments on commit f0dbd17

Please sign in to comment.