From 64cbf3e8e70690e66a7a062cd7891b5ec66e8ee3 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Thu, 28 Sep 2023 14:42:55 -0700 Subject: [PATCH] Use openapi spec to find return resource --- generate-routes.ts | 77 ++++++++----------- package-lock.json | 17 ---- package.json | 2 - .../seam/connect/routes/acs-access-groups.ts | 7 +- src/lib/seam/connect/routes/locks.ts | 8 +- 5 files changed, 40 insertions(+), 71 deletions(-) diff --git a/generate-routes.ts b/generate-routes.ts index daacd623..81f0c551 100644 --- a/generate-routes.ts +++ b/generate-routes.ts @@ -4,7 +4,6 @@ import { resolve } from 'node:path' import { openapi } from '@seamapi/types/connect' import { camelCase, paramCase, pascalCase, snakeCase } from 'change-case' import { ESLint } from 'eslint' -import pluralize from 'pluralize' import { format, resolveConfig } from 'prettier' const rootClassPath = resolve('src', 'lib', 'seam', 'connect', 'client.ts') @@ -53,24 +52,14 @@ const endpointResources: Partial< | null > > = { - '/access_codes/generate_code': 'generated_code', - '/access_codes/create_multiple': 'access_codes', - '/access_codes/pull_backup_access_code': 'backup_access_code', - '/access_codes/unmanaged/convert_to_managed': null, - '/acs/access_groups/list_users': 'acs_users', - '/acs/access_groups/remove_user': null, - '/acs/users/add_to_access_group': null, - '/acs/users/remove_from_access_group': null, + '/access_codes/delete': null, + '/access_codes/unmanaged/delete': null, + '/access_codes/update': null, '/connect_webviews/view': null, - '/devices/list_device_providers': 'device_providers', - '/locks/lock_door': 'action_attempt', - '/locks/unlock_door': 'action_attempt', - '/noise_sensors/noise_thresholds/create': null, // could return action action_attempt - '/thermostats/cool': null, - '/thermostats/heat': null, - '/thermostats/heat_cool': null, - '/thermostats/off': null, - '/thermostats/set_fan_mode': null, + '/noise_sensors/noise_thresholds/create': null, + '/noise_sensors/noise_thresholds/delete': null, + '/noise_sensors/noise_thresholds/update': null, + '/thermostats/climate_setting_schedules/update': null, '/workspaces/reset_sandbox': null, } @@ -88,7 +77,7 @@ interface Endpoint { requestFormat: 'params' | 'body' } -type Method = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH' +type Method = 'GET' | 'POST' interface ClassMeta { constructors: string @@ -150,46 +139,48 @@ const createEndpoint = ( namespace, path: endpointPath, method, - resource: deriveResource(endpointPath, routePath, name, method), + resource: deriveResource(endpointPath, method), requestFormat: ['GET', 'DELETE'].includes(method) ? 'params' : 'body', } } const deriveResource = ( endpointPath: string, - routePath: string, - name: string, method: Method, ): string | null => { if (isEndpointResource(endpointPath)) { return endpointResources[endpointPath] ?? null } - if (['DELETE', 'PATCH', 'PUT'].includes(method)) return null - if (['update', 'delete'].includes(name)) return null - const group = deriveGroupFromRoutePath(routePath) - if (group == null) throw new Error(`Could not parse group from ${routePath}`) - if (name === 'list') return group - return pluralize.singular(group) -} - -const deriveGroupFromRoutePath = (routePath: string): string | undefined => { - const parts = routePath.split('/').slice(1) - - if (routePath.endsWith('/unmanaged')) { - return parts[0] - } - - if (routePath.startsWith('/acs')) { - return [parts[0], parts[1]].join('_') - } - if (parts.length === 2) { - return parts[1] + if (isOpenApiPath(endpointPath)) { + const spec = openapi.paths[endpointPath] + const methodKey = method.toLowerCase() + + if (methodKey === 'post' && 'post' in spec) { + const response = spec.post.responses[200] + if (!('content' in response)) return null + return deriveResourceFromSchema( + response.content['application/json']?.schema?.properties ?? {}, + ) + } + + if (methodKey === 'get' && 'get' in spec) { + const response = spec.get.responses[200] + if (!('content' in response)) { + throw new Error(`Missing resource for ${method} ${endpointPath}`) + } + return deriveResourceFromSchema( + response.content['application/json']?.schema?.properties ?? {}, + ) + } } - return parts[0] + throw new Error(`Could not derive resource for ${method} ${endpointPath}`) } +const deriveResourceFromSchema = (properties: object): string | null => + Object.keys(properties).filter((key) => key !== 'ok')[0] ?? null + const deriveSemanticMethod = (methods: string[]): Method => { if (methods.includes('get')) return 'GET' if (methods.includes('post')) return 'POST' diff --git a/package-lock.json b/package-lock.json index 58ac13cb..a4be6bf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@seamapi/types": "^1.14.0", "@types/eslint": "^8.44.2", "@types/node": "^18.11.18", - "@types/pluralize": "^0.0.31", "ava": "^5.0.1", "c8": "^8.0.0", "change-case": "^4.1.2", @@ -28,7 +27,6 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^3.0.0", "landlubber": "^1.0.0", - "pluralize": "^8.0.0", "prettier": "^3.0.0", "tsc-alias": "^1.8.2", "tsup": "^7.2.0", @@ -860,12 +858,6 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, - "node_modules/@types/pluralize": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.31.tgz", - "integrity": "sha512-MQh69PPwFlYAL2qz/Mw5Zc34VTdt7pTck0Xbb6pbPSzdt5oaLB87iyJJxEMS5Dco/s7lXHunEezAvQurZZdrsQ==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.5.2", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", @@ -5450,15 +5442,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-load-config": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", diff --git a/package.json b/package.json index cbd9e751..9de30067 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "@seamapi/types": "^1.14.0", "@types/eslint": "^8.44.2", "@types/node": "^18.11.18", - "@types/pluralize": "^0.0.31", "ava": "^5.0.1", "c8": "^8.0.0", "change-case": "^4.1.2", @@ -103,7 +102,6 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^3.0.0", "landlubber": "^1.0.0", - "pluralize": "^8.0.0", "prettier": "^3.0.0", "tsc-alias": "^1.8.2", "tsup": "^7.2.0", diff --git a/src/lib/seam/connect/routes/acs-access-groups.ts b/src/lib/seam/connect/routes/acs-access-groups.ts index 36c88183..5bb983f4 100644 --- a/src/lib/seam/connect/routes/acs-access-groups.ts +++ b/src/lib/seam/connect/routes/acs-access-groups.ts @@ -64,15 +64,12 @@ export class SeamHttpAcsAccessGroups { return new SeamHttpAcsAccessGroups(opts) } - async addUser( - body: AcsAccessGroupsAddUserBody, - ): Promise { - const { data } = await this.client.request({ + async addUser(body: AcsAccessGroupsAddUserBody): Promise { + await this.client.request({ url: '/acs/access_groups/add_user', method: 'post', data: body, }) - return data.acs_access_group } async create( diff --git a/src/lib/seam/connect/routes/locks.ts b/src/lib/seam/connect/routes/locks.ts index 311b7732..fac5d6b3 100644 --- a/src/lib/seam/connect/routes/locks.ts +++ b/src/lib/seam/connect/routes/locks.ts @@ -64,22 +64,22 @@ export class SeamHttpLocks { return new SeamHttpLocks(opts) } - async get(body: LocksGetBody): Promise { + async get(body: LocksGetBody): Promise { const { data } = await this.client.request({ url: '/locks/get', method: 'post', data: body, }) - return data.lock + return data.device } - async list(body: LocksListBody): Promise { + async list(body: LocksListBody): Promise { const { data } = await this.client.request({ url: '/locks/list', method: 'post', data: body, }) - return data.locks + return data.devices } async lockDoor(