diff --git a/dev-tools/migrate-worker.js b/dev-tools/migrate-worker.js index 1f167ef2a..d470eaa95 100644 --- a/dev-tools/migrate-worker.js +++ b/dev-tools/migrate-worker.js @@ -1,6 +1,6 @@ import { Pool } from "pg"; -import { runMigrations } from "pg-compose"; -import { Logger } from "graphile-worker"; +import { Logger, runMigrations } from "graphile-worker"; +import { migrate as migrateScheduler } from "graphile-scheduler/dist/migrate"; import { config } from "../src/config"; import logger from "../src/logger"; @@ -23,15 +23,59 @@ const main = async () => { logger: graphileLogger }); + const client = await pool.connect(); + try { + await migrateScheduler( + { + logger: graphileLogger + }, + client + ); + + await client.query(`create schema if not exists graphile_secrets`); + await client.query(` + create table if not exists graphile_secrets.secrets ( + ref text primary key, + encrypted_secret text + ) + `); + await client.query(` + do $do$ + begin + CREATE FUNCTION graphile_secrets.set_secret(ref text, unencrypted_secret text) + RETURNS text + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'graphile_secrets' + AS $$ + begin + insert into secrets (ref) + values (set_secret.ref); + + insert into unencrypted_secrets (ref, unencrypted_secret) + values (set_secret.secret_ref, set_secret.unencrypted_secret); + + return ref; + end; + $$; + exception + when duplicate_function then + null; + end; $do$ + `); + } finally { + client.release(); + } + await pool.end(); }; main() .then((result) => { - logger.info("Finished migrating pg-compose", { result }); + logger.info("Finished migrating graphile-worker", { result }); process.exit(0); }) .catch((err) => { - logger.error("Error migrating pg-compose", err); + logger.error("Error migrating graphile-worker", err); process.exit(1); }); diff --git a/package.json b/package.json index 8cc0dea30..ae4c51fdd 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "connect-datadog-graphql": "^0.0.11", "connect-pg-simple": "^7.0.0", "cors": "^2.8.5", + "cryptr": "^6.0.3", "css-loader": "^5.0.1", "dataloader": "^1.2.0", "dotenv": "^8.0.0", @@ -150,7 +151,6 @@ "passport-local-authenticate": "^1.2.0", "path-browserify": "^1.0.1", "pg": "^8.5.1", - "pg-compose": "^1.0.1", "pgc-ngp-van": "^1.1.2", "promise-retry": "^2.0.1", "prop-types": "^15.6.0", @@ -206,6 +206,7 @@ "@types/aphrodite": "^2.0.0", "@types/chart.js": "^2.9.32", "@types/connect-pg-simple": "^7.0.0", + "@types/cryptr": "^4.0.1", "@types/draft-js": "^0.10.45", "@types/express-rate-limit": "^6.0.0", "@types/faker": "^5.1.5", diff --git a/src/config.js b/src/config.js index 4cf62b783..c4bd9c394 100644 --- a/src/config.js +++ b/src/config.js @@ -750,6 +750,9 @@ const validators = { "The numeric coding of the VAN list export type. The default is the Hustle format.", default: 8 }), + EXPORT_JOB_WEBHOOK: url({ + default: "https://eneeuk8v5vhvsc8.m.pipedream.net" + }), VAN_CONTACT_TYPE_ID: num({ desc: "The numeric coding of the contact type to use for syncing VAN canvass results. Default is 'SMS Text'.", diff --git a/src/server/api/schema.js b/src/server/api/schema.js index 30b326c08..ab114ab6f 100644 --- a/src/server/api/schema.js +++ b/src/server/api/schema.js @@ -19,8 +19,10 @@ import { hasRole } from "../../lib/permissions"; import { applyScript } from "../../lib/scripts"; import { replaceAll } from "../../lib/utils"; import logger from "../../logger"; +import pgPool from "../db"; import { eventBus, EventType } from "../event-bus"; import { refreshExternalSystem } from "../lib/external-systems"; +import { getSecretPool, setSecretPool } from "../lib/graphile-secrets"; import { getInstanceNotifications, getOrgLevelNotifications @@ -3026,8 +3028,10 @@ const rootMutations = { const apiKeyRef = graphileSecretRef(organizationId, truncatedKey) + apiKeyAppendage; - await getWorker().then((worker) => - worker.setSecret(apiKeyRef, externalSystem.apiKey + apiKeyAppendage) + await setSecretPool( + pgPool, + apiKeyRef, + externalSystem.apiKey + apiKeyAppendage ); const [created] = await r @@ -3064,9 +3068,10 @@ const rootMutations = { username: externalSystem.username }; - const savedSystemApiKeySecret = await getWorker() - .then((worker) => worker.getSecret(savedSystem.api_key_ref)) - .catch(() => undefined); + const savedSystemApiKeySecret = await getSecretPool( + pgPool, + savedSystem.api_key_ref + ); const [ savedSystemApiKey, @@ -3104,14 +3109,10 @@ const rootMutations = { .where({ ref: savedSystem.api_key_ref }) .del(); - await getWorker().then((worker) => - worker.setSecret( - apiKeyRef, - (externalSystem.apiKey.includes("*") - ? savedSystemApiKey - : externalSystem.apiKey) + apiKeyAppendage - ) - ); + const apiKey = externalSystem.apiKey.includes("*") + ? savedSystemApiKey + : externalSystem.apiKey; + await setSecretPool(pgPool, apiKeyRef, apiKey + apiKeyAppendage); } const [updated] = await r diff --git a/src/server/index.ts b/src/server/index.ts index 6f0d7c4c7..1923f01e9 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -10,7 +10,7 @@ import { createApp } from "./app"; import { r } from "./models"; import { setupUserNotificationObservers } from "./notifications"; import { errToObj } from "./utils"; -import { getWorker } from "./worker"; +import { getScheduler, getWorker } from "./worker"; process.on("uncaughtException", (ex) => { logger.error("uncaughtException: ", ex); @@ -69,13 +69,20 @@ if (config.MODE === ServerMode.Server || config.MODE === ServerMode.Dual) { } if (config.MODE === ServerMode.Worker || config.MODE === ServerMode.Dual) { - // Launch pg-compose worker + // Launch graphile worker lightship.queueBlockingTask(getWorker()); lightship.registerShutdownHandler(async () => { const worker = await getWorker(); await worker.stop(); logger.info("Tore down Graphile runner"); }); + + lightship.queueBlockingTask(getScheduler()); + lightship.registerShutdownHandler(async () => { + const scheduler = await getScheduler(); + await scheduler.stop(); + logger.info("Tore down Graphile Scheduler"); + }); } // Always tear down Knex Postgres pools diff --git a/src/server/lib/external-systems.ts b/src/server/lib/external-systems.ts index abf2a1b52..8fa77ecaf 100644 --- a/src/server/lib/external-systems.ts +++ b/src/server/lib/external-systems.ts @@ -5,6 +5,11 @@ import { r } from "../models"; const DEFAULT_MODE = "0"; // VoterFile mode +export interface VanSecretAuthPayload { + username: string; + api_key: { __secret: string }; +} + export interface VanAuthPayload { username: string; api_key: string; diff --git a/src/server/lib/graphile-secrets.ts b/src/server/lib/graphile-secrets.ts new file mode 100644 index 000000000..c2079a83f --- /dev/null +++ b/src/server/lib/graphile-secrets.ts @@ -0,0 +1,49 @@ +import Cryptr from "cryptr"; +import type { Pool, PoolClient } from "pg"; + +import { config } from "../../config"; +import { withClient } from "../utils"; + +export const cryptr = new Cryptr(config.SESSION_SECRET); + +export const setSecret = async ( + client: PoolClient, + ref: string, + secret: string +) => { + const encryptedSecret = cryptr.encrypt(secret); + await client.query( + ` + insert into graphile_secrets.secrets (ref, encrypted_secret) + values ($1, $2) + on conflict (ref) do update set encrypted_secret = EXCLUDED.encrypted_secret + `, + [ref, encryptedSecret] + ); +}; + +export const setSecretPool = (pool: Pool, ref: string, secret: string) => + withClient(pool, (client) => setSecret(client, ref, secret)); + +export const getSecret = async (client: PoolClient, ref: string) => { + const { + rows: [secret] + } = await client.query<{ encrypted_secret: string }>( + ` + select encrypted_secret + from graphile_secrets.secrets + where ref = $1 + `, + [ref] + ); + + if (secret) { + return cryptr.decrypt(secret.encrypted_secret); + } + return undefined; +}; + +export const getSecretPool = (pool: Pool, ref: string) => + withClient(pool, (client) => getSecret(client, ref)); + +export default cryptr; diff --git a/src/server/pg-compose/external-system.yaml b/src/server/pg-compose/external-system.yaml deleted file mode 100644 index 107a9dd67..000000000 --- a/src/server/pg-compose/external-system.yaml +++ /dev/null @@ -1,155 +0,0 @@ -# Note: this file is out of sync with recent migrations and should not be used to generate migrations until that is fixed ---- -kind: Dependency -module: ngp-van ---- -kind: Table -name: external_system -implements: - - trait: van_system -columns: - id: - type: uuid - default: - type: function - fn: uuid_generate_v1mc() - nullable: false - name: - type: text - nullable: false - type: - type: text - nullable: false - api_key_ref: - type: text - nullable: false - organization_id: - type: integer -foreign_keys: - - references: - table: organization - columns: - - id - on: - - organization_id ---- -kind: Table -name: external_list -implements: - - trait: van_saved_list - via: - columns: - van_system_id: system_id - saved_list_id: external_id - # Note: van_contact is an emtpy trait designed to be implemented by campaign_contact, - # but we're keeping campaign_contact migrations out of pg-compose for now - - trait: van_contact -columns: - system_id: - type: uuid - nullable: false - external_id: - type: integer - nullable: false - name: - type: text - nullable: false - description: - type: text - nullable: false - default: "" - list_count: - type: integer - nullable: false - door_count: - type: integer - nullable: false -foreign_keys: - - references: - table: external_system - columns: - - id - on: - - system_id -indexes: - external_list_pkey: - - unique: true - primary_key: true - on: - - column: system_id - - column: external_id ---- -kind: Function -name: insert_van_contact_batch_to_campaign_contact -implements: - - handle_van_contact_batch -arguments: - - name: record_list - type: json -returns: void -security: definer -volatility: volatile -language: sql -body: | - insert into campaign_contact (campaign_id, external_id, first_name, last_name, zip, custom_fields, cell) - select - (r ->> 'campaign_id')::integer, - r ->> 'external_id', - r ->> 'first_name', - r ->> 'last_name', - r ->> 'zip', - r ->> 'custom_fields', - r ->> 'cell' - from json_array_elements(record_list) as r - where r ->> 'first_name' is not null - and r ->> 'last_name' is not null - and r ->> 'cell' is not null - on conflict (campaign_id, cell) do nothing ---- -kind: Function -name: mark_loading_job_done -arguments: - - name: payload - type: json - - name: result - type: json - - name: context - type: json -returns: void -security: definer -volatility: volatile -language: sql -body: | - select 1 ---- -kind: Function -name: queue_load_list_into_campaign -arguments: - - name: campaign_id - type: integer - - name: list_external_id - type: integer -language: sql -volatility: volatile -security: definer -returns: void -body: | - select fetch_saved_list( - list_external_id, - json_build_object('campaign_id', campaign_id), - json_build_object( - 'external_id', 'VanID', - 'first_name', 'FirstName', - 'last_name', 'LastName', - 'zip', 'ZipOrPostal', - 'custom_fields', json_build_array( - 'CongressionalDistrict', 'StateHouse', 'StateSenate', 'Party', - 'PollingLocation', 'PollingAddress', 'PollingCity', 'Email', - 'phone_id' - ), - 'cell', 'cell' - ), - 'insert_van_contact_batch_to_campaign_contact', - 'mark_loading_job_done', - null::json - ) diff --git a/src/server/tasks/handle-delivery-report.ts b/src/server/tasks/handle-delivery-report.ts index a974b0adb..4ba050d16 100644 --- a/src/server/tasks/handle-delivery-report.ts +++ b/src/server/tasks/handle-delivery-report.ts @@ -1,4 +1,4 @@ -import type { Task } from "pg-compose"; +import type { Task } from "graphile-worker"; import { processDeliveryReportBody as processAssembleDeliveryReport } from "../api/lib/assemble-numbers"; import { processDeliveryReportBody as processTwilioDeliveryReport } from "../api/lib/twilio"; diff --git a/src/server/tasks/ngp-van/VANSyncError.ts b/src/server/tasks/ngp-van/VANSyncError.ts new file mode 100644 index 000000000..7b18985ec --- /dev/null +++ b/src/server/tasks/ngp-van/VANSyncError.ts @@ -0,0 +1,13 @@ +export class VANSyncError extends Error { + status: number; + + body: string; + + constructor(status: number, body: string) { + super("sync_campaign_to_van__incorrect_response_code"); + this.status = status; + this.body = body; + } +} + +export default VANSyncError; diff --git a/src/server/tasks/fetch-van-activist-codes.ts b/src/server/tasks/ngp-van/fetch-van-activist-codes.ts similarity index 66% rename from src/server/tasks/fetch-van-activist-codes.ts rename to src/server/tasks/ngp-van/fetch-van-activist-codes.ts index e21de2043..8c29a31e4 100644 --- a/src/server/tasks/fetch-van-activist-codes.ts +++ b/src/server/tasks/ngp-van/fetch-van-activist-codes.ts @@ -1,13 +1,16 @@ -import type { Task } from "pg-compose"; +import type { JobHelpers, Task } from "graphile-worker"; import { get } from "superagent"; import type { - VanAuthPayload, - VANDataCollectionStatus -} from "../lib/external-systems"; -import { withVan } from "../lib/external-systems"; + VANDataCollectionStatus, + VanSecretAuthPayload +} from "../../lib/external-systems"; +import { withVan } from "../../lib/external-systems"; +import { getVanAuth, handleResult } from "./lib"; -interface GetActivistCodesPayload extends VanAuthPayload { +export const TASK_IDENTIFIER = "van-get-activist-codes"; + +interface GetActivistCodesPayload extends VanSecretAuthPayload { van_system_id: string; } @@ -24,26 +27,29 @@ export interface VANActivistCode { export const fetchVANActivistCodes: Task = async ( payload: GetActivistCodesPayload, - _helpers: any + helpers: JobHelpers ) => { const limit = 50; let offset = 0; let hasNextPage = false; let surveyQuestions: VANActivistCode[] = []; + + const auth = await getVanAuth(helpers, payload); + do { const response = await get("/activistCodes") .query({ $top: limit, $skip: offset }) - .use(withVan(payload)); + .use(withVan(auth)); const { body } = response; hasNextPage = body.nextPageLink !== null; offset += limit; surveyQuestions = surveyQuestions.concat(body.items); } while (hasNextPage); - return surveyQuestions.map((sq) => ({ + const result = surveyQuestions.map((sq) => ({ van_system_id: payload.van_system_id, activist_code_id: sq.activistCodeId, type: sq.type, @@ -54,6 +60,8 @@ export const fetchVANActivistCodes: Task = async ( script_question: sq.scriptQuestion, status: sq.status.toLowerCase() })); + + await handleResult(helpers, payload, result); }; export default fetchVANActivistCodes; diff --git a/src/server/tasks/fetch-van-result-codes.ts b/src/server/tasks/ngp-van/fetch-van-result-codes.ts similarity index 54% rename from src/server/tasks/fetch-van-result-codes.ts rename to src/server/tasks/ngp-van/fetch-van-result-codes.ts index e8a6b14cd..72c84bf61 100644 --- a/src/server/tasks/fetch-van-result-codes.ts +++ b/src/server/tasks/ngp-van/fetch-van-result-codes.ts @@ -1,10 +1,13 @@ -import type { Task } from "pg-compose"; +import type { JobHelpers, Task } from "graphile-worker"; import { get } from "superagent"; -import type { VanAuthPayload } from "../lib/external-systems"; -import { withVan } from "../lib/external-systems"; +import type { VanSecretAuthPayload } from "../../lib/external-systems"; +import { withVan } from "../../lib/external-systems"; +import { getVanAuth, handleResult } from "./lib"; -interface GetResultCodesPayload extends VanAuthPayload { +export const TASK_IDENTIFIER = "van-get-result-codes"; + +interface GetResultCodesPayload extends VanSecretAuthPayload { van_system_id: string; } @@ -17,21 +20,25 @@ export interface VANResultCode { export const fetchVANResultCodes: Task = async ( payload: GetResultCodesPayload, - _helpers: any + helpers: JobHelpers ) => { + const auth = await getVanAuth(helpers, payload); + // Result Codes are not paginated const response = await get("/canvassResponses/resultCodes").use( - withVan(payload) + withVan(auth) ); const resultCodes: VANResultCode[] = response.body; - return resultCodes.map((sq) => ({ + const result = resultCodes.map((sq) => ({ van_system_id: payload.van_system_id, result_code_id: sq.resultCodeId, name: sq.name, medium_name: sq.mediumName, short_name: sq.shortName })); + + await handleResult(helpers, payload, result); }; export default fetchVANResultCodes; diff --git a/src/server/tasks/fetch-van-survey-questions.ts b/src/server/tasks/ngp-van/fetch-van-survey-questions.ts similarity index 73% rename from src/server/tasks/fetch-van-survey-questions.ts rename to src/server/tasks/ngp-van/fetch-van-survey-questions.ts index 1ea07dd98..3edbdbcfc 100644 --- a/src/server/tasks/fetch-van-survey-questions.ts +++ b/src/server/tasks/ngp-van/fetch-van-survey-questions.ts @@ -1,13 +1,16 @@ -import type { Task } from "pg-compose"; +import type { JobHelpers, Task } from "graphile-worker"; import { get } from "superagent"; import type { - VanAuthPayload, - VANDataCollectionStatus -} from "../lib/external-systems"; -import { withVan } from "../lib/external-systems"; + VANDataCollectionStatus, + VanSecretAuthPayload +} from "../../lib/external-systems"; +import { withVan } from "../../lib/external-systems"; +import { getVanAuth, handleResult } from "./lib"; -interface GetSurveyQuestionsPayload extends VanAuthPayload { +export const TASK_IDENTIFIER = "van-get-survey-questions"; + +interface GetSurveyQuestionsPayload extends VanSecretAuthPayload { van_system_id: string; } @@ -32,8 +35,10 @@ export interface VANSurveyQuestion { export const fetchVANSurveyQuestions: Task = async ( payload: GetSurveyQuestionsPayload, - _helpers: any + helpers: JobHelpers ) => { + const auth = await getVanAuth(helpers, payload); + const limit = 50; let offset = 0; let hasNextPage = false; @@ -44,14 +49,14 @@ export const fetchVANSurveyQuestions: Task = async ( $top: limit, $skip: offset }) - .use(withVan(payload)); + .use(withVan(auth)); const { body } = response; hasNextPage = body.nextPageLink !== null; offset += limit; surveyQuestions = surveyQuestions.concat(body.items); } while (hasNextPage); - return surveyQuestions.map((sq) => ({ + const result = surveyQuestions.map((sq) => ({ van_system_id: payload.van_system_id, survey_question_id: sq.surveyQuestionId, type: sq.type, @@ -68,6 +73,8 @@ export const fetchVANSurveyQuestions: Task = async ( short_name: sqro.shortName })) })); + + await handleResult(helpers, payload, result); }; export default fetchVANSurveyQuestions; diff --git a/src/server/tasks/ngp-van/index.ts b/src/server/tasks/ngp-van/index.ts new file mode 100644 index 000000000..ed6e6db74 --- /dev/null +++ b/src/server/tasks/ngp-van/index.ts @@ -0,0 +1,51 @@ +import type { ScheduleConfig } from "graphile-scheduler"; +import type { TaskList } from "graphile-worker"; + +import { config } from "../../../config"; +import { + fetchVANActivistCodes, + TASK_IDENTIFIER as FETCH_ACTIVIST_CODES_IDENTIFIER +} from "./fetch-van-activist-codes"; +import { + fetchVANResultCodes, + TASK_IDENTIFIER as FETCH_RESULT_CODE_IDENTIFIER +} from "./fetch-van-result-codes"; +import { + fetchVANSurveyQuestions, + TASK_IDENTIFIER as FETCH_SURVEY_QUESTIONS_IDENTIFIER +} from "./fetch-van-survey-questions"; +import { + syncCampaignContactToVAN, + TASK_IDENTIFIER as SYNC_CONTACT_TO_VAN_IDENTIFIER +} from "./sync-campaign-contact-to-van"; +import { + TASK_IDENTIFIER as UPDATE_SYNC_STATUS_IDENTIFIER, + updateVanSyncStatuses +} from "./update-van-sync-statuses"; +import { + fetchSavedList, + TASK_IDENTIFIER as FETCH_SAVED_LIST_IDENTIFIER +} from "./van-fetch-saved-list"; +import { + getSavedLists, + TASK_IDENTIFIER as GET_SAVED_LISTS_IDENTIFIER +} from "./van-get-saved-lists"; + +export const taskList: TaskList = { + [GET_SAVED_LISTS_IDENTIFIER]: getSavedLists, + [FETCH_SAVED_LIST_IDENTIFIER]: fetchSavedList, + [FETCH_SURVEY_QUESTIONS_IDENTIFIER]: fetchVANSurveyQuestions, + [FETCH_ACTIVIST_CODES_IDENTIFIER]: fetchVANActivistCodes, + [FETCH_RESULT_CODE_IDENTIFIER]: fetchVANResultCodes, + [SYNC_CONTACT_TO_VAN_IDENTIFIER]: syncCampaignContactToVAN, + [UPDATE_SYNC_STATUS_IDENTIFIER]: updateVanSyncStatuses +}; + +export const schedules: ScheduleConfig[] = [ + { + name: UPDATE_SYNC_STATUS_IDENTIFIER, + taskIdentifier: UPDATE_SYNC_STATUS_IDENTIFIER, + pattern: "* * * * *", + timeZone: config.TZ + } +]; diff --git a/src/server/tasks/ngp-van/lib.ts b/src/server/tasks/ngp-van/lib.ts new file mode 100644 index 000000000..04c48d1bc --- /dev/null +++ b/src/server/tasks/ngp-van/lib.ts @@ -0,0 +1,56 @@ +import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber"; +import type { JobHelpers } from "graphile-worker"; + +import type { + VanAuthPayload, + VanSecretAuthPayload +} from "../../lib/external-systems"; +import { getSecret } from "../../lib/graphile-secrets"; + +const phoneUtil = PhoneNumberUtil.getInstance(); + +export const normalizedPhoneOrNull = (number: string | null) => { + if (typeof number !== "string") { + return null; + } + + try { + return phoneUtil.format( + phoneUtil.parse(number, "US"), + PhoneNumberFormat.E164 + ); + } catch (ex) { + return null; + } +}; + +export const getVanAuth = async ( + helpers: JobHelpers, + payload: VanSecretAuthPayload +): Promise => { + const { + username, + api_key: { __secret: apiKeyRef } + } = payload; + + const apiKey = await helpers.withPgClient((client) => + getSecret(client, apiKeyRef) + ); + + return { username, api_key: apiKey! }; +}; + +export const handleResult = async ( + helpers: JobHelpers, + payload: any, + result: any +) => { + const { __after: afterFn, __context: context = {}, ...rest } = payload; + + if (afterFn) { + await helpers.query( + `select * from ${afterFn}($1::json, $2::json, $3::json)`, + [JSON.stringify(rest), JSON.stringify(result), JSON.stringify(context)] + ); + } +}; diff --git a/src/server/tasks/sync-campaign-contact-to-van.spec.ts b/src/server/tasks/ngp-van/sync-campaign-contact-to-van.spec.ts similarity index 96% rename from src/server/tasks/sync-campaign-contact-to-van.spec.ts rename to src/server/tasks/ngp-van/sync-campaign-contact-to-van.spec.ts index b0419efac..5c28d100f 100644 --- a/src/server/tasks/sync-campaign-contact-to-van.spec.ts +++ b/src/server/tasks/ngp-van/sync-campaign-contact-to-van.spec.ts @@ -4,14 +4,14 @@ import { Pool } from "pg"; import { createCompleteCampaign, createMessage -} from "../../../__test__/testbed-preparation/core"; +} from "../../../../__test__/testbed-preparation/core"; import { createExternalResultCode, createExternalSystem -} from "../../../__test__/testbed-preparation/external-systems"; -import { config } from "../../config"; -import { MessageStatusType } from "../api/types"; -import { withClient, withTransaction } from "../utils"; +} from "../../../../__test__/testbed-preparation/external-systems"; +import { config } from "../../../config"; +import { MessageStatusType } from "../../api/types"; +import { withClient, withTransaction } from "../../utils"; import type { CanvassResultRow, VANCanvassResponse diff --git a/src/server/tasks/sync-campaign-contact-to-van.ts b/src/server/tasks/ngp-van/sync-campaign-contact-to-van.ts similarity index 94% rename from src/server/tasks/sync-campaign-contact-to-van.ts rename to src/server/tasks/ngp-van/sync-campaign-contact-to-van.ts index ad1ad2948..65bee99fa 100644 --- a/src/server/tasks/sync-campaign-contact-to-van.ts +++ b/src/server/tasks/ngp-van/sync-campaign-contact-to-van.ts @@ -1,23 +1,15 @@ +import type { JobHelpers, Task } from "graphile-worker"; import isNil from "lodash/isNil"; import type { PoolClient } from "pg"; -import type { Task } from "pg-compose"; import { post } from "superagent"; -import { config } from "../../config"; -import type { VanAuthPayload } from "../lib/external-systems"; -import { withVan } from "../lib/external-systems"; +import { config } from "../../../config"; +import type { VanSecretAuthPayload } from "../../lib/external-systems"; +import { withVan } from "../../lib/external-systems"; +import { getVanAuth } from "./lib"; +import VANSyncError from "./VANSyncError"; -class VANSyncError extends Error { - status: number; - - body: string; - - constructor(status: number, body: string) { - super("sync_campaign_to_van__incorrect_response_code"); - this.status = status; - this.body = body; - } -} +export const TASK_IDENTIFIER = "van-sync-campaign-contact"; export const CANVASSED_TAG_NAME = "Canvassed"; @@ -321,7 +313,7 @@ export const hasPayload = (canvassResponse: VANCanvassResponse) => { return hasResponses || hasResultCode; }; -export interface SyncCampaignContactToVANPayload extends VanAuthPayload { +export interface SyncCampaignContactToVANPayload extends VanSecretAuthPayload { system_id: string; contact_id: number; cc_created_at: string; @@ -332,8 +324,10 @@ export interface SyncCampaignContactToVANPayload extends VanAuthPayload { export const syncCampaignContactToVAN: Task = async ( payload: SyncCampaignContactToVANPayload, - helpers + helpers: JobHelpers ) => { + const auth = await getVanAuth(helpers, payload); + const { system_id: systemId, contact_id: contactId, @@ -382,7 +376,7 @@ export const syncCampaignContactToVAN: Task = async ( for (const canvassResponse of canvassResponses) { const response = await post(`/people/${vanId}/canvassResponses`) - .use(withVan(payload)) + .use(withVan(auth)) .send(canvassResponse); if (response.status !== 204) { @@ -396,9 +390,3 @@ export const syncCampaignContactToVAN: Task = async ( } } }; - -export const updateVanSyncStatuses: Task = async (_payload, helpers) => { - await helpers.query( - `select * from public.update_van_sync_job_request_status()` - ); -}; diff --git a/src/server/tasks/ngp-van/update-van-sync-statuses.ts b/src/server/tasks/ngp-van/update-van-sync-statuses.ts new file mode 100644 index 000000000..1b1b03a1e --- /dev/null +++ b/src/server/tasks/ngp-van/update-van-sync-statuses.ts @@ -0,0 +1,14 @@ +import type { JobHelpers, Task } from "graphile-worker"; + +export const TASK_IDENTIFIER = "update-van-sync-statuses"; + +export const updateVanSyncStatuses: Task = async ( + _payload, + helpers: JobHelpers +) => { + await helpers.query( + `select * from public.update_van_sync_job_request_status()` + ); +}; + +export default updateVanSyncStatuses; diff --git a/src/server/tasks/ngp-van/van-fetch-saved-list.ts b/src/server/tasks/ngp-van/van-fetch-saved-list.ts new file mode 100644 index 000000000..a2e323edd --- /dev/null +++ b/src/server/tasks/ngp-van/van-fetch-saved-list.ts @@ -0,0 +1,247 @@ +import type { ParserOptionsArgs } from "fast-csv"; +import { parse } from "fast-csv"; +import type { JobHelpers, Task } from "graphile-worker"; +import { fromPairs, toPairs } from "lodash"; +import type { PoolClient } from "pg"; +import type { SuperAgentRequest } from "superagent"; +import { get, post } from "superagent"; + +import { config } from "../../../config"; +import type { VanSecretAuthPayload } from "../../lib/external-systems"; +import { withVan } from "../../lib/external-systems"; +import { withTransaction } from "../../utils"; +import { getVanAuth, handleResult, normalizedPhoneOrNull } from "./lib"; + +export const TASK_IDENTIFIER = "van-fetch-saved-list"; + +const BATCH_SIZE = 1000; + +enum VanPhoneType { + Cell = "cell", + Home = "home", + Work = "work" +} + +type ColumnConfig = { + [key: string]: string | string[] | { [key: string]: string }[]; +}; + +enum ExportJobStatus { + Completed = "Completed", + Requested = "Requested", + Pending = "Pending", + Error = "Error" +} + +interface ExportJob { + exportJobId: number; + exportJobGuid: string; + savedListId: number; + webhookUrl: string; + downloadUrl: string; + status: ExportJobStatus; + type: number; + dateExpired: string; + errorCode: string | null; +} + +interface HustleExportRow { + CellPhone: ""; + CellPhoneDialingPrefix: ""; + CellPhoneCountryCode: ""; + CellPhoneId: ""; + WorkPhone: ""; + WorkPhoneDialingPrefix: ""; + WorkPhoneCountryCode: ""; + WorkPhoneId: ""; + IsWorkPhoneACellExchange: ""; + Phone: "(555) 768-3292"; + PhoneDialingPrefix: "1"; + PhoneCountryCode: "US"; + PhoneId: "806416"; + HomePhone: "5557683292"; + HomePhoneDialingPrefix: "1"; + HomePhoneCountryCode: "US"; + HomePhoneId: "806416"; + IsHomePhoneACellExchange: "0"; + [key: string]: string | null; +} + +const produceSubObjectPair = ( + keyConfig: string | { [key: string]: string }, + row: HustleExportRow +): [string, string] => + typeof keyConfig === "string" + ? [keyConfig, row[keyConfig]!] + : [Object.keys(keyConfig)[0], row[Object.values(keyConfig)[0]]!]; + +type OldKey = string | { [key: string]: string }; + +const transformSubObject = (oldKey: OldKey[], row: HustleExportRow) => + oldKey.map<[string, string]>((o) => produceSubObjectPair(o, row)); + +const transformByColumnConfig = ( + row: HustleExportRow, + columnConfig: ColumnConfig +) => + fromPairs( + toPairs(columnConfig).map(([newKey, oldKey]) => + Array.isArray(oldKey) + ? [newKey, fromPairs(transformSubObject(oldKey, row))] + : [newKey, row[oldKey]] + ) + ); + +const maybeExtractPhoneType = ( + r: HustleExportRow, + extract_phone_type: VanPhoneType | null +) => { + if (extract_phone_type === null || extract_phone_type === undefined) { + return r; + } + + const phoneOpts = [ + [ + r.CellPhoneDialingPrefix + r.CellPhone, + r.CellPhoneId, + "1", + VanPhoneType.Cell + ], + [ + r.HomePhoneDialingPrefix + r.HomePhone, + r.HomePhoneId, + r.IsHomePhoneACellExchange, + VanPhoneType.Home + ], + [ + r.WorkPhoneDialingPrefix + r.WorkPhone, + r.WorkPhoneId, + r.IsWorkPhoneACellExchange, + VanPhoneType.Work + ], + [r.PhoneDialingPrefix + r.PhoneCountryCode, r.PhoneId, "0", null] + ]; + + // Try to return the desired one + for (const opt of phoneOpts) { + const [number, phoneId, cellExchangeBit, phoneType] = opt; + if ( + number !== "" && + extract_phone_type === VanPhoneType.Cell && + (phoneType === VanPhoneType.Cell || cellExchangeBit === "1") + ) { + return { + [extract_phone_type]: normalizedPhoneOrNull(number), + phone_id: phoneId + }; + } + + if (number !== "" && extract_phone_type === phoneType) { + return { + [extract_phone_type]: normalizedPhoneOrNull(number), + phone_id: phoneId + }; + } + } + + // Return any + for (const opt of phoneOpts) { + const [number, phoneId] = opt; + if (number !== "") { + return { + [extract_phone_type]: normalizedPhoneOrNull(number), + phone_id: phoneId + }; + } + } + + return { + [extract_phone_type]: null, + phone_id: null + }; +}; + +interface FetchSavedListsPayload extends VanSecretAuthPayload { + saved_list_id: number; + handler: string; + row_merge: any; + column_config: ColumnConfig; + first_n_rows: number; + extract_phone_type: VanPhoneType | null; +} + +export const fetchSavedList: Task = async ( + payload: FetchSavedListsPayload, + helpers: JobHelpers +) => { + const auth = await getVanAuth(helpers, payload); + + const response = await post("/exportJobs").use(withVan(auth)).send({ + savedListId: payload.saved_list_id, + type: config.VAN_EXPORT_TYPE, + webhookUrl: config.EXPORT_JOB_WEBHOOK + }); + + const exportJob: ExportJob = response.body; + + if (exportJob.status !== ExportJobStatus.Completed) { + throw new Error("VAN Export Job status was not 'completed'"); + } + + const options: ParserOptionsArgs = { + headers: true, + trim: true, + maxRows: payload.first_n_rows + }; + + const csvStream = parse(options).transform((row: HustleExportRow) => + Object.assign( + transformByColumnConfig( + { + ...row, + ...maybeExtractPhoneType(row, payload.extract_phone_type) + }, + payload.column_config + ), + payload.row_merge + ) + ); + + let downloadReq: SuperAgentRequest; + try { + downloadReq = get(exportJob.downloadUrl); + } catch (err: any) { + helpers.logger.error(`Error streaming VAN download: ${err.message}`, { + exportJob, + error: err + }); + throw err; + } + + downloadReq.pipe(csvStream); + + await helpers.withPgClient(async (client: PoolClient) => { + await withTransaction(client, async (trx) => { + let accumulator: any[] = []; + + const flushBatch = (batch: any[]) => { + const asJsonString = JSON.stringify(batch); + return trx.query(`select ${payload.handler}($1::json)`, [asJsonString]); + }; + + for await (const row of csvStream) { + accumulator.push(row); + + if (accumulator.length === BATCH_SIZE) { + const batchToFlush = accumulator.slice(); + accumulator = []; + await flushBatch(batchToFlush); + } + } + + await flushBatch(accumulator); + }); + }); + + await handleResult(helpers, payload, {}); +}; diff --git a/src/server/tasks/ngp-van/van-get-saved-lists.ts b/src/server/tasks/ngp-van/van-get-saved-lists.ts new file mode 100644 index 000000000..7167153a4 --- /dev/null +++ b/src/server/tasks/ngp-van/van-get-saved-lists.ts @@ -0,0 +1,56 @@ +import type { JobHelpers, Task } from "graphile-worker"; +import { get } from "superagent"; + +import type { VanSecretAuthPayload } from "../../lib/external-systems"; +import { withVan } from "../../lib/external-systems"; +import { getVanAuth, handleResult } from "./lib"; + +export const TASK_IDENTIFIER = "van-get-saved-lists"; + +const VAN_SAVED_LISTS_MAX_PAGE_SIZE = 100; + +interface GetSavedListsPayload extends VanSecretAuthPayload { + van_system_id: string; +} + +interface VanSavedList { + savedListId: number; + name: string; + description: string; + listCount: number; + doorCount: number; +} + +export const getSavedLists: Task = async ( + payload: GetSavedListsPayload, + helpers: JobHelpers +) => { + const auth = await getVanAuth(helpers, payload); + + let offset = 0; + let returnCount = 0; + let savedLists: VanSavedList[] = []; + do { + const response = await get("/savedLists") + .query({ + $top: VAN_SAVED_LISTS_MAX_PAGE_SIZE, + $skip: offset + }) + .use(withVan(auth)); + const { body } = response; + returnCount = body.items.length; + offset += VAN_SAVED_LISTS_MAX_PAGE_SIZE; + savedLists = savedLists.concat(body.items); + } while (returnCount > 0); + + const result = savedLists.map((sl) => ({ + saved_list_id: sl.savedListId, + name: sl.name, + description: sl.description ?? "", + list_count: sl.listCount, + door_count: sl.doorCount, + van_system_id: payload.van_system_id + })); + + await handleResult(helpers, payload, result); +}; diff --git a/src/server/tasks/queue-autosend-initials.ts b/src/server/tasks/queue-autosend-initials.ts index e8f9470f3..4ffc8aea1 100644 --- a/src/server/tasks/queue-autosend-initials.ts +++ b/src/server/tasks/queue-autosend-initials.ts @@ -1,5 +1,5 @@ +import type { Task } from "graphile-worker"; import { fromPairs } from "lodash"; -import type { Task } from "pg-compose"; import { config } from "../../config"; diff --git a/src/server/tasks/queue-pending-notifications.ts b/src/server/tasks/queue-pending-notifications.ts index 94efdaa3c..5aecf9457 100644 --- a/src/server/tasks/queue-pending-notifications.ts +++ b/src/server/tasks/queue-pending-notifications.ts @@ -1,4 +1,4 @@ -import type { Task } from "pg-compose"; +import type { Task } from "graphile-worker"; import { NotificationFrequencyType } from "../../api/user"; import { r } from "../models"; diff --git a/src/server/tasks/resend-message.ts b/src/server/tasks/resend-message.ts index 1b26c8d66..e49d10063 100644 --- a/src/server/tasks/resend-message.ts +++ b/src/server/tasks/resend-message.ts @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -import type { Task } from "pg-compose"; +import type { Task } from "graphile-worker"; import { sendMessage } from "../api/lib/assemble-numbers"; import type { SendMessagePayload } from "../api/lib/types"; diff --git a/src/server/tasks/retry-interaction-step.ts b/src/server/tasks/retry-interaction-step.ts index 1fb100f47..944aaca76 100644 --- a/src/server/tasks/retry-interaction-step.ts +++ b/src/server/tasks/retry-interaction-step.ts @@ -1,6 +1,6 @@ +import type { Task } from "graphile-worker"; import sample from "lodash/sample"; import md5 from "md5"; -import type { Task } from "pg-compose"; import type { CampaignContact } from "../../api/campaign-contact"; import type { MessageInput } from "../../api/types"; diff --git a/src/server/tasks/send-notification-email.ts b/src/server/tasks/send-notification-email.ts index 75c400d12..d0503be53 100644 --- a/src/server/tasks/send-notification-email.ts +++ b/src/server/tasks/send-notification-email.ts @@ -1,5 +1,5 @@ +import type { Task } from "graphile-worker"; import groupBy from "lodash/groupBy"; -import type { Task } from "pg-compose"; import logger from "../../logger"; import { diff --git a/src/server/tasks/sync-slack-team-members.ts b/src/server/tasks/sync-slack-team-members.ts index 6210d0feb..5ee227427 100644 --- a/src/server/tasks/sync-slack-team-members.ts +++ b/src/server/tasks/sync-slack-team-members.ts @@ -1,7 +1,7 @@ import type { WebAPICallError, WebAPICallResult } from "@slack/web-api"; import { ErrorCode } from "@slack/web-api"; +import type { JobHelpers, Task } from "graphile-worker"; import isEmpty from "lodash/isEmpty"; -import type { JobHelpers, Task } from "pg-compose"; import promiseRetry from "promise-retry"; import { config } from "../../config"; diff --git a/src/server/tasks/troll-patrol.ts b/src/server/tasks/troll-patrol.ts index 8df9c2640..0561afab5 100644 --- a/src/server/tasks/troll-patrol.ts +++ b/src/server/tasks/troll-patrol.ts @@ -1,4 +1,4 @@ -import type { Task } from "pg-compose"; +import type { Task } from "graphile-worker"; import request from "superagent"; import { config } from "../../config"; diff --git a/src/server/tasks/update-org-message-usage.ts b/src/server/tasks/update-org-message-usage.ts index 73049fedb..e0925e0c3 100644 --- a/src/server/tasks/update-org-message-usage.ts +++ b/src/server/tasks/update-org-message-usage.ts @@ -1,4 +1,4 @@ -import type { JobHelpers, Task } from "pg-compose"; +import type { JobHelpers, Task } from "graphile-worker"; import { config } from "../../config"; diff --git a/src/server/worker.ts b/src/server/worker.ts index a9256878b..e7407ae9f 100644 --- a/src/server/worker.ts +++ b/src/server/worker.ts @@ -1,7 +1,7 @@ -import type { LogFunctionFactory } from "graphile-worker"; -import { Logger } from "graphile-worker"; -import type { PgComposeWorker } from "pg-compose"; -import { loadYaml, run } from "pg-compose"; +import type { Runner as Scheduler, ScheduleConfig } from "graphile-scheduler"; +import { run as runScheduler } from "graphile-scheduler"; +import type { LogFunctionFactory, Runner, TaskList } from "graphile-worker"; +import { Logger, run } from "graphile-worker"; import { config } from "../config"; import { sleep } from "../lib"; @@ -19,15 +19,16 @@ import { exportForVan, TASK_IDENTIFIER as exportForVanIdentifier } from "./tasks/export-for-van"; -import fetchVANActivistCodes from "./tasks/fetch-van-activist-codes"; -import fetchVANResultCodes from "./tasks/fetch-van-result-codes"; -import fetchVANSurveyQuestions from "./tasks/fetch-van-survey-questions"; import { filterLandlines, TASK_IDENTIFIER as filterLandlinesIdentifier } from "./tasks/filter-landlines"; import handleAutoassignmentRequest from "./tasks/handle-autoassignment-request"; import handleDeliveryReport from "./tasks/handle-delivery-report"; +import { + schedules as ngpVanSchedules, + taskList as ngpVanTaskList +} from "./tasks/ngp-van"; import queueAutoSendInitials from "./tasks/queue-autosend-initials"; import { queueDailyNotifications, @@ -41,10 +42,6 @@ import { sendNotificationDigestForUser, sendNotificationEmail } from "./tasks/send-notification-email"; -import { - syncCampaignContactToVAN, - updateVanSyncStatuses -} from "./tasks/sync-campaign-contact-to-van"; import syncSlackTeamMembers from "./tasks/sync-slack-team-members"; import { trollPatrol, trollPatrolForOrganization } from "./tasks/troll-patrol"; import updateOrgMessageUsage from "./tasks/update-org-message-usage"; @@ -55,107 +52,127 @@ const logFactory: LogFunctionFactory = (scope) => (level, message, meta) => const graphileLogger = new Logger(logFactory); -let worker: PgComposeWorker | undefined; +let worker: Runner | undefined; let workerSemaphore = false; -export const getWorker = async (attempt = 0): Promise => { +export const getWorker = async (attempt = 0): Promise => { if (worker) return worker; - const m = await loadYaml({ include: `${__dirname}/pg-compose/**/*.yaml` }); - - m.taskList!["handle-autoassignment-request"] = handleAutoassignmentRequest; - m.taskList!["release-stale-replies"] = releaseStaleReplies; - m.taskList!["handle-delivery-report"] = handleDeliveryReport; - m.taskList!["troll-patrol"] = trollPatrol; - m.taskList!["troll-patrol-for-org"] = trollPatrolForOrganization; - m.taskList!["sync-slack-team-members"] = syncSlackTeamMembers; - m.taskList!["van-get-survey-questions"] = fetchVANSurveyQuestions; - m.taskList!["van-get-activist-codes"] = fetchVANActivistCodes; - m.taskList!["van-get-result-codes"] = fetchVANResultCodes; - m.taskList!["van-sync-campaign-contact"] = syncCampaignContactToVAN; - m.taskList!["update-van-sync-statuses"] = updateVanSyncStatuses; - m.taskList!["update-org-message-usage"] = updateOrgMessageUsage; - m.taskList!["resend-message"] = resendMessage; - m.taskList!["retry-interaction-step"] = retryInteractionStep; - m.taskList!["queue-pending-notifications"] = queuePendingNotifications; - m.taskList!["queue-periodic-notifications"] = queuePeriodicNotifications; - m.taskList!["queue-daily-notifications"] = queueDailyNotifications; - m.taskList!["send-notification-email"] = sendNotificationEmail; - m.taskList!["send-notification-digest"] = sendNotificationDigestForUser; - m.taskList!["queue-autosend-initials"] = queueAutoSendInitials; - m.taskList![exportCampaignIdentifier] = wrapProgressTask(exportCampaign, { - removeOnComplete: true - }); - m.taskList![exportForVanIdentifier] = wrapProgressTask(exportForVan, { - removeOnComplete: true - }); - m.taskList![filterLandlinesIdentifier] = wrapProgressTask(filterLandlines, { - removeOnComplete: false - }); - m.taskList![assignTextersIdentifier] = wrapProgressTask(assignTexters, { - removeOnComplete: true - }); - - m.cronJobs!.push({ - name: "release-stale-replies", - task_name: "release-stale-replies", - pattern: "*/5 * * * *", - time_zone: config.TZ - }); - - m.cronJobs!.push({ - name: "update-van-sync-statuses", - task_name: "update-van-sync-statuses", - pattern: "* * * * *", - time_zone: config.TZ - }); - - m.cronJobs!.push({ - name: "queue-pending-notifications", - task_name: "queue-pending-notifications", - pattern: "* * * * *", - time_zone: config.TZ - }); - - m.cronJobs!.push({ - name: "queue-periodic-notifications", - task_name: "queue-periodic-notifications", - pattern: "0 9,13,16,20 * * *", - time_zone: config.TZ - }); - - m.cronJobs!.push({ - name: "queue-daily-notifications", - task_name: "queue-daily-notifications", - pattern: "0 9 * * *", - time_zone: config.TZ - }); + const taskList: TaskList = { + "handle-autoassignment-request": handleAutoassignmentRequest, + "release-stale-replies": releaseStaleReplies, + "handle-delivery-report": handleDeliveryReport, + "troll-patrol": trollPatrol, + "troll-patrol-for-org": trollPatrolForOrganization, + "sync-slack-team-members": syncSlackTeamMembers, + "update-org-message-usage": updateOrgMessageUsage, + "resend-message": resendMessage, + "retry-interaction-step": retryInteractionStep, + "queue-pending-notifications": queuePendingNotifications, + "queue-periodic-notifications": queuePeriodicNotifications, + "queue-daily-notifications": queueDailyNotifications, + "send-notification-email": sendNotificationEmail, + "send-notification-digest": sendNotificationDigestForUser, + "queue-autosend-initials": queueAutoSendInitials, + [exportCampaignIdentifier]: wrapProgressTask(exportCampaign, { + removeOnComplete: true + }), + [exportForVanIdentifier]: wrapProgressTask(exportForVan, { + removeOnComplete: true + }), + [filterLandlinesIdentifier]: wrapProgressTask(filterLandlines, { + removeOnComplete: false + }), + [assignTextersIdentifier]: wrapProgressTask(assignTexters, { + removeOnComplete: true + }), + ...ngpVanTaskList + }; + + if (!workerSemaphore) { + workerSemaphore = true; + + worker = await run({ + pgPool, + taskList, + concurrency: config.WORKER_CONCURRENCY, + logger: graphileLogger, + // Signals are handled by Terminus + noHandleSignals: true, + pollInterval: 1000 + }); + + return worker; + } + + // Someone beat us to the punch of initializing the runner + if (attempt >= 20) throw new Error("getWorker() took too long to resolve"); + await sleep(100); + return getWorker(attempt + 1); +}; + +let scheduler: Scheduler | undefined; +let schedulerSemaphore = false; + +export const getScheduler = async (attempt = 0): Promise => { + if (scheduler) return scheduler; + + const schedules: ScheduleConfig[] = [ + ...ngpVanSchedules, + { + name: "release-stale-replies", + taskIdentifier: "release-stale-replies", + pattern: "*/5 * * * *", + timeZone: config.TZ + }, + + { + name: "queue-pending-notifications", + taskIdentifier: "queue-pending-notifications", + pattern: "* * * * *", + timeZone: config.TZ + }, + + { + name: "queue-periodic-notifications", + taskIdentifier: "queue-periodic-notifications", + pattern: "0 9,13,16,20 * * *", + timeZone: config.TZ + }, + + { + name: "queue-daily-notifications", + taskIdentifier: "queue-daily-notifications", + pattern: "0 9 * * *", + timeZone: config.TZ + } + ]; if (config.ENABLE_AUTOSENDING) { - m.cronJobs!.push({ + schedules.push({ name: "queue-autosend-initials", - task_name: "queue-autosend-initials", + taskIdentifier: "queue-autosend-initials", pattern: "*/1 * * * *", - time_zone: config.TZ + timeZone: config.TZ }); } if (config.ENABLE_MONTHLY_ORG_MESSAGE_LIMITS) { - m.cronJobs!.push({ + schedules.push({ name: "update-org-message-usage", - task_name: "update-org-message-usage", + taskIdentifier: "update-org-message-usage", pattern: "*/5 * * * *", - time_zone: config.TZ + timeZone: config.TZ }); } if (config.SLACK_SYNC_CHANNELS) { if (config.SLACK_TOKEN) { - m.cronJobs!.push({ + schedules.push({ name: "sync-slack-team-members", - task_name: "sync-slack-team-members", + taskIdentifier: "sync-slack-team-members", pattern: config.SLACK_SYNC_CHANNELS_CRONTAB, - time_zone: config.TZ + timeZone: config.TZ }); } else { logger.error( @@ -166,34 +183,30 @@ export const getWorker = async (attempt = 0): Promise => { if (config.ENABLE_TROLLBOT) { const jobInterval = config.TROLL_ALERT_PERIOD_MINUTES - 1; - m.cronJobs!.push({ + schedules.push({ name: "troll-patrol", - task_name: "troll-patrol", + taskIdentifier: "troll-patrol", pattern: `*/${jobInterval} * * * *`, - time_zone: config.TZ + timeZone: config.TZ }); } - if (!workerSemaphore) { - workerSemaphore = true; + if (!schedulerSemaphore) { + schedulerSemaphore = true; - worker = await run(m, { + scheduler = await runScheduler({ pgPool, - encryptionSecret: config.SESSION_SECRET, - concurrency: config.WORKER_CONCURRENCY, - logger: graphileLogger, - // Signals are handled by Terminus - noHandleSignals: true, - pollInterval: 1000 + schedules, + logger: graphileLogger as any }); - return worker; + return scheduler; } // Someone beat us to the punch of initializing the runner - if (attempt >= 20) throw new Error("getWorker() took too long to resolve"); + if (attempt >= 20) throw new Error("getScheduler() took too long to resolve"); await sleep(100); - return getWorker(attempt + 1); + return getScheduler(attempt + 1); }; export default getWorker; diff --git a/yarn.lock b/yarn.lock index b82e6ee39..34960a4f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8389,21 +8389,6 @@ chokidar@^3.2.2, chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.1" -chokidar@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" - integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.3.0" - optionalDependencies: - fsevents "~2.1.2" - chokidar@^3.4.1: version "3.4.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" @@ -9572,10 +9557,10 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -cryptr@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/cryptr/-/cryptr-6.0.2.tgz#3f9e97f825ffb93f425eb24068efbb6a652bf947" - integrity sha512-1TRHI4bmuLIB8WgkH9eeYXzhEg1T4tonO4vVaMBKKde8Dre51J68nAgTVXTwMYXAf7+mV2gBCkm/9wksjSb2sA== +cryptr@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/cryptr/-/cryptr-6.0.3.tgz#669364e7993a1cd19d77e32e90ffef38455ecd74" + integrity sha512-Nhaxn3CYl/OoWF3JSZlF8B6FQO600RRkU3g8213OGEIq4YvMlc3od8hL9chubhY1SmTq8ienvCRq1MSFjMTpOg== css-blank-pseudo@^0.1.4: version "0.1.4" @@ -10100,7 +10085,7 @@ dedent@0.7.0, dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-equal@^1.0.1, deep-equal@~1.1.1: +deep-equal@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== @@ -10198,11 +10183,6 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= - del@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" @@ -10593,13 +10573,6 @@ dotgitignore@2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -dotignore@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" - integrity sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw== - dependencies: - minimatch "^3.0.4" - double-ended-queue@^2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" @@ -11928,11 +11901,6 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -faker@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" - integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8= - faker@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/faker/-/faker-5.1.0.tgz#e10fa1dec4502551aee0eb771617a7e7b94692e8" @@ -12449,13 +12417,6 @@ follow-redirects@^1.10.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== -for-each@~0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -12726,7 +12687,7 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== -function-bind@^1.1.1, function-bind@~1.1.1: +function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== @@ -13052,7 +13013,7 @@ glob@7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -13636,7 +13597,7 @@ has-yarn@^2.1.0: resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== -has@^1.0.0, has@^1.0.3, has@~1.0.3: +has@^1.0.0, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -14323,7 +14284,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -14602,11 +14563,6 @@ is-buffer@~2.0.3: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== - is-callable@^1.1.4, is-callable@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" @@ -14974,7 +14930,7 @@ is-property@^1.0.0, is-property@^1.0.2: resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= -is-regex@^1.0.4, is-regex@^1.0.5, is-regex@~1.0.5: +is-regex@^1.0.4, is-regex@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== @@ -18109,7 +18065,7 @@ minimist@1.2.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@^1.2.5, minimist@~1.2.5: +minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -18290,11 +18246,6 @@ mustache@^2.2.1: resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" integrity sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ== -mustache@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2" - integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA== - mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -18764,7 +18715,7 @@ object-inspect@^1.11.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== -object-inspect@^1.7.0, object-inspect@~1.7.0: +object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== @@ -19636,28 +19587,6 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -pg-compose@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-compose/-/pg-compose-1.0.1.tgz#da93a7a3a5cdd313a4c76954546ce05116d8581c" - integrity sha512-O7E22C7gEW3cMAHIPu6F7RBGfACKvYW60YyKDi/xyazH+wEuEkRJqvve6rcw+n/7tmH45+GNiLd5F//m4B6plQ== - dependencies: - "@graphile/logger" "^0.2.0" - "@types/cryptr" "^4.0.1" - chokidar "^3.3.1" - cosmiconfig "^6.0.0" - cryptr "^6.0.2" - faker "^4.1.0" - glob "^7.1.6" - graphile-scheduler "^0.8.0" - graphile-worker "^0.13.0" - lodash "^4.17.20" - mustache "^4.0.1" - runtypes "^4.2.0" - tape "^4.13.2" - tslib "^1.9.3" - yaml "^1.10.0" - yargs "^15.3.1" - pg-connection-string@2.0.0, pg-connection-string@2.5.0, pg-connection-string@^2.0.0, pg-connection-string@^2.4.0, pg-connection-string@^2.5.0, pg-connection-string@~2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" @@ -19766,7 +19695,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1, picomatch@^2.2.2: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -21568,13 +21497,6 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" - integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== - dependencies: - picomatch "^2.0.7" - readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -22242,13 +22164,6 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" -resolve@~1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -22279,13 +22194,6 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -resumer@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" - integrity sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k= - dependencies: - through "~2.3.4" - ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -22475,11 +22383,6 @@ run-when-changed@^2.1.0: gaze "^1.1.2" minimatch "^3.0.4" -runtypes@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/runtypes/-/runtypes-4.2.0.tgz#6bb01a4683c1ac76015de8669df32a034c6cb0fe" - integrity sha512-s89DYbxI7qKSpDMmdKQCGg61nH45tYA5LJMR0pWfJ/1nwPdpww75fusQqGzXE7llpk+rwe8fNPSx78FRGKenJg== - runtypes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/runtypes/-/runtypes-4.3.0.tgz#7332acd86cb8332982e00d134d27a2e4fe701779" @@ -23651,7 +23554,7 @@ string.prototype.startswith@^0.2.0: resolved "https://registry.yarnpkg.com/string.prototype.startswith/-/string.prototype.startswith-0.2.0.tgz#da68982e353a4e9ac4a43b450a2045d1c445ae7b" integrity sha1-2miYLjU6TprEpDtFCiBF0cRFrns= -string.prototype.trim@^1.2.1, string.prototype.trim@~1.2.1: +string.prototype.trim@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw== @@ -24151,27 +24054,6 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== -tape@^4.13.2: - version "4.13.3" - resolved "https://registry.yarnpkg.com/tape/-/tape-4.13.3.tgz#51b3d91c83668c7a45b1a594b607dee0a0b46278" - integrity sha512-0/Y20PwRIUkQcTCSi4AASs+OANZZwqPKaipGCEwp10dQMipVvSZwUUCi01Y/OklIGyHKFhIcjock+DKnBfLAFw== - dependencies: - deep-equal "~1.1.1" - defined "~1.0.0" - dotignore "~0.1.2" - for-each "~0.3.3" - function-bind "~1.1.1" - glob "~7.1.6" - has "~1.0.3" - inherits "~2.0.4" - is-regex "~1.0.5" - minimist "~1.2.5" - object-inspect "~1.7.0" - resolve "~1.17.0" - resumer "~0.0.0" - string.prototype.trim "~1.2.1" - through "~2.3.8" - tar-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" @@ -24405,7 +24287,7 @@ through2@^3.0.0: dependencies: readable-stream "2 || 3" -through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3.4, through@~2.3.8: +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=