2021-11-10 19:14:23 +00:00
|
|
|
import {LocalActionBundle} from 'app/common/ActionBundle';
|
2023-06-01 14:00:52 +00:00
|
|
|
import {summarizeAction} from 'app/common/ActionSummarizer';
|
2021-11-10 19:14:23 +00:00
|
|
|
import {ActionSummary, TableDelta} from 'app/common/ActionSummary';
|
2023-03-01 20:43:22 +00:00
|
|
|
import {ApiError} from 'app/common/ApiError';
|
2022-05-16 12:47:34 +00:00
|
|
|
import {MapWithTTL} from 'app/common/AsyncCreate';
|
2023-07-18 07:24:10 +00:00
|
|
|
import {WebhookMessageType} from "app/common/CommTypes";
|
2021-11-10 19:14:23 +00:00
|
|
|
import {fromTableDataAction, RowRecord, TableColValues, TableDataAction} from 'app/common/DocActions';
|
|
|
|
import {StringUnion} from 'app/common/StringUnion';
|
|
|
|
import {MetaRowRecord} from 'app/common/TableData';
|
|
|
|
import {CellDelta} from 'app/common/TabularDiff';
|
2023-05-08 22:06:24 +00:00
|
|
|
import {WebhookBatchStatus, WebhookStatus, WebhookSummary, WebhookUsage} from 'app/common/Triggers';
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
import {decodeObject} from 'app/plugin/objtypes';
|
2021-11-10 19:14:23 +00:00
|
|
|
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
|
|
|
import {makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
2022-07-04 14:14:55 +00:00
|
|
|
import log from 'app/server/lib/log';
|
2023-06-01 14:00:52 +00:00
|
|
|
import {proxyAgent} from 'app/server/lib/ProxyAgent';
|
2022-11-30 12:04:27 +00:00
|
|
|
import {matchesBaseDomain} from 'app/server/lib/requestUtils';
|
|
|
|
import {delayAbort} from 'app/server/lib/serverUtils';
|
2023-06-01 14:00:52 +00:00
|
|
|
import {LogSanitizer} from "app/server/utils/LogSanitizer";
|
2021-11-10 19:14:23 +00:00
|
|
|
import {promisifyAll} from 'bluebird';
|
2021-10-15 13:12:13 +00:00
|
|
|
import * as _ from 'lodash';
|
2022-11-30 12:04:27 +00:00
|
|
|
import {AbortController, AbortSignal} from 'node-abort-controller';
|
2021-10-15 13:12:13 +00:00
|
|
|
import fetch from 'node-fetch';
|
2021-11-10 19:14:23 +00:00
|
|
|
import {createClient, Multi, RedisClient} from 'redis';
|
2021-10-15 13:12:13 +00:00
|
|
|
|
|
|
|
promisifyAll(RedisClient.prototype);
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
|
|
|
// Only owners can manage triggers, but any user's activity can trigger them
|
|
|
|
// and the corresponding actions get the full values
|
|
|
|
const docSession = makeExceptionalDocSession('system');
|
|
|
|
|
|
|
|
// Describes the change in existence to a record, which determines the event type
|
|
|
|
interface RecordDelta {
|
|
|
|
existedBefore: boolean;
|
|
|
|
existedAfter: boolean;
|
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
type RecordDeltas = Map<number, RecordDelta>;
|
|
|
|
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
// Union discriminated by type
|
|
|
|
type TriggerAction = WebhookAction | PythonAction;
|
|
|
|
|
|
|
|
export interface WebhookAction {
|
|
|
|
type: "webhook";
|
|
|
|
id: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Just hypothetical
|
|
|
|
interface PythonAction {
|
2023-03-01 20:43:22 +00:00
|
|
|
id: string;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
type: "python";
|
|
|
|
code: string;
|
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
interface WebHookEvent {
|
|
|
|
payload: RowRecord;
|
|
|
|
id: string;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const allowedEventTypes = StringUnion("add", "update");
|
|
|
|
|
|
|
|
type EventType = typeof allowedEventTypes.type;
|
|
|
|
|
|
|
|
type Trigger = MetaRowRecord<"_grist_Triggers">;
|
|
|
|
|
|
|
|
export interface WebHookSecret {
|
|
|
|
url: string;
|
|
|
|
unsubscribeKey: string;
|
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
// Work to do after fetching values from the document
|
|
|
|
interface Task {
|
|
|
|
tableDelta: TableDelta;
|
2021-11-11 10:55:53 +00:00
|
|
|
triggers: Trigger[];
|
2021-10-15 13:12:13 +00:00
|
|
|
tableDataAction: Promise<TableDataAction>;
|
|
|
|
recordDeltas: RecordDeltas;
|
|
|
|
}
|
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
const MAX_QUEUE_SIZE =
|
|
|
|
process.env.GRIST_MAX_QUEUE_SIZE ? parseInt(process.env.GRIST_MAX_QUEUE_SIZE, 10) : 1000;
|
2021-11-03 19:09:27 +00:00
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
const WEBHOOK_CACHE_TTL = 10_000;
|
|
|
|
|
|
|
|
const WEBHOOK_STATS_CACHE_TTL = 1000 /*s*/ * 60 /*m*/ * 24/*h*/;
|
2022-05-16 12:47:34 +00:00
|
|
|
|
2022-11-30 12:04:27 +00:00
|
|
|
// A time to wait for between retries of a webhook. Exposed for tests.
|
|
|
|
const TRIGGER_WAIT_DELAY =
|
|
|
|
process.env.GRIST_TRIGGER_WAIT_DELAY ? parseInt(process.env.GRIST_TRIGGER_WAIT_DELAY, 10) : 1000;
|
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
const TRIGGER_MAX_ATTEMPTS =
|
|
|
|
process.env.GRIST_TRIGGER_MAX_ATTEMPTS ? parseInt(process.env.GRIST_TRIGGER_MAX_ATTEMPTS, 10) : 20;
|
|
|
|
|
2021-11-10 19:14:23 +00:00
|
|
|
// Processes triggers for records changed as described in action bundles.
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
// initiating webhooks and automations.
|
2021-11-03 19:09:27 +00:00
|
|
|
// The interesting stuff starts in the handle() method.
|
|
|
|
// Webhooks are placed on an event queue in memory which is replicated on redis as backup.
|
|
|
|
// The same class instance consumes the queue and sends webhook requests in the background - see _sendLoop().
|
|
|
|
// Triggers are configured in the document, while details of webhooks (URLs) are kept
|
|
|
|
// in the Secrets table of the Home DB.
|
2021-10-15 13:12:13 +00:00
|
|
|
export class DocTriggers {
|
2023-03-01 20:43:22 +00:00
|
|
|
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
// Events that need to be sent to webhooks in FIFO order.
|
2021-11-03 19:09:27 +00:00
|
|
|
// This is the primary place where events are stored and consumed,
|
|
|
|
// while a copy of this queue is kept on redis as a backup.
|
|
|
|
// Modifications to this queue should be replicated on the redis queue.
|
2021-10-15 13:12:13 +00:00
|
|
|
private _webHookEventQueue: WebHookEvent[] = [];
|
|
|
|
|
|
|
|
// DB cache for webhook secrets
|
2022-05-16 12:47:34 +00:00
|
|
|
private _webhookCache = new MapWithTTL<string, WebHookSecret>(WEBHOOK_CACHE_TTL);
|
2021-10-15 13:12:13 +00:00
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
// Set to true by shutdown().
|
|
|
|
// Indicates that loops (especially for sending requests) should stop.
|
2021-10-15 13:12:13 +00:00
|
|
|
private _shuttingDown: boolean = false;
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
// true if there is a webhook request sending loop running in the background
|
|
|
|
// to ensure only one loop is running at a time.
|
2021-10-15 13:12:13 +00:00
|
|
|
private _sending: boolean = false;
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
// Client lazily initiated by _redisClient getter, since most documents don't have triggers
|
|
|
|
// and therefore don't need a redis connection.
|
|
|
|
private _redisClientField: RedisClient | undefined;
|
|
|
|
|
|
|
|
// Promise which resolves after we finish fetching the backup queue from redis on startup.
|
|
|
|
private _getRedisQueuePromise: Promise<void> | undefined;
|
2021-10-15 13:12:13 +00:00
|
|
|
|
2022-11-30 12:04:27 +00:00
|
|
|
// Abort controller for the loop that sends webhooks.
|
|
|
|
private _loopAbort: AbortController|undefined;
|
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
private _stats: WebhookStatistics;
|
2023-06-01 14:00:52 +00:00
|
|
|
private _sanitizer = new LogSanitizer();
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
constructor(private _activeDoc: ActiveDoc) {
|
2021-10-15 13:12:13 +00:00
|
|
|
const redisUrl = process.env.REDIS_URL;
|
|
|
|
if (redisUrl) {
|
2021-11-03 19:09:27 +00:00
|
|
|
// We create a transient client just for this purpose because it makes it easy
|
|
|
|
// to quit it afterwards and avoid keeping a client open for documents without triggers.
|
|
|
|
this._getRedisQueuePromise = this._getRedisQueue(createClient(redisUrl));
|
2021-10-15 13:12:13 +00:00
|
|
|
}
|
2023-05-08 22:06:24 +00:00
|
|
|
this._stats = new WebhookStatistics(this._docId, _activeDoc, () => this._redisClient ?? null);
|
2021-10-15 13:12:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public shutdown() {
|
|
|
|
this._shuttingDown = true;
|
2022-11-30 12:04:27 +00:00
|
|
|
this._loopAbort?.abort();
|
2021-10-15 13:12:13 +00:00
|
|
|
if (!this._sending) {
|
(core) Speed up and upgrade build.
Summary:
- Upgrades to build-related packages:
- Upgrade typescript, related libraries and typings.
- Upgrade webpack, eslint; add tsc-watch, node-dev, eslint_d.
- Build organization changes:
- Build webpack from original typescript, transpiling only; with errors still
reported by a background tsc watching process.
- Typescript-related changes:
- Reduce imports of AWS dependencies (very noticeable speedup)
- Avoid auto-loading global @types
- Client code is now built with isolatedModules flag (for safe transpilation)
- Use allowJs to avoid copying JS files manually.
- Linting changes
- Enhance Arcanist ESLintLinter to run before/after commands, and set up to use eslint_d
- Update eslint config, and include .eslintignore to avoid linting generated files.
- Include a bunch of eslint-prompted and eslint-generated fixes
- Add no-unused-expression rule to eslint, and fix a few warnings about it
- Other items:
- Refactor cssInput to avoid circular dependency
- Remove a bit of unused code, libraries, dependencies
Test Plan: No behavior changes, all existing tests pass. There are 30 tests fewer reported because `test_gpath.py` was removed (it's been unused for years)
Reviewers: paulfitz
Reviewed By: paulfitz
Subscribers: paulfitz
Differential Revision: https://phab.getgrist.com/D3498
2022-06-27 20:09:41 +00:00
|
|
|
void(this._redisClientField?.quitAsync());
|
2021-10-15 13:12:13 +00:00
|
|
|
}
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
// Called after applying actions to a document and updating its data.
|
|
|
|
// Checks for triggers configured in a meta table,
|
|
|
|
// and whether any of those triggers monitor tables which were modified by the actions
|
2021-11-10 19:14:23 +00:00
|
|
|
// in the given bundle.
|
2021-11-03 19:09:27 +00:00
|
|
|
// If so, generates events which are pushed to the local and redis queues.
|
2021-11-10 19:14:23 +00:00
|
|
|
//
|
|
|
|
// Returns an ActionSummary generated from the given LocalActionBundle.
|
|
|
|
//
|
|
|
|
// Generating the summary here makes it easy to specify which columns need to
|
|
|
|
// have all their changes included in the summary without truncation
|
|
|
|
// so that we can accurately identify which records are ready for sending.
|
|
|
|
public async handle(localActionBundle: LocalActionBundle): Promise<ActionSummary> {
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
const docData = this._activeDoc.docData;
|
|
|
|
if (!docData) {
|
2021-11-10 19:14:23 +00:00
|
|
|
return summarizeAction(localActionBundle);
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
} // Happens on doc creation while processing InitNewDoc action.
|
|
|
|
|
|
|
|
const triggersTable = docData.getMetaTable("_grist_Triggers");
|
2021-12-07 11:21:16 +00:00
|
|
|
const getTableId = docData.getMetaTable("_grist_Tables").getRowPropFunc("tableId");
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
|
|
|
const triggersByTableRef = _.groupBy(triggersTable.getRecords(), "tableRef");
|
2021-11-10 19:14:23 +00:00
|
|
|
const triggersByTableId: Array<[string, Trigger[]]> = [];
|
|
|
|
|
|
|
|
// First we need a list of columns which must be included in full in the action summary
|
|
|
|
const isReadyColIds: string[] = [];
|
|
|
|
for (const tableRef of Object.keys(triggersByTableRef).sort()) {
|
|
|
|
const triggers = triggersByTableRef[tableRef];
|
|
|
|
const tableId = getTableId(Number(tableRef))!; // groupBy makes tableRef a string
|
|
|
|
triggersByTableId.push([tableId, triggers]);
|
|
|
|
for (const trigger of triggers) {
|
|
|
|
if (trigger.isReadyColRef) {
|
|
|
|
const colId = this._getColId(trigger.isReadyColRef);
|
|
|
|
if (colId) {
|
|
|
|
isReadyColIds.push(colId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const summary = summarizeAction(localActionBundle, {alwaysPreserveColIds: isReadyColIds});
|
2021-10-15 13:12:13 +00:00
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
// Work to do after fetching values from the document
|
2021-10-15 13:12:13 +00:00
|
|
|
const tasks: Task[] = [];
|
2021-11-03 19:09:27 +00:00
|
|
|
|
|
|
|
// For each table in the document which is monitored by one or more triggers...
|
2021-11-10 19:14:23 +00:00
|
|
|
for (const [tableId, triggers] of triggersByTableId) {
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
const tableDelta = summary.tableDeltas[tableId];
|
2021-11-03 19:09:27 +00:00
|
|
|
// ...if the monitored table was modified by the summarized actions,
|
|
|
|
// fetch the modified/created records and note the work that needs to be done.
|
2021-10-15 13:12:13 +00:00
|
|
|
if (tableDelta) {
|
|
|
|
const recordDeltas = this._getRecordDeltas(tableDelta);
|
|
|
|
const filters = {id: [...recordDeltas.keys()]};
|
|
|
|
|
|
|
|
// Fetch the modified records in full so they can be sent in webhooks
|
|
|
|
// They will also be used to check if the record is ready
|
2022-12-21 16:40:00 +00:00
|
|
|
const tableDataAction = this._activeDoc.fetchQuery(docSession, {tableId, filters})
|
|
|
|
.then(tableFetchResult => tableFetchResult.tableData);
|
2021-11-11 10:55:53 +00:00
|
|
|
tasks.push({tableDelta, triggers, tableDataAction, recordDeltas});
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-15 13:12:13 +00:00
|
|
|
|
|
|
|
// Fetch values from document DB in parallel
|
|
|
|
await Promise.all(tasks.map(t => t.tableDataAction));
|
|
|
|
|
|
|
|
const events: WebHookEvent[] = [];
|
|
|
|
for (const task of tasks) {
|
|
|
|
events.push(...this._handleTask(task, await task.tableDataAction));
|
|
|
|
}
|
2021-11-03 19:09:27 +00:00
|
|
|
if (!events.length) {
|
2021-11-10 19:14:23 +00:00
|
|
|
return summary;
|
2021-11-03 19:09:27 +00:00
|
|
|
}
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log("Total number of webhook events generated by bundle", {numEvents: events.length});
|
2021-11-03 19:09:27 +00:00
|
|
|
|
|
|
|
// Only add events to the queue after we finish fetching the backup from redis
|
|
|
|
// to ensure that events are delivered in the order they were generated.
|
|
|
|
await this._getRedisQueuePromise;
|
|
|
|
|
|
|
|
if (this._redisClient) {
|
|
|
|
await this._pushToRedisQueue(events);
|
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
this._webHookEventQueue.push(...events);
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
this._startSendLoop();
|
2021-10-15 13:12:13 +00:00
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
// Prevent further document activity while the queue is too full.
|
|
|
|
while (this._drainingQueue && !this._shuttingDown) {
|
2023-07-18 07:24:10 +00:00
|
|
|
const sendNotificationPromise = this._activeDoc.sendWebhookNotification(WebhookMessageType.Overflow);
|
|
|
|
const delayPromise = delayAbort(5000, this._loopAbort?.signal);
|
|
|
|
await Promise.all([sendNotificationPromise, delayPromise]);
|
2021-10-15 13:12:13 +00:00
|
|
|
}
|
2021-11-10 19:14:23 +00:00
|
|
|
|
|
|
|
return summary;
|
2021-11-03 19:09:27 +00:00
|
|
|
}
|
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
/**
|
|
|
|
* Creates summary for all webhooks in the document.
|
|
|
|
*/
|
|
|
|
public async summary(): Promise<WebhookSummary[]> {
|
|
|
|
// Prepare some data we will use.
|
|
|
|
const docData = this._activeDoc.docData!;
|
|
|
|
const triggersTable = docData.getMetaTable("_grist_Triggers");
|
|
|
|
const getTableId = docData.getMetaTable("_grist_Tables").getRowPropFunc("tableId");
|
|
|
|
const getColId = docData.getMetaTable("_grist_Tables_column").getRowPropFunc("colId");
|
|
|
|
const getUrl = async (id: string) => (await this._getWebHook(id))?.url ?? '';
|
|
|
|
const getUnsubscribeKey = async (id: string) => (await this._getWebHook(id))?.unsubscribeKey ?? '';
|
|
|
|
const result: WebhookSummary[] = [];
|
|
|
|
|
|
|
|
// Go through all triggers int the document that we have.
|
|
|
|
for (const t of triggersTable.getRecords()) {
|
|
|
|
// Each trigger has associated table and a bunch of trigger actions (currently only 1 that is webhook).
|
|
|
|
const actions = JSON.parse(t.actions) as TriggerAction[];
|
|
|
|
// Get only webhooks for this trigger.
|
|
|
|
const webhookActions = actions.filter(act => act.type === "webhook") as WebhookAction[];
|
|
|
|
for (const act of webhookActions) {
|
|
|
|
// Url, probably should be hidden for non-owners (but currently this API is owners only).
|
|
|
|
const url = await getUrl(act.id);
|
|
|
|
// Same story, should be hidden.
|
|
|
|
const unsubscribeKey = await getUnsubscribeKey(act.id);
|
|
|
|
if (!url || !unsubscribeKey) {
|
|
|
|
// Webhook might have been deleted in the mean time.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Report some basic info and usage stats.
|
|
|
|
const entry: WebhookSummary = {
|
|
|
|
// Id of the webhook
|
|
|
|
id: act.id,
|
|
|
|
fields: {
|
|
|
|
// Url, probably should be hidden for non-owners (but currently this API is owners only).
|
|
|
|
url,
|
|
|
|
unsubscribeKey,
|
|
|
|
// Other fields used to register this webhook.
|
|
|
|
eventTypes: decodeObject(t.eventTypes) as string[],
|
|
|
|
isReadyColumn: getColId(t.isReadyColRef) ?? null,
|
|
|
|
tableId: getTableId(t.tableRef) ?? null,
|
|
|
|
// For future use - for now every webhook is enabled.
|
2023-05-08 22:06:24 +00:00
|
|
|
enabled: t.enabled,
|
|
|
|
name: t.label,
|
|
|
|
memo: t.memo,
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
},
|
|
|
|
// Create some statics and status info.
|
|
|
|
usage: await this._stats.getUsage(act.id, this._webHookEventQueue),
|
|
|
|
};
|
|
|
|
result.push(entry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
(core) API reworked to use POST to create webhook and DELET to remove it
Summary:
introduces POST /api/docs/{docId}/webhooks and DELETE /api/docs/{docId}/webhooks/{webhookId} on place of old _subscribe and _unsubscribe endpoints.
Remove checking for unsubscribeKey while deleting webhook - only owner can delete webhook using DELETE endpoint. subscription key is still needed for _unsubscribe endpoint.
old _unsubscribe and _subscribe endpoints are still active and work as before - no changes there.
Posting schema:
```
POST /api/docs/[docId]/webhooks
```
Request Body:
```
{
"webhooks": [
{
"fields": {
"url": "https://webhook.site/3bd02246-f122-445e-ba7f-bf5ea5bb6eb1",
"eventTypes": [
"add",
"update"
],
"enabled": true,
"name": "WebhookName",
"memo": "just a text",
"tableId": "Table1"
}
},
{
"fields": {
"url": "https://webhook.site/3bd02246-f122-445e-ba7f-bf5ea5bb6eb2",
"eventTypes": [
"add",
],
"enabled": true,
"name": "OtherWebhookName",
"memo": "just a text",
"tableId": "Table1"
}
}
]
}
```
Expected response: WebhookId for each webhook posted:
```
{
"webhooks": [
{
"id": "85c77108-f1e1-4217-a50d-acd1c5996da2"
},
{
"id": "d87a6402-cfd7-4822-878c-657308fcc8c3"
}
]
}
```
Deleting webhooks:
```
DELETE api/docs/[docId]/webhooks/[webhookId]
```
there is no payload in DELETE request. Therefore only one webhook can be deleted at once
Response:
```
{
"success": true
}
```
Test Plan: Old unit test improved to handle new endpoints, and one more added to check if endpoints are in fact created/removed
Reviewers: alexmojaki
Reviewed By: alexmojaki
Subscribers: paulfitz, alexmojaki
Differential Revision: https://phab.getgrist.com/D3916
2023-07-14 10:05:22 +00:00
|
|
|
public getWebhookTriggerRecord(webhookId: string) {
|
2023-03-01 20:43:22 +00:00
|
|
|
const docData = this._activeDoc.docData!;
|
|
|
|
const triggersTable = docData.getMetaTable("_grist_Triggers");
|
|
|
|
const trigger = triggersTable.getRecords().find(t => {
|
|
|
|
const actions: TriggerAction[] = JSON.parse((t.actions || '[]') as string);
|
|
|
|
return actions.some(action => action.id === webhookId && action?.type === "webhook");
|
|
|
|
});
|
|
|
|
if (!trigger) {
|
|
|
|
throw new ApiError(`Webhook not found "${webhookId || ''}"`, 404);
|
|
|
|
}
|
|
|
|
return trigger;
|
|
|
|
}
|
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
public webhookDeleted(id: string) {
|
|
|
|
// We can't do much about that as the loop might be in progress and it is not safe to modify the queue.
|
|
|
|
// But we can clear the webHook cache, so that the next time we check the webhook url it will be gone.
|
2023-05-08 22:06:24 +00:00
|
|
|
this.clearWebhookCache(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
public clearWebhookCache(id: string) {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
this._webhookCache.delete(id);
|
|
|
|
}
|
2022-11-30 12:04:27 +00:00
|
|
|
|
|
|
|
public async clearWebhookQueue() {
|
|
|
|
// Make sure we are after start and in sync with redis.
|
|
|
|
if (this._getRedisQueuePromise) {
|
|
|
|
await this._getRedisQueuePromise;
|
|
|
|
}
|
|
|
|
// Clear in-memory queue.
|
|
|
|
const removed = this._webHookEventQueue.splice(0, this._webHookEventQueue.length).length;
|
|
|
|
// Notify the loop that it should restart.
|
|
|
|
this._loopAbort?.abort();
|
|
|
|
// If we have backup in redis, clear it also.
|
|
|
|
// NOTE: this is subject to a race condition, currently it is not possible, but any future modification probably
|
|
|
|
// will require some kind of locking over the queue (or a rewrite)
|
|
|
|
if (removed && this._redisClient) {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
await this._redisClient.multi().del(this._redisQueueKey).execAsync();
|
2022-11-30 12:04:27 +00:00
|
|
|
}
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
await this._stats.clear();
|
2022-11-30 12:04:27 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 07:24:10 +00:00
|
|
|
public async clearSingleWebhookQueue(webhookId: string) {
|
|
|
|
// Make sure we are after start and in sync with redis.
|
|
|
|
if (this._getRedisQueuePromise) {
|
|
|
|
await this._getRedisQueuePromise;
|
|
|
|
}
|
|
|
|
// Clear in-memory queue for given webhook key.
|
|
|
|
let removed = 0;
|
|
|
|
for(let i=0; i< this._webHookEventQueue.length; i++){
|
|
|
|
if(this._webHookEventQueue[i].id == webhookId){
|
|
|
|
this._webHookEventQueue.splice(i, 1);
|
|
|
|
removed++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Notify the loop that it should restart.
|
|
|
|
this._loopAbort?.abort();
|
|
|
|
// If we have backup in redis, clear it also.
|
|
|
|
// NOTE: this is subject to a race condition, currently it is not possible, but any future modification probably
|
|
|
|
// will require some kind of locking over the queue (or a rewrite)
|
|
|
|
if (removed && this._redisClient) {
|
|
|
|
const multi = this._redisClient.multi();
|
|
|
|
multi.del(this._redisQueueKey);
|
|
|
|
|
|
|
|
// Re-add all the remaining events to the queue.
|
|
|
|
const strings = this._webHookEventQueue.map(e => JSON.stringify(e));
|
|
|
|
multi.rpush(this._redisQueueKey, ...strings);
|
|
|
|
await multi.execAsync();
|
|
|
|
}
|
|
|
|
await this._stats.clear();
|
|
|
|
}
|
|
|
|
|
2023-03-01 20:43:22 +00:00
|
|
|
// Converts a table to tableId by looking it up in _grist_Tables.
|
|
|
|
private _getTableId(rowId: number) {
|
|
|
|
const docData = this._activeDoc.docData;
|
|
|
|
if (!docData) {
|
|
|
|
throw new Error("ActiveDoc not ready");
|
|
|
|
}
|
|
|
|
return docData.getMetaTable("_grist_Tables").getValue(rowId, "tableId");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return false if colRef does not belong to tableRef
|
|
|
|
private _validateColId(colRef: number, tableRef: number) {
|
|
|
|
const docData = this._activeDoc.docData;
|
|
|
|
if (!docData) {
|
|
|
|
throw new Error("ActiveDoc not ready");
|
|
|
|
}
|
|
|
|
return docData.getMetaTable("_grist_Tables_column").getValue(colRef, "parentId") === tableRef;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converts a column ref to colId by looking it up in _grist_Tables_column. If tableRef is
|
|
|
|
// provided, check whether col belongs to table and throws if not.
|
|
|
|
private _getColId(rowId: number, tableRef?: number) {
|
|
|
|
const docData = this._activeDoc.docData;
|
|
|
|
if (!docData) {
|
|
|
|
throw new Error("ActiveDoc not ready");
|
|
|
|
}
|
|
|
|
if (!rowId) { return ''; }
|
|
|
|
const colId = docData.getMetaTable("_grist_Tables_column").getValue(rowId, "colId");
|
|
|
|
if (tableRef !== undefined &&
|
|
|
|
docData.getMetaTable("_grist_Tables_column").getValue(rowId, "parentId") !== tableRef) {
|
|
|
|
throw new ApiError(`Column ${colId} does not belong to table ${this._getTableId(tableRef)}`, 400);
|
|
|
|
}
|
|
|
|
return colId;
|
|
|
|
}
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
private get _docId() {
|
|
|
|
return this._activeDoc.docName;
|
|
|
|
}
|
|
|
|
|
|
|
|
private get _redisQueueKey() {
|
|
|
|
return `webhook-queue-${this._docId}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
private get _drainingQueue() {
|
|
|
|
return this._webHookEventQueue.length >= MAX_QUEUE_SIZE;
|
|
|
|
}
|
|
|
|
|
2021-11-11 10:55:53 +00:00
|
|
|
private _log(msg: string, {level = 'info', ...meta}: any = {}) {
|
|
|
|
log.origLog(level, 'DocTriggers: ' + msg, {
|
|
|
|
...meta,
|
|
|
|
docId: this._docId,
|
|
|
|
queueLength: this._webHookEventQueue.length,
|
|
|
|
drainingQueue: this._drainingQueue,
|
|
|
|
shuttingDown: this._shuttingDown,
|
|
|
|
sending: this._sending,
|
|
|
|
redisClient: Boolean(this._redisClientField),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
private async _pushToRedisQueue(events: WebHookEvent[]) {
|
|
|
|
const strings = events.map(e => JSON.stringify(e));
|
2023-06-01 14:00:52 +00:00
|
|
|
try {
|
|
|
|
await this._redisClient?.rpushAsync(this._redisQueueKey, ...strings);
|
|
|
|
}
|
|
|
|
catch(e){
|
|
|
|
// It's very hard to test this with integration tests, because it requires a redis failure.
|
|
|
|
// And it's not easy to simulate redis failure.
|
|
|
|
// So on this point we have only unit test in core/test/server/utils/LogSanitizer.ts
|
|
|
|
throw this._sanitizer.sanitize(e);
|
|
|
|
}
|
2021-11-03 19:09:27 +00:00
|
|
|
}
|
2021-10-15 13:12:13 +00:00
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
private async _getRedisQueue(redisClient: RedisClient) {
|
|
|
|
const strings = await redisClient.lrangeAsync(this._redisQueueKey, 0, -1);
|
|
|
|
if (strings.length) {
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log("Webhook events found on redis queue", {numEvents: strings.length});
|
2021-11-03 19:09:27 +00:00
|
|
|
const events = strings.map(s => JSON.parse(s));
|
|
|
|
this._webHookEventQueue.unshift(...events);
|
|
|
|
this._startSendLoop();
|
|
|
|
}
|
|
|
|
await redisClient.quitAsync();
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
private _getRecordDeltas(tableDelta: TableDelta): RecordDeltas {
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
const recordDeltas = new Map<number, RecordDelta>();
|
2021-10-15 13:12:13 +00:00
|
|
|
tableDelta.updateRows.forEach(id =>
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
recordDeltas.set(id, {existedBefore: true, existedAfter: true}));
|
|
|
|
// A row ID can appear in both updateRows and addRows, although it probably shouldn't
|
|
|
|
// Added row IDs override updated rows because they didn't exist before
|
2021-10-15 13:12:13 +00:00
|
|
|
tableDelta.addRows.forEach(id =>
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
recordDeltas.set(id, {existedBefore: false, existedAfter: true}));
|
|
|
|
|
|
|
|
// If we allow subscribing to deletion in the future
|
|
|
|
// delta.removeRows.forEach(id =>
|
|
|
|
// recordDeltas.set(id, {existedBefore: true, existedAfter: false}));
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
return recordDeltas;
|
|
|
|
}
|
|
|
|
|
|
|
|
private _handleTask(
|
2021-11-11 10:55:53 +00:00
|
|
|
{tableDelta, triggers, recordDeltas}: Task,
|
2021-10-15 13:12:13 +00:00
|
|
|
tableDataAction: TableDataAction,
|
|
|
|
) {
|
|
|
|
const bulkColValues = fromTableDataAction(tableDataAction);
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
2021-11-11 10:55:53 +00:00
|
|
|
const meta = {numTriggers: triggers.length, numRecords: bulkColValues.id.length};
|
|
|
|
this._log(`Processing triggers`, meta);
|
2021-09-27 20:50:29 +00:00
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
const makePayload = _.memoize((rowIndex: number) =>
|
|
|
|
_.mapValues(bulkColValues, col => col[rowIndex]) as RowRecord
|
|
|
|
);
|
|
|
|
|
|
|
|
const result: WebHookEvent[] = [];
|
2021-09-27 20:50:29 +00:00
|
|
|
for (const trigger of triggers) {
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
const actions = JSON.parse(trigger.actions) as TriggerAction[];
|
2021-10-15 13:12:13 +00:00
|
|
|
const webhookActions = actions.filter(act => act.type === "webhook") as WebhookAction[];
|
|
|
|
if (!webhookActions.length) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-09-27 20:50:29 +00:00
|
|
|
|
2023-03-01 20:43:22 +00:00
|
|
|
if (trigger.isReadyColRef) {
|
|
|
|
if (!this._validateColId(trigger.isReadyColRef, trigger.tableRef)) {
|
|
|
|
// ready column does not belong to table, let's ignore trigger and log stats
|
|
|
|
for (const action of webhookActions) {
|
|
|
|
const colId = this._getColId(trigger.isReadyColRef); // no validation
|
|
|
|
const tableId = this._getTableId(trigger.tableRef);
|
|
|
|
const error = `isReadyColumn is not valid: colId ${colId} does not belong to ${tableId}`;
|
|
|
|
this._stats.logInvalid(action.id, error).catch(e => log.error("Webhook stats failed to log", e));
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: would be worth checking that the trigger's fields are valid (ie: eventTypes, url,
|
|
|
|
// ...) as there's no guarantee that they are.
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
const rowIndexesToSend: number[] = _.range(bulkColValues.id.length).filter(rowIndex => {
|
|
|
|
const rowId = bulkColValues.id[rowIndex];
|
|
|
|
return this._shouldTriggerActions(
|
|
|
|
trigger, bulkColValues, rowIndex, rowId, recordDeltas.get(rowId)!, tableDelta,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
for (const action of webhookActions) {
|
|
|
|
for (const rowIndex of rowIndexesToSend) {
|
|
|
|
const event = {id: action.id, payload: makePayload(rowIndex)};
|
|
|
|
result.push(event);
|
|
|
|
}
|
2021-09-27 20:50:29 +00:00
|
|
|
}
|
|
|
|
}
|
2021-11-11 10:55:53 +00:00
|
|
|
|
|
|
|
this._log("Generated events from triggers", {numEvents: result.length, ...meta});
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
return result;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
/**
|
|
|
|
* Determines if actions should be triggered for a single record and trigger.
|
|
|
|
*/
|
|
|
|
private _shouldTriggerActions(
|
|
|
|
trigger: Trigger,
|
|
|
|
bulkColValues: TableColValues,
|
|
|
|
rowIndex: number,
|
|
|
|
rowId: number,
|
|
|
|
recordDelta: RecordDelta,
|
|
|
|
tableDelta: TableDelta,
|
|
|
|
): boolean {
|
|
|
|
let readyBefore: boolean;
|
2023-05-08 22:06:24 +00:00
|
|
|
if (!trigger.enabled) {
|
|
|
|
return false;
|
|
|
|
} else if (!trigger.isReadyColRef) {
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
// User hasn't configured a column, so all records are considered ready immediately
|
2021-10-15 13:12:13 +00:00
|
|
|
readyBefore = recordDelta.existedBefore;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
} else {
|
2021-10-15 13:12:13 +00:00
|
|
|
const isReadyColId = this._getColId(trigger.isReadyColRef)!;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
// Must be the actual boolean `true`, not just anything truthy
|
|
|
|
const isReady = bulkColValues[isReadyColId][rowIndex] === true;
|
|
|
|
if (!isReady) {
|
|
|
|
return false;
|
|
|
|
}
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
const cellDelta: CellDelta | undefined = tableDelta.columnDeltas[isReadyColId]?.[rowId];
|
|
|
|
if (!recordDelta.existedBefore) {
|
|
|
|
readyBefore = false;
|
|
|
|
} else if (!cellDelta ) {
|
|
|
|
// Cell wasn't changed, and the record is ready now, so it was ready before.
|
2021-11-10 19:14:23 +00:00
|
|
|
// This requires that the ActionSummary contains all changes to the isReady column.
|
2021-10-15 13:12:13 +00:00
|
|
|
readyBefore = true;
|
|
|
|
} else {
|
|
|
|
const deltaBefore = cellDelta[0];
|
|
|
|
if (deltaBefore === null) {
|
|
|
|
// The record didn't exist before, so it definitely wasn't ready
|
|
|
|
// (although we probably shouldn't reach this since we already checked recordDelta.existedBefore)
|
|
|
|
readyBefore = false;
|
|
|
|
} else if (deltaBefore === "?") {
|
|
|
|
// The ActionSummary shouldn't contain this kind of delta at all
|
|
|
|
// since it comes from a single action bundle, not a combination of summaries.
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log('Unexpected deltaBefore === "?"', {level: 'warn', trigger});
|
2021-10-15 13:12:13 +00:00
|
|
|
readyBefore = true;
|
|
|
|
} else {
|
|
|
|
// Only remaining case is that deltaBefore is a single-element array containing the previous value.
|
|
|
|
const [valueBefore] = deltaBefore;
|
|
|
|
|
|
|
|
// Must be the actual boolean `true`, not just anything truthy
|
|
|
|
readyBefore = valueBefore === true;
|
|
|
|
}
|
|
|
|
}
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let eventType: EventType;
|
2021-10-15 13:12:13 +00:00
|
|
|
if (readyBefore) {
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
eventType = "update";
|
|
|
|
// If we allow subscribing to deletion in the future
|
|
|
|
// if (recordDelta.existedAfter) {
|
|
|
|
// eventType = "update";
|
|
|
|
// } else {
|
|
|
|
// eventType = "remove";
|
|
|
|
// }
|
|
|
|
} else {
|
|
|
|
eventType = "add";
|
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
return trigger.eventTypes!.includes(eventType);
|
|
|
|
}
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
private async _getWebHook(id: string): Promise<WebHookSecret | undefined> {
|
2021-10-15 13:12:13 +00:00
|
|
|
let webhook = this._webhookCache.get(id);
|
|
|
|
if (!webhook) {
|
2021-11-11 10:55:53 +00:00
|
|
|
const secret = await this._activeDoc.getHomeDbManager()?.getSecret(id, this._docId);
|
2021-10-15 13:12:13 +00:00
|
|
|
if (!secret) {
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log(`No webhook secret found`, {level: 'warn', id});
|
2021-10-15 13:12:13 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
webhook = JSON.parse(secret);
|
|
|
|
this._webhookCache.set(id, webhook!);
|
|
|
|
}
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
return webhook!;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async _getWebHookUrl(id: string): Promise<string | undefined> {
|
|
|
|
const url = (await this._getWebHook(id))?.url ?? '';
|
2021-10-15 13:12:13 +00:00
|
|
|
if (!isUrlAllowed(url)) {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
// TODO: this is not a good place for a validation.
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log(`Webhook not sent to forbidden URL`, {level: 'warn', url});
|
2021-10-15 13:12:13 +00:00
|
|
|
return;
|
2021-09-27 20:50:29 +00:00
|
|
|
}
|
2021-10-15 13:12:13 +00:00
|
|
|
return url;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
private _startSendLoop() {
|
|
|
|
if (!this._sending) { // only run one loop at a time
|
|
|
|
this._sending = true;
|
|
|
|
this._sendLoop().catch((e) => { // run _sendLoop asynchronously (in the background)
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log(`_sendLoop failed: ${e}`, {level: 'error'});
|
2021-11-03 19:09:27 +00:00
|
|
|
this._sending = false; // otherwise the following line will complete instantly
|
|
|
|
this._startSendLoop(); // restart the loop on failure
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Consumes the webhook event queue and sends HTTP requests.
|
|
|
|
// Should only be called if there are events to send.
|
|
|
|
// Managed by _startSendLoop. Runs in the background. Only one loop should run at a time.
|
|
|
|
// Runs until shutdown.
|
2021-10-15 13:12:13 +00:00
|
|
|
private async _sendLoop() {
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log("Starting _sendLoop");
|
2021-10-15 13:12:13 +00:00
|
|
|
|
|
|
|
// TODO delay/prevent shutting down while queue isn't empty?
|
|
|
|
while (!this._shuttingDown) {
|
2022-11-30 12:04:27 +00:00
|
|
|
this._loopAbort = new AbortController();
|
2021-10-15 13:12:13 +00:00
|
|
|
if (!this._webHookEventQueue.length) {
|
2022-11-30 12:04:27 +00:00
|
|
|
await delayAbort(TRIGGER_WAIT_DELAY, this._loopAbort.signal).catch(() => {});
|
2021-10-15 13:12:13 +00:00
|
|
|
continue;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
2021-10-15 13:12:13 +00:00
|
|
|
const id = this._webHookEventQueue[0].id;
|
|
|
|
const batch = _.takeWhile(this._webHookEventQueue.slice(0, 100), {id});
|
|
|
|
const body = JSON.stringify(batch.map(e => e.payload));
|
|
|
|
const url = await this._getWebHookUrl(id);
|
2022-11-30 12:04:27 +00:00
|
|
|
if (this._loopAbort.signal.aborted) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-11-11 10:55:53 +00:00
|
|
|
let meta: Record<string, any>|undefined;
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
let success: boolean;
|
|
|
|
if (!url) {
|
|
|
|
success = true;
|
|
|
|
} else {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
await this._stats.logStatus(id, 'sending');
|
2021-11-11 10:55:53 +00:00
|
|
|
meta = {numEvents: batch.length, webhookId: id, host: new URL(url).host};
|
|
|
|
this._log("Sending batch of webhook events", meta);
|
2023-05-18 22:35:39 +00:00
|
|
|
this._activeDoc.logTelemetryEvent(null, 'sendingWebhooks', {
|
2023-06-06 17:08:50 +00:00
|
|
|
limited: {numEvents: meta.numEvents},
|
2023-05-18 22:35:39 +00:00
|
|
|
});
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
success = await this._sendWebhookWithRetries(id, url, body, batch.length, this._loopAbort.signal);
|
|
|
|
if (this._loopAbort.signal.aborted) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-11-30 12:04:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this._loopAbort.signal.aborted) {
|
|
|
|
continue;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
2021-11-03 19:09:27 +00:00
|
|
|
|
|
|
|
this._webHookEventQueue.splice(0, batch.length);
|
|
|
|
|
|
|
|
let multi: Multi | null = null;
|
|
|
|
if (this._redisClient) {
|
|
|
|
multi = this._redisClient.multi();
|
|
|
|
multi.ltrim(this._redisQueueKey, batch.length, -1);
|
|
|
|
}
|
|
|
|
|
2021-11-11 10:55:53 +00:00
|
|
|
if (!success) {
|
|
|
|
this._log("Failed to send batch of webhook events", {...meta, level: 'warn'});
|
|
|
|
if (!this._drainingQueue) {
|
|
|
|
// Put the failed events at the end of the queue to try again later
|
|
|
|
// while giving other URLs a chance to receive events.
|
|
|
|
this._webHookEventQueue.push(...batch);
|
|
|
|
if (multi) {
|
|
|
|
const strings = batch.map(e => JSON.stringify(e));
|
|
|
|
multi.rpush(this._redisQueueKey, ...strings);
|
|
|
|
}
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
// We are postponed, so mark that.
|
|
|
|
await this._stats.logStatus(id, 'postponed');
|
|
|
|
} else {
|
|
|
|
// We are draining the queue and we skipped some events, so mark that.
|
|
|
|
await this._stats.logStatus(id, 'error');
|
|
|
|
await this._stats.logBatch(id, 'rejected');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
await this._stats.logStatus(id, 'idle');
|
|
|
|
if (meta) {
|
|
|
|
this._log("Successfully sent batch of webhook events", meta);
|
2021-11-03 19:09:27 +00:00
|
|
|
}
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
2021-11-03 19:09:27 +00:00
|
|
|
|
|
|
|
await multi?.execAsync();
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log("Ended _sendLoop");
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
this._redisClient?.quitAsync().catch(e =>
|
|
|
|
// Catch error to prevent sendLoop being restarted
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log("Error quitting redis: " + e, {level: 'warn'})
|
2021-10-15 13:12:13 +00:00
|
|
|
);
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
|
2021-11-03 19:09:27 +00:00
|
|
|
private get _redisClient() {
|
|
|
|
if (this._redisClientField) {
|
|
|
|
return this._redisClientField;
|
|
|
|
}
|
|
|
|
const redisUrl = process.env.REDIS_URL;
|
|
|
|
if (redisUrl) {
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log("Creating redis client");
|
2021-11-03 19:09:27 +00:00
|
|
|
this._redisClientField = createClient(redisUrl);
|
|
|
|
}
|
|
|
|
return this._redisClientField;
|
|
|
|
}
|
|
|
|
|
|
|
|
private get _maxWebhookAttempts() {
|
|
|
|
if (this._shuttingDown) {
|
|
|
|
return 0;
|
|
|
|
}
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
return this._drainingQueue ? Math.min(5, TRIGGER_MAX_ATTEMPTS) : TRIGGER_MAX_ATTEMPTS;
|
2021-11-03 19:09:27 +00:00
|
|
|
}
|
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
private async _sendWebhookWithRetries(id: string, url: string, body: string, size: number, signal: AbortSignal) {
|
2021-10-15 13:12:13 +00:00
|
|
|
const maxWait = 64;
|
|
|
|
let wait = 1;
|
2021-11-03 19:09:27 +00:00
|
|
|
for (let attempt = 0; attempt < this._maxWebhookAttempts; attempt++) {
|
2021-10-15 13:12:13 +00:00
|
|
|
if (this._shuttingDown) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
try {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
if (attempt > 0) {
|
|
|
|
await this._stats.logStatus(id, 'retrying');
|
|
|
|
}
|
2021-10-15 13:12:13 +00:00
|
|
|
const response = await fetch(url, {
|
|
|
|
method: 'POST',
|
|
|
|
body,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
},
|
2022-11-30 12:04:27 +00:00
|
|
|
signal,
|
2023-05-08 09:49:53 +00:00
|
|
|
agent: proxyAgent(new URL(url)),
|
2021-10-15 13:12:13 +00:00
|
|
|
});
|
|
|
|
if (response.status === 200) {
|
2022-12-22 17:15:06 +00:00
|
|
|
await this._stats.logBatch(id, 'success', { size, httpStatus: 200, error: null, attempts: attempt + 1 });
|
2021-10-15 13:12:13 +00:00
|
|
|
return true;
|
|
|
|
}
|
2022-12-22 17:15:06 +00:00
|
|
|
await this._stats.logBatch(id, 'failure', {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
httpStatus: response.status,
|
|
|
|
error: await response.text(),
|
|
|
|
attempts: attempt + 1,
|
|
|
|
size,
|
|
|
|
});
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log(`Webhook responded with non-200 status`, {level: 'warn', status: response.status, attempt});
|
2021-10-15 13:12:13 +00:00
|
|
|
} catch (e) {
|
2022-12-22 17:15:06 +00:00
|
|
|
await this._stats.logBatch(id, 'failure', {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
httpStatus: null,
|
|
|
|
error: (e.message || 'Unrecognized error during fetch'),
|
|
|
|
attempts: attempt + 1,
|
|
|
|
size,
|
|
|
|
});
|
2021-11-11 10:55:53 +00:00
|
|
|
this._log(`Webhook sending error: ${e}`, {level: 'warn', attempt});
|
2021-10-15 13:12:13 +00:00
|
|
|
}
|
|
|
|
|
2022-11-30 12:04:27 +00:00
|
|
|
if (signal.aborted) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-10-15 13:12:13 +00:00
|
|
|
// Don't wait any more if this is the last attempt.
|
2021-11-03 19:09:27 +00:00
|
|
|
if (attempt >= this._maxWebhookAttempts - 1) {
|
2021-10-15 13:12:13 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wait `wait` seconds, checking this._shuttingDown every second.
|
|
|
|
for (let waitIndex = 0; waitIndex < wait; waitIndex++) {
|
|
|
|
if (this._shuttingDown) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-11-30 12:04:27 +00:00
|
|
|
try {
|
|
|
|
await delayAbort(TRIGGER_WAIT_DELAY, signal);
|
|
|
|
} catch (e) {
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
// If signal was aborted, don't log anything as we probably was cleared.
|
2022-11-30 12:04:27 +00:00
|
|
|
return false;
|
|
|
|
}
|
2021-10-15 13:12:13 +00:00
|
|
|
}
|
|
|
|
if (wait < maxWait) {
|
|
|
|
wait *= 2;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-15 13:12:13 +00:00
|
|
|
return false;
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isUrlAllowed(urlString: string) {
|
|
|
|
let url: URL;
|
|
|
|
try {
|
|
|
|
url = new URL(urlString);
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-05-23 16:51:58 +00:00
|
|
|
// Support at most https and http.
|
|
|
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Support a wildcard that allows all domains.
|
|
|
|
// Allow either https or http if it is set.
|
|
|
|
if (process.env.ALLOWED_WEBHOOK_DOMAINS === '*') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
// http (no s) is only allowed for localhost for testing.
|
|
|
|
// localhost still needs to be explicitly permitted, and it shouldn't be outside dev
|
|
|
|
if (url.protocol !== "https:" && url.hostname !== "localhost") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (process.env.ALLOWED_WEBHOOK_DOMAINS || "").split(",").some(domain =>
|
2022-09-28 16:33:53 +00:00
|
|
|
domain && matchesBaseDomain(url.host, domain)
|
(core) Initial webhooks implementation
Summary:
See https://grist.quip.com/VKd3ASF99ezD/Outgoing-Webhooks
- 2 new DocApi endpoints: _subscribe and _unsubscribe, not meant to be user friendly or publicly documented. _unsubscribe should be given the response from _subscribe in the body, e.g:
```
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_subscribe" -H "Content-type: application/json" -d '{"url": "https://webhook.site/a916b526-8afc-46e6-aa8f-a625d0d83ec3", "eventTypes": ["add"], "isReadyColumn": "C"}'
{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}
$ curl -X POST -H "Authorization: Bearer 8fd4dc59ecb05ab29ae5a183c03101319b8e6ca9" "http://localhost:8080/api/docs/6WYa23FqWxGNe3AR6DLjCJ/tables/Table2/_unsubscribe" -H "Content-type: application/json" -d '{"unsubscribeKey":"3246f158-55b5-4fc7-baa5-093b75ffa86c","triggerId":2,"webhookId":"853b4bfa-9d39-4639-aa33-7d45354903c0"}'
{"success":true}
```
- New DB entity Secret to hold the webhook URL and unsubscribe key
- New document metatable _grist_Triggers subscribes to table changes and points to a secret to use for a webhook
- New file Triggers.ts processes action summaries and uses the two new tables to send webhooks.
- Also went on a bit of a diversion and made a typesafe subclass of TableData for metatables.
I think this is essentially good enough for a first diff, to keep the diffs manageable and to talk about the overall structure. Future diffs can add tests and more robustness using redis etc. After this diff I can also start building the Zapier integration privately.
Test Plan: Tested manually: see curl commands in summary for an example. Payloads can be seen in https://webhook.site/#!/a916b526-8afc-46e6-aa8f-a625d0d83ec3/0b9fe335-33f7-49fe-b90b-2db5ba53382d/1 . Great site for testing webhooks btw.
Reviewers: dsagal, paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3019
2021-09-22 23:06:23 +00:00
|
|
|
);
|
|
|
|
}
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implementation detail, helper to provide a persisted storage to a derived class.
|
|
|
|
*/
|
|
|
|
class PersistedStore<Keys> {
|
|
|
|
/** In memory fallback if redis is not available */
|
|
|
|
private _statsCache = new MapWithTTL<string, string>(WEBHOOK_STATS_CACHE_TTL);
|
|
|
|
private _redisKey: string;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
docId: string,
|
2023-05-08 22:06:24 +00:00
|
|
|
private _activeDoc: ActiveDoc,
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
private _redisClientDep: () => RedisClient | null
|
|
|
|
) {
|
|
|
|
this._redisKey = `webhooks:${docId}:statistics`;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async clear() {
|
|
|
|
this._statsCache.clear();
|
|
|
|
if (this._redisClient) {
|
|
|
|
await this._redisClient.delAsync(this._redisKey).catch(() => {});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-08 22:06:24 +00:00
|
|
|
protected async markChange() {
|
|
|
|
await this._activeDoc.sendWebhookNotification();
|
|
|
|
}
|
|
|
|
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
protected async set(id: string, keyValues: [Keys, string][]) {
|
|
|
|
if (this._redisClient) {
|
|
|
|
const multi = this._redisClient.multi();
|
|
|
|
for (const [key, value] of keyValues) {
|
|
|
|
multi.hset(this._redisKey, `${id}:${key}`, value);
|
|
|
|
multi.expire(this._redisKey, WEBHOOK_STATS_CACHE_TTL);
|
|
|
|
}
|
|
|
|
await multi.execAsync();
|
|
|
|
} else {
|
|
|
|
for (const [key, value] of keyValues) {
|
|
|
|
this._statsCache.set(`${id}:${key}`, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async get(id: string, keys: Keys[]): Promise<[Keys, string][]> {
|
|
|
|
if (this._redisClient) {
|
|
|
|
const values = (await this._redisClient.hgetallAsync(this._redisKey)) || {};
|
|
|
|
return keys.map(key => [key, values[`${id}:${key}`] || '']);
|
|
|
|
} else {
|
|
|
|
return keys.map(key => [key, this._statsCache.get(`${id}:${key}`) || '']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private get _redisClient() {
|
|
|
|
return this._redisClientDep();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper class that monitors and saves (either in memory or in Redis) usage statics and current
|
|
|
|
* status of webhooks.
|
|
|
|
*/
|
|
|
|
class WebhookStatistics extends PersistedStore<StatsKey> {
|
|
|
|
/**
|
|
|
|
* Retrieves and calculates all the statistics for a given webhook.
|
|
|
|
* @param id Webhook ID
|
|
|
|
* @param queue Current webhook task queue
|
|
|
|
*/
|
|
|
|
public async getUsage(id: string, queue: WebHookEvent[]): Promise<WebhookUsage|null> {
|
|
|
|
// Get all the keys from the store for this webhook, and create a dictionary.
|
|
|
|
const values: Record<StatsKey, string> = _.fromPairs(await this.get(id, [
|
|
|
|
`batchStatus`,
|
|
|
|
`httpStatus`,
|
|
|
|
`errorMessage`,
|
|
|
|
`size`,
|
|
|
|
`status`,
|
|
|
|
`updatedTime`,
|
|
|
|
`lastFailureTime`,
|
|
|
|
`lastSuccessTime`,
|
|
|
|
`lastErrorMessage`,
|
|
|
|
`lastHttpStatus`,
|
|
|
|
`attempts`,
|
|
|
|
])) as Record<StatsKey, string>;
|
|
|
|
|
|
|
|
// If everything is empty, we don't have any stats yet.
|
|
|
|
if (Array.from(Object.values(values)).every(v => !v)) {
|
|
|
|
return {
|
|
|
|
status: 'idle',
|
|
|
|
numWaiting: queue.filter(e => e.id === id).length,
|
|
|
|
lastEventBatch: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const usage: WebhookUsage = {
|
|
|
|
// Overall status of the webhook.
|
|
|
|
status: values.status as WebhookStatus || 'idle',
|
|
|
|
numWaiting: queue.filter(x => x.id === id).length,
|
|
|
|
updatedTime: parseInt(values.updatedTime || "0", 10),
|
|
|
|
// Last values from batches.
|
|
|
|
lastEventBatch: null,
|
|
|
|
lastSuccessTime: parseInt(values.lastSuccessTime, 10),
|
|
|
|
lastFailureTime: parseInt(values.lastFailureTime, 10),
|
|
|
|
lastErrorMessage: values.lastErrorMessage || null,
|
|
|
|
lastHttpStatus: values.lastHttpStatus ? parseInt(values.lastHttpStatus, 10) : null,
|
|
|
|
};
|
|
|
|
|
|
|
|
// If we have a batchStatus (so we actually run it at least once - or it wasn't cleared).
|
|
|
|
if (values.batchStatus) {
|
|
|
|
usage.lastEventBatch = {
|
|
|
|
status: values.batchStatus as WebhookBatchStatus,
|
|
|
|
httpStatus: values.httpStatus ? parseInt(values.httpStatus || "0", 10) : null,
|
|
|
|
errorMessage: values.errorMessage || null,
|
|
|
|
size: parseInt(values.size || "0", 10),
|
|
|
|
attempts: parseInt(values.attempts|| "0", 10),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return usage;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logs a status of a webhook. Now is passed as a parameter so that updates that happen in almost the same
|
|
|
|
* millisecond were seen as the same update.
|
|
|
|
*/
|
|
|
|
public async logStatus(id: string, status: WebhookStatus, now?: number|null) {
|
2023-03-01 20:43:22 +00:00
|
|
|
const stats: [StatsKey, string][] = [
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
['status', status],
|
|
|
|
['updatedTime', (now ?? Date.now()).toString()],
|
2023-03-01 20:43:22 +00:00
|
|
|
];
|
|
|
|
if (status === 'sending') {
|
|
|
|
// clear any error message that could have been left from an earlier bad state (ie: invalid
|
|
|
|
// fields)
|
|
|
|
stats.push(['errorMessage', '']);
|
|
|
|
}
|
|
|
|
await this.set(id, stats);
|
2023-05-08 22:06:24 +00:00
|
|
|
await this.markChange();
|
2023-03-01 20:43:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public async logInvalid(id: string, errorMessage: string) {
|
|
|
|
await this.logStatus(id, 'invalid');
|
|
|
|
await this.set(id, [
|
|
|
|
['errorMessage', errorMessage]
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
]);
|
2023-05-08 22:06:24 +00:00
|
|
|
await this.markChange();
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logs a status of the active batch.
|
|
|
|
*/
|
|
|
|
public async logBatch(
|
|
|
|
id: string,
|
|
|
|
status: WebhookBatchStatus,
|
|
|
|
stats?: {
|
|
|
|
httpStatus?: number|null,
|
|
|
|
error?: string|null,
|
|
|
|
size?: number|null,
|
|
|
|
attempts?: number|null,
|
|
|
|
}
|
|
|
|
) {
|
2022-12-22 17:15:06 +00:00
|
|
|
const now = Date.now();
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
|
|
|
|
// Update batchStats.
|
|
|
|
const batchStats: [StatsKey, string][] = [
|
|
|
|
[`batchStatus`, status],
|
|
|
|
[`updatedTime`, now.toString()],
|
|
|
|
];
|
|
|
|
if (stats?.httpStatus !== undefined) {
|
|
|
|
batchStats.push([`httpStatus`, (stats.httpStatus || '').toString()]);
|
|
|
|
}
|
|
|
|
if (stats?.attempts !== undefined) {
|
|
|
|
batchStats.push([`attempts`, (stats.attempts || '0').toString()]);
|
|
|
|
}
|
|
|
|
if (stats?.error !== undefined) {
|
|
|
|
batchStats.push([`errorMessage`, stats?.error || '']);
|
|
|
|
}
|
|
|
|
if (stats?.size !== undefined) {
|
|
|
|
batchStats.push([`size`, (stats.size || '').toString()]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const batchSummary: [StatsKey, string][] = [];
|
|
|
|
// Update webhook stats.
|
|
|
|
if (status === 'success') {
|
|
|
|
batchSummary.push([`lastSuccessTime`, now.toString()]);
|
|
|
|
} else if (status === 'failure') {
|
|
|
|
batchSummary.push([`lastFailureTime`, now.toString()]);
|
|
|
|
}
|
|
|
|
if (stats?.error) {
|
|
|
|
batchSummary.push([`lastErrorMessage`, stats.error]);
|
|
|
|
}
|
|
|
|
if (stats?.httpStatus) {
|
|
|
|
batchSummary.push([`lastHttpStatus`, (stats.httpStatus || '').toString()]);
|
|
|
|
}
|
|
|
|
await this.set(id, batchStats.concat(batchSummary));
|
2023-05-08 22:06:24 +00:00
|
|
|
await this.markChange();
|
(core) Adding /webhooks endpoint
Summary:
- New /webhooks event that lists all webhooks in a document (available for owners),
- Monitoring webhooks usage and saving it in memory or Redis,
- Loosening _usubscribe API endpoint, so that the information returned from the /webhook endpoint is enough to unsubscribe,
- Owners can remove webhook without the unsubscribe key.
The endpoint lists all webhooks that are registered in a document, not just webhooks from a single table.
There are two status fields. First for the webhook, second for the last request attempt.
Webhook can have 5 statuses: 'idle', 'sending', 'retrying', 'postponed', 'error', which roughly describes what the
sendLoop is currently doing. The 'error' status describes a situation when all request attempts failed and the queue needs
to be drained, so some requests were dropped.
The last request status can only be: 'success', 'failure' or 'rejected'. Rejected means that the last batch was dropped because the
queue was too long.
Test Plan: New and updated tests
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3727
2022-12-13 11:47:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type StatsKey =
|
|
|
|
'batchStatus' |
|
|
|
|
'httpStatus' |
|
|
|
|
'errorMessage' |
|
|
|
|
'attempts' |
|
|
|
|
'size'|
|
|
|
|
'updatedTime' |
|
|
|
|
'lastFailureTime' |
|
|
|
|
'lastSuccessTime' |
|
|
|
|
'lastErrorMessage' |
|
|
|
|
'lastHttpStatus' |
|
|
|
|
'status';
|