diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index 6eb9a3f..821f9a5 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -1,5 +1,3 @@ -import { openapi } from '@seamapi/types/connect' - import type { Openapi } from './openapi.js' export interface Blueprint { @@ -134,11 +132,9 @@ export const createBlueprint = ({ resources: {}, } - // Filter for /acs/systems/list const targetPath = '/acs/systems/list' const targetSchema = 'acs_system' - // Check for fake data const isFakeData = openapi.info.title === 'Foo' for (const [path, pathItem] of Object.entries(openapi.paths)) { @@ -146,21 +142,25 @@ export const createBlueprint = ({ const route: Route = { path, - namespace: { path: '/acs' }, // Hardcoded namespace for now + namespace: { path: '/acs' }, endpoints: [], subroutes: [], } - for (const [method, operation] of Object.entries(pathItem)) { - if (!operation) continue + for (const [method, operation] of Object.entries(pathItem as Record)) { + if (typeof operation !== 'object' || operation === null) continue const endpoint: Endpoint = { - name: operation.operationId || `${method}${path.replace(/\//g, '_')}`, + name: 'operationId' in operation && typeof operation.operationId === 'string' + ? operation.operationId + : `${method}${path.replace(/\//g, '_')}`, path, methods: [method.toUpperCase() as Method], semanticMethod: method.toUpperCase() as Method, preferredMethod: method.toUpperCase() as Method, - description: operation.summary || '', + description: 'summary' in operation && typeof operation.summary === 'string' + ? operation.summary + : '', isUndocumented: false, isDeprecated: false, deprecationMessage: '', @@ -171,21 +171,23 @@ export const createBlueprint = ({ preferredMethod: method.toUpperCase() as Method, parameters: [], }, - response: createResponse(operation.responses), + response: createResponse('responses' in operation ? operation.responses : {}), } - if (operation.parameters) { + if ('parameters' in operation && Array.isArray(operation.parameters)) { for (const param of operation.parameters) { - const parameter: Parameter = { - name: param.name, - isRequired: param.required || false, - isUndocumented: false, - isDeprecated: false, - deprecationMessage: '', - description: param.description || '', + if (typeof param === 'object' && param !== null) { + const parameter: Parameter = { + name: 'name' in param && typeof param.name === 'string' ? param.name : '', + isRequired: 'required' in param && typeof param.required === 'boolean' ? param.required : false, + isUndocumented: false, + isDeprecated: false, + deprecationMessage: '', + description: 'description' in param && typeof param.description === 'string' ? param.description : '', + } + endpoint.parameters.push(parameter) + endpoint.request.parameters.push(parameter) } - endpoint.parameters.push(parameter) - endpoint.request.parameters.push(parameter) } } @@ -195,81 +197,110 @@ export const createBlueprint = ({ blueprint.routes.push(route) } - for (const [schemaName, schema] of Object.entries( - openapi.components.schemas, - )) { + for (const [schemaName, schema] of Object.entries(openapi.components.schemas)) { if (!isFakeData && schemaName !== targetSchema) continue - blueprint.resources[schemaName] = { - resourceType: schemaName, - properties: createProperties(schema.properties), + if (typeof schema === 'object' && schema !== null && 'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null) { + blueprint.resources[schemaName] = { + resourceType: schemaName, + properties: createProperties(schema.properties as Record), + } } } return blueprint } -function createResponse(responses: any): Response { - // Only checking for 200 response for now - const okResponse = responses['200'] - if (!okResponse) return { responseType: 'void', description: 'No content' } - - const schema = okResponse.content?.['application/json']?.schema - if (!schema) - return { responseType: 'void', description: okResponse.description || '' } - - if (schema.type === 'array') { - return { - responseType: 'resource_list', - responseKey: 'items', - resourceType: schema.items?.$ref?.split('/').pop() || 'unknown', - description: okResponse.description || '', - } - } else if (schema.type === 'object') { - const refKey = Object.keys(schema.properties).find( - (key) => schema.properties[key].$ref, - ) - if (refKey) { +function createResponse(responses: unknown): Response { + if (typeof responses !== 'object' || responses === null) { + return { responseType: 'void', description: 'No content' } + } + + const okResponse = (responses as Record)['200'] + if (typeof okResponse !== 'object' || okResponse === null) { + return { responseType: 'void', description: 'No content' } + } + + const content = 'content' in okResponse ? okResponse.content : null + if (typeof content !== 'object' || content === null) { + return { responseType: 'void', description: 'description' in okResponse && typeof okResponse.description === 'string' ? okResponse.description : '' } + } + + const jsonContent = 'application/json' in content ? content['application/json'] : null + if (typeof jsonContent !== 'object' || jsonContent === null) { + return { responseType: 'void', description: 'description' in okResponse && typeof okResponse.description === 'string' ? okResponse.description : '' } + } + + const schema = 'schema' in jsonContent ? jsonContent.schema : null + if (typeof schema !== 'object' || schema === null) { + return { responseType: 'void', description: 'description' in okResponse && typeof okResponse.description === 'string' ? okResponse.description : '' } + } + + if ('type' in schema && 'properties' in schema) { + if (schema.type === 'array' && 'items' in schema && typeof schema.items === 'object' && schema.items !== null) { + const refString = '$ref' in schema.items ? schema.items.$ref : null return { - responseType: 'resource', - responseKey: refKey, - resourceType: - schema.properties[refKey].$ref.split('/').pop() || 'unknown', - description: okResponse.description || '', + responseType: 'resource_list', + responseKey: 'items', + resourceType: typeof refString === 'string' && refString.length > 0 ? refString.split('/').pop() ?? 'unknown' : 'unknown', + description: 'description' in okResponse && typeof okResponse.description === 'string' ? okResponse.description : '', + } + } else if (schema.type === 'object' && typeof schema.properties === 'object' && schema.properties !== null) { + const properties = schema.properties as Record + const refKey = Object.keys(properties).find( + (key) => typeof properties[key] === 'object' && + properties[key] !== null && + '$ref' in (properties[key] as Record) + ) + if (refKey != null) { + const refString = '$ref' in (properties[refKey] as Record) + ? (properties[refKey] as Record)['$ref'] + : null + return { + responseType: 'resource', + responseKey: refKey, + resourceType: typeof refString === 'string' && refString.length > 0 ? refString.split('/').pop() ?? 'unknown' : 'unknown', + description: 'description' in okResponse && typeof okResponse.description === 'string' ? okResponse.description : '', + } } } } - return { responseType: 'void', description: okResponse.description || '' } + return { responseType: 'void', description: 'description' in okResponse && typeof okResponse.description === 'string' ? okResponse.description : '' } } -function createProperties(properties: any): Property[] { - if (!properties) return [] +function createProperties(properties: Record): Property[] { + return Object.entries(properties).map(([name, prop]): Property => { + if (typeof prop !== 'object' || prop === null) { + return { name, type: 'string', isDeprecated: false, deprecationMessage: '' } + } - return Object.entries(properties).map(([name, prop]: [string, any]) => { const baseProperty = { name, - description: prop.description || '', + description: 'description' in prop && typeof prop.description === 'string' ? prop.description : '', isDeprecated: false, deprecationMessage: '', } - switch (prop.type) { - case 'string': - return { ...baseProperty, type: 'string' } - case 'object': - return { - ...baseProperty, - type: 'object', - properties: createProperties(prop.properties), - } - case 'array': - return { ...baseProperty, type: 'list' } - default: - return { ...baseProperty, type: 'string' } + if ('type' in prop) { + switch (prop.type) { + case 'string': + return { ...baseProperty, type: 'string' } + case 'object': + return { + ...baseProperty, + type: 'object', + properties: 'properties' in prop && typeof prop.properties === 'object' && prop.properties !== null + ? createProperties(prop.properties as Record) + : [], + } + case 'array': + return { ...baseProperty, type: 'list' } + default: + return { ...baseProperty, type: 'string' } + } } - }) -} -const blueprint = createBlueprint({ openapi }) -console.log(JSON.stringify(blueprint, null, 2)) + return { ...baseProperty, type: 'string' } + }) +} \ No newline at end of file diff --git a/src/lib/openapi.ts b/src/lib/openapi.ts index 140a788..d83f9d8 100644 --- a/src/lib/openapi.ts +++ b/src/lib/openapi.ts @@ -1,5 +1,45 @@ export interface Openapi { + openapi: string info: { title: string + version: string } -} + servers: Array<{ + url: string + }> + tags: Array<{ + name: string + description: string + }> + components: { + schemas: Record + required: string[] + }> + } + paths: Record + required: string[] + type: string + } + }> + }> + summary?: string + tags?: string[] + } + }> +} \ No newline at end of file diff --git a/test/snapshots/blueprint.test.ts.md b/test/snapshots/blueprint.test.ts.md index 53fe393..8877f07 100644 --- a/test/snapshots/blueprint.test.ts.md +++ b/test/snapshots/blueprint.test.ts.md @@ -10,6 +10,92 @@ Generated by [AVA](https://avajs.dev). { name: 'Foo', - resources: {}, - routes: [], + resources: { + foo: { + properties: [ + { + deprecationMessage: '', + description: 'Foo id', + isDeprecated: false, + name: 'foo_id', + type: 'string', + }, + { + deprecationMessage: '', + description: 'Foo name', + isDeprecated: false, + name: 'name', + type: 'string', + }, + ], + resourceType: 'foo', + }, + }, + routes: [ + { + endpoints: [ + { + deprecationMessage: '', + description: '/foos/get', + isDeprecated: false, + isUndocumented: false, + methods: [ + 'GET', + ], + name: 'foosGetGet', + parameters: [], + path: '/foos/get', + preferredMethod: 'GET', + request: { + methods: [ + 'GET', + ], + parameters: [], + preferredMethod: 'GET', + semanticMethod: 'GET', + }, + response: { + description: 'OK', + resourceType: 'foo', + responseKey: 'foo', + responseType: 'resource', + }, + semanticMethod: 'GET', + }, + { + deprecationMessage: '', + description: '/foos/get', + isDeprecated: false, + isUndocumented: false, + methods: [ + 'POST', + ], + name: 'foosGetPost', + parameters: [], + path: '/foos/get', + preferredMethod: 'POST', + request: { + methods: [ + 'POST', + ], + parameters: [], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + resourceType: 'foo', + responseKey: 'foo', + responseType: 'resource', + }, + semanticMethod: 'POST', + }, + ], + namespace: { + path: '/acs', + }, + path: '/foos/get', + subroutes: [], + }, + ], } diff --git a/test/snapshots/blueprint.test.ts.snap b/test/snapshots/blueprint.test.ts.snap index 75db6a7..5b01773 100644 Binary files a/test/snapshots/blueprint.test.ts.snap and b/test/snapshots/blueprint.test.ts.snap differ diff --git a/test/snapshots/seam-blueprint.test.ts.md b/test/snapshots/seam-blueprint.test.ts.md index a6c7320..d6915af 100644 --- a/test/snapshots/seam-blueprint.test.ts.md +++ b/test/snapshots/seam-blueprint.test.ts.md @@ -10,6 +10,175 @@ Generated by [AVA](https://avajs.dev). { name: 'Seam Connect', - resources: {}, - routes: [], + resources: { + acs_system: { + properties: [ + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'acs_system_id', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'can_add_acs_users_to_acs_access_groups', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'can_automate_enrollment', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'can_create_acs_access_groups', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'can_remove_acs_users_from_acs_access_groups', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'connected_account_ids', + type: 'list', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'created_at', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'errors', + type: 'list', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'external_type', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'external_type_display_name', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'image_alt_text', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'image_url', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'name', + type: 'string', + }, + { + deprecationMessage: '', + description: `␊ + ---␊ + deprecated: use external_type␊ + ---␊ + `, + isDeprecated: false, + name: 'system_type', + type: 'string', + }, + { + deprecationMessage: '', + description: `␊ + ---␊ + deprecated: use external_type_display_name␊ + ---␊ + `, + isDeprecated: false, + name: 'system_type_display_name', + type: 'string', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'warnings', + type: 'list', + }, + { + deprecationMessage: '', + description: '', + isDeprecated: false, + name: 'workspace_id', + type: 'string', + }, + ], + resourceType: 'acs_system', + }, + }, + routes: [ + { + endpoints: [ + { + deprecationMessage: '', + description: '/acs/systems/list', + isDeprecated: false, + isUndocumented: false, + methods: [ + 'POST', + ], + name: 'acsSystemsListPost', + parameters: [], + path: '/acs/systems/list', + preferredMethod: 'POST', + request: { + methods: [ + 'POST', + ], + parameters: [], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + responseType: 'void', + }, + semanticMethod: 'POST', + }, + ], + namespace: { + path: '/acs', + }, + path: '/acs/systems/list', + subroutes: [], + }, + ], } diff --git a/test/snapshots/seam-blueprint.test.ts.snap b/test/snapshots/seam-blueprint.test.ts.snap index e9b7da9..2ae1929 100644 Binary files a/test/snapshots/seam-blueprint.test.ts.snap and b/test/snapshots/seam-blueprint.test.ts.snap differ