UNPKG

gcf-helper

Version:
263 lines (262 loc) 10 kB
"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;