diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index cc8118f5..fcf7805e 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -135,7 +135,7 @@ class WebhookExternalTable implements IExternalTable { } public async fetchAll(): Promise { - const webhooks = await this._docApi.getWebhooks(); + const webhooks = (await this._docApi.getWebhooks()).webhooks; this._initalizeWebhookList(webhooks); const indices = range(webhooks.length); return ['TableData', this.name, indices.map(i => i + 1), @@ -222,7 +222,7 @@ class WebhookExternalTable implements IExternalTable { // brute force, on the assumption that there won't be many // webhooks, or that "updating" something that hasn't actually // changed is not disruptive. - const webhooks = await this._docApi.getWebhooks(); + const webhooks = (await this._docApi.getWebhooks()).webhooks; this._initalizeWebhookList(webhooks); for (const webhook of webhooks) { const values = _mapWebhookValues(webhook); diff --git a/app/common/Triggers-ti.ts b/app/common/Triggers-ti.ts index 44271718..2489181d 100644 --- a/app/common/Triggers-ti.ts +++ b/app/common/Triggers-ti.ts @@ -4,6 +4,14 @@ import * as t from "ts-interface-checker"; // tslint:disable:object-literal-key-quotes +export const WebhookSubscribeCollection = t.iface([], { + "webhooks": t.array("Webhook"), +}); + +export const Webhook = t.iface([], { + "fields": "WebhookFields", +}); + export const WebhookFields = t.iface([], { "url": "string", "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), @@ -14,10 +22,6 @@ export const WebhookFields = t.iface([], { "memo": t.opt("string"), }); -export const Webhook = t.iface([], { - "fields": "WebhookFields", -}); - export const WebhookBatchStatus = t.union(t.lit('success'), t.lit('failure'), t.lit('rejected')); export const WebhookStatus = t.union(t.lit('idle'), t.lit('sending'), t.lit('retrying'), t.lit('postponed'), t.lit('error'), t.lit('invalid')); @@ -31,8 +35,8 @@ export const WebhookSubscribe = t.iface([], { "memo": t.opt("string"), }); -export const WebhookSubscribeCollection = t.iface([], { - "webhooks": t.array("Webhook"), +export const WebhookSummaryCollection = t.iface([], { + "webhooks": t.array("WebhookSummary"), }); export const WebhookSummary = t.iface([], { @@ -87,12 +91,13 @@ export const WebhookUsage = t.iface([], { }); const exportedTypeSuite: t.ITypeSuite = { - WebhookFields, + WebhookSubscribeCollection, Webhook, + WebhookFields, WebhookBatchStatus, WebhookStatus, WebhookSubscribe, - WebhookSubscribeCollection, + WebhookSummaryCollection, WebhookSummary, WebhookUpdate, WebhookPatch, diff --git a/app/common/Triggers.ts b/app/common/Triggers.ts index 54eea8cd..5a822f11 100644 --- a/app/common/Triggers.ts +++ b/app/common/Triggers.ts @@ -33,7 +33,9 @@ export interface WebhookSubscribe { } - +export interface WebhookSummaryCollection { + webhooks: Array; +} export interface WebhookSummary { id: string; fields: { diff --git a/app/common/UserAPI.ts b/app/common/UserAPI.ts index 0992fd85..3204e1b6 100644 --- a/app/common/UserAPI.ts +++ b/app/common/UserAPI.ts @@ -14,7 +14,12 @@ import {encodeQueryParams} from 'app/common/gutil'; import {FullUser, UserProfile} from 'app/common/LoginSessionAPI'; import {OrgPrefs, UserOrgPrefs, UserPrefs} from 'app/common/Prefs'; import * as roles from 'app/common/roles'; -import {WebhookFields, WebhookSubscribe, WebhookSummary, WebhookUpdate} from 'app/common/Triggers'; +import { + WebhookFields, + WebhookSubscribe, + WebhookSummaryCollection, + WebhookUpdate +} from 'app/common/Triggers'; import {addCurrentOrgToPath} from 'app/common/urlUtils'; import omitBy from 'lodash/omitBy'; @@ -457,7 +462,7 @@ export interface DocAPI { // Get users that are worth proposing to "View As" for access control purposes. getUsersForViewAs(): Promise; - getWebhooks(): Promise; + getWebhooks(): Promise; addWebhook(webhook: WebhookFields): Promise<{webhookId: string}>; removeWebhook(webhookId: string, tableId: string): Promise; // Update webhook @@ -915,7 +920,7 @@ export class DocAPIImpl extends BaseAPI implements DocAPI { return this.requestJson(`${this._url}/usersForViewAs`); } - public async getWebhooks(): Promise { + public async getWebhooks(): Promise { return this.requestJson(`${this._url}/webhooks`); } diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 39a6d212..e9e947f6 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -8,7 +8,13 @@ import {fromTableDataAction, RowRecord, TableColValues, TableDataAction} from 'a import {StringUnion} from 'app/common/StringUnion'; import {MetaRowRecord} from 'app/common/TableData'; import {CellDelta} from 'app/common/TabularDiff'; -import {WebhookBatchStatus, WebhookStatus, WebhookSummary, WebhookUsage} from 'app/common/Triggers'; +import { + WebhookBatchStatus, + WebhookStatus, + WebhookSummary, + WebhookSummaryCollection, + WebhookUsage +} from 'app/common/Triggers'; import {decodeObject} from 'app/plugin/objtypes'; import {ActiveDoc} from 'app/server/lib/ActiveDoc'; import {makeExceptionalDocSession} from 'app/server/lib/DocSession'; @@ -246,7 +252,7 @@ export class DocTriggers { /** * Creates summary for all webhooks in the document. */ - public async summary(): Promise { + public async summary(): Promise { // Prepare some data we will use. const docData = this._activeDoc.docData!; const triggersTable = docData.getMetaTable("_grist_Triggers"); @@ -254,7 +260,7 @@ export class DocTriggers { 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[] = []; + const resultTable: WebhookSummary[] = []; // Go through all triggers int the document that we have. for (const t of triggersTable.getRecords()) { @@ -291,10 +297,10 @@ export class DocTriggers { // Create some statics and status info. usage: await this._stats.getUsage(act.id, this._webHookEventQueue), }; - result.push(entry); + resultTable.push(entry); } } - return result; + return {webhooks: resultTable}; } public getWebhookTriggerRecord(webhookId: string) { diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index e488c148..f00c8fb9 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -2800,9 +2800,23 @@ function testDocApi() { return resp.data; } - it("POST /docs/{did}/tables/{tid}/_subscribe validates inputs", async function () { - + it("GET /docs/{did}/webhooks retrieves a list of webhooks", async function () { + const registerResponse = await postWebhookCheck({webhooks:[{fields:{tableId: "Table1", eventTypes: ["add"], url: "https://example.com"}}]}, 200); + const resp = await axios.get(`${serverUrl}/api/docs/${docIds.Timesheets}/webhooks`, chimpy); + try{ + assert.equal(resp.status, 200); + assert.isAtLeast(resp.data.webhooks.length, 1); + assert.containsAllKeys(resp.data.webhooks[0], ['id', 'fields']); + assert.containsAllKeys(resp.data.webhooks[0].fields, + ['enabled', 'isReadyColumn', 'memo', 'name', 'tableId', 'eventTypes', 'url']); + } + finally{ + //cleanup + await deleteWebhookCheck(registerResponse.webhooks[0].id); + } + }); + it("POST /docs/{did}/tables/{tid}/_subscribe validates inputs", async function () { await oldSubscribeCheck({}, 400, /eventTypes is missing/); await oldSubscribeCheck({eventTypes: 0}, 400, /url is missing/, /eventTypes is not an array/); await oldSubscribeCheck({eventTypes: []}, 400, /url is missing/); @@ -2922,7 +2936,7 @@ function testDocApi() { async function getRegisteredWebhooks() { const response = await axios.get( `${serverUrl}/api/docs/${docIds.Timesheets}/webhooks`, chimpy); - return response.data; + return response.data.webhooks; } async function deleteWebhookCheck(webhookId: any) { @@ -3311,7 +3325,7 @@ function testDocApi() { `${serverUrl}/api/docs/${docId}/webhooks`, chimpy ); assert.equal(result.status, 200); - return result.data; + return result.data.webhooks; } before(async function () {