gcf-helper
Version:
Google Cloud Functions Helper
263 lines (262 loc) • 10 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ErrorHandler_1 = require("./ErrorHandler");
const ConfigReader_1 = require("./ConfigReader");
const MetricsHandler_1 = require("./MetricsHandler");
class GCFHelper {
constructor(functionOptions) {
this.configLoaded = false;
this.functionOptions = functionOptions || {};
this.errorHandler = new ErrorHandler_1.default(this);
this.metricsHandler = new MetricsHandler_1.default(this);
}
/**
* used for tests, is automatically handled otherwise
*/
parseConfig() {
return this.ensureConfigAdapted();
}
async handleError(error, eventPayload) {
await this.ensureConfigAdapted();
// prevent people from putting thrown errors back into this lib
// because that will end in endless error handling loops and pubsub messages
if (this.errorHandler.isGCFHelperError(error)) {
return;
}
const errorPayload = this.errorHandler.generateErrorPayload(error, eventPayload);
const gcfError = this.errorHandler.generateErrorFromErrorPayload(errorPayload);
if ((await this.hasPubSubClient()) && this.canPublish()) {
// throw clean
await this.functionOptions
.pubSubClient.topic(this.functionOptions.errorTopic)
.publish(Buffer.from(JSON.stringify(errorPayload)));
throw gcfError;
}
else {
// we cannot handle the shortened version of our gcf error logic
// because we cannot publish it, thats why we have to throw the original one
throw error;
}
}
async validateAPIFSRequest(request, eventPayload) {
await this.ensureConfigAdapted();
const resultCode = this.isRequestAuthorizationValid(request);
if (resultCode !== 0) {
await this.handleError(new Error("Invalid apifs authentication secret."), eventPayload);
}
// secret okay, continue
}
async validateAPIFSRequestNoError(request, response) {
await this.ensureConfigAdapted();
const resultCode = this.isRequestAuthorizationValid(request);
if (resultCode !== 0) {
if (response) {
response.status(403).json({
error: "APIFS secret is not provided or incorrect.",
});
}
return false;
}
return true;
}
async writeBigQueryRows(rows, etl, eventPayload, datasetName, tableName) {
await this.ensureConfigAdapted();
if (!rows || !rows.length) {
return;
}
if ((await this.hasBigQueryClient()) && this.canWriteToBigQuery(datasetName, tableName)) {
const targetDataset = datasetName ? datasetName : this.functionOptions.bqDatasetId;
const targetTable = tableName ? tableName : this.functionOptions.bqTableId;
try {
await this.functionOptions
.bigQueryClient.dataset(targetDataset)
.table(targetTable)
.insert(etl ? rows.map(etl) : rows);
}
catch (error) {
await this.handleError(error, eventPayload ? eventPayload : rows);
}
}
else {
// throw clean
throw new Error("Cannot write to big query, because preconditions are missing to setup the client or " +
"a table/dataset name is not given.");
}
}
getPubSubDataFromEvent(event) {
if (!event || !event.data) {
return null;
}
let asString = "";
try {
asString = Buffer.from(event.data, "base64").toString();
}
catch (_) {
return asString;
}
try {
return JSON.parse(asString);
}
catch (_) {
return asString;
}
}
async sqlQuery(queryString, params) {
await this.ensureConfigAdapted();
if ((await this.hasSqlPool()) && this.functionOptions.sqlPool) {
try {
return this.functionOptions.sqlPool.query({
text: queryString,
values: params,
});
}
catch (error) {
return this.handleError(error);
}
}
else {
// throw clean
throw new Error("Cannot write to sql, because preconditions are missing to setup the client.");
}
}
async metricsIncCounter(metricName, value = 1, labels = {}) {
if (this.functionOptions.disableMetrics) {
return false;
}
if (!await this.ensureMetricsReady()) {
throw new Error("Metrics are not ready, make sure pubsub is configured and a topic var is present.");
}
this.metricsHandler.increment(metricName, value, labels);
return true;
}
async metricsSetGauge(metricName, value = 0, labels = {}) {
if (this.functionOptions.disableMetrics) {
return false;
}
if (!await this.ensureMetricsReady()) {
throw new Error("Metrics are not ready, make sure pubsub is configured and a topic var is present.");
}
this.metricsHandler.set(metricName, value, labels);
return true;
}
getConfig() {
return this.functionOptions;
}
kill() {
this.metricsHandler.kill();
}
hasPubSubClient() {
// check if we can cover the pubsub client instance automatically
if (this.functionOptions.errorTopic &&
!this.functionOptions.pubSubClient &&
this.functionOptions.projectId) {
return Promise.resolve().then(() => require("@google-cloud/pubsub")).then((packageImport) => {
const { PubSub } = packageImport;
this.functionOptions.pubSubClient = new PubSub({
projectId: this.functionOptions.projectId,
});
return true;
})
.catch((_) => false);
}
if (this.functionOptions.pubSubClient) {
return Promise.resolve(true);
}
else {
return Promise.resolve(false);
}
}
hasBigQueryClient() {
// check if we can cover the bigquery client instance automatically
if (!this.functionOptions.bigQueryClient &&
this.functionOptions.projectId) {
return Promise.resolve().then(() => require("@google-cloud/bigquery")).then((packageImport) => {
const { BigQuery } = packageImport;
this.functionOptions.bigQueryClient = new BigQuery({
projectId: this.functionOptions.projectId,
});
return true;
})
.catch((_) => false);
}
if (this.functionOptions.bigQueryClient) {
return Promise.resolve(true);
}
else {
return Promise.resolve(false);
}
}
hasSqlPool() {
// check if we can cover the sql client instance automatically
if (!this.functionOptions.sqlPool &&
this.functionOptions.sqlConnectionName &&
this.functionOptions.sqlMaxConnections &&
this.functionOptions.sqlUsername &&
this.functionOptions.sqlPassword &&
this.functionOptions.sqlDatabaseName) {
return Promise.resolve().then(() => require("pg")).then((packageImport) => {
const { Pool } = packageImport;
const pgConfig = {
max: this.functionOptions.sqlMaxConnections,
connectionTimeoutMillis: 4500,
idleTimeoutMillis: 4500,
user: this.functionOptions.sqlUsername,
password: this.functionOptions.sqlPassword,
database: this.functionOptions.sqlDatabaseName,
};
if (process.env.NODE_ENV === "production") {
pgConfig.host = `/cloudsql/${this.functionOptions.sqlConnectionName}`;
}
this.functionOptions.sqlPool = new Pool(pgConfig);
return true;
})
.catch((_) => false);
}
if (this.functionOptions.sqlPool) {
return Promise.resolve(true);
}
else {
return Promise.resolve(false);
}
}
isRequestAuthorizationValid(request) {
if (!this.functionOptions.apifsSecretHeader ||
!this.functionOptions.apifsSecretValue) {
// throw clean
throw new Error("You have not configured an apifs secret value.");
}
if (!request || !request.headers) {
return 1;
}
const header = request.headers[this.functionOptions.apifsSecretHeader];
if (!header) {
return 2;
}
if (header !== this.functionOptions.apifsSecretValue) {
return 3;
}
return 0;
}
canPublish() {
return !!this.functionOptions.pubSubClient && !!this.functionOptions.errorTopic;
}
canPublishMetrics() {
return !!this.functionOptions.pubSubClient && !!this.functionOptions.metricsTopic;
}
canWriteToBigQuery(datasetName, tableName) {
return (this.functionOptions.bigQueryClient &&
(datasetName || this.functionOptions.bqDatasetId) &&
(tableName || this.functionOptions.bqTableId));
}
async ensureConfigAdapted() {
if (!this.configLoaded) {
this.functionOptions = await ConfigReader_1.default.adaptConfig(this.functionOptions);
this.configLoaded = true;
}
}
async ensureMetricsReady() {
await this.ensureConfigAdapted();
return (await this.hasPubSubClient()) && this.canPublishMetrics();
}
}
exports.default = GCFHelper;