Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically determine endpoint resources #87

Merged
merged 13 commits into from
Apr 17, 2024
Merged
83 changes: 25 additions & 58 deletions generate-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ async function main(): Promise<void> {
])
}

const openapiResponseKeyProp = 'x-fern-sdk-return-value'

const routePaths = [
'/access_codes',
'/access_codes/simulate',
andrii-balitskyi marked this conversation as resolved.
Show resolved Hide resolved
'/access_codes/unmanaged',
'/acs',
'/acs/access_groups',
Expand All @@ -54,6 +57,7 @@ const routePaths = [
'/networks',
'/noise_sensors',
'/noise_sensors/noise_thresholds',
'/noise_sensors/simulate',
andrii-balitskyi marked this conversation as resolved.
Show resolved Hide resolved
'/phones',
'/phones/simulate',
'/thermostats',
Expand All @@ -67,7 +71,7 @@ const routePaths = [
const routePathSubresources: Partial<
Record<(typeof routePaths)[number], string[]>
> = {
'/access_codes': ['unmanaged'],
'/access_codes': ['unmanaged', 'simulate'],
'/acs': [
'access_groups',
'credential_pools',
Expand All @@ -79,41 +83,11 @@ const routePathSubresources: Partial<
],
'/phones': ['simulate'],
'/devices': ['unmanaged', 'simulate'],
'/noise_sensors': ['noise_thresholds'],
'/noise_sensors': ['noise_thresholds', 'simulate'],
'/thermostats': ['climate_setting_schedules'],
'/user_identities': ['enrollment_automations'],
}

const ignoredEndpointPaths = [
'/access_codes/simulate/create_unmanaged_access_code',
'/connect_webviews/view',
'/health',
'/health/get_health',
'/health/get_service_health',
'/health/service/[service_name]',
'/noise_sensors/simulate/trigger_noise_threshold',
'/workspaces/reset_sandbox',
] as const

const endpointResources: Partial<
Record<
keyof typeof openapi.paths,
null | 'action_attempt' | 'noise_threshold'
>
> = {
// Set all ignored endpoints null to simplify code generation.
...ignoredEndpointPaths.reduce((acc, cur) => ({ ...acc, [cur]: null }), {}),

// These endpoints return a deprecated action attempt or resource.
'/access_codes/delete': null,
'/access_codes/unmanaged/delete': null,
'/access_codes/update': null,
'/noise_sensors/noise_thresholds/create': 'noise_threshold',
'/noise_sensors/noise_thresholds/delete': null,
'/noise_sensors/noise_thresholds/update': null,
'/thermostats/climate_setting_schedules/update': null,
} as const

interface Route {
namespace: string
endpoints: Endpoint[]
Expand All @@ -139,14 +113,10 @@ interface ClassMeta {
const createRoutes = (): Route[] => {
const paths = Object.keys(openapi.paths)

const unmatchedEndpointPaths = paths
.filter(
(path) =>
!routePaths.some((routePath) => isEndpointUnderRoute(path, routePath)),
)
.filter(
(path) => !(ignoredEndpointPaths as unknown as string[]).includes(path),
)
const unmatchedEndpointPaths = paths.filter(
(path) =>
!routePaths.some((routePath) => isEndpointUnderRoute(path, routePath)),
)

if (unmatchedEndpointPaths.length > 0) {
throw new Error(
Expand Down Expand Up @@ -206,39 +176,40 @@ const deriveResource = (
endpointPath: string,
method: Method,
): string | null => {
if (isEndpointResource(endpointPath)) {
return endpointResources[endpointPath] ?? null
}

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(
andrii-balitskyi marked this conversation as resolved.
Show resolved Hide resolved
response.content['application/json']?.schema?.properties ?? {},
)
const postSpec = spec.post
const openapiEndpointResource =
openapiResponseKeyProp in postSpec
? postSpec[openapiResponseKeyProp]
: null

return openapiEndpointResource
}

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 ?? {},

const responseSchemaProperties =
response.content['application/json']?.schema?.properties ?? {}
const endpointResource = Object.keys(responseSchemaProperties).find(
(key) => key !== 'ok',
)

return endpointResource ?? null
}
}

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 => {
// UPSTREAM: This should return GET before POST.
// Blocked on https://github.com/seamapi/nextlove/issues/117
Expand All @@ -248,10 +219,6 @@ const deriveSemanticMethod = (methods: string[]): Method => {
throw new Error(`Could not find valid method in ${methods.join(', ')}`)
}

const isEndpointResource = (
key: string,
): key is keyof typeof endpointResources => key in endpointResources

const isOpenapiPath = (key: string): key is keyof typeof openapi.paths =>
key in openapi.paths

Expand Down
1 change: 1 addition & 0 deletions package-lock.json

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

182 changes: 182 additions & 0 deletions src/lib/seam/connect/routes/access-codes-simulate.ts

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

5 changes: 5 additions & 0 deletions src/lib/seam/connect/routes/access-codes.ts

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

Loading
Loading