mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) GET endpoint for webhooks returns now data in format {webhooks:[...]}
Summary:
Rework of endpoint GET  for webhooks to make it coherent with other endpoints. Now data should be return in {webhooks:[{id:"...",fields:{"..."}]} format
```
{
    "webhooks": [
        {
            "id": ...
            "fields": {
                "url": ...
                "unsubscribeKey": ...
                "eventTypes": [
                    "add",
                    "update"
                ],
                "isReadyColumn": null,
                "tableId": "...",
                "enabled": false,
                "name": "...",
                "memo": "..."
            },
            "usage": {
                "status": "idle",
                "numWaiting": 0,
                "lastEventBatch": null
            }
        },
        {
            "id": "...",
            "fields": {
                "url": "...",
                "unsubscribeKey": "...",
                "eventTypes": [
                    "add",
                    "update"
                ],
                "isReadyColumn": null,
                "tableId": "...",
                "enabled": true,
                "name": "...",
                "memo": "..."
            },
            "usage": {
                "status": "error",
                "numWaiting": 0,
                "updatedTime": 1689076978098,
                "lastEventBatch": {
                    "status": "rejected",
                    "httpStatus": 404,
                    "errorMessage": "{\"success\":false,\"error\":{\"message\":\"Alias 5a9bf6a8-4865-403a-bec6-b4ko not found\",\"id\":null}}",
                    "size": 49,
                    "attempts": 5
                },
                "lastSuccessTime": null,
                "lastFailureTime": 1689076978097,
                "lastErrorMessage": "{\"success\":false,\"error\":{\"message\":\"Alias 5a9bf6a8-4865-403a-bec6-b4ko not found\",\"id\":null}}",
                "lastHttpStatus": 404
            }
        }
    ]
}
```
Test Plan: new test added to check if GET data fromat is correct. Other tests fixed to handle changed endpoint.
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3966
			
			
This commit is contained in:
		
							parent
							
								
									cd8eac36b8
								
							
						
					
					
						commit
						f7fdfab6bf
					
				@ -135,7 +135,7 @@ class WebhookExternalTable implements IExternalTable {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async fetchAll(): Promise<TableDataAction> {
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,9 @@ export interface WebhookSubscribe {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export interface  WebhookSummaryCollection {
 | 
			
		||||
  webhooks: Array<WebhookSummary>;
 | 
			
		||||
}
 | 
			
		||||
export interface WebhookSummary {
 | 
			
		||||
  id: string;
 | 
			
		||||
  fields: {
 | 
			
		||||
 | 
			
		||||
@ -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<PermissionDataWithExtraUsers>;
 | 
			
		||||
 | 
			
		||||
  getWebhooks(): Promise<WebhookSummary[]>;
 | 
			
		||||
  getWebhooks(): Promise<WebhookSummaryCollection>;
 | 
			
		||||
  addWebhook(webhook: WebhookFields): Promise<{webhookId: string}>;
 | 
			
		||||
  removeWebhook(webhookId: string, tableId: string): Promise<void>;
 | 
			
		||||
  // Update webhook
 | 
			
		||||
@ -915,7 +920,7 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {
 | 
			
		||||
    return this.requestJson(`${this._url}/usersForViewAs`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getWebhooks(): Promise<WebhookSummary[]> {
 | 
			
		||||
  public async getWebhooks(): Promise<WebhookSummaryCollection> {
 | 
			
		||||
    return this.requestJson(`${this._url}/webhooks`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<WebhookSummary[]> {
 | 
			
		||||
  public async summary(): Promise<WebhookSummaryCollection> {
 | 
			
		||||
    // 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) {
 | 
			
		||||
 | 
			
		||||
@ -2800,9 +2800,23 @@ function testDocApi() {
 | 
			
		||||
      return resp.data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 () {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user