О чем эта статья
Введение моей библиотеки firebase-function-client-type-gen.
Резюме
Если вы используете облачные функции для firebase, вы создаете клиента, используя httpsOnCallable. Этот api принимает args и тип ответа как параметры типа typescript.
Однако синхронизация параметров запроса и ответа с бэкендом обычно не поддерживается. Данная библиотека решает эту проблему с помощью ограничений на определение функций firebase.
Пояснение
Моя библиотека извлекает тип Arg и Response, используя их псевдоним типа и имя функции, используя API компилятора Typescript, затем импортирует весь объект определения функций firebase из вашей точки входа фактического развертывания.
Учитывая, если у вас есть вложенный объект определения функции в качестве точки входа, например,
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
}
Некоторые API firebase запускают код на верхнем уровне, поэтому они должны быть имитированы. Если у вас есть другой код, выполняемый в области видимости верхнего уровня, и если он вызывает ошибку во время выполнения, его также необходимо передразнить. Посмотрите на следующий пример. Я рекомендую proxyquire как инъекционный mock в ваш код и использовать его на этом примере. Mock может быть таким.
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
}
})
Затем найдите командный файл, генерирующий ваш код.
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);
}
В symbolConfig можно изменить имя псевдонима вашего типа. Запустите этот код, используя среду выполнения ts, например ts-node, после чего должен появиться вывод
export type FunctionDefinitions = {
"includeTest": {
args: { id: string; };
result: { result: "ok" | "ng"; };
};
};
export const functionsMap = {
includeTest: "nameSpace-includeTest",
};
Вывод, конечно, может быть передан через fileWriter типа fs.
Если вы выводите его вашему приложению, то вы можете создать автоматический клиент с сохранением типов, если у каждой функции есть разные регионы.
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})
Если вам нужно изменить регион как основу функции, вручную вызовите const someCallable = httpsCallable(getFunction(getApp(), region), functionId)
вместо initializeFunctions выше.
Вот и все. Другие возможности или некоторые предостережения можно найти в Readme в моем репозитории.
Если вас заинтересовала эта библиотека, не стесняйтесь спрашивать меня.
** Обновление **
Я взял регионы из нашего клиентского генератора, поэтому больше не нужно разделять определения клиентских функций.
Кроме того, мы адаптируем простые типы псевдонимов типов или интерфейсных ссылок в наш выходной файл.