From dbe009ab52a9f11eaa36e3914549455ce8d10d00 Mon Sep 17 00:00:00 2001 From: Pawel Date: Wed, 3 Jul 2024 23:54:47 +0200 Subject: [PATCH] fix type errors, improve openapi type --- src/lib/blueprint.ts | 179 ++++++++++++--------- src/lib/openapi.ts | 42 ++++- test/snapshots/blueprint.test.ts.md | 90 ++++++++++- test/snapshots/blueprint.test.ts.snap | Bin 245 -> 1079 bytes test/snapshots/seam-blueprint.test.ts.md | 173 +++++++++++++++++++- test/snapshots/seam-blueprint.test.ts.snap | Bin 258 -> 1583 bytes 6 files changed, 405 insertions(+), 79 deletions(-) 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 75db6a75caafbd99f4d6c3dfcd602bce7c82edb2..5b017732caf09f82d9bb26a2bfba38395c0e89ab 100644 GIT binary patch literal 1079 zcmV-71jzeARzVB>+FB>Tne2@lA6sAuqCL`s8EPrp2at*)@IlfYlZ;^5f!0pmae>zS5 zO)|@;R;yKzq#}|*+mY06&de@M4$&bRrLTqroFU)>NugCaw0@a@j|uqU?vdXKXe-hY zYeFrhAo7H=O~g)8p0G5F^0aG8x}{EQn+dD)_CQ*Jv{pfCusBS36lX(SU#*(*qKL!~ zm|U&I&o;(hi(Sse;yTL;fDftwV$j*HjrxrOe6IjMDs|ztn(!8>*A(D}0{kI4HY7*o zmIBH5etY>nxPXp4SOWNaMLfFl~< zYrvKUT-Jb}G>NY+w}|n#2K=J|2X$ao2ZRp1rAx;Q`Iz^q4t%Ktzuk3TFn}WlaM}O{ z2Jo%{Trq&_2GB5p$4y|#lpyU1$gTg2QA=v3)rxLH!R?D3%F_l ze_9gc+(a|$l63n*mZU83vxvthnGpVZ*|v~8osfwjjgs7RJp0Eve=G{+A&A7YBUBa^ zFO~MJjbn5x-tFK@3V}h6A;t3S{OL(%3c{TF=X+2#bBjOBs>;!otlF6{Wyz)oxmTIa(%tWayg@N zt@4plzy1&P(pFbq92N}rl!85J|LcN{&@EEBHqf){-!G^B{i@kq>D#8iW&;;&;5{4o z!Uk^I(rWc4t`=RzV@wj_rBs~9*0z_>};BrZDA#=OtY}OmNi$pC~aX%QJJH%rfKtBSh0J#7@<1~ z$y)$e0@wq%0!S#BQLs%e2+W-0nMr6*63;_TD!f0~W 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 e9b7da93d4e7379df530ec45af40949da663064b..2ae192977f3efa04eb2988eac546c6ee895d4740 100644 GIT binary patch literal 1583 zcmV+~2GIFIRzVm!@y1Tl1U&d>6M9>Er0xIrX;&NRgiNqC+ zO-=R5q>}EcwyHXsAew;6MG!&oq5(y7FX-hWDxyy=D5!{vh=QmnKDb~&)E6HVMXP!y z(|u}83<}!z)4%F`|F6Ea&Z(~--aOy+>}|pBy)tyduJmkQTA^Iq9mu}#xSdl_(xvsQBgek;*z|v~gmw`9%Z5hN%FN`c@d~Xm&_jQr_6xgR4 zk{UNe7paT@LoA9-t?_+phiELJVzC%oE8-_L#PXAJmL0T%9YH92h+Hg@7h;WA42Kp9 z*eT%OLa{(oEtq;)!0Q4&7a&rwI;BvWiRh(bdoE6m*wgpDzVt&U@_sC`#!|R0-_e&V znqNSqV}*~8D9lQ?)At-V97g%w(WNL?LEo~an#8^pZdtl0ueR)9_3#aG(EGx@`) zqF^w;oUuw%gH;+FtHJRaoT-C5M~Vj8Np%@ndv6FxB^S5#<)1Z}PeClp~* z5q1^fKSlUJrI`uZk44Z*Fjs;NB?wCJU`e4B6SNmg@M;OZEkUgeCzoMUS)r8@v|VMm zqYO`!;hi#kSB7Rqp_LP~(<*RI1-4e;%?f-|fmT(aRT8w*tFWdD+p6$k6@IG12{naQ zP0+5a!F4sbqXz%0!5cOBR;ASvv`QUXby!;mS%-hs;mNu}tE;rayLI@W4(SG*+JNgC zu)6_!8t_^JK5Ia=31>Iq>L&P2MdeIlPnuF*5S5PX`_i^T$8#^0L0~N`?LH`y`Hl>1 z-|0t^zanBd!Fh}7WGB9vDjnzqQCQyJ+D=E63`cgtOc45xyYSa@;TYR;+g7L3j$9c8 z(hu697hhPmErX!F;Cq9fdnj^Vk-F4d5WcE%*`o8B+ zV&n_*rcnB>)on+{PGs;88*`!Eae{u=+R=_gCek0%=~)Z1ZFR$TC~um~hR6EG#je(|*Vr@altx z6UFJC47Yg6qugjjDwZ8=92&FE3BnED;$-rr=IA)p@~xf>W%6utuo>r=bg}Qt&C>T} z=hE1){aQAx-*|ca)Q?B4_vH-(xoA$gwY;GFGU$75ptg@0$e!hfj=iFGU8K%v!P?gN zwDYj>Y3JD5Ka@%6)Kx9m)Pn6TxTgirx0FG^;p%Kb`?3XJx1c_wh#omEIvg?@kv}Mw z@1IDYHM3G~O(E==-;%6@g#Qp?IUKqlwA54UFIi#y9;a?}{GQ(KMa`&-)U7jc`^pc(!CkhF(=Mmp5JIJ?iKe?rO&fSc>T zF3G$hOCO5}00000000A3k1=k-KoCX$*v27Jz_!c*I0Y3=$N^HMp8@95e&PpbRBNi=HM0B~~>&gr_%F&GN-r z%UVElqE7&Wee@h$>otcTb^WJO?^@~m>3BaH@BiA#v-j#u@jzLYu~8M9x*DFo0U|Tr IWEBAb07}YrWdHyG