diff --git a/src/containers/hoc/load-data.jsx b/src/containers/hoc/load-data.jsx
index b0e6b22d2..ac215d9ac 100644
--- a/src/containers/hoc/load-data.jsx
+++ b/src/containers/hoc/load-data.jsx
@@ -1,6 +1,6 @@
import React from "react";
-import { graphql, compose } from "react-apollo";
-import { withProps, branch, renderComponent } from "recompose";
+import { graphql } from "@apollo/client/react/hoc";
+import { flowRight as compose } from "lodash";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
@@ -19,20 +19,25 @@ import LoadingIndicator from "../../components/LoadingIndicator";
* queries are loading.
* @param {string[]} queryNames The names of the queries to check loading state
*/
-const isLoading = queryNames =>
- withProps(parentProps => {
- const loadingReducer = (loadingAcc, queryName) =>
- loadingAcc || (parentProps[queryName] && parentProps[queryName].loading);
- const loading = queryNames.reduce(loadingReducer, false);
- const errorReducer = (errorAcc, queryName) => {
- const error = parentProps[queryName] && parentProps[queryName].error;
- return error ? errorAcc.concat([error]) : errorAcc;
- };
- const errors = queryNames.reduce(errorReducer, []);
+// The isLoading function below utilizes currying, a technique where a function
+// returns another function with specific parameters. The purpose is to create a
+// sequence of functions, making it flexible and reusable. In this case,
+// isLoading is curried to take queryNames, then the Component, and finally
+// parentProps. ie: isLoading(queryNames)(Component)(parentProps)
- return { loading, errors };
- });
+const isLoading = queryNames => Component => parentProps => {
+ const loadingReducer = (loadingAcc, queryName) =>
+ loadingAcc || (parentProps[queryName] && parentProps[queryName].loading);
+ const loading = queryNames.reduce(loadingReducer, false);
+
+ const errorReducer = (errorAcc, queryName) => {
+ const error = parentProps[queryName] && parentProps[queryName].error;
+ return error ? errorAcc.concat([error]) : errorAcc;
+ };
+ const errors = queryNames.reduce(errorReducer, []);
+ return
;
+};
export const withQueries = (queries = {}) => {
const enhancers = Object.entries(
@@ -44,24 +49,28 @@ export const withQueries = (queries = {}) => {
return compose(...enhancers, isLoading(Object.keys(queries)));
};
-const withMutations = (mutations = {}) =>
- compose(
- withProps(parentProps => {
- return { client: ApolloClientSingleton };
- }),
- withProps(parentProps => {
- const reducer = (propsAcc, [name, constructor]) => {
+const withMutations = (mutations = {}) => {
+ const withClient = Component => props => (
+
+ );
+
+ const withMutationFuncs = Component => props => {
+ const mutationFuncs = Object.entries(mutations).reduce(
+ (propsAcc, [name, constructor]) => {
propsAcc[name] = async (...args) => {
- const options = constructor(parentProps)(...args);
- return await parentProps.client.mutate(options);
+ const options = constructor(props)(...args);
+ return await props.client.mutate(options);
};
return propsAcc;
- };
+ },
+ {}
+ );
- const mutationFuncs = Object.entries(mutations).reduce(reducer, {});
- return { mutations: mutationFuncs };
- })
- );
+ return
;
+ };
+
+ return compose(withClient, withMutationFuncs);
+};
/**
* Takes multiple GraphQL queries and/or mutation definitions and wraps Component in appropriate
@@ -94,9 +103,17 @@ const PrettyErrors = ({ errors }) => (
* @param {Object} options
* @see withOperations
*/
+
export default options =>
compose(
withOperations(options),
- branch(({ loading }) => loading, renderComponent(LoadingIndicator)),
- branch(({ errors }) => errors.length > 0, renderComponent(PrettyErrors))
+ Component => ({ loading, errors, ...props }) => {
+ if (loading) {
+ return
;
+ } else if (errors.length > 0) {
+ return
;
+ } else {
+ return
;
+ }
+ }
);
diff --git a/src/extensions/action-handlers/index.js b/src/extensions/action-handlers/index.js
index 3262d03f5..e24b898b1 100644
--- a/src/extensions/action-handlers/index.js
+++ b/src/extensions/action-handlers/index.js
@@ -43,7 +43,7 @@ const CONFIGURED_TAG_HANDLERS = _.pickBy(
export async function getSetCacheableResult(cacheKey, fallbackFunc) {
if (r.redis && cacheKey) {
- const cacheRes = await r.redis.getAsync(String(cacheKey));
+ const cacheRes = await r.redis.GET(String(cacheKey));
if (cacheRes) {
return JSON.parse(cacheRes);
}
@@ -51,10 +51,10 @@ export async function getSetCacheableResult(cacheKey, fallbackFunc) {
const slowRes = await fallbackFunc();
if (r.redis && cacheKey && slowRes && slowRes.expiresSeconds) {
await r.redis
- .multi()
- .set(String(cacheKey), JSON.stringify(slowRes))
- .expire(String(cacheKey), slowRes.expiresSeconds)
- .execAsync();
+ .MULTI()
+ .SET(String(cacheKey), JSON.stringify(slowRes))
+ .EXPIRE(String(cacheKey), slowRes.expiresSeconds)
+ .exec();
}
return slowRes;
}
@@ -197,8 +197,8 @@ export async function getActionChoiceData(actionHandler, organization, user) {
parsedData = {};
}
- let items = parsedData.items;
- if (items && !(items instanceof Array)) {
+ let { items } = parsedData;
+ if (items && !Array.isArray(items)) {
log.error(
`Data received from ${actionHandler.name}.getClientChoiceData is not an array`
);
@@ -214,7 +214,7 @@ export const clearCacheForOrganization = async organizationId => {
const handlerNames = Object.keys(CONFIGURED_ACTION_HANDLERS);
const promiseArray = handlerNames.map(handlerName => [
- r.redis.keysAsync(
+ r.redis.KEYS(
`${choiceDataCacheKey(
handlerName,
{
@@ -223,7 +223,7 @@ export const clearCacheForOrganization = async organizationId => {
"*"
)}`
),
- r.redis.keysAsync(
+ r.redis.KEYS(
`${availabilityCacheKey(
handlerName,
{
@@ -245,6 +245,6 @@ export const clearCacheForOrganization = async organizationId => {
keys.push(...keysResult);
});
- const delPromises = keys.map(key => r.redis.delAsync(key));
+ const delPromises = keys.map(key => r.redis.DEL(key));
await Promise.all(delPromises);
};
diff --git a/src/extensions/action-handlers/mobilecommons-signup.js b/src/extensions/action-handlers/mobilecommons-signup.js
index ba2dc3f17..71d8c2334 100644
--- a/src/extensions/action-handlers/mobilecommons-signup.js
+++ b/src/extensions/action-handlers/mobilecommons-signup.js
@@ -1,5 +1,4 @@
import request from "request";
-import aws from "aws-sdk";
import { r } from "../../server/models";
import { actionKitSignup } from "./helper-ak-sync.js";
import { getConfig } from "../../server/api/lib/config";
diff --git a/src/extensions/action-handlers/ngpvan-action.js b/src/extensions/action-handlers/ngpvan-action.js
index ad08d6ff7..100fbbdc7 100644
--- a/src/extensions/action-handlers/ngpvan-action.js
+++ b/src/extensions/action-handlers/ngpvan-action.js
@@ -370,15 +370,22 @@ export async function getClientChoiceData(organization) {
// Besides this returning true, "test-action" will also need to be added to
// process.env.ACTION_HANDLERS
export async function available(organization) {
- let result =
- (hasConfig("NGP_VAN_API_KEY", organization) ||
- hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)) &&
- hasConfig("NGP_VAN_APP_NAME", organization);
+ const hasVanApiKey = hasConfig("NGP_VAN_API_KEY", organization);
+ const hasVanApiKeyEncrypted = hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization);
+ const hasVanAppName = hasConfig("NGP_VAN_APP_NAME", organization);
+
+ let result = (hasVanApiKey || hasVanApiKeyEncrypted) && hasVanAppName;
if (!result) {
// eslint-disable-next-line no-console
console.info(
- "ngpvan-action unavailable. Missing one or more required environment variables"
+ `${organization.name} :: ngpvan-action unavailable. Status:
+ Needs either:\n
+ \tNGP_VAN_API_KEY: ${hasVanApiKey}\n
+ \tNGP_VAN_API_KEY_ENCRYPTED: ${hasVanApiKeyEncrypted}\n
+ Needs:\n
+ \tNGP_VAN_APP_NAME: ${hasVanAppName}
+ `
);
}
diff --git a/src/extensions/action-handlers/revere-signup.js b/src/extensions/action-handlers/revere-signup.js
index 647fd786b..4fb5c2c4f 100644
--- a/src/extensions/action-handlers/revere-signup.js
+++ b/src/extensions/action-handlers/revere-signup.js
@@ -1,9 +1,9 @@
import request from "request";
-import aws from "aws-sdk";
+import { SQS } from "@aws-sdk/client-sqs";
import { r } from "../../server/models";
import { actionKitSignup } from "./helper-ak-sync.js";
-const sqs = new aws.SQS();
+const sqs = new SQS();
export const name = "revere-signup";
diff --git a/src/extensions/contact-loaders/csv-s3-upload/index.js b/src/extensions/contact-loaders/csv-s3-upload/index.js
index 4c2a52f92..50bc53a72 100644
--- a/src/extensions/contact-loaders/csv-s3-upload/index.js
+++ b/src/extensions/contact-loaders/csv-s3-upload/index.js
@@ -3,7 +3,8 @@ import { unzipPayload } from "../../../workers/jobs";
import { getConfig, hasConfig } from "../../../server/api/lib/config";
import { gunzip } from "../../../lib";
-import AWS from "aws-sdk";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
+import { PutObjectCommand, S3 } from "@aws-sdk/client-s3";
export const name = "csv-s3-upload";
@@ -67,8 +68,11 @@ export async function getClientChoiceData(
/// The react-component will be sent this data as a property
/// return a json object which will be cached for expiresSeconds long
/// `data` should be a single string -- it can be JSON which you can parse in the client component
- const s3 = new AWS.S3({
+ const s3 = new S3({
+ // The key signatureVersion is no longer supported in v3, and can be removed.
+ // @deprecated SDK v3 only supports signature v4.
signatureVersion: "v4",
+
region: getConfig("AWS_REGION")
});
const key = `contacts-upload/${campaign.id}/contacts.json.gz`;
@@ -80,7 +84,9 @@ export async function getClientChoiceData(
Expires: 1800 // 30 minutes
};
- const result = await s3.getSignedUrl("putObject", params);
+ const result = await await getSignedUrl(s3, new PutObjectCommand(params), {
+ expiresIn: "/* add value from 'Expires' from v2 call if present, else remove */"
+ });
return {
data: JSON.stringify({ s3Url: result, s3key: key }),
@@ -116,8 +122,11 @@ export async function processContactLoad(job, maxContacts, organization) {
/// * Batching
/// * Error handling
/// * "Request of Doom" scenarios -- queries or jobs too big to complete
- const s3 = new AWS.S3({
+ const s3 = new S3({
+ // The key signatureVersion is no longer supported in v3, and can be removed.
+ // @deprecated SDK v3 only supports signature v4.
signatureVersion: "v4",
+
region: getConfig("AWS_REGION")
});
var params = {
@@ -125,7 +134,7 @@ export async function processContactLoad(job, maxContacts, organization) {
Key: job.payload
};
- const contactsData = (await s3.getObject(params).promise()).Body.toString(
+ const contactsData = (await s3.getObject(params)).Body.toString(
"utf-8"
);
const parsedData = JSON.parse(
diff --git a/src/extensions/contact-loaders/csv-s3-upload/react-component.js b/src/extensions/contact-loaders/csv-s3-upload/react-component.js
index bada76554..1e175824d 100644
--- a/src/extensions/contact-loaders/csv-s3-upload/react-component.js
+++ b/src/extensions/contact-loaders/csv-s3-upload/react-component.js
@@ -5,7 +5,6 @@ import axios from "axios";
import * as yup from "yup";
import humps from "humps";
import { StyleSheet, css } from "aphrodite";
-import { compose } from "recompose";
import Button from "@material-ui/core/Button";
import Divider from "@material-ui/core/Divider";
@@ -354,7 +353,7 @@ CampaignContactsFormBase.propTypes = {
jobResultMessage: type.string
};
-const CampaignContactsForm = compose(withMuiTheme)(CampaignContactsFormBase);
+const CampaignContactsForm = withMuiTheme(CampaignContactsFormBase);
CampaignContactsForm.prototype.renderAfterStart = true;
diff --git a/src/extensions/contact-loaders/csv-upload/react-component.js b/src/extensions/contact-loaders/csv-upload/react-component.js
index 94b8603fd..744f3af52 100644
--- a/src/extensions/contact-loaders/csv-upload/react-component.js
+++ b/src/extensions/contact-loaders/csv-upload/react-component.js
@@ -4,7 +4,6 @@ import * as yup from "yup";
import humps from "humps";
import { StyleSheet, css } from "aphrodite";
import Form from "react-formal";
-import { compose } from "recompose";
import Divider from "@material-ui/core/Divider";
import Button from "@material-ui/core/Button";
@@ -357,7 +356,7 @@ CampaignContactsFormBase.propTypes = {
contactsPerPhoneNumber: type.number
};
-const CampaignContactsForm = compose(withMuiTheme)(CampaignContactsFormBase);
+const CampaignContactsForm = withMuiTheme(CampaignContactsFormBase);
CampaignContactsForm.prototype.renderAfterStart = true;
diff --git a/src/extensions/contact-loaders/datawarehouse/react-component.js b/src/extensions/contact-loaders/datawarehouse/react-component.js
index abcfceefa..cd8ef3099 100644
--- a/src/extensions/contact-loaders/datawarehouse/react-component.js
+++ b/src/extensions/contact-loaders/datawarehouse/react-component.js
@@ -3,7 +3,6 @@ import React from "react";
import Form from "react-formal";
import { StyleSheet, css } from "aphrodite";
import * as yup from "yup";
-import { compose } from "recompose";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
@@ -225,6 +224,6 @@ CampaignContactsFormBase.propTypes = {
jobResultMessage: type.string
};
-const CampaignContactsForm = compose(withMuiTheme)(CampaignContactsFormBase);
+const CampaignContactsForm = withMuiTheme(CampaignContactsFormBase);
export { CampaignContactsForm };
diff --git a/src/extensions/contact-loaders/index.js b/src/extensions/contact-loaders/index.js
index e8ba51b65..d2848ca09 100644
--- a/src/extensions/contact-loaders/index.js
+++ b/src/extensions/contact-loaders/index.js
@@ -12,7 +12,7 @@ const CONFIGURED_INGEST_METHODS = getIngestMethods();
async function getSetCacheableResult(cacheKey, fallbackFunc) {
if (r.redis) {
- const cacheRes = await r.redis.getAsync(cacheKey);
+ const cacheRes = await r.redis.GET(cacheKey);
if (cacheRes) {
return JSON.parse(cacheRes);
}
@@ -20,10 +20,10 @@ async function getSetCacheableResult(cacheKey, fallbackFunc) {
const slowRes = await fallbackFunc();
if (r.redis && slowRes && slowRes.expiresSeconds) {
await r.redis
- .multi()
- .set(cacheKey, JSON.stringify(slowRes))
- .expire(cacheKey, slowRes.expiresSeconds)
- .execAsync();
+ .MULTI()
+ .SET(cacheKey, JSON.stringify(slowRes))
+ .EXPIRE(cacheKey, slowRes.expiresSeconds)
+ .exec();
}
return slowRes;
}
@@ -101,12 +101,8 @@ export const clearCacheForOrganization = async organizationId => {
const handlerNames = Object.keys(CONFIGURED_INGEST_METHODS);
const promiseArray = handlerNames.map(handlerName => [
- r.redis.keysAsync(
- `${choiceDataCacheKey(handlerName, organizationId, "*")}`
- ),
- r.redis.keysAsync(
- `${availabilityCacheKey(handlerName, organizationId, "*")}`
- )
+ r.redis.KEYS(`${choiceDataCacheKey(handlerName, organizationId, "*")}`),
+ r.redis.KEYS(`${availabilityCacheKey(handlerName, organizationId, "*")}`)
]);
const flattenedPromises = [];
@@ -120,7 +116,7 @@ export const clearCacheForOrganization = async organizationId => {
keys.push(...keysResult);
});
- const delPromises = keys.map(key => r.redis.delAsync(key));
+ const delPromises = keys.map(key => r.redis.DEL(key));
await Promise.all(delPromises);
};
diff --git a/src/extensions/contact-loaders/ngpvan/index.js b/src/extensions/contact-loaders/ngpvan/index.js
index 02d8df3f9..3ca444567 100644
--- a/src/extensions/contact-loaders/ngpvan/index.js
+++ b/src/extensions/contact-loaders/ngpvan/index.js
@@ -56,16 +56,23 @@ export async function available(organization, user) {
// / If this is instantaneous, you can have it be 0 (i.e. always), but if it takes time
// / to e.g. verify credentials or test server availability,
// / then it's better to allow the result to be cached
+
+ const hasRawKey = hasConfig("NGP_VAN_API_KEY", organization);
+ const hasEncryptedKey = hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)
+ const hasAppName = hasConfig("NGP_VAN_APP_NAME", organization);
+ const hasWebhook = hasConfig("NGP_VAN_WEBHOOK_BASE_URL", organization)
- const result =
- (hasConfig("NGP_VAN_API_KEY", organization) ||
- hasConfig("NGP_VAN_API_KEY_ENCRYPTED", organization)) &&
- hasConfig("NGP_VAN_APP_NAME", organization) &&
- hasConfig("NGP_VAN_WEBHOOK_BASE_URL", organization);
+ const result = (hasRawKey || hasEncryptedKey) && hasAppName && hasWebhook;
if (!result) {
console.log(
- "ngpvan contact loader unavailable. Missing one or more required environment variables."
+ `${organization.name} :: ngpvan contact loader unavailable. Status:\n
+ Needs one:\n
+ \tNGP_VAN_API_KEY: ${hasRawKey}\n
+ \tNGP_VAN_API_KEY_ENCRYPTED: ${hasEncryptedKey}\n
+ Needs both:\n
+ \tNGP_VAN_APP_NAME: ${hasAppName}\n
+ \tNGP_VAN_WEBHOOK_BASE_URL: ${hasWebhook}`
);
}
@@ -145,7 +152,7 @@ export async function getClientChoiceData(organization, campaign, user) {
}
}
} catch (error) {
- const message = `Error retrieving saved list metadata from VAN ${error}`;
+ const message = `${organization.name} :: Error retrieving saved list metadata from VAN ${error}`;
// eslint-disable-next-line no-console
console.log(message);
return { data: `${JSON.stringify({ error: message })}` };
diff --git a/src/extensions/contact-loaders/past-contacts/index.js b/src/extensions/contact-loaders/past-contacts/index.js
index 4e2ba9c5c..ec4e28188 100644
--- a/src/extensions/contact-loaders/past-contacts/index.js
+++ b/src/extensions/contact-loaders/past-contacts/index.js
@@ -1,7 +1,7 @@
import { completeContactLoad, failedContactLoad } from "../../../workers/jobs";
import { r, cacheableData } from "../../../server/models";
import { getConfig, hasConfig } from "../../../server/api/lib/config";
-import queryString from "query-string";
+import queryString from "node:querystring";
import { getConversationFiltersFromQuery } from "../../../lib";
import { getConversations } from "../../../server/api/conversations";
import { getTags } from "../../../server/api/tag";
diff --git a/src/extensions/contact-loaders/s3-pull/index.js b/src/extensions/contact-loaders/s3-pull/index.js
index c78e29174..61dfec43e 100644
--- a/src/extensions/contact-loaders/s3-pull/index.js
+++ b/src/extensions/contact-loaders/s3-pull/index.js
@@ -11,7 +11,7 @@ import { log, gunzip } from "../../../lib";
import path from "path";
import Papa from "papaparse";
-import AWS from "aws-sdk";
+import { S3 } from "@aws-sdk/client-s3";
export const name = "s3-pull";
@@ -89,8 +89,11 @@ export async function loadContactS3PullProcessFile(jobEvent, contextVars) {
customIndexes,
region
} = jobEvent;
- const s3 = new AWS.S3({
+ const s3 = new S3({
region,
+
+ // The key signatureVersion is no longer supported in v3, and can be removed.
+ // @deprecated SDK v3 only supports signature v4.
signatureVersion: "v4"
});
@@ -111,8 +114,7 @@ export async function loadContactS3PullProcessFile(jobEvent, contextVars) {
.split("/")
.slice(3)
.join("/")
- })
- .promise();
+ });
const fileString = await gunzip(fileData.Body);
const { data, errors } = await new Promise((resolve, reject) => {
Papa.parse(fileString.toString(), {
@@ -258,8 +260,11 @@ export async function processContactLoad(job, maxContacts, organization) {
const s3Path = JSON.parse(job.payload).s3Path;
const s3Bucket = getConfig("AWS_S3_BUCKET_NAME", organization);
const region = getConfig("AWS_REGION", organization);
- const s3 = new AWS.S3({
+ const s3 = new S3({
region,
+
+ // The key signatureVersion is no longer supported in v3, and can be removed.
+ // @deprecated SDK v3 only supports signature v4.
signatureVersion: "v4"
});
@@ -273,8 +278,7 @@ export async function processContactLoad(job, maxContacts, organization) {
.getObject({
Bucket: s3Bucket,
Key: manifestPath.replace(/^\//, "")
- })
- .promise();
+ });
manifestData = JSON.parse(manifestFile.Body.toString("utf-8"));
} catch (err) {
await failedContactLoad(
diff --git a/src/extensions/contact-loaders/s3-pull/react-component.js b/src/extensions/contact-loaders/s3-pull/react-component.js
index 4820c26be..d2b3be8ab 100644
--- a/src/extensions/contact-loaders/s3-pull/react-component.js
+++ b/src/extensions/contact-loaders/s3-pull/react-component.js
@@ -1,7 +1,6 @@
import type from "prop-types";
import React from "react";
import Form from "react-formal";
-import { compose } from "recompose";
import * as yup from "yup";
import List from "@material-ui/core/List";
@@ -130,7 +129,7 @@ CampaignContactsFormBase.propTypes = {
jobResultMessage: type.string
};
-const CampaignContactsForm = compose(withMuiTheme)(CampaignContactsFormBase);
+const CampaignContactsForm = withMuiTheme(CampaignContactsFormBase);
CampaignContactsForm.prototype.renderAfterStart = true;
diff --git a/src/extensions/dynamicassignment-batches/index.js b/src/extensions/dynamicassignment-batches/index.js
index 7aaedb8bd..6a4392a8c 100644
--- a/src/extensions/dynamicassignment-batches/index.js
+++ b/src/extensions/dynamicassignment-batches/index.js
@@ -1,5 +1,10 @@
import { getConfig } from "../../server/api/lib/config";
+// Checks the gloabl var DYNAMICASSIGNMENT_BATCHES and
+// whether the handler loads, similar to how texter-sideboxes works
+
+// https://github.com/StateVoicesNational/Spoke/blob/main/docs/HOWTO-use-dynamicassignment-batches.md
+
export const getDynamicAssignmentBatchPolicies = ({
organization,
campaign
@@ -9,7 +14,7 @@ export const getDynamicAssignmentBatchPolicies = ({
const configuredHandlers =
campaignEnabled ||
getConfig(handlerKey, organization) ||
- "finished-replies-tz,vetted-texters,finished-replies";
+ "finished-replies-tz,vetted-texters,finished-replies";
const enabledHandlers =
(configuredHandlers && configuredHandlers.split(",")) || [];
if (!campaignEnabled) {
diff --git a/src/extensions/job-runners/lambda-async/index.js b/src/extensions/job-runners/lambda-async/index.js
index c30a4f8a4..88c2e34b6 100644
--- a/src/extensions/job-runners/lambda-async/index.js
+++ b/src/extensions/job-runners/lambda-async/index.js
@@ -1,11 +1,11 @@
-import AWS from "aws-sdk";
+import { Lambda } from "@aws-sdk/client-lambda";
import { saveJob } from "../helpers";
const functionName =
process.env.WORKER_LAMBDA_FUNCTION_NAME ||
process.env.AWS_LAMBDA_FUNCTION_NAME;
-const client = new AWS.Lambda();
+const client = new Lambda();
export const fullyConfigured = () => !!functionName;
@@ -33,8 +33,7 @@ export const dispatchJob = async (
FunctionName: functionName,
InvocationType: "Event",
Payload: JSON.stringify({ type: "JOB", jobId: job.id })
- })
- .promise();
+ });
return job;
};
@@ -44,6 +43,5 @@ export const dispatchTask = async (taskName, payload) => {
FunctionName: functionName,
InvocationType: "Event",
Payload: JSON.stringify({ type: "TASK", taskName, payload })
- })
- .promise();
+ });
};
diff --git a/src/extensions/message-handlers/auto-optout/index.js b/src/extensions/message-handlers/auto-optout/index.js
index 9f0af417f..0b750fd64 100644
--- a/src/extensions/message-handlers/auto-optout/index.js
+++ b/src/extensions/message-handlers/auto-optout/index.js
@@ -6,6 +6,13 @@ import { sendRawMessage } from "../../../server/api/mutations/sendMessage";
const DEFAULT_AUTO_OPTOUT_REGEX_LIST_BASE64 =
"W3sicmVnZXgiOiAiXlxccypzdG9wXFxifFxcYnJlbW92ZSBtZVxccyokfHJlbW92ZSBteSBuYW1lfFxcYnRha2UgbWUgb2ZmIHRoXFx3KyBsaXN0fFxcYmxvc2UgbXkgbnVtYmVyfGRvblxcVz90IGNvbnRhY3QgbWV8ZGVsZXRlIG15IG51bWJlcnxJIG9wdCBvdXR8c3RvcDJxdWl0fHN0b3BhbGx8Xlxccyp1bnN1YnNjcmliZVxccyokfF5cXHMqY2FuY2VsXFxzKiR8XlxccyplbmRcXHMqJHxeXFxzKnF1aXRcXHMqJCIsICJyZWFzb24iOiAic3RvcCJ9XQ==";
+// DEFAULT_AUTO_OPTOUT_REGEX_LIST_BASE64 converts to:
+
+// [{"regex": "^\\s*stop\\b|\\bremove me\\s*$|remove my name|\\btake me off th\\w+ list|
+// \\blose my number|don\\W?t contact me|delete my number|I opt out|stop2quit|stopall|
+// ^\\s*unsubscribe\\s*$|^\\s*cancel\\s*$|^\\s*end\\s*$|^\\s*quit\\s*$",
+// "reason": "stop"}]
+
export const serverAdministratorInstructions = () => {
return {
description: `
@@ -44,26 +51,29 @@ export const available = organization => {
}
};
+// Part of the auto-opt out process.
+// checks if message recieved states something like "stop", "quit", or "stop2quit"
export const preMessageSave = async ({ messageToSave, organization }) => {
- if (messageToSave.is_from_contact) {
+ if (messageToSave.is_from_contact) { // checks if message is from the contact
const config = Buffer.from(
getConfig("AUTO_OPTOUT_REGEX_LIST_BASE64", organization) ||
DEFAULT_AUTO_OPTOUT_REGEX_LIST_BASE64,
"base64"
- ).toString();
+ ).toString(); // converts DEFAULT_AUTO_OPTOUT_REGEX_LIST_BASE64 to regex
+ // can be custom set in .env w/ AUTO_OPTOUT_REGEX_LIST_BASE64
const regexList = JSON.parse(config || "[]");
- const matches = regexList.filter(matcher => {
+ const matches = regexList.filter(matcher => { // checks if message contains opt-out langauge
const re = new RegExp(matcher.regex, "i");
return String(messageToSave.text).match(re);
});
- // console.log("auto-optout", matches, messageToSave.text, regexList);
- if (matches.length) {
+ if (matches.length) { // if more than one match, opt-out
console.log(
"auto-optout MATCH",
- messageToSave.campaign_contact_id,
- matches
+ `| campaign_contact_id: ${messageToSave.campaign_contact_id}`,
+ `| reason: "${matches[0].reason}"`
);
- const reason = matches[0].reason || "auto_optout";
+ const reason = matches[0].reason || "auto_optout"; // with default opt-out regex,
+ // reason will always be "stop"
messageToSave.error_code = -133;
return {
contactUpdates: {
@@ -90,8 +100,8 @@ export const postMessageSave = async ({
if (message.is_from_contact && handlerContext.autoOptOutReason) {
console.log(
"auto-optout.postMessageSave",
- message.campaign_contact_id,
- handlerContext.autoOptOutReason
+ `| campaign_contact_id: ${message.campaign_contact_id}`,
+ `| opt-out reason: ${handlerContext.autoOptOutReason}`
);
let contact = await cacheableData.campaignContact.load(
message.campaign_contact_id,
@@ -158,4 +168,4 @@ export const postMessageSave = async ({
});
}
}
-};
+};
\ No newline at end of file
diff --git a/src/extensions/service-managers/per-campaign-messageservices/react-component.js b/src/extensions/service-managers/per-campaign-messageservices/react-component.js
index d4dcab4e8..43733ff12 100644
--- a/src/extensions/service-managers/per-campaign-messageservices/react-component.js
+++ b/src/extensions/service-managers/per-campaign-messageservices/react-component.js
@@ -3,7 +3,6 @@ import type from "prop-types";
import { StyleSheet, css } from "aphrodite";
import _ from "lodash";
import * as yup from "yup";
-import { compose } from "recompose";
import Form from "react-formal";
import Button from "@material-ui/core/Button";
@@ -966,7 +965,7 @@ export class CampaignConfigBase extends React.Component {
}
}
-export const CampaignConfig = compose(withMuiTheme)(CampaignConfigBase);
+export const CampaignConfig = withMuiTheme(CampaignConfigBase);
export class CampaignStats extends React.Component {
static propTypes = {
diff --git a/src/extensions/service-managers/scrub-bad-mobilenums/index.js b/src/extensions/service-managers/scrub-bad-mobilenums/index.js
index 1933b9e19..493b39017 100644
--- a/src/extensions/service-managers/scrub-bad-mobilenums/index.js
+++ b/src/extensions/service-managers/scrub-bad-mobilenums/index.js
@@ -5,6 +5,7 @@ import { Jobs } from "../../../workers/job-processes";
import { Tasks } from "../../../workers/tasks";
import { jobRunner } from "../../job-runners";
import { getServiceFromOrganization } from "../../service-vendors";
+import { log } from "../../../lib/log.js"
// / All functions are OPTIONAL EXCEPT metadata() and const name=.
// / DO NOT IMPLEMENT ANYTHING YOU WILL NOT USE -- the existence of a function adds behavior/UI (sometimes costly)
@@ -306,6 +307,7 @@ export async function nextBatchJobLookups({
lastCount,
steps
);
+ log.error("scrub-bad-mobilenums error: ", err);
await r
.knex("job_request")
.where("id", job.id)
diff --git a/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js b/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js
index a2a27359d..055d71d4c 100644
--- a/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js
+++ b/src/extensions/service-managers/scrub-bad-mobilenums/react-component.js
@@ -102,9 +102,9 @@ export class CampaignConfig extends React.Component {
{scrubMobileOptional
? ""
- : "This is a required step to lookup numbers you’ve uploaded, but"}
- FIRST you need to upload your contacts -- go to the Contacts section
- and upload your list -- then check back here to look them up.
+ : "This is a required step to lookup numbers you have uploaded. "}
+
But first, please go to the Contacts section
+ and upload your list, then check back here to look them up!
)}
{scrubState === states.C_NEEDS_RUN && (
diff --git a/src/extensions/service-vendors/bandwidth/messaging.js b/src/extensions/service-vendors/bandwidth/messaging.js
index 56f8ca768..f817e8d47 100644
--- a/src/extensions/service-vendors/bandwidth/messaging.js
+++ b/src/extensions/service-vendors/bandwidth/messaging.js
@@ -1,11 +1,10 @@
-import { ApiController, Client } from "@bandwidth/messaging";
-
+import { Configuration, MessagesApi } from "bandwidth-sdk";
import { log } from "../../../lib";
import { getFormattedPhoneNumber } from "../../../lib/phone-format";
-import { getConfig, hasConfig } from "../../../server/api/lib/config";
+import { getConfig } from "../../../server/api/lib/config";
import { r, cacheableData, Log, Message } from "../../../server/models";
import { saveNewIncomingMessage, parseMessageText } from "../message-sending";
-import { getMessageServiceConfig, getConfigKey } from "../service_map";
+import { getMessageServiceConfig } from "../service_map";
const ENABLE_DB_LOG = getConfig("ENABLE_DB_LOG");
@@ -35,12 +34,11 @@ export function errorDescription(errorCode) {
}
export async function getBandwidthController(organization, config) {
- const client = new Client({
- timeout: 0,
- basicAuthUserName: config.userName,
- basicAuthPassword: config.password
+ const client = new Configuration({
+ username: config.userName,
+ password: config.password
});
- return new ApiController(client);
+ return new MessagesApi(client);
}
export async function sendMessage({
@@ -108,7 +106,6 @@ export async function sendMessage({
bandwidthMessage.media = [parsedMessage.mediaUrl];
}
- let response;
if (/bandwidthapitest/.test(message.text)) {
let err;
const response = {
@@ -133,15 +130,24 @@ export async function sendMessage({
organization,
config
);
- response = await messagingController.createMessage(
+ const { status, data } = await messagingController.createMessage(
config.accountId,
bandwidthMessage
);
console.log(
"bandwidth.sendMessage createMessage response",
- response && response.statusCode,
- response && response.result
+ status,
+ data
);
+ await postMessageSend({
+ status,
+ data,
+ message,
+ contact,
+ trx,
+ organization,
+ changes
+ });
} catch (err) {
console.log("bandwidth.sendMessage ERROR", err);
await postMessageSend({
@@ -154,14 +160,6 @@ export async function sendMessage({
});
return;
}
- await postMessageSend({
- response,
- message,
- contact,
- trx,
- organization,
- changes
- });
}
export async function postMessageSend({
@@ -169,7 +167,8 @@ export async function postMessageSend({
contact,
trx,
err,
- response,
+ status,
+ data,
organization,
changes
}) {
@@ -183,10 +182,10 @@ export async function postMessageSend({
organization_id: organization.id,
service: "bandwidth"
};
- if (response && response.statusCode === 202 && response.result) {
- changesToSave.service_id = response.result.id;
+ if (status && status === 202 && data) {
+ changesToSave.service_id = data.id;
organizationContact.status_code = 1;
- organizationContact.user_number = response.result.from;
+ organizationContact.user_number = data.from;
cacheableData.campaignContact.updateStatus(
contact,
undefined,
@@ -195,8 +194,8 @@ export async function postMessageSend({
} else {
// ERROR
changesToSave.send_status = "ERROR";
- changesToSave.error_code = response.statusCode;
- organizationContact.last_error_code = response.statusCode;
+ changesToSave.error_code = status;
+ organizationContact.last_error_code = status;
}
let updateQuery = r.knex("message").where("id", message.id);
if (trx) {
@@ -300,12 +299,12 @@ export async function getFreeContactInfo({
let messageData;
if (messageSid) {
- messageData = await messagingController.getMessages(
+ messageData = await messagingController.listMessages(
config.accountId,
messageSid
);
} else if (contactNumber) {
- messageData = await messagingController.getMessages(
+ messageData = await messagingController.listMessages(
config.accountId,
null,
null,
diff --git a/src/extensions/service-vendors/bandwidth/react-component.js b/src/extensions/service-vendors/bandwidth/react-component.js
index 829412740..6748f3f15 100644
--- a/src/extensions/service-vendors/bandwidth/react-component.js
+++ b/src/extensions/service-vendors/bandwidth/react-component.js
@@ -4,7 +4,6 @@ import PropTypes from "prop-types";
import React from "react";
import Form from "react-formal";
import * as yup from "yup";
-import { compose } from "recompose";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
@@ -432,4 +431,4 @@ OrgConfigBase.propTypes = {
requestRefetch: PropTypes.func
};
-export const OrgConfig = compose(withMuiTheme)(OrgConfigBase);
+export const OrgConfig = withMuiTheme(OrgConfigBase);
diff --git a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js
index 0f21afd74..034f94848 100644
--- a/src/extensions/service-vendors/bandwidth/setup-and-numbers.js
+++ b/src/extensions/service-vendors/bandwidth/setup-and-numbers.js
@@ -1,16 +1,14 @@
import { createHmac } from "crypto";
import BandwidthNumbers from "@bandwidth/numbers";
-import BandwidthMessaging from "@bandwidth/messaging";
-import { log } from "../../../lib";
import { getFormattedPhoneNumber } from "../../../lib/phone-format";
import { sleep } from "../../../workers/lib";
import { r } from "../../../server/models";
import { getConfig } from "../../../server/api/lib/config";
import { getSecret, convertSecret } from "../../secret-manager";
-import { getMessageServiceConfig, getConfigKey } from "../service_map";
+import { getMessageServiceConfig } from "../service_map";
export async function getNumbersClient(organization, options) {
let config;
diff --git a/src/extensions/service-vendors/twilio/index.js b/src/extensions/service-vendors/twilio/index.js
index 82b3745fe..79c34933a 100644
--- a/src/extensions/service-vendors/twilio/index.js
+++ b/src/extensions/service-vendors/twilio/index.js
@@ -1,7 +1,8 @@
/* eslint-disable no-use-before-define, no-console */
import _ from "lodash";
import Twilio, { twiml } from "twilio";
-import urlJoin from "url-join";
+import { format as formatUrl } from "url";
+import { join as joinPath } from "path";
import { log } from "../../../lib";
import { getFormattedPhoneNumber } from "../../../lib/phone-format";
import {
@@ -581,7 +582,21 @@ export async function handleIncomingMessage(message) {
const finalMessage = await convertMessagePartsToMessage([
pendingMessagePart
]);
- console.log("Contact reply", finalMessage, pendingMessagePart);
+ console.log(
+ "Contact Reply\n",
+ `\t| Message Status: ${finalMessage.send_status}\n`,
+ `\t| From Contact? : ${finalMessage.is_from_contact}\n`,
+ `\t| Contact Number: ${finalMessage.contact_number}\n`,
+ `\t| User Number: ${finalMessage.user_number}\n`,
+ `\t| Text: ${finalMessage.text.replace(/(\r\n|\n|\r)/gm, " ").substring(0, 45)}\n`,
+ `\t| Error Code: ${finalMessage.error_code}\n`,
+ `\t| Service: ${finalMessage.service || pendingMessagePart.service}\n`,
+ `\t| Media: ${finalMessage.media.length === 0 ? "No media" : finalMessage.media}\n`,
+ `\t| Message Service SID: ${finalMessage.messageservice_sid}\n`,
+ `\t| Service ID: ${finalMessage.service_id}\n`,
+ `\t| Parent ID: ${pendingMessagePart.parent_id}\n`,
+ `\t| User ID: ${finalMessage.user_id}`,
+ );
if (finalMessage) {
if (message.spokeCreatedAt) {
finalMessage.created_at = message.spokeCreatedAt;
@@ -618,10 +633,10 @@ export async function getContactInfo({
return {};
}
const twilio = await exports.getTwilio(organization);
- const types = ["carrier"];
+ const types = { fields: "line_type_intelligence" };
if (lookupName) {
// caller-name is more expensive
- types.push("caller-name");
+ types.fields = "line_type_intelligence,caller-name";
}
const contactInfo = {
contact_number: contactNumber,
@@ -629,24 +644,26 @@ export async function getContactInfo({
service: "twilio"
};
try {
- const phoneNumber = await twilio.lookups.v1
+ const phoneNumber = await twilio.lookups.v2
.phoneNumbers(contactNumber)
- .fetch({ type: types });
+ .fetch(types);
- if (phoneNumber.carrier) {
- contactInfo.carrier = phoneNumber.carrier.name;
+ if (phoneNumber.lineTypeIntelligence.carrier_name) {
+ contactInfo.carrier = phoneNumber.lineTypeIntelligence.carrier_name;
}
- if (phoneNumber.carrier.error_code) {
+ if (phoneNumber.lineTypeIntelligence.error_code) {
// e.g. 60600: Unprovisioned or Out of Coverage
contactInfo.status_code = -2;
- contactInfo.last_error_code = phoneNumber.carrier.error_code;
+ contactInfo.last_error_code = phoneNumber.lineTypeIntelligence.error_code;
} else if (
- phoneNumber.carrier.type &&
- phoneNumber.carrier.type === "landline"
+ // mobile or landline
+ phoneNumber.lineTypeIntelligence.type &&
+ phoneNumber.lineTypeIntelligence.type === "landline"
) {
// landline (not mobile or voip)
contactInfo.status_code = -1;
} else if (
+ // example: US
phoneNumber.countryCode &&
getConfig("PHONE_NUMBER_COUNTRY", organization) &&
getConfig("PHONE_NUMBER_COUNTRY", organization) !==
@@ -654,8 +671,8 @@ export async function getContactInfo({
) {
contactInfo.status_code = -3; // wrong country
} else if (
- phoneNumber.carrier.type &&
- phoneNumber.carrier.type !== "landline"
+ phoneNumber.lineTypeIntelligence.type &&
+ phoneNumber.lineTypeIntelligence.type !== "landline"
) {
// mobile, voip
contactInfo.status_code = 1;
@@ -695,6 +712,7 @@ export async function getContactInfo({
*/
export async function createMessagingService(organization, friendlyName) {
console.log("twilio.createMessagingService", organization.id, friendlyName);
+ const urlJoin = (...parts) => formatUrl({ pathname: joinPath(...parts) });
const twilio = await exports.getTwilio(organization);
const twilioBaseUrl =
getConfig("TWILIO_BASE_CALLBACK_URL", organization) ||
diff --git a/src/extensions/texter-sideboxes/contact-notes/react-component.js b/src/extensions/texter-sideboxes/contact-notes/react-component.js
index 3c634379e..085f8c51a 100644
--- a/src/extensions/texter-sideboxes/contact-notes/react-component.js
+++ b/src/extensions/texter-sideboxes/contact-notes/react-component.js
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import React from "react";
import { withRouter } from "react-router";
-import gql from "graphql-tag";
+import { gql } from "@apollo/client";
import _ from "lodash";
import TextField from "@material-ui/core/TextField";
diff --git a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js
index 72a0ec0da..05889770c 100644
--- a/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js
+++ b/src/extensions/texter-sideboxes/default-dynamicassignment/react-component.js
@@ -5,7 +5,7 @@ import Form from "react-formal";
import Badge from "@material-ui/core/Badge";
import Button from "@material-ui/core/Button";
import { withRouter } from "react-router";
-import gql from "graphql-tag";
+import { gql } from "@apollo/client";
import GSTextField from "../../../components/forms/GSTextField";
import { dataTest } from "../../../lib/attributes";
diff --git a/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js b/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js
index 149c746db..2c991c8e1 100644
--- a/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js
+++ b/src/extensions/texter-sideboxes/default-releasecontacts/react-component.js
@@ -8,7 +8,7 @@ import Button from "@material-ui/core/Button";
import Switch from "@material-ui/core/Switch";
import FormControlLabel from "@material-ui/core/FormControlLabel";
-import gql from "graphql-tag";
+import { gql } from "@apollo/client";
import GSTextField from "../../../components/forms/GSTextField";
import loadData from "../../../containers/hoc/load-data";
import theme from "../../../styles/mui-theme";
diff --git a/src/extensions/texter-sideboxes/tag-contact/react-component.js b/src/extensions/texter-sideboxes/tag-contact/react-component.js
index de451c200..4f1cc9f04 100644
--- a/src/extensions/texter-sideboxes/tag-contact/react-component.js
+++ b/src/extensions/texter-sideboxes/tag-contact/react-component.js
@@ -4,7 +4,6 @@ import { Link } from "react-router";
import * as yup from "yup";
import Form from "react-formal";
import { css } from "aphrodite";
-import { compose } from "recompose";
import CheckIcon from "@material-ui/icons/Check";
import DoneIcon from "@material-ui/icons/Done";
@@ -152,7 +151,7 @@ TexterSideboxBase.propTypes = {
onUpdateTags: type.func
};
-const TexterSidebox = compose(withMuiTheme)(TexterSideboxBase);
+const TexterSidebox = withMuiTheme(TexterSideboxBase);
export { TexterSidebox };
diff --git a/src/extensions/texter-sideboxes/take-conversations/react-component.js b/src/extensions/texter-sideboxes/take-conversations/react-component.js
index 1ebaf95f7..667a641b4 100644
--- a/src/extensions/texter-sideboxes/take-conversations/react-component.js
+++ b/src/extensions/texter-sideboxes/take-conversations/react-component.js
@@ -3,12 +3,13 @@ import React from "react";
import * as yup from "yup";
import Form from "react-formal";
import { withRouter } from "react-router";
-import gql from "graphql-tag";
+import { gql } from "@apollo/client";
import Button from "@material-ui/core/Button";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
+import GSIntegerField from "../../../components/forms/GSIntegerField"
import GSTextField from "../../../components/forms/GSTextField";
import loadData from "../../../containers/hoc/load-data";
@@ -196,7 +197,7 @@ export class AdminConfig extends React.Component {
/>
If batchsize is set to 0 it will stop showing this side panel
// has feedback to acknowledge
diff --git a/src/lib/__mocks__/timezones.js b/src/lib/__mocks__/timezones.js
deleted file mode 100644
index d3963efca..000000000
--- a/src/lib/__mocks__/timezones.js
+++ /dev/null
@@ -1,2 +0,0 @@
-const timezones = jest.genMockFromModule("../timezones");
-module.exports = timezones;
diff --git a/src/lib/__mocks__/tz-helpers.js b/src/lib/__mocks__/tz-helpers.js
deleted file mode 100644
index a7ed63207..000000000
--- a/src/lib/__mocks__/tz-helpers.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const tzHelpers = jest.genMockFromModule("../tz-helpers");
-
-tzHelpers.getProcessEnvDstReferenceTimezone = () => "US/Eastern";
-
-module.exports = tzHelpers;
diff --git a/src/lib/__mocks__/zip-format.js b/src/lib/__mocks__/zip-format.js
deleted file mode 100644
index d8e5a3fca..000000000
--- a/src/lib/__mocks__/zip-format.js
+++ /dev/null
@@ -1,2 +0,0 @@
-const zipFormat = jest.genMockFromModule("../zip-format");
-module.exports = zipFormat;
diff --git a/src/lib/conversations.js b/src/lib/conversations.js
index 220c3f2c8..4a8179e1f 100644
--- a/src/lib/conversations.js
+++ b/src/lib/conversations.js
@@ -97,7 +97,7 @@ export const getConversationFiltersFromQuery = (query, organizationTags) => {
filters.contactsFilter.messageStatus = query.messageStatus;
}
if (query.errorCode) {
- filters.contactsFilter.errorCode = query.errorCode.split(",");
+ filters.contactsFilter.errorCode = query.errorCode.split(",").map(Number);
}
if (query.tags) {
if (/^[a-z]/.test(query.tags)) {
diff --git a/src/lib/permissions.js b/src/lib/permissions.js
index a5fe5cc00..f65fa08db 100644
--- a/src/lib/permissions.js
+++ b/src/lib/permissions.js
@@ -13,8 +13,7 @@ export const isRoleGreater = (role1, role2) =>
export const hasRoleAtLeast = (hasRole, wantsRole) =>
ROLE_HIERARCHY.indexOf(hasRole) >= ROLE_HIERARCHY.indexOf(wantsRole);
-export const getHighestRole = roles =>
- roles.sort(isRoleGreater)[roles.length - 1];
+export const getHighestRole = roles => [...roles].sort(isRoleGreater).pop();
export const hasRole = (role, roles) =>
hasRoleAtLeast(getHighestRole(roles), role);
diff --git a/src/network/apollo-client-singleton.js b/src/network/apollo-client-singleton.js
index d100c3e29..a86af1405 100644
--- a/src/network/apollo-client-singleton.js
+++ b/src/network/apollo-client-singleton.js
@@ -1,10 +1,9 @@
import fetch from "isomorphic-fetch";
-import { ApolloClient } from "apollo-client";
-import { ApolloLink } from "apollo-link";
-import { createHttpLink } from "apollo-link-http";
-import { onError } from "apollo-link-error";
-import { InMemoryCache } from "apollo-cache-inmemory";
-import { getMainDefinition } from "apollo-utilities";
+import { ApolloClient, ApolloLink } from "@apollo/client";
+import { createHttpLink } from "@apollo/client/link/http";
+import { onError } from "@apollo/client/link/error";
+import { InMemoryCache } from "@apollo/client/cache";
+import { getMainDefinition } from "@apollo/client/utilities";
import omitDeep from "omit-deep-lodash";
const httpLink = createHttpLink({
@@ -61,11 +60,32 @@ const cache = new InMemoryCache({
}
return null;
},
- // FUTURE: Apollo Client 3.0 allows this much more easily:
typePolicies: {
ContactTag: {
// key is just the tag id and the value is contact-specific
keyFields: false
+ },
+ // Define custom merge functions for multiple fields
+ // https://go.apollo.dev/c/merging-non-normalized-objects
+ // https://www.apollographql.com/docs/react/caching/cache-field-behavior/#merging-arrays
+ Query: {
+ fields: {
+ organization: {
+ merge: true
+ }
+ }
+ },
+ Campaign: {
+ fields: {
+ ingestMethod: {
+ merge: true
+ },
+ pendingJobs: {
+ merge(existing = [], incoming) {
+ return incoming;
+ }
+ }
+ }
}
}
});
diff --git a/src/network/response-middleware-network-interface.js b/src/network/response-middleware-network-interface.js
index 6778f6bc1..284bebd9e 100644
--- a/src/network/response-middleware-network-interface.js
+++ b/src/network/response-middleware-network-interface.js
@@ -1,9 +1,8 @@
-import { createNetworkInterface } from "apollo-client";
-import fetch from "isomorphic-fetch";
+import { HttpLink } from "@apollo/client/link/http";
class ResponseMiddlewareNetworkInterface {
constructor(endpoint = "/graphql", options = {}) {
- this.defaultNetworkInterface = createNetworkInterface(endpoint, options);
+ this.defaultNetworkInterface = HttpLink(endpoint, options);
this.responseMiddlewares = [];
}
diff --git a/src/server/api/conversations.js b/src/server/api/conversations.js
index 202019768..c5719db5f 100644
--- a/src/server/api/conversations.js
+++ b/src/server/api/conversations.js
@@ -4,6 +4,7 @@ import { addWhereClauseForContactsFilterMessageStatusIrrespectiveOfPastDue } fro
import { addCampaignsFilterToQuery } from "./campaign";
import { log } from "../../lib";
import { getConfig } from "../api/lib/config";
+import { isSqlite } from "../models/index";
function getConversationsJoinsAndWhereClause(
queryParam,
@@ -157,6 +158,12 @@ function mapQueryFieldsToResolverFields(queryResult, fieldsMap) {
}
return key;
});
+ if (typeof data.updated_at != "undefined") {
+ data.updated_at = (
+ data.updated_at instanceof Date || !data.updated_at
+ ? data.updated_at || null
+ : new Date(data.updated_at))
+ }
return data;
}
@@ -198,20 +205,20 @@ export async function getConversations(
.offset(cursor.offset);
}
console.log(
- "getConversations sql",
- awsContext && awsContext.awsRequestId,
- cursor,
- assignmentsFilter,
- offsetLimitQuery.toString()
+ `Org Id: ${organizationId} :: getConversations sql -- \n`,
+ `\tawsContext: ${awsContext && awsContext.awsRequestId ? true : false}\n`,
+ `\tcursor: limit=${cursor.limit}, offset=${cursor.offset}\n`,
+ `\tassignmentsFilter: ${Object.keys(assignmentsFilter).length > 0 ? assignmentsFilter : "no filter"}\n`,
+ `\toffsetLimitQuery: ${offsetLimitQuery.toString()}`
);
const ccIdRows = await offsetLimitQuery;
console.log(
- "getConversations contact ids",
- awsContext && awsContext.awsRequestId,
- Number(new Date()) - Number(starttime),
- ccIdRows.length
+ `Org Id: ${organizationId} :: getConversations query1 contact ids -- \n`,
+ `\tawsContext: ${awsContext && awsContext.awsRequestId === undefined ? true : false}\n`,
+ `\ttime: ${Number(new Date()) - Number(starttime)}ms\n`,
+ `\tccIdRows length: ${ccIdRows.length}`
);
const ccIds = ccIdRows.map(ccIdRow => {
return ccIdRow.cc_id;
@@ -265,10 +272,10 @@ export async function getConversations(
query = query.orderBy("cc_id", "desc").orderBy("message.id");
const conversationRows = await query;
console.log(
- "getConversations query2 result",
- awsContext && awsContext.awsRequestId,
- Number(new Date()) - Number(starttime),
- conversationRows.length
+ `Org Id: ${organizationId} :: getConversations query2 conversations -- \n`,
+ `\tawsContext: ${awsContext && awsContext.awsRequestId === undefined ? true : false}\n`,
+ `\ttime: ${Number(new Date()) - Number(starttime)}ms\n`,
+ `\tconversationRows lenght: ${conversationRows.length}`
);
/* collapse the rows to produce an array of objects, with each object
* containing the fields for one conversation, each having an array of
@@ -331,8 +338,9 @@ export async function getConversations(
/* Query #3 -- get the count of all conversations matching the criteria.
* We need this to show total number of conversations to support paging */
console.log(
- "getConversations query3",
- Number(new Date()) - Number(starttime)
+ "getConversations query3 total count + time for total completion of queries\n",
+ `\ttime: ${Number(new Date()) - Number(starttime)}ms\n`,
+ `\ttotal conversations: ${conversations.length}`
);
const conversationsCountQuery = getConversationsJoinsAndWhereClause(
r.knexReadOnly,
@@ -347,7 +355,9 @@ export async function getConversations(
let conversationCount;
try {
conversationCount = await r.getCount(
- conversationsCountQuery.timeout(4000, { cancel: true })
+ !isSqlite ?
+ conversationsCountQuery.timeout(4000, { cancel: true }) :
+ conversationsCountQuery
);
} catch (err) {
// default fake value that means 'a lot'
@@ -473,7 +483,7 @@ export async function reassignConversations(
returnCampaignIdAssignmentIds.push({
campaignId,
- assignmentId: assignmentId.toString()
+ assignmentId: assignmentId?.toString()
});
}
}
diff --git a/src/server/api/errors.js b/src/server/api/errors.js
index 617b00d05..4e9134650 100644
--- a/src/server/api/errors.js
+++ b/src/server/api/errors.js
@@ -1,12 +1,9 @@
-import { GraphQLError } from "graphql/error";
+import { GraphQLError } from "graphql";
import { r, cacheableData } from "../models";
export function authRequired(user) {
if (!user) {
- throw new GraphQLError({
- status: 401,
- message: "You must login to access that resource."
- });
+ throw new GraphQLError("You must login to access that resource.");
}
}
@@ -24,7 +21,11 @@ export async function accessRequired(
return;
}
// require a permission at-or-higher than the permission requested
- const hasRole = await cacheableData.user.userHasRole(user, orgId, role);
+ const hasRole = await cacheableData.user.userHasRole(
+ user,
+ orgId.toString(),
+ role
+ );
if (!hasRole) {
const error = new GraphQLError(
"You are not authorized to access that resource."
@@ -68,7 +69,7 @@ export async function assignmentRequiredOrAdminRole(
const roleRequired = userHasAssignment ? "TEXTER" : "SUPERVOLUNTEER";
const hasPermission = await cacheableData.user.userHasRole(
user,
- orgId,
+ orgId.toString(),
roleRequired
);
if (!hasPermission) {
diff --git a/src/server/api/lib/import-script.js b/src/server/api/lib/import-script.js
index d1eb96e0b..924c01939 100644
--- a/src/server/api/lib/import-script.js
+++ b/src/server/api/lib/import-script.js
@@ -5,19 +5,35 @@ import { compose, map, reduce, getOr, find, filter, has } from "lodash/fp";
import { r, cacheableData } from "../../models";
import { getConfig } from "./config";
+import { base64ToString } from "./utils";
const textRegex = RegExp(".*[A-Za-z0-9]+.*");
const getDocument = async documentId => {
- const auth = google.auth.fromJSON(JSON.parse(getConfig("GOOGLE_SECRET")));
- auth.scopes = ["https://www.googleapis.com/auth/documents"];
+ let result = null;
+ let base64Key = getConfig("BASE64_GOOGLE_SECRET");
+
+ if (!base64Key) {
+ throw new Error('The BASE64_GOOGLE_SECRET enviroment variable was not found!');
+ }
+
+ // decodes
+ let key = base64ToString(base64Key);
+
+ try {
+ key = JSON.parse(key);
+ } catch(err) {
+ throw new Error('BASE64_GOOGLE_SECRET failed to parse', err);
+ };
+
+ const auth = google.auth.fromJSON(key);
+ auth.scopes = ["https://www.googleapis.com/auth/documents.readonly"];
const docs = google.docs({
version: "v1",
auth
});
- let result = null;
try {
result = await docs.documents.get({
documentId
@@ -286,7 +302,7 @@ const saveInteractionsHierarchyNode = async (
.returning("id");
for (const child of interactionsHierarchyNode.children) {
- await saveInteractionsHierarchyNode(trx, campaignId, child, nodeId[0]);
+ await saveInteractionsHierarchyNode(trx, campaignId, child, nodeId[0].id);
}
};
diff --git a/src/server/api/lib/utils.js b/src/server/api/lib/utils.js
index c0b9218b0..128d84a30 100644
--- a/src/server/api/lib/utils.js
+++ b/src/server/api/lib/utils.js
@@ -60,3 +60,11 @@ export const groupCannedResponses = cannedResponses => {
export const replaceAll = (str, find, replace) =>
str.replace(new RegExp(escapeRegExp(find), "g"), replace);
+
+export const base64ToString = (str) => {
+ if(str && typeof(str) === "string") {
+ const buff = new Buffer.from(str, 'base64');
+ return buff.toString('utf-8');
+ }
+ return "";
+}
diff --git a/src/server/api/message.js b/src/server/api/message.js
index 0f9aa5fd0..b46530751 100644
--- a/src/server/api/message.js
+++ b/src/server/api/message.js
@@ -4,9 +4,14 @@ import { Message } from "../models";
export const resolvers = {
Message: {
...mapFieldsToModel(
- ["text", "userNumber", "contactNumber", "createdAt", "isFromContact"],
+ ["text", "userNumber", "contactNumber", "isFromContact"],
Message
),
+ createdAt: msg => (
+ msg.created_at instanceof Date || !msg.created_at
+ ? msg.created_at || null
+ : new Date(msg.created_at)
+ ),
media: msg =>
// Sometimes it's array, sometimes string. Maybe db vs. cache?
typeof msg.media === "string" ? JSON.parse(msg.media) : msg.media || [],
diff --git a/src/server/api/mutations/bulkSendMessages.js b/src/server/api/mutations/bulkSendMessages.js
index 8750f2a66..9503947d6 100644
--- a/src/server/api/mutations/bulkSendMessages.js
+++ b/src/server/api/mutations/bulkSendMessages.js
@@ -1,10 +1,10 @@
-import camelCaseKeys from "camelcase-keys";
+import { GraphQLError } from "graphql";
+import { camelizeKeys } from "humps";
import { getContacts } from "../assignment";
-import { GraphQLError } from "graphql/error";
import { getConfig } from "../lib/config";
import { applyScript } from "../../../lib/scripts";
-import { Assignment, User, r, cacheableData } from "../../models";
+import { User, r, cacheableData } from "../../models";
import { log } from "../../../lib";
@@ -25,10 +25,7 @@ export const bulkSendMessages = async (
!getConfig("ALLOW_SEND_ALL_ENABLED", organization)
) {
log.error("Not allowed to send all messages at once");
- throw new GraphQLError({
- status: 403,
- message: "Not allowed to send all messages at once"
- });
+ throw new GraphQLError("Not allowed to send all messages at once");
}
// Assign some contacts
@@ -76,7 +73,7 @@ export const bulkSendMessages = async (
const topmostParent = interactionSteps[0];
- const texter = camelCaseKeys(await User.get(assignment.user_id));
+ const texter = camelizeKeys(await User.get(assignment.user_id));
let customFields = Object.keys(JSON.parse(contacts[0].custom_fields));
const texterSideboxes = getConfig("TEXTER_SIDEBOXES") || "";
@@ -89,7 +86,7 @@ export const bulkSendMessages = async (
const promises = contacts.map(async contact => {
contact.customFields = contact.custom_fields;
const text = applyScript({
- contact: camelCaseKeys(contact),
+ contact: camelizeKeys(contact),
texter,
script: topmostParent.script,
customFields
diff --git a/src/server/api/mutations/getOptOutMessage.js b/src/server/api/mutations/getOptOutMessage.js
index 541ee18c0..8e907412a 100644
--- a/src/server/api/mutations/getOptOutMessage.js
+++ b/src/server/api/mutations/getOptOutMessage.js
@@ -1,3 +1,4 @@
+import { getConfig } from "../lib/config";
import optOutMessageCache from "../../models/cacheable_queries/opt-out-message";
import zipStateCache from "../../models/cacheable_queries/zip";
@@ -5,6 +6,9 @@ export const getOptOutMessage = async (
_,
{ organizationId, zip, defaultMessage }
) => {
+ if (!getConfig("OPT_OUT_PER_STATE")) {
+ return defaultMessage;
+ }
try {
const queryResult = await optOutMessageCache.query({
organizationId: organizationId,
diff --git a/src/server/api/mutations/joinOrganization.js b/src/server/api/mutations/joinOrganization.js
index a865295d7..3ae4e59ac 100644
--- a/src/server/api/mutations/joinOrganization.js
+++ b/src/server/api/mutations/joinOrganization.js
@@ -1,4 +1,4 @@
-import { GraphQLError } from "graphql/error";
+import { GraphQLError } from "graphql";
import { r, cacheableData } from "../../models";
import { hasRole } from "../../../lib";
@@ -11,6 +11,7 @@ const INVALID_JOIN = () => {
return error;
};
+// eslint-disable-next-line import/prefer-default-export
export const joinOrganization = async (
_,
{ organizationUuid, campaignId, queryParams },
@@ -64,8 +65,8 @@ export const joinOrganization = async (
userOrg = await r
.knex("user_organization")
.where({
- user_id: user.id,
- organization_id: organization.id
+ user_id: user.id.toString(),
+ organization_id: organization.id.toString()
})
.select("role")
.first();
diff --git a/src/server/api/mutations/sendMessage.js b/src/server/api/mutations/sendMessage.js
index 8c285655e..4fd17ce9c 100644
--- a/src/server/api/mutations/sendMessage.js
+++ b/src/server/api/mutations/sendMessage.js
@@ -1,4 +1,4 @@
-import { GraphQLError } from "graphql/error";
+import { GraphQLError } from "graphql";
import { Message, cacheableData } from "../../models";
diff --git a/src/server/api/mutations/updateServiceManager.js b/src/server/api/mutations/updateServiceManager.js
index d6e74b395..ae54c5cc1 100644
--- a/src/server/api/mutations/updateServiceManager.js
+++ b/src/server/api/mutations/updateServiceManager.js
@@ -1,5 +1,3 @@
-import { GraphQLError } from "graphql/error";
-import { getConfig } from "../../../server/api/lib/config";
import { cacheableData } from "../../../server/models";
import { processServiceManagers } from "../../../extensions/service-managers";
import { accessRequired } from "../errors";
diff --git a/src/server/api/mutations/updateServiceVendorConfig.js b/src/server/api/mutations/updateServiceVendorConfig.js
index 940903cae..e688868c9 100644
--- a/src/server/api/mutations/updateServiceVendorConfig.js
+++ b/src/server/api/mutations/updateServiceVendorConfig.js
@@ -1,4 +1,4 @@
-import { GraphQLError } from "graphql/error";
+import { GraphQLError } from "graphql";
import {
getConfigKey,
getService,
diff --git a/src/server/api/phone.js b/src/server/api/phone.js
index c1b4203ad..6e6a8317b 100644
--- a/src/server/api/phone.js
+++ b/src/server/api/phone.js
@@ -1,5 +1,5 @@
import { GraphQLScalarType } from "graphql";
-import { GraphQLError } from "graphql/error";
+import { GraphQLError } from "graphql";
import { Kind } from "graphql/language";
const identity = value => value;
@@ -15,13 +15,12 @@ export const GraphQLPhone = new GraphQLScalarType({
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
- `Query error: Can only parse strings got a: ${ast.kind}`,
- [ast]
+ `Query error: Can only parse strings got a: ${ast.kind}`
);
}
if (!pattern.test(ast.value)) {
- throw new GraphQLError("Query error: Not a valid Phone", [ast]);
+ throw new GraphQLError("Query error: Not a valid Phone");
}
return ast.value;
diff --git a/src/server/api/schema.js b/src/server/api/schema.js
index c0abaa43b..86730b90a 100644
--- a/src/server/api/schema.js
+++ b/src/server/api/schema.js
@@ -1,6 +1,6 @@
import GraphQLDate from "graphql-date";
import GraphQLJSON from "graphql-type-json";
-import { GraphQLError } from "graphql/error";
+import { GraphQLError } from "graphql";
import isUrl from "is-url";
import _ from "lodash";
import { gzip, makeTree, getHighestRole } from "../../lib";
@@ -202,7 +202,7 @@ async function editCampaign(id, campaign, loaders, user, origCampaignRecord) {
campaign.organizationId || origCampaignRecord.organization_id;
await accessRequired(
user,
- organizationId,
+ organizationId.toString(),
"SUPERVOLUNTEER",
/* superadmin*/ true
);
@@ -421,7 +421,10 @@ async function editCampaign(id, campaign, loaders, user, origCampaignRecord) {
});
}
- return Campaign.get(id);
+ const toReturn = await Campaign.get(id.toString());
+ toReturn.id = toReturn.id.toString();
+ toReturn.organization_id = toReturn.organization_id.toString();
+ return toReturn;
}
async function updateInteractionSteps(
@@ -681,7 +684,8 @@ const rootMutations = {
const organization = await loaders.organization.load(organizationId);
- const passportStrategy = getConfig("PASSPORT_STRATEGY", organization) || "auth0";
+ const passportStrategy =
+ getConfig("PASSPORT_STRATEGY", organization) || "auth0";
if (passportStrategy === "auth0") {
const { email } = await r
.knex("user")
@@ -1060,7 +1064,7 @@ const rootMutations = {
} else {
await accessRequired(
user,
- origCampaign.organization_id,
+ origCampaign.organization_id.toString(),
"SUPERVOLUNTEER"
);
}
@@ -1069,10 +1073,9 @@ const rootMutations = {
campaign.hasOwnProperty("contacts") &&
campaign.contacts
) {
- throw new GraphQLError({
- status: 400,
- message: "Not allowed to add contacts after the campaign starts"
- });
+ throw new GraphQLError(
+ "Not allowed to add contacts after the campaign starts"
+ );
}
return editCampaign(id, campaign, loaders, user, origCampaign);
},
@@ -1126,10 +1129,7 @@ const rootMutations = {
authRequired(user);
const invite = await Invite.get(inviteId);
if (!invite || !invite.is_valid) {
- throw new GraphQLError({
- status: 400,
- message: "That invitation is no longer valid"
- });
+ throw new GraphQLError("That invitation is no longer valid");
}
const newOrganization = await Organization.save({
@@ -1150,6 +1150,7 @@ const rootMutations = {
{ conflict: "update" }
);
+ newOrganization.id = newOrganization.id.toString();
return newOrganization;
},
resetOrganizationJoinLink: async (_, { organizationId }, { user }) => {
diff --git a/src/server/index.js b/src/server/index.js
index bf539ef76..21458f5f7 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -2,10 +2,8 @@ import "babel-polyfill";
import bodyParser from "body-parser";
import express from "express";
import appRenderer from "./middleware/app-renderer";
-import { graphqlExpress, graphiqlExpress } from "apollo-server-express";
-import { makeExecutableSchema } from "graphql-tools";
+import { makeExecutableSchema } from "@graphql-tools/schema";
// ORDERING: ./models import must be imported above ./api to help circular imports
-import { replaceEasyGsmWins } from "../lib/gsm";
import { createLoaders, createTablesIfNecessary, r } from "./models";
import { resolvers } from "./api/schema";
import { schema } from "../api/schema";
@@ -21,7 +19,12 @@ import { setupUserNotificationObservers } from "./notifications";
import { existsSync } from "fs";
import { rawAllMethods } from "../extensions/contact-loaders";
import herokuSslRedirect from "heroku-ssl-redirect";
-import { GraphQLError } from "graphql/error";
+import { GraphQLError } from "graphql";
+import { ApolloServer } from "@apollo/server";
+import { expressMiddleware } from "@apollo/server/express4";
+import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
+import http from "http";
+import cors from "cors";
process.on("uncaughtException", ex => {
log.error(ex);
@@ -53,153 +56,150 @@ const app = express();
// Heroku requires you to use process.env.PORT
const port = process.env.DEV_APP_PORT || process.env.PORT;
-// Don't rate limit heroku
-app.enable("trust proxy");
+const httpServer = http.createServer(app);
-if (process.env.HEROKU_APP_NAME) {
- // if on Heroku redirect to https if accessed via http
- app.use(herokuSslRedirect());
-}
+const executableSchema = makeExecutableSchema({
+ typeDefs: schema,
+ resolvers,
+ allowUndefinedInResolve: false
+});
+
+const server = new ApolloServer({
+ typeDefs: schema,
+ schema: executableSchema,
+ resolvers,
+ introspection: true,
+ plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
+ formatError: error => {
+ if (process.env.SHOW_SERVER_ERROR || process.env.DEBUG) {
+ if (error instanceof GraphQLError) {
+ return error;
+ }
+ return new GraphQLError(error.message);
+ }
+
+ return new GraphQLError(
+ error &&
+ error.originalError &&
+ error.originalError.code === "UNAUTHORIZED"
+ ? "UNAUTHORIZED"
+ : "Internal server error"
+ );
+ }
+});
+
+server.start().then(() => {
+ // Don't rate limit heroku
+ app.enable("trust proxy");
+
+ if (process.env.HEROKU_APP_NAME) {
+ // if on Heroku redirect to https if accessed via http
+ app.use(herokuSslRedirect());
+ }
+
+ // Serve static assets
+ if (existsSync(process.env.ASSETS_DIR)) {
+ app.use(
+ "/assets",
+ express.static(process.env.ASSETS_DIR, {
+ maxAge: "180 days"
+ })
+ );
+ }
+
+ app.use(bodyParser.json({ limit: "50mb" }));
+ app.use(bodyParser.urlencoded({ extended: true }));
-// Serve static assets
-if (existsSync(process.env.ASSETS_DIR)) {
app.use(
- "/assets",
- express.static(process.env.ASSETS_DIR, {
- maxAge: "180 days"
+ cookieSession({
+ cookie: {
+ httpOnly: true,
+ secure: !DEBUG,
+ maxAge: null
+ },
+ secret: process.env.SESSION_SECRET || global.SESSION_SECRET
})
);
-}
-app.use(bodyParser.json({ limit: "50mb" }));
-app.use(bodyParser.urlencoded({ extended: true }));
-
-app.use(
- cookieSession({
- cookie: {
- httpOnly: true,
- secure: !DEBUG,
- maxAge: null
- },
- secret: process.env.SESSION_SECRET || global.SESSION_SECRET
- })
-);
-app.use(passport.initialize());
-app.use(passport.session());
-
-app.use((req, res, next) => {
- const getContext = app.get("awsContextGetter");
- if (typeof getContext === "function") {
- const [event, context] = getContext(req, res);
- req.awsEvent = event;
- req.awsContext = context;
- }
- next();
-});
+ app.use(passport.initialize());
+ app.use(passport.session());
-// Simulate latency in local development
-if (process.env.SIMULATE_DELAY_MILLIS) {
app.use((req, res, next) => {
- setTimeout(next, Number(process.env.SIMULATE_DELAY_MILLIS));
+ const getContext = app.get("awsContextGetter");
+ if (typeof getContext === "function") {
+ const [event, context] = getContext(req, res);
+ req.awsEvent = event;
+ req.awsContext = context;
+ }
+ next();
});
-}
-// give contact loaders a chance
-const configuredIngestMethods = rawAllMethods();
-Object.keys(configuredIngestMethods).forEach(ingestMethodName => {
- const ingestMethod = configuredIngestMethods[ingestMethodName];
- if (ingestMethod && ingestMethod.addServerEndpoints) {
- ingestMethod.addServerEndpoints(app);
+ // Simulate latency in local development
+ if (process.env.SIMULATE_DELAY_MILLIS) {
+ app.use((req, res, next) => {
+ setTimeout(next, Number(process.env.SIMULATE_DELAY_MILLIS));
+ });
}
-});
-const routeAdders = {
- get: (_app, route, ...handlers) => _app.get(route, ...handlers),
- post: (_app, route, ...handlers) => _app.post(route, ...handlers)
-};
+ // give contact loaders a chance
+ const configuredIngestMethods = rawAllMethods();
+ Object.keys(configuredIngestMethods).forEach(ingestMethodName => {
+ const ingestMethod = configuredIngestMethods[ingestMethodName];
+ if (ingestMethod && ingestMethod.addServerEndpoints) {
+ ingestMethod.addServerEndpoints(app);
+ }
+ });
-messagingServicesAddServerEndpoints(app, routeAdders);
+ const routeAdders = {
+ get: (_app, route, ...handlers) => _app.get(route, ...handlers),
+ post: (_app, route, ...handlers) => _app.post(route, ...handlers)
+ };
-app.get("/logout-callback", (req, res) => {
- req.logOut();
- res.redirect("/");
-});
+ messagingServicesAddServerEndpoints(app, routeAdders);
-const loginCallbacks = passportSetup[
- process.env.PASSPORT_STRATEGY || global.PASSPORT_STRATEGY || "auth0"
-](app);
+ app.get("/logout-callback", (req, res) => {
+ req.logOut();
+ res.redirect("/");
+ });
-if (loginCallbacks) {
- app.get("/login-callback", ...loginCallbacks.loginCallback);
- app.post("/login-callback", ...loginCallbacks.loginCallback);
-}
+ const loginCallbacks = passportSetup[
+ process.env.PASSPORT_STRATEGY || global.PASSPORT_STRATEGY || "auth0"
+ ](app);
-const executableSchema = makeExecutableSchema({
- typeDefs: schema,
- resolvers,
- allowUndefinedInResolve: false
-});
+ if (loginCallbacks) {
+ app.get("/login-callback", ...loginCallbacks.loginCallback);
+ app.post("/login-callback", ...loginCallbacks.loginCallback);
+ }
-app.use(
- "/graphql",
- graphqlExpress(request => ({
- schema: executableSchema,
- context: {
- loaders: createLoaders(),
- user: request.user,
- awsContext: request.awsContext || null,
- awsEvent: request.awsEvent || null,
- remainingMilliseconds: () =>
- request.awsContext && request.awsContext.getRemainingTimeInMillis
- ? request.awsContext.getRemainingTimeInMillis()
- : 5 * 60 * 1000 // default saying 5 min, no matter what
- },
- formatError: error => {
- log.error({
- userId: request.user && request.user.id,
- code:
- (error && error.originalError && error.originalError.code) ||
- "INTERNAL_SERVER_ERROR",
- error,
- msg: "GraphQL error"
- });
- telemetry
- .formatRequestError(error, request)
- // drop if this fails
- .catch(() => {})
- .then(() => {});
-
- if (process.env.SHOW_SERVER_ERROR || process.env.DEBUG) {
- if (error instanceof GraphQLError) {
- return error;
- }
- return new GraphQLError(error.message);
+ app.use(
+ "/graphql",
+ cors(),
+ expressMiddleware(server, {
+ schema: executableSchema,
+ context: data => {
+ const req = data.req;
+ return {
+ loaders: createLoaders(),
+ user: req.user,
+ awsContext: req.awsContext || null,
+ awsEvent: req.awsEvent || null,
+ remainingMilliseconds: () =>
+ req.awsContext && req.awsContext.getRemainingTimeInMillis
+ ? req.awsContext.getRemainingTimeInMillis()
+ : 5 * 60 * 1000 // default saying 5 min, no matter what
+ };
}
+ })
+ );
- return new GraphQLError(
- error &&
- error.originalError &&
- error.originalError.code === "UNAUTHORIZED"
- ? "UNAUTHORIZED"
- : "Internal server error"
- );
- }
- }))
-);
-app.get(
- "/graphiql",
- graphiqlExpress({
- endpointURL: "/graphql"
- })
-);
-
-// This middleware should be last. Return the React app only if no other route is hit.
-app.use(appRenderer);
-
-if (port) {
- app.listen(port, () => {
- log.info(`Node app is running on port ${port}`);
- });
-}
+ // This middleware should be last. Return the React app only if no other route is hit.
+ app.use(appRenderer);
+
+ if (port) {
+ httpServer.listen(port, () => {
+ log.info(`Node app is running on port ${port}`);
+ });
+ }
+});
export default app;
diff --git a/src/server/knex-connect.js b/src/server/knex-connect.js
index 5252c8bf2..a3cc7556e 100644
--- a/src/server/knex-connect.js
+++ b/src/server/knex-connect.js
@@ -21,6 +21,14 @@ const {
const min = parseInt(DB_MIN_POOL, 10);
const max = parseInt(DB_MAX_POOL, 10);
+if (!global.TextEncoder || !global.TextDecoder) {
+ // eslint-disable-next-line global-require
+ const util = require("util");
+
+ global.TextEncoder = util.TextEncoder;
+ global.TextDecoder = util.TextDecoder;
+}
+
const pg = require("pg");
const { parse: pgDbUrlParser } = require("pg-connection-string");
diff --git a/src/server/lib/http-request.js b/src/server/lib/http-request.js
index a9e82e3f9..3e953f667 100644
--- a/src/server/lib/http-request.js
+++ b/src/server/lib/http-request.js
@@ -1,5 +1,4 @@
import originalFetch from "node-fetch";
-import AbortController from "node-abort-controller";
import { log } from "../../lib";
import { sleep } from "../../workers/lib";
import { v4 as uuid } from "uuid";
diff --git a/src/server/middleware/render-index.js b/src/server/middleware/render-index.js
index d8ad777f0..f2e52be38 100644
--- a/src/server/middleware/render-index.js
+++ b/src/server/middleware/render-index.js
@@ -1,7 +1,28 @@
import { hasConfig, getConfig } from "../api/lib/config";
import { getProcessEnvTz, getProcessEnvDstReferenceTimezone } from "../../lib";
+import { base64ToString } from "../api/lib/utils";
-const canGoogleImport = hasConfig("GOOGLE_SECRET");
+const canGoogleImport = hasConfig("BASE64_GOOGLE_SECRET");
+
+const googleClientEmail = () => {
+ let output;
+ if (canGoogleImport) {
+ try {
+ const s_GOOGLE_SECRET = base64ToString(process.env.BASE64_GOOGLE_SECRET);
+ output = (JSON.parse((
+ s_GOOGLE_SECRET
+ .replace(/(\r\n|\n|\r)/gm, ""))) // new lines gum up parsing
+ .client_email)
+ .replaceAll(" ", "");
+ } catch (err) {
+ console.error(`
+ Google API failed to load client email.
+ Please check your BASE64_GOOGLE_SECRET environment variable is intact: `,
+ err);
+ }
+ }
+ return (output || "");
+};
const rollbarScript = process.env.ROLLBAR_CLIENT_TOKEN
? `