diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index 537553671b..7382185559 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -58,7 +58,7 @@ const WEBHOOK_COLUMNS = [ widgetOptions: JSON.stringify({ widget: 'TextBox', alignment: 'left', - choices: ['add', 'update'], + choices: ['add', 'update', 'remove'], choiceOptions: {}, }), }, diff --git a/app/common/Triggers-ti.ts b/app/common/Triggers-ti.ts index f93d12ae4f..168c17a89f 100644 --- a/app/common/Triggers-ti.ts +++ b/app/common/Triggers-ti.ts @@ -15,7 +15,7 @@ export const Webhook = t.iface([], { export const WebhookFields = t.iface([], { "url": "string", "authorization": t.opt("string"), - "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), + "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"), t.lit("remove"))), "tableId": "string", "watchedColIds": t.opt(t.array("string")), "enabled": t.opt("boolean"), @@ -31,7 +31,7 @@ export const WebhookStatus = t.union(t.lit('idle'), t.lit('sending'), t.lit('ret export const WebhookSubscribe = t.iface([], { "url": "string", "authorization": t.opt("string"), - "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), + "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"), t.lit("remove"))), "watchedColIds": t.opt(t.array("string")), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), @@ -68,7 +68,7 @@ export const WebhookUpdate = t.iface([], { export const WebhookPatch = t.iface([], { "url": t.opt("string"), "authorization": t.opt("string"), - "eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update")))), + "eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update"), t.lit("remove")))), "tableId": t.opt("string"), "watchedColIds": t.opt(t.array("string")), "enabled": t.opt("boolean"), diff --git a/app/common/Triggers.ts b/app/common/Triggers.ts index a53dd1feed..3e9852e058 100644 --- a/app/common/Triggers.ts +++ b/app/common/Triggers.ts @@ -9,7 +9,7 @@ export interface Webhook { export interface WebhookFields { url: string; authorization?: string; - eventTypes: Array<"add"|"update">; + eventTypes: Array<"add"|"update"|"remove">; tableId: string; watchedColIds?: string[]; enabled?: boolean; @@ -28,7 +28,7 @@ export type WebhookStatus = 'idle'|'sending'|'retrying'|'postponed'|'error'|'inv export interface WebhookSubscribe { url: string; authorization?: string; - eventTypes: Array<"add"|"update">; + eventTypes: Array<"add"|"update"|"remove">; watchedColIds?: string[]; enabled?: boolean; isReadyColumn?: string|null; @@ -68,7 +68,7 @@ export interface WebhookUpdate { export interface WebhookPatch { url?: string; authorization?: string; - eventTypes?: Array<"add"|"update">; + eventTypes?: Array<"add"|"update"|"remove">; tableId?: string; watchedColIds?: string[]; enabled?: boolean; diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 68fc3490b0..7c10fd3d7e 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -62,7 +62,7 @@ interface WebHookEvent { id: string; } -export const allowedEventTypes = StringUnion("add", "update"); +export const allowedEventTypes = StringUnion("add", "update", "remove"); type EventType = typeof allowedEventTypes.type; @@ -485,9 +485,8 @@ export class DocTriggers { tableDelta.addRows.forEach(id => 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})); + tableDelta.removeRows.forEach(id => + recordDeltas.set(id, {existedBefore: true, existedAfter: false})); return recordDeltas; } @@ -497,6 +496,9 @@ export class DocTriggers { tableDataAction: TableDataAction, ) { const bulkColValues = fromTableDataAction(tableDataAction); + // HACK: bulkColValues don't include removed data because the data no longer exist on the Database + // but tableDataAction is got from Database query + this._appendRemovedRowsIntoTableColValues(tableDelta, bulkColValues); const meta = {numTriggers: triggers.length, numRecords: bulkColValues.id.length}; this._log(`Processing triggers`, meta); @@ -632,12 +634,12 @@ export class DocTriggers { } else { return false; } - // If we allow subscribing to deletion in the future - // if (recordDelta.existedAfter) { - // eventType = "update"; - // } else { - // eventType = "remove"; - // } + + if (recordDelta.existedAfter) { + eventType = "update"; + } else { + eventType = "remove"; + } } else { eventType = "add"; } @@ -876,6 +878,32 @@ export class DocTriggers { } return false; } + + /** + * Use this to append removed rows in TableColValues data from current tableDelta. + * This method modify bulkColValues parameter directly. + */ + private _appendRemovedRowsIntoTableColValues(tableDelta: TableDelta, bulkColValues: TableColValues) { + for (const removedRow of tableDelta.removeRows) { + for (const key in bulkColValues) { + if (key === "id") { + bulkColValues.id.push(removedRow); + } else { + const columnDelta = tableDelta.columnDeltas[key]; + const cellDelta = columnDelta[removedRow]; + const [previousValue, ] = cellDelta; + + // previousValue is always include on a array for typescript type workaround + // See CellDelta type declaration comment + if (previousValue instanceof Array) { + bulkColValues[key].push(previousValue[0]); + } else { + bulkColValues[key].push(previousValue); + } + } + } + } + } } export function isUrlAllowed(urlString: string) { diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index cb742cbbfa..2eb7ef58f3 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -3914,7 +3914,7 @@ function testDocApi(settings: { await oldSubscribeCheck({eventTypes: 0}, 400, /url is missing/, /eventTypes is not an array/); await oldSubscribeCheck({eventTypes: []}, 400, /url is missing/); await oldSubscribeCheck({eventTypes: [], url: "https://example.com"}, 400, /eventTypes must be a non-empty array/); - await oldSubscribeCheck({eventTypes: ["foo"], url: "https://example.com"}, 400, /eventTypes\[0] is none of "add", "update"/); + await oldSubscribeCheck({eventTypes: ["foo"], url: "https://example.com"}, 400, /eventTypes\[0] is none of "add", "update", "remove"/); await oldSubscribeCheck({eventTypes: ["add"]}, 400, /url is missing/); await oldSubscribeCheck({eventTypes: ["add"], url: "https://evil.com"}, 403, /Provided url is forbidden/); await oldSubscribeCheck({eventTypes: ["add"], url: "http://example.com"}, 403, /Provided url is forbidden/); // not https @@ -3935,7 +3935,7 @@ function testDocApi(settings: { 400, /eventTypes must be a non-empty array/); await postWebhookCheck({webhooks:[{fields: {tableId: "Table1", eventTypes: ["foo"], url: "https://example.com"}}]}, - 400, /eventTypes\[0] is none of "add", "update"/); + 400, /eventTypes\[0] is none of "add", "update", "remove"/); await postWebhookCheck({webhooks:[{fields: {tableId: "Table1", eventTypes: ["add"]}}]}, 400, /url is missing/); await postWebhookCheck({webhooks:[{fields: {tableId: "Table1", eventTypes: ["add"], @@ -4411,7 +4411,7 @@ function testDocApi(settings: { const {data, status} = await axios.post( `${serverUrl}/api/docs/${docId}/tables/${options?.tableId ?? 'Table1'}/_subscribe`, { - eventTypes: options?.eventTypes ?? ['add', 'update'], + eventTypes: options?.eventTypes ?? ['add', 'update', 'remove'], url: `${serving.url}/${endpoint}`, isReadyColumn: options?.isReadyColumn === undefined ? 'B' : options?.isReadyColumn, ...pick(options, 'name', 'memo', 'enabled', 'watchedColIds'), @@ -4993,7 +4993,7 @@ function testDocApi(settings: { // Webhook with only one watchedColId. const webhook1 = await autoSubscribe('200', docId, { - watchedColIds: ['A'], eventTypes: ['add', 'update'] + watchedColIds: ['A'], eventTypes: ['add', 'update', 'remove'] }); successCalled.reset(); // Create record, that will call the webhook. @@ -5046,7 +5046,7 @@ function testDocApi(settings: { url: `${serving.url}/200`, authorization: '', unsubscribeKey: first.unsubscribeKey, - eventTypes: ['add', 'update'], + eventTypes: ['add', 'update', 'remove'], enabled: true, isReadyColumn: 'B', tableId: 'Table1', @@ -5065,7 +5065,7 @@ function testDocApi(settings: { url: `${serving.url}/404`, authorization: '', unsubscribeKey: second.unsubscribeKey, - eventTypes: ['add', 'update'], + eventTypes: ['add', 'update', 'remove'], enabled: true, isReadyColumn: 'B', tableId: 'Table1', @@ -5511,9 +5511,9 @@ function testDocApi(settings: { await check({tableId: 'Santa'}, 404, `Table not found "Santa"`); await check({tableId: 'Table2', isReadyColumn: 'Foo', watchedColIds: []}, 200); - await check({eventTypes: ['add', 'update']}, 200); + await check({eventTypes: ['add', 'update', 'remove']}, 200); await check({eventTypes: []}, 400, "eventTypes must be a non-empty array"); - await check({eventTypes: ["foo"]}, 400, /eventTypes\[0] is none of "add", "update"/); + await check({eventTypes: ["foo"]}, 400, /eventTypes\[0] is none of "add", "update", "remove"/); await check({isReadyColumn: null}, 200); await check({isReadyColumn: "bar"}, 404, `Column not found "bar"`); diff --git a/test/server/lib/Webhooks-Proxy.ts b/test/server/lib/Webhooks-Proxy.ts index 341c2bb29b..2327ea6cf2 100644 --- a/test/server/lib/Webhooks-Proxy.ts +++ b/test/server/lib/Webhooks-Proxy.ts @@ -197,7 +197,7 @@ describe('Webhooks-Proxy', function () { const {data, status} = await axios.post( `${serverUrl}/api/docs/${docId}/tables/${options?.tableId ?? 'Table1'}/_subscribe`, { - eventTypes: options?.eventTypes ?? ['add', 'update'], + eventTypes: options?.eventTypes ?? ['add', 'update', 'remove'], url: `${serving.url}/${endpoint}`, isReadyColumn: options?.isReadyColumn === undefined ? 'B' : options?.isReadyColumn }, chimpy