Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions codegen/layouts/partials/request-list-item-type.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{#if (eq itemFormat "object")}}
{{> request-object parameters=itemParameters}}
{{else if (eq itemFormat "discriminated_object")}}
{{#each variants}}{{#unless @first}} | {{/unless}}{{> request-object parameters=parameters}}{{/each}}
{{else}}
{{> request-scalar-type format=itemFormat values=itemEnumValues}}
{{/if}}
5 changes: 5 additions & 0 deletions codegen/layouts/partials/request-object.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
{{#each parameters}}
{{json name}}{{#unless isRequired}}?{{/unless}}: {{> request-parameter-type }}{{#unless isRequired}} | undefined{{/unless}}
{{/each}}
}
7 changes: 7 additions & 0 deletions codegen/layouts/partials/request-parameter-type.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{#if (eq format "object")}}
{{> request-object parameters=parameters}}
{{else if (eq format "list")}}
Array<{{> request-list-item-type }}>
{{else}}
{{> request-scalar-type}}
{{/if}}
1 change: 1 addition & 0 deletions codegen/layouts/partials/request-scalar-type.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#if (eq format "boolean")}}boolean{{else if (eq format "number")}}number{{else if (eq format "record")}}Record<string, unknown>{{else if (eq format "enum")}}{{#each values}}{{#unless @first}} | {{/unless}}{{json name}}{{else}}string{{/each}}{{else}}string{{/if}}
7 changes: 7 additions & 0 deletions codegen/layouts/partials/resource-list-item-type.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{#if (eq itemFormat "object")}}
{{> resource-object properties=itemProperties}}
{{else if (eq itemFormat "discriminated_object")}}
{{#each variants}}{{#unless @first}} | {{/unless}}{{> resource-object properties=properties}}{{/each}}
{{else}}
{{> resource-scalar-type format=itemFormat values=itemEnumValues}}
{{/if}}
5 changes: 5 additions & 0 deletions codegen/layouts/partials/resource-object.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
{{#each properties}}
{{json name}}{{#if isOptional}}?{{/if}}: {{> resource-property-type }}{{#if isNullable}} | null{{/if}}{{#if isOptional}} | undefined{{/if}}
{{/each}}
}
7 changes: 7 additions & 0 deletions codegen/layouts/partials/resource-property-type.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{#if (eq format "object")}}
{{> resource-object properties=properties}}
{{else if (eq format "list")}}
Array<{{> resource-list-item-type }}>
{{else}}
{{> resource-scalar-type}}
{{/if}}
1 change: 1 addition & 0 deletions codegen/layouts/partials/resource-scalar-type.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#if (eq format "boolean")}}boolean{{else if (eq format "number")}}number{{else if (eq format "record")}}Record<string, unknown>{{else if (eq format "enum")}}{{#each values}}{{#unless @first}} | {{/unless}}{{json name}}{{else}}string{{/each}}{{else}}string{{/if}}
4 changes: 2 additions & 2 deletions codegen/layouts/partials/route-class-endpoint-export.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type {{parametersTypeName}} = RouteRequest{{requestFormatSuffix}}<'{{path}}'>
export type {{parametersTypeName}} = {{#if isUndocumented}}RouteRequest{{requestFormatSuffix}}<'{{path}}'>{{else}}{{> request-object parameters=parameters}}{{/if}}

/**
* @deprecated Use {{parametersTypeName}} instead.
Expand All @@ -8,7 +8,7 @@ export type {{legacyRequestTypeName}} = {{parametersTypeName}}
/**
* @deprecated Use {{requestTypeName}} instead.
*/
export type {{responseTypeName}} = RouteResponse<'{{path}}'>
export type {{responseTypeName}} = {{#if usesLegacyResponseType}}RouteResponse<'{{path}}'>{{else if returnsVoid}}void{{else}}{ {{json responseKey}}: {{#if responseIsList}}Array<{{responseResourceTypeName}}>{{else}}{{responseResourceTypeName}}{{/if}} }{{/if}}

export type {{requestTypeName}} = SeamHttpRequest<{{#if returnsVoid}}void, undefined{{else}}{{responseTypeName}}, '{{responseKey}}'{{/if}}>

Expand Down
5 changes: 5 additions & 0 deletions codegen/layouts/partials/route-imports.hbs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { seamApiLtsVersion } from 'lib/lts-version.js'
{{#if hasLegacyTypes}}
import type {
RouteRequestBody,
RouteRequestParams,
RouteResponse,
} from '@seamapi/types/connect'
{{/if}}
{{#each resourceTypeImports}}
import type { {{typeName}} } from 'lib/seam/connect/resources/{{fileName}}'
{{/each}}

import {
getAuthHeadersForClientSessionToken,
Expand Down
8 changes: 8 additions & 0 deletions codegen/layouts/resource-index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Automatically generated by codegen/smith.ts.
* Do not edit this file.
*/

{{#each resources}}
export type { {{typeName}} } from './{{replaceExtension fileName ".js"}}'
{{/each}}
6 changes: 6 additions & 0 deletions codegen/layouts/resource.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Automatically generated by codegen/smith.ts.
* Do not edit this file.
*/

export type {{typeName}} = {{#if isUnknown}}Record<string, unknown>{{else}}{{#each resources}}{{#unless @first}} | {{/unless}}{{> resource-object properties=properties}}{{/each}}{{/if}}
26 changes: 25 additions & 1 deletion codegen/lib/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import {
type EndpointsLayoutContext,
setEndpointsLayoutContext,
} from './layouts/endpoints.js'
import {
getResourceLayoutContexts,
type ResourceIndexLayoutContext,
type ResourceLayoutContext,
} from './layouts/resources.js'
import {
type RouteIndexLayoutContext,
type RouteLayoutContext,
Expand All @@ -19,9 +24,12 @@ interface Metadata {

type File = RouteLayoutContext &
RouteIndexLayoutContext &
EndpointsLayoutContext & { layout: string }
EndpointsLayoutContext &
ResourceLayoutContext &
ResourceIndexLayoutContext & { layout: string }

const rootPath = 'src/lib/seam/connect/routes'
const resourcesPath = 'src/lib/seam/connect/resources'

const supportedAuthMethods: SeamAuthMethod[] = [
'api_key',
Expand Down Expand Up @@ -90,6 +98,22 @@ export const connect = (
})
routeIndexes['']?.add('seam-http-endpoints-without-workspace.js')

const resourceContexts = getResourceLayoutContexts(blueprint)
for (const resource of resourceContexts) {
const resourceFile = {
contents: Buffer.from('\n'),
layout: 'resource.hbs',
...resource,
}
files[`${resourcesPath}/${resource.fileName}`] = resourceFile
}
const resourceIndexFile = {
contents: Buffer.from('\n'),
layout: 'resource-index.hbs',
resources: resourceContexts,
}
files[`${resourcesPath}/index.ts`] = resourceIndexFile

for (const node of nodes) {
const path = toFilePath(node.path)
const name = kebabCase(node.name)
Expand Down
5 changes: 5 additions & 0 deletions codegen/lib/handlebars-helpers.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export const identity = (x: unknown): unknown => x

export const json = (value: string): string => JSON.stringify(value)

export const replaceExtension = (fileName: string, extension: string): string =>
fileName.replace(/\.[^.]+$/, extension)
46 changes: 46 additions & 0 deletions codegen/lib/layouts/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Blueprint, Resource } from '@seamapi/blueprint'
import { kebabCase, pascalCase } from 'change-case'

export interface ResourceLayoutContext {
fileName: string
typeName: string
resources: Resource[]
isUnknown: boolean
}

export interface ResourceIndexLayoutContext {
resources: Array<Pick<ResourceLayoutContext, 'fileName' | 'typeName'>>
}

export const getResourceTypeName = (resourceType: string): string =>
`${pascalCase(resourceType)}Resource`

export const getResourceLayoutContexts = (
blueprint: Blueprint,
): ResourceLayoutContext[] => {
const resources = [
...blueprint.resources,
...blueprint.events,
...blueprint.actionAttempts,
]
const resourceTypes = [
...new Set(resources.map(({ resourceType }) => resourceType)),
]

return [
{
fileName: 'unknown.ts',
typeName: 'UnknownResource',
resources: [],
isUnknown: true,
},
...resourceTypes.map((resourceType) => ({
fileName: `${kebabCase(resourceType)}.ts`,
typeName: getResourceTypeName(resourceType),
resources: resources.filter(
(resource) => resource.resourceType === resourceType,
),
isUnknown: false,
})),
]
}
52 changes: 51 additions & 1 deletion codegen/lib/layouts/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { Endpoint, Namespace, Route } from '@seamapi/blueprint'
import type { Endpoint, Namespace, Parameter, Route } from '@seamapi/blueprint'
import type { Method } from 'axios'
import { camelCase, kebabCase, pascalCase } from 'change-case'

import { getResourceTypeName } from './resources.js'

export interface RouteLayoutContext {
className: string
isUndocumented: boolean
endpoints: EndpointLayoutContext[]
subroutes: SubrouteLayoutContext[]
skipClientSessionImport: boolean
hasLegacyTypes: boolean
resourceTypeImports: ResourceTypeImport[]
}

export interface RouteIndexLayoutContext {
Expand All @@ -32,6 +36,10 @@ export interface EndpointLayoutContext {
returnsVoid: boolean
isOptionalParamsOk: boolean
isUndocumented: boolean
usesLegacyResponseType: boolean
parameters: Parameter[]
responseIsList: boolean
responseResourceTypeName: string
}

export interface SubrouteLayoutContext {
Expand All @@ -40,6 +48,11 @@ export interface SubrouteLayoutContext {
fileName: string
}

interface ResourceTypeImport {
fileName: string
typeName: string
}

export const setRouteLayoutContext = (
file: Partial<RouteLayoutContext>,
node: Route | Namespace | null,
Expand All @@ -49,6 +62,36 @@ export const setRouteLayoutContext = (
file.isUndocumented = node?.isUndocumented ?? false
file.skipClientSessionImport =
node == null || node?.path === '/client_sessions'
file.hasLegacyTypes =
node != null && 'endpoints' in node
? node.endpoints.some(
(endpoint) =>
endpoint.isUndocumented ||
(endpoint.response.responseType === 'resource' &&
endpoint.response.resourceType === 'action_attempt'),
)
: false
file.resourceTypeImports =
node != null && 'endpoints' in node
? [
...new Set(
node.endpoints.flatMap((endpoint) => {
if (
endpoint.isUndocumented ||
endpoint.response.responseType === 'void' ||
(endpoint.response.responseType === 'resource' &&
endpoint.response.resourceType === 'action_attempt')
) {
return []
}
return [endpoint.response.resourceType]
}),
),
].map((resourceType) => ({
fileName: `${kebabCase(resourceType)}.js`,
typeName: getResourceTypeName(resourceType),
}))
: []

file.endpoints = []
if (node != null && 'endpoints' in node) {
Expand Down Expand Up @@ -117,6 +160,13 @@ export const getEndpointLayoutContext = (
(parameter) => !parameter.isRequired,
),
isUndocumented: endpoint.isUndocumented,
usesLegacyResponseType: endpoint.isUndocumented || returnsActionAttempt,
parameters: endpoint.request.parameters,
responseIsList: endpoint.response.responseType === 'resource_list',
responseResourceTypeName:
endpoint.response.responseType === 'void'
? ''
: getResourceTypeName(endpoint.response.resourceType),
...getResponseContext(endpoint),
}
}
Expand Down
5 changes: 4 additions & 1 deletion codegen/smith.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { connect, helpers } from './lib/index.js'

const rootDir = dirname(fileURLToPath(import.meta.url))

await Promise.all([deleteAsync('./src/lib/seam/connect/routes')])
await Promise.all([
deleteAsync('./src/lib/seam/connect/routes'),
deleteAsync('./src/lib/seam/connect/resources'),
])

const partials = await getHandlebarsPartials(`${rootDir}/layouts/partials`)

Expand Down
Loading
Loading