DEV Community

tkow
tkow

Posted on • Edited on

Nodejs Cloud Functions args and response Type Generator from backend

What is this article

Introduction of my firebase-function-client-type-gen library.

Summary

If you use cloud functions for firebase, you create client using httpsOnCallable. This api accept args and response type as type parameters of typescript.

However, the synchronization of request and response parameters with backend normally is not supported. This library resolve this having constraint of firebase function definition way.

Explanation

My library extract Arg and Response type using their type alias name and the function name using Typescript compiler API, then import whole firebase functions definition object from your entry point of actual deployment.

Given if you have nested function definition object as your entry point, For example,

import * as functions from 'firebase-functions'

// You define two types in function definition file and they must be in a file include function declaration.
type RequestArgs = {
    id: string
}
type ResponseResult = {
    result: 'ok' | 'ng'
}

// You must export "only one const https onCall" in a file.
// If you export many httpsOnCall functions, it may happen unexpected result when mapping args and result types.'
const includeTest = functions
    .region('asia-northeast1')
    .runWith({
        memory: '1GB'
    })
    .https.onCall((data: RequestArgs, _): ResponseResult => {
        return {
            result: 'ok'
        }
    })

export const nameSpace = {
    includeTest
}
Enter fullscreen mode Exit fullscreen mode

Some firebase API run code at top level scope, so they must be mocked. If you have other run code at top level scope, and if it cause error at runtime, they also must be mocked. Look at the followed by an example. I recommend proxyquire as injection mock to your code and using it at this example. Mock may be like that.

export const DUMMY_MOCKS = new Proxy<any>(
    () => DUMMY_MOCKS,
    {
        get(_, __): any {
            return DUMMY_MOCKS
        }
    }
)

export const MOCKS_BASE = {
    'firebase-functions': {
        region() {
            return DUMMY_MOCKS
        },
        config: () => {
            return {
            }
        },
        '@global': true,
        '@noCallThru': true
    },
    'firebase-admin': {
        apps: DUMMY_MOCKS,
        initializeApp: () => { return DUMMY_MOCKS },

        '@global': true,
        '@noCallThru': true
    },
}

export const MOCKS = new Proxy(MOCKS_BASE, {
    get(target, name) {
        const returnValue = target[name as keyof typeof MOCKS_BASE]
        return returnValue ?? DUMMY_MOCKS
    }
})
Enter fullscreen mode Exit fullscreen mode

Then, locate your code generating command file.

import proxyquire from 'proxyquire'
import { MOCKS } from './mock'
import { outDefinitions } from 'firebase-function-client-type-gen'
import path from 'path'
import glob from 'glob'
import {EOL} from 'os'

const functionDefs = proxyquire('./functions/entrypoint.ts' ,Mocks)

// Get document, or throw exception on error
try {
  const sources = glob.sync(path.resolve(__dirname, './', 'functions/**/*.ts'))
  const result = outDefinitions(sources, namedFunctions, {
    symbolConfig: {
      args: 'RequestArgs',
      result: 'ResponseResult'
    }
  })
  console.log(result)
  console.log('named functions type generated' + EOL);
} catch (e) {
  console.error(e);
}
Enter fullscreen mode Exit fullscreen mode

The symbolConfig can change your type alias name. Run this code using ts runtime environment like ts-node output should be followed by

export type FunctionDefinitions = {
    "includeTest": {
        args: { id: string; };
        result: { result: "ok" | "ng"; };
    };
};

export const functionsMap = {
    includeTest: "nameSpace-includeTest",
};
Enter fullscreen mode Exit fullscreen mode

The output of course can be passed by fileWriter like fs.
You output it your application, then you can create automatic type-safe client if each functions have different regions.

import { getFunctions, httpsCallable, HttpsCallable } from 'firebase/functions'
import { getApp } from 'firebase/app'

type IFunctionDefnitions = {
    [key: string]: {
        args: any,
        result: any
    }
}

type HttpsCallableFuntions<FunctionDefnitions extends IFunctionDefnitions> = {
    [functionName in keyof FunctionDefnitions]: HttpsCallable<FunctionDefnitions[functionName]['args'], FunctionDefnitions[functionName]['result']>
}


type HttpsCallableFuntionIds<FunctionDefnitions> = {
    [functionName in keyof FunctionDefnitions]: string
}

export function initializeFunctions<FunctionDefnitions extends IFunctionDefnitions>(functionNameObject: HttpsCallableFuntionIds<FunctionDefnitions>, app = getApp(), region = 'us-east-1'): HttpsCallableFuntions<FunctionDefnitions> {
    const functions = getFunctions(app, region)
    const functionDefinitions = Object.entries(functionNameObject)
    return functionDefinitions.reduce((current, [functionName, functionId]) => {
        return {
            ...current,
            [functionName]: httpsCallable(functions, functionId)
        }
    }, {} as HttpsCallableFuntions<FunctionDefnitions>)
}

// At your entrypoint file, import generated types from your generated types file.
import { FunctionDefinitions, functionsMap } from './functions-types'
const client = initializeFunctions<FunctionDefinitions>(functionsMap)
// Fully type-safed api call functions.
client.callSomethingReuest({...args})
Enter fullscreen mode Exit fullscreen mode

If you need to change region as basis as function, manually call const someCallable = httpsCallable(getFunction(getApp(), region), functionId) instead of initializeFunctions above.

That's all. Other features or some cautions are in Readme in my repository.

If you are interested in this library, feel free to ask me.

** Update **

I have picked regions from our client generator, so no longer needs to separate client function definitions.
Furthermore, we adapt simple type alias or interface pull reference types our output file.

Top comments (0)