faasjs/faasjs

View on GitHub
packages/cloud_function/src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
F
47%
/**
 * A FaasJS plugin, let function could create, config and invoke CloudFunction.
 *
 * [![License: MIT](https://img.shields.io/npm/l/@faasjs/cloud_function.svg)](https://github.com/faasjs/faasjs/blob/main/packages/faasjs/cloud_function/LICENSE)
 * [![NPM Version](https://img.shields.io/npm/v/@faasjs/cloud_function.svg)](https://www.npmjs.com/package/@faasjs/cloud_function)
 *
 * ## Install
 *
 * ```sh
 * npm install @faasjs/cloud_function
 * ```
 *
 * @packageDocumentation
 */
import { deepMerge } from '@faasjs/deep_merge'
import {
  type Plugin,
  type DeployData,
  type Next,
  type MountData,
  type InvokeData,
  usePlugin,
  type UseifyPlugin,
} from '@faasjs/func'
import { Logger } from '@faasjs/logger'
import { Validator, type ValidatorConfig } from './validator'

/** 云函数配置项 */
export type CloudFunctionConfig = {
  /** 插件名称 */
  name?: string
  /** 配置项 */
  config?: {
    /** 配置名称 */
    name?: string
    /** 内存大小,单位为MB,默认 64 */
    memorySize?: 64 | 128 | 256 | 384 | 512 | 640 | 768 | 896 | 1024 | number
    /** 执行超时时间,单位为秒,默认 30 */
    timeout?: number
    /** 触发器配置 */
    triggers?: {
      type: 'timer' | string
      name?: string
      value: string
    }[]
    /** 预制并发配置 */
    provisionedConcurrent?: {
      /** 预制并发数量 */
      executions: number
    }
    [key: string]: any
  }
  validator?: {
    event?: ValidatorConfig
  }
  [key: string]: any
}

export type CloudFunctionAdapter = {
  invokeCloudFunction: (name: string, data: any, options?: any) => Promise<void>
  invokeSyncCloudFunction: <TResult>(
    name: string,
    data: any,
    options?: any
  ) => Promise<TResult>
}

const Name = 'cloud_function'

const globals: {
  [name: string]: CloudFunction
} = {}

export class CloudFunction implements Plugin {
  public readonly type: string = Name
  public readonly name: string = Name
  public event: any
  public context: any
  public config: {
    name?: string
    memorySize?: number
    timeout?: number
    triggers?: {
      type: string
      name: string
      value: string
    }[]
    [key: string]: any
  }

  private adapter: CloudFunctionAdapter
  private readonly validatorConfig?: {
    event?: ValidatorConfig
  }

  private validator?: Validator
  private readonly logger: Logger

  /**
   * 创建云函数配置
   * @param config {object} 配置项,这些配置将强制覆盖默认配置
   * @param config.name {string} 云资源名
   * @param config.config {object} 云资源配置
   * @param config.config.name {string} 云函数名
   * @param config.config.memorySize {number} 内存大小,单位为 MB
   * @param config.config.timeout {number} 最长执行时间,单位为 秒
   * @param config.config.triggers {object[]} 触发器配置
   * @param config.config.provisionedConcurrent {object} 预制并发配置
   * @param config.config.provisionedConcurrent.executions {number} 并发数
   * @param config.validator {object} 事件校验配置
   * @param config.validator.event {object} event 校验配置
   * @param config.validator.event.whitelist {string} 白名单配置
   * @param config.validator.event.onError {function} 自定义报错
   * @param config.validator.event.rules {object} 参数校验规则
   */
  constructor(config?: CloudFunctionConfig) {
    if (config) {
      this.name = config.name || Name
      this.config = config.config || Object.create(null)
      if (config.validator) this.validatorConfig = config.validator
    } else {
      this.name = this.type
      this.config = Object.create(null)
    }
    this.logger = new Logger(this.name)
  }

  public async onDeploy(data: DeployData, next: Next): Promise<void> {
    this.logger.debug('[CloudFunction] Merge configuration...')
    this.logger.debug('%j', data)

    const config = data.config.plugins
      ? deepMerge(data.config.plugins[this.name], { config: this.config })
      : { config: this.config }

    this.logger.debug('[CloudFunction] Merged configuration: %j', config)

    // 引用服务商部署插件
    const Provider = require(config.provider.type).Provider
    const provider = new Provider(config.provider.config)

    data.dependencies['@faasjs/cloud_function'] = '*'
    data.dependencies[config.provider.type as string] = '*'

    // 部署云函数
    await provider.deploy(this.type, data, config)

    await next()
  }

  public async onMount(data: MountData, next: Next): Promise<void> {
    if (data.config.plugins?.[this.name || this.type])
      this.config = deepMerge(
        { config: this.config },
        data.config.plugins[this.name || this.type],
        {}
      )

    if (this.config.provider) {
      const Provider = require(this.config.provider.type).Provider
      this.adapter = new Provider(this.config.provider.config)
    } else
      this.logger.warn(
        '[onMount] Unknown provider, will use invoke and invokeSync with local mode.'
      )

    if (this.validatorConfig) {
      this.logger.debug('[onMount] prepare validator')
      this.validator = new Validator(this.validatorConfig)
    }

    globals[this.name] = this

    await next()
  }

  public async onInvoke(data: InvokeData, next: Next): Promise<void> {
    this.event = data.event
    this.context = data.context
    if (this.validator) {
      this.logger.debug('[onInvoke] Valid')
      this.validator.valid({ event: this.event })
    }
    await next()
  }

  /**
   * 异步触发云函数
   * @param name {string} 云函数文件名或云函数名
   * @param data {any} 参数
   * @param options {object} 额外配置项
   */
  public async invoke<TData = any>(
    name: string,
    data?: TData,
    options?: Record<string, any>
  ): Promise<void> {
    if (data == null) data = Object.create(null)

    if (process.env.FaasMode !== 'remote') {
      const test = require('@faasjs/test')
      const func = new test.FuncWarper(
        `${process.env.FaasRoot || ''}${name.toLowerCase()}.func`
      )
      return func.handler(data, { request_id: this.logger.label })
    }
    return this.adapter.invokeCloudFunction(name.toLowerCase(), data, options)
  }

  /**
   * 同步调用云函数
   * @param name {string} 云函数文件名或云函数名
   * @param data {any} 参数
   * @param options {object} 额外配置项
   */
  public async invokeSync<TResult = any, TData = any>(
    name: string,
    data?: TData,
    options?: Record<string, any>
  ): Promise<TResult> {
    if (data == null) data = Object.create(null)

    if (process.env.FaasMode !== 'remote') {
      const test = require('@faasjs/test')
      const func = new test.FuncWarper(
        `${process.env.FaasRoot || ''}${name.toLowerCase()}.func`
      )
      return func.handler(data, { request_id: this.logger.label })
    }
    return this.adapter.invokeSyncCloudFunction<TResult>(
      name.toLowerCase(),
      data,
      options
    )
  }
}

export function useCloudFunction(
  config?: CloudFunctionConfig | (() => CloudFunctionConfig)
): UseifyPlugin<CloudFunction> {
  let configs: CloudFunctionConfig
  if (config)
    if (typeof config === 'function') configs = config()
    else configs = config

  const name = configs?.name || Name

  if (globals[name]) return usePlugin<CloudFunction>(globals[name])

  return usePlugin<CloudFunction>(new CloudFunction(configs))
}

/**
 * 异步触发云函数
 * @param name {string} 云函数文件名或云函数名
 * @param data {any} 参数
 * @param options {object} 额外配置项
 */
export async function invoke<TData = any>(
  name: string,
  data?: TData,
  options?: {
    [key: string]: any
  }
): Promise<void> {
  return await useCloudFunction().invoke<TData>(name, data, options)
}

/**
 * 同步触发云函数
 * @param name {string} 云函数文件名或云函数名
 * @param data {any} 参数
 * @param options {object} 额外配置项
 */
export async function invokeSync<TResult = any, TData = any>(
  name: string,
  data?: TData,
  options?: {
    [key: string]: any
  }
): Promise<TResult> {
  return await useCloudFunction().invokeSync<TResult, TData>(
    name,
    data,
    options
  )
}