(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
This commit is contained in:
Alex Hall
2021-09-23 01:06:23 +02:00
parent de76cc48d1
commit 3c4d71aeca
14 changed files with 584 additions and 27 deletions

View File

@@ -3,13 +3,13 @@
* subscribes to actions which change it, and forwards those actions to individual tables.
* It also provides the interface to apply actions to data.
*/
import {schema} from 'app/common/schema';
import {schema, SchemaTypes} from 'app/common/schema';
import fromPairs = require('lodash/fromPairs');
import groupBy = require('lodash/groupBy');
import {ActionDispatcher} from './ActionDispatcher';
import {BulkColValues, ColInfo, ColInfoWithId, ColValues, DocAction,
RowRecord, TableDataAction} from './DocActions';
import {ColTypeMap, TableData} from './TableData';
import {ColTypeMap, MetaTableData, TableData} from './TableData';
type FetchTableFunc = (tableId: string) => Promise<TableDataAction>;
@@ -46,7 +46,7 @@ export class DocData extends ActionDispatcher {
* Creates a new TableData object. A derived class may override to return an object derived from TableData.
*/
public createTableData(tableId: string, tableData: TableDataAction|null, colTypes: ColTypeMap): TableData {
return new TableData(tableId, tableData, colTypes);
return new (tableId in schema ? MetaTableData : TableData)(tableId, tableData, colTypes);
}
/**
@@ -56,6 +56,13 @@ export class DocData extends ActionDispatcher {
return this._tables.get(tableId);
}
/**
* Like getTable, but the result knows about the types of its records
*/
public getMetaTable<TableId extends keyof SchemaTypes>(tableId: TableId): MetaTableData<TableId> {
return this.getTable(tableId) as any;
}
/**
* Returns an unsorted list of all tableIds in this doc, including both metadata and user tables.
*/

View File

@@ -7,6 +7,7 @@ import {ActionDispatcher} from './ActionDispatcher';
import {BulkColValues, CellValue, ColInfo, ColInfoWithId, ColValues, DocAction,
isSchemaAction, ReplaceTableData, RowRecord, TableDataAction} from './DocActions';
import {arrayRemove, arraySplice} from './gutil';
import {SchemaTypes} from "./schema";
export interface ColTypeMap { [colId: string]: string; }
@@ -470,6 +471,34 @@ export class TableData extends ActionDispatcher implements SkippableRows {
}
}
export type MetaRowRecord<TableId extends keyof SchemaTypes> = SchemaTypes[TableId] & RowRecord;
/**
* Behaves the same as TableData, but uses SchemaTypes for type safety of its columns.
*/
export class MetaTableData<TableId extends keyof SchemaTypes> extends TableData {
constructor(tableId: TableId, tableData: TableDataAction | null, colTypes: ColTypeMap) {
super(tableId, tableData, colTypes);
}
public getRecords(): Array<MetaRowRecord<TableId>> {
return super.getRecords() as any;
}
public getRecord(rowId: number): MetaRowRecord<TableId> | undefined {
return super.getRecord(rowId) as any;
}
/**
* Same as getRowPropFunc, but I couldn't get a direct override to compile.
*/
public getMetaRowPropFunc<ColId extends keyof SchemaTypes[TableId]>(
colId: ColId
): ((rowId: number | "new") => SchemaTypes[TableId][ColId]) {
return super.getRowPropFunc(colId as any) as any;
}
}
function reassignArray<T>(targetArray: T[], sourceArray: T[]): void {
targetArray.length = 0;
arraySplice(targetArray, 0, sourceArray);

View File

@@ -143,6 +143,13 @@ export const schema = {
timeUploaded : "DateTime",
},
"_grist_Triggers": {
tableRef : "Ref:_grist_Tables",
eventTypes : "ChoiceList",
isReadyColRef : "Ref:_grist_Tables_column",
actions : "Text",
},
"_grist_ACLRules": {
resource : "Ref:_grist_ACLResources",
permissions : "Int",
@@ -317,6 +324,13 @@ export interface SchemaTypes {
timeUploaded: number;
};
"_grist_Triggers": {
tableRef: number;
eventTypes: ['L', ...string[]]|null;
isReadyColRef: number;
actions: string;
};
"_grist_ACLRules": {
resource: number;
permissions: number;