lukeautry / tsoa

Build OpenAPI-compliant REST APIs using TypeScript and Node
MIT License
3.54k stars 499 forks source link

Suggestion: Azure function v4 support #1463

Closed bompi88 closed 1 year ago

bompi88 commented 1 year ago

Create a route generation option for Azure functions v4.

Sorting

Possible Solution

Add a suitable template for Azure functions entry file.

github-actions[bot] commented 1 year ago

Hello there bompi88 👋

Thank you for opening your very first issue in this project.

We will try to get back to you as soon as we can.👀

github-actions[bot] commented 1 year ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days

Tyranwyn commented 11 months ago

this would be actually very nice, i am making a template now

fgoulet commented 3 months ago

this would be actually very nice, i am making a template now

Does the template for Azure Functions v4 has been completed already ?

Thanks

Tyranwyn commented 1 month ago

i made this, but it's not complete. You'll have to adjust this to your usecase

/* tslint:disable */
/* eslint-disable */
import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse, fetchMiddlewares } from '@tsoa/runtime'
{{#each controllers}}
import { {{name}} } from '{{modulePath}}'
{{/each}}
import { app, HttpHandler, HttpMethod, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'
import * as fs from 'fs'
import * as intercept from 'azure-function-log-intercept'

const models: TsoaRoute.Models = {
  {{#each models}}
  {{@key}}: {
    {{#if enums}}
    dataType: "refEnum",
    enums: {{{json enums}}},
    {{/if}}
    {{#if properties}}
    dataType: "refObject",
    properties: {
      {{#each properties}}
      {{@key}}: {{{json this}}},
      {{/each}}
    },
    additionalProperties: {{{json additionalProperties}}},
    {{/if}}
    {{#if type}}
    dataType: "refAlias",
    type: {{{json type}}},
    {{/if}}
  },
  {{/each}}
}
const validationService = new ValidationService(models)

{{#each controllers}}
{{#each actions}}
app.http('{{name}}', {
  methods: ['{{method}}'.toUpperCase() as HttpMethod ],
  authLevel: 'function',
  route: convertToAzureFunctionRoute('{{fullPath}}'),
  handler: globalErrorHandler(async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
    intercept(context)
    const args = {
      {{#each parameters}}
        {{@key}}: {{{json this}}},
      {{/each}}
    }

    const validatedArgs = await getValidatedArgs(args, request)

    const controller = new {{../name}}()
    return {
      {{#if successStatus}}
      status: {{successStatus}},
      {{/if}}
      jsonBody: await controller.{{name}}.apply(controller, validatedArgs)
    }
  })
})
{{/each}}
{{/each}}

const SWAGGER_UI_ROUTE = process.env.SWAGGER_UI_ROUTE || 'v3/swagger-ui'
const SWAGGER_UI_INDEX_LOCATION = process.env.SWAGGER_LOCATION || 'public/swagger-ui.html'
const SWAGGER_UI_HTML = fs.readFileSync(SWAGGER_UI_INDEX_LOCATION).toString()

app.get('getSwaggerUI', {
  authLevel: 'anonymous',
  route: SWAGGER_UI_ROUTE,
  handler: async (
    request: HttpRequest,
    context: InvocationContext,
  ): Promise<HttpResponseInit> => {
    return {
      headers: [['Content-Type', 'text/html']],
      body: SWAGGER_UI_HTML,
    }
  },
})

function convertToAzureFunctionRoute(path: string): string {
  return path.slice(1).replace(/:(\w+)|:(\w+)\//g, '{$1}')
    .replace('{id}', '{id:int}')
}

async function getValidatedArgs(args: any, request: HttpRequest): Promise<any[]> {
  const fieldErrors: FieldErrors  = {}
  let queryParams = {}
  for (let [key, value] of request.query) {
    queryParams[key] = value
  }
  let body = {}
  try {
    body = await request.json()
  } catch {}
  const values = Object.keys(args).map((key) => {
    const name = args[key].name
    switch (args[key].in) {
      case 'request':
        // return request
        throw new Error("needs to be implemented")
      case 'query':
        return validationService.ValidateParam(args[key], parseAzureQueryParameter(request.query.get(name)), name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
      case 'queries':
        return validationService.ValidateParam(args[key], queryParams, name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
      case 'path':
        return validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
      case 'header':
        // return validationService.ValidateParam(args[key], request.headers.get(name), name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
        throw new Error("needs to be implemented")
      case 'body':
        // return validationService.ValidateParam(args[key], body, name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
        throw new Error("needs to be implemented")
      case 'body-prop':
        return validationService.ValidateParam(args[key], parseAzureQueryParameter(body[name]), name, fieldErrors, 'body.', {{{json minimalSwaggerConfig}}})
      case 'formData':
        // if (args[key].dataType === 'file') {
        //     return validationService.ValidateParam(args[key], request.file, name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
        // } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') {
        //     return validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
        // } else {
        //     return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, {{{json minimalSwaggerConfig}}})
        // }
        throw new Error("needs to be implemented")
      case 'res':
        // return responder(response)
        throw new Error("needs to be implemented")
    }
  })

  if (Object.keys(fieldErrors).length > 0) {
    throw new ValidateError(fieldErrors, '')
  }
  return values
}

function parseAzureQueryParameter(value: string): unknown {
  if (value == null) {
    return undefined
  }
  if (value.includes(',')) {
    return value.split(',')
  }
  return value
}

export function globalErrorHandler(trigger: HttpHandler) {
  return async (
    req: HttpRequest,
    context: InvocationContext,
  ): Promise<HttpResponseInit> => {
    try {
      return await trigger(req, context)
    } catch (err) {
      if (err instanceof ValidateError) {
        return new InvalidParameterException(getErrorFieldsString(err.fields)).response
      } else {
        throw err
      }
    }
  }
}

function getErrorFieldsString(fieldErrors: FieldErrors): string {
  return Object.keys(fieldErrors)
    .map((key) => `${key}=${fieldErrors[key].value}: ${fieldErrors[key].message}`)
    .join('; ')
}