From 55bde5fedb1169289da628bb1111dc75097b2c80 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Thu, 25 Jan 2024 10:31:36 +0100 Subject: [PATCH 01/31] show column settings in webhooks and consolelog to know how to take it in account --- app/client/ui/WebhookPage.ts | 49 ++++++++++++++++++++++++++++++++- app/common/Triggers.ts | 4 +++ app/common/UserAPI.ts | 4 +++ app/common/schema.ts | 4 ++- app/server/lib/DocApi.ts | 28 +++++++++++++++++-- app/server/lib/Triggers.ts | 15 ++++++++++ app/server/lib/initialDocSql.ts | 14 +++++++--- sandbox/grist/migrations.py | 9 ++++++ sandbox/grist/schema.py | 3 +- 9 files changed, 120 insertions(+), 10 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index fcf7805e5f..637d536bab 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -59,6 +59,12 @@ const WEBHOOK_COLUMNS = [ choiceOptions: {}, }), }, + { + id: 'vt_webhook_fc10', + colId: 'columnIds', + type: 'Text', + label: 'Columns (séparé par des ;)', + }, { id: 'vt_webhook_fc4', colId: 'enabled', @@ -107,6 +113,7 @@ const WEBHOOK_VIEW_FIELDS: Array<(typeof WEBHOOK_COLUMNS)[number]['colId']> = [ 'name', 'memo', 'eventTypes', 'url', 'tableId', 'isReadyColumn', + 'columnIds', 'webhookId', 'enabled', 'status' ]; @@ -127,7 +134,7 @@ class WebhookExternalTable implements IExternalTable { public name = 'GristHidden_WebhookTable'; public initialActions = _prepareWebhookInitialActions(this.name); public saveableFields = [ - 'tableId', 'url', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn', + 'tableId', 'columnIds', 'url', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn', ]; public webhooks: ObservableArray = observableArray([]); @@ -178,6 +185,25 @@ class WebhookExternalTable implements IExternalTable { const {delta} = editor; const updates = new Set(delta.updateRows); const addsAndUpdates = new Set([...delta.addRows, ...delta.updateRows]); + // if (delta.columnDeltas.tableId) { + // // TODO CHANGE THIS CODE !!! the first [1] have ti correspond to the webhook id + // // if tableId has been changes it appears in columnDeltas, + // // the deltas are indexed by the webhookId and is an array [oldValue, newValue] + // const newTableId = delta.columnDeltas.tableId[1][1]; + // if (newTableId) { + // const choicesColIds = editor.gristDoc.docModel.dataTables[newTableId[0]].tableData.getColIds(); + // // CAN NOT DO THAT !!! : modification pour toutes les instances ! il faut faire juste un filtre + // // ou rendre no selectionnable les colonnes qui ne correspondent pas à la table sélectionnée + // editor.gristDoc.docData.receiveAction([ + // 'UpdateRecord', '_grist_Tables_column', 'vt_webhook_fc10' as any, { + // widgetOptions: JSON.stringify({ + // widget: 'TextBox', + // alignment: 'left', + // choices: choicesColIds, + // }) + // }]); + // } + // } for (const recId of addsAndUpdates) { const rec = editor.getRecord(recId); if (!rec) { @@ -264,6 +290,27 @@ class WebhookExternalTable implements IExternalTable { choices, }) }]); + // const choicesColumns = editor.gristDoc.docModel.visibleTables.all().reduce( + // (obj1, tableRec) => { + // const listCol = tableRec.columns().peek().map(columnRec => columnRec.colId()); + // return { ...obj1, [tableRec.tableId()]: listCol }; + // }, {} + // ); + // const initialArray: string[] = []; + // const choicesColumns = editor.gristDoc.docModel.visibleTables.all().reduce( + // (obj, tableRec) => { + // const listCol = tableRec.columns().peek().map(columnRec => tableRec.tableId() + ": " + columnRec.colId()); + // return [ ...obj, ...listCol]; + // }, initialArray + // ); + // editor.gristDoc.docData.receiveAction([ + // 'UpdateRecord', '_grist_Tables_column', 'vt_webhook_fc10' as any, { + // widgetOptions: JSON.stringify({ + // widget: 'TextBox', + // alignment: 'left', + // choices: choicesColumns, + // }) + // }]); } private _initalizeWebhookList(webhooks: WebhookSummary[]){ diff --git a/app/common/Triggers.ts b/app/common/Triggers.ts index 5a822f116b..5232211228 100644 --- a/app/common/Triggers.ts +++ b/app/common/Triggers.ts @@ -10,6 +10,7 @@ export interface WebhookFields { url: string; eventTypes: Array<"add"|"update">; tableId: string; + columnIds: string; enabled?: boolean; isReadyColumn?: string|null; name?: string; @@ -26,6 +27,7 @@ export type WebhookStatus = 'idle'|'sending'|'retrying'|'postponed'|'error'|'inv export interface WebhookSubscribe { url: string; eventTypes: Array<"add"|"update">; + columnIds?: string; enabled?: boolean; isReadyColumn?: string|null; name?: string; @@ -44,6 +46,7 @@ export interface WebhookSummary { eventTypes: string[]; isReadyColumn: string|null; tableId: string; + columnIds: string; enabled: boolean; name: string; memo: string; @@ -63,6 +66,7 @@ export interface WebhookPatch { url?: string; eventTypes?: Array<"add"|"update">; tableId?: string; + columnIds?: string; enabled?: boolean; isReadyColumn?: string|null; name?: string; diff --git a/app/common/UserAPI.ts b/app/common/UserAPI.ts index 9bdaf6bee7..de06c8477b 100644 --- a/app/common/UserAPI.ts +++ b/app/common/UserAPI.ts @@ -980,6 +980,8 @@ export class DocAPIImpl extends BaseAPI implements DocAPI { public async addWebhook(webhook: WebhookSubscribe & {tableId: string}): Promise<{webhookId: string}> { const {tableId} = webhook; + console.log("INSIDE the User API addWebhook"); + console.log(webhook); return this.requestJson(`${this._url}/tables/${tableId}/_subscribe`, { method: 'POST', body: JSON.stringify( @@ -988,6 +990,8 @@ export class DocAPIImpl extends BaseAPI implements DocAPI { } public async updateWebhook(webhook: WebhookUpdate): Promise { + console.log("-------- INSIDE the User API updateWebhook"); + console.log(webhook); return this.requestJson(`${this._url}/webhooks/${webhook.id}`, { method: 'PATCH', body: JSON.stringify(webhook.fields), diff --git a/app/common/schema.ts b/app/common/schema.ts index 996e457e71..7bf8d4483f 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData"; // tslint:disable:object-literal-key-quotes -export const SCHEMA_VERSION = 41; +export const SCHEMA_VERSION = 42; export const schema = { @@ -167,6 +167,7 @@ export const schema = { label : "Text", memo : "Text", enabled : "Bool", + columnRefList : "RefList:_grist_Tables_column", }, "_grist_ACLRules": { @@ -388,6 +389,7 @@ export interface SchemaTypes { label: string; memo: string; enabled: boolean; + columnRefList: [GristObjCode.List, ...number[]]|null; }; "_grist_ACLRules": { diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 3ea0a04259..f8d2300542 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -323,6 +323,7 @@ export class DocWorkerApi { if (!fields.tableRef) { throw new ApiError(`tableId is required`, 400); } + console.log(" JE SUIS DANS LE REGISTER WEBHOOK DU FICHIER DOC API"); const unsubscribeKey = uuidv4(); const webhookSecret: WebHookSecret = {unsubscribeKey, url}; @@ -381,7 +382,7 @@ export class DocWorkerApi { const tablesTable = activeDoc.docData!.getMetaTable("_grist_Tables"); const trigger = webhookId ? activeDoc.triggers.getWebhookTriggerRecord(webhookId) : undefined; let currentTableId = trigger ? tablesTable.getValue(trigger.tableRef, 'tableId')! : undefined; - const {url, eventTypes, isReadyColumn, name} = webhook; + const {url, eventTypes, columnIds, isReadyColumn, name} = webhook; const tableId = await getRealTableId(req.params.tableId || webhook.tableId, {metaTables}); const fields: Partial = {}; @@ -402,6 +403,24 @@ export class DocWorkerApi { currentTableId = tableId; } + console.log("HERE 6666 DOC API SAVING TRIGGER"); + console.log(columnIds); + console.log(tableId); + + if (columnIds !== undefined) { + if (columnIds !== null && columnIds !== '') { + if (!currentTableId) { + throw new ApiError(`Cannot find columns "${columnIds}" because table is not known`, 404); + } + // columnIds have to be of shape "columnId; columnId; columnId" + fields.columnRefList = [GristObjCode.List, ...columnIds.split(";").map( + columnId => { return colIdToReference(metaTables, currentTableId!, columnId.trim()); } + )]; + } else { + fields.columnRefList = [GristObjCode.List, 0]; + } + } + if (isReadyColumn !== undefined) { // When isReadyColumn is defined let's explicitly change the ready column to the new col // id, null or empty string being a special case that unsets it. @@ -890,10 +909,13 @@ export class DocWorkerApi { '/api/docs/:docId/webhooks/:webhookId', isOwner, validate(WebhookPatch), withDocTriggersLock(async (activeDoc, req, res) => { + console.log("----- INSIDE DOCAPI - api call update webhook"); + const docId = activeDoc.docName; const webhookId = req.params.webhookId; const {fields, url} = await getWebhookSettings(activeDoc, req, webhookId, req.body); - + console.log(fields); + console.log(req.body); if (fields.enabled === false) { await activeDoc.triggers.clearSingleWebhookQueue(webhookId); } @@ -915,7 +937,7 @@ export class DocWorkerApi { await activeDoc.sendWebhookNotification(); - res.json({success: true}); + res.json({success: false}); }) ); diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 561e4eee20..294ec31284 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -15,6 +15,7 @@ import { WebhookSummaryCollection, WebhookUsage } from 'app/common/Triggers'; +import { GristObjCode } from 'app/plugin/GristData'; import {decodeObject} from 'app/plugin/objtypes'; import {ActiveDoc} from 'app/server/lib/ActiveDoc'; import {makeExceptionalDocSession} from 'app/server/lib/DocSession'; @@ -177,11 +178,15 @@ export class DocTriggers { const triggersByTableRef = _.groupBy(triggersTable.getRecords().filter(t => t.enabled), "tableRef"); const triggersByTableId: Array<[string, Trigger[]]> = []; + console.log("TRIGGER BY TABLE ID --------"); + // 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 + console.log(triggersByTableId); + console.log(triggers); triggersByTableId.push([tableId, triggers]); for (const trigger of triggers) { if (trigger.isReadyColRef) { @@ -199,11 +204,16 @@ export class DocTriggers { const tasks: Task[] = []; // For each table in the document which is monitored by one or more triggers... + console.log("////////////////////////////"); for (const [tableId, triggers] of triggersByTableId) { const tableDelta = summary.tableDeltas[tableId]; // ...if the monitored table was modified by the summarized actions, // fetch the modified/created records and note the work that needs to be done. if (tableDelta) { + console.log("---------------------------------------"); + console.log(tableDelta); + console.log(tableDelta.columnDeltas); + console.log(tableDelta.columnDeltas.Region); const recordDeltas = this._getRecordDeltas(tableDelta); const filters = {id: [...recordDeltas.keys()]}; @@ -288,6 +298,10 @@ export class DocTriggers { // Other fields used to register this webhook. eventTypes: decodeObject(t.eventTypes) as string[], isReadyColumn: getColId(t.isReadyColRef) ?? null, + columnIds: [ + GristObjCode.List, + t.columnRefList?.slice(1).map(columnRef => getColId(columnRef as number)) + ].join("; "), tableId: getTableId(t.tableRef) ?? null, // For future use - for now every webhook is enabled. enabled: t.enabled, @@ -502,6 +516,7 @@ export class DocTriggers { for (const action of webhookActions) { const colId = this._getColId(trigger.isReadyColRef); // no validation const tableId = this._getTableId(trigger.tableRef); + // TODO copy that for columns !! 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)); } diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index 0b57a95d28..183f721696 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',41,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0); @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS "_grist_Views_section_field" (id INTEGER PRIMARY KEY, CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "fileExt" TEXT DEFAULT '', "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL); -CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0); +CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "columnRefList" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT ''); INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'',''); CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT ''); @@ -39,12 +39,16 @@ CREATE TABLE IF NOT EXISTS "_grist_Shares" (id INTEGER PRIMARY KEY, "linkId" TEX CREATE INDEX _grist_Attachments_fileIdent ON _grist_Attachments(fileIdent); COMMIT; `; +TRIGGER BY TABLE ID -------- +//////////////////////////// +TRIGGER BY TABLE ID -------- +//////////////////////////// export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',41,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2,3); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); @@ -80,7 +84,7 @@ INSERT INTO _grist_Views_section_field VALUES(9,3,9,4,0,'',0,0,'',NULL); CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "fileExt" TEXT DEFAULT '', "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL); -CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0); +CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "columnRefList" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT ''); INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'',''); CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT ''); @@ -98,3 +102,5 @@ CREATE TABLE IF NOT EXISTS "Table1" (id INTEGER PRIMARY KEY, "manualSort" NUMERI CREATE INDEX _grist_Attachments_fileIdent ON _grist_Attachments(fileIdent); COMMIT; `; +TRIGGER BY TABLE ID -------- +//////////////////////////// diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index b6d7c42c25..0e6786ef59 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1307,3 +1307,12 @@ def migration41(tdset): ] return tdset.apply_doc_actions(doc_actions) + +@migration(schema_version=42) +def migration42(tdset): + """ + Adds columns for register witch table columns are triggered in webhooks. + """ + return tdset.apply_doc_actions([ + add_column('_grist_Triggers_column', 'columnRefList', 'RefList:_grist_Tables_column'), + ]) diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index 8126e76bf2..f499577f79 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -15,7 +15,7 @@ import actions -SCHEMA_VERSION = 41 +SCHEMA_VERSION = 42 def make_column(col_id, col_type, formula='', isFormula=False): return { @@ -261,6 +261,7 @@ def schema_create_actions(): make_column("label", "Text"), make_column("memo", "Text"), make_column("enabled", "Bool"), + make_column("columnRefList", "RefList:_grist_Tables_column") ]), # All of the ACL rules. From f497288ead45fdab73272b161dcdfdaebd7415bf Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Thu, 25 Jan 2024 17:59:11 +0100 Subject: [PATCH 02/31] remove text typo --- app/server/lib/initialDocSql.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index 183f721696..cca4579f79 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -39,10 +39,6 @@ CREATE TABLE IF NOT EXISTS "_grist_Shares" (id INTEGER PRIMARY KEY, "linkId" TEX CREATE INDEX _grist_Attachments_fileIdent ON _grist_Attachments(fileIdent); COMMIT; `; -TRIGGER BY TABLE ID -------- -//////////////////////////// -TRIGGER BY TABLE ID -------- -//////////////////////////// export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; @@ -102,5 +98,3 @@ CREATE TABLE IF NOT EXISTS "Table1" (id INTEGER PRIMARY KEY, "manualSort" NUMERI CREATE INDEX _grist_Attachments_fileIdent ON _grist_Attachments(fileIdent); COMMIT; `; -TRIGGER BY TABLE ID -------- -//////////////////////////// From 0f0dda3ce3e29537168f5aabb2ca33af7c3d5895 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Thu, 1 Feb 2024 12:27:23 +0100 Subject: [PATCH 03/31] add columnsIds param in webhook register in db --- app/client/ui/WebhookPage.ts | 41 +------------------------------- app/common/UserAPI.ts | 4 ---- app/common/schema.ts | 2 +- app/server/lib/Triggers.ts | 9 +------ app/server/lib/initialDocSql.ts | 4 ++-- sandbox/grist/migrations.py | 6 ++--- sandbox/grist/schema.py | 2 +- test/nbrowser/WebhookOverflow.ts | 1 + test/server/lib/DocApi.ts | 3 +++ 9 files changed, 13 insertions(+), 59 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index 637d536bab..399553e642 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -185,25 +185,7 @@ class WebhookExternalTable implements IExternalTable { const {delta} = editor; const updates = new Set(delta.updateRows); const addsAndUpdates = new Set([...delta.addRows, ...delta.updateRows]); - // if (delta.columnDeltas.tableId) { - // // TODO CHANGE THIS CODE !!! the first [1] have ti correspond to the webhook id - // // if tableId has been changes it appears in columnDeltas, - // // the deltas are indexed by the webhookId and is an array [oldValue, newValue] - // const newTableId = delta.columnDeltas.tableId[1][1]; - // if (newTableId) { - // const choicesColIds = editor.gristDoc.docModel.dataTables[newTableId[0]].tableData.getColIds(); - // // CAN NOT DO THAT !!! : modification pour toutes les instances ! il faut faire juste un filtre - // // ou rendre no selectionnable les colonnes qui ne correspondent pas à la table sélectionnée - // editor.gristDoc.docData.receiveAction([ - // 'UpdateRecord', '_grist_Tables_column', 'vt_webhook_fc10' as any, { - // widgetOptions: JSON.stringify({ - // widget: 'TextBox', - // alignment: 'left', - // choices: choicesColIds, - // }) - // }]); - // } - // } +// if table change :remove columnIds list for (const recId of addsAndUpdates) { const rec = editor.getRecord(recId); if (!rec) { @@ -290,27 +272,6 @@ class WebhookExternalTable implements IExternalTable { choices, }) }]); - // const choicesColumns = editor.gristDoc.docModel.visibleTables.all().reduce( - // (obj1, tableRec) => { - // const listCol = tableRec.columns().peek().map(columnRec => columnRec.colId()); - // return { ...obj1, [tableRec.tableId()]: listCol }; - // }, {} - // ); - // const initialArray: string[] = []; - // const choicesColumns = editor.gristDoc.docModel.visibleTables.all().reduce( - // (obj, tableRec) => { - // const listCol = tableRec.columns().peek().map(columnRec => tableRec.tableId() + ": " + columnRec.colId()); - // return [ ...obj, ...listCol]; - // }, initialArray - // ); - // editor.gristDoc.docData.receiveAction([ - // 'UpdateRecord', '_grist_Tables_column', 'vt_webhook_fc10' as any, { - // widgetOptions: JSON.stringify({ - // widget: 'TextBox', - // alignment: 'left', - // choices: choicesColumns, - // }) - // }]); } private _initalizeWebhookList(webhooks: WebhookSummary[]){ diff --git a/app/common/UserAPI.ts b/app/common/UserAPI.ts index 20e2c1fabd..29befe8200 100644 --- a/app/common/UserAPI.ts +++ b/app/common/UserAPI.ts @@ -1012,8 +1012,6 @@ export class DocAPIImpl extends BaseAPI implements DocAPI { public async addWebhook(webhook: WebhookSubscribe & {tableId: string}): Promise<{webhookId: string}> { const {tableId} = webhook; - console.log("INSIDE the User API addWebhook"); - console.log(webhook); return this.requestJson(`${this._url}/tables/${tableId}/_subscribe`, { method: 'POST', body: JSON.stringify( @@ -1022,8 +1020,6 @@ export class DocAPIImpl extends BaseAPI implements DocAPI { } public async updateWebhook(webhook: WebhookUpdate): Promise { - console.log("-------- INSIDE the User API updateWebhook"); - console.log(webhook); return this.requestJson(`${this._url}/webhooks/${webhook.id}`, { method: 'PATCH', body: JSON.stringify(webhook.fields), diff --git a/app/common/schema.ts b/app/common/schema.ts index 7bf8d4483f..77f4baf304 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData"; // tslint:disable:object-literal-key-quotes -export const SCHEMA_VERSION = 42; +export const SCHEMA_VERSION = 43; export const schema = { diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 294ec31284..3426908cf3 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -15,7 +15,6 @@ import { WebhookSummaryCollection, WebhookUsage } from 'app/common/Triggers'; -import { GristObjCode } from 'app/plugin/GristData'; import {decodeObject} from 'app/plugin/objtypes'; import {ActiveDoc} from 'app/server/lib/ActiveDoc'; import {makeExceptionalDocSession} from 'app/server/lib/DocSession'; @@ -178,8 +177,6 @@ export class DocTriggers { const triggersByTableRef = _.groupBy(triggersTable.getRecords().filter(t => t.enabled), "tableRef"); const triggersByTableId: Array<[string, Trigger[]]> = []; - console.log("TRIGGER BY TABLE ID --------"); - // 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()) { @@ -204,7 +201,6 @@ export class DocTriggers { const tasks: Task[] = []; // For each table in the document which is monitored by one or more triggers... - console.log("////////////////////////////"); for (const [tableId, triggers] of triggersByTableId) { const tableDelta = summary.tableDeltas[tableId]; // ...if the monitored table was modified by the summarized actions, @@ -298,10 +294,7 @@ export class DocTriggers { // Other fields used to register this webhook. eventTypes: decodeObject(t.eventTypes) as string[], isReadyColumn: getColId(t.isReadyColRef) ?? null, - columnIds: [ - GristObjCode.List, - t.columnRefList?.slice(1).map(columnRef => getColId(columnRef as number)) - ].join("; "), + columnIds: t.columnRefList?.slice(1).map(columnRef => getColId(columnRef as number)).join("; ") || "", tableId: getTableId(t.tableRef) ?? null, // For future use - for now every webhook is enabled. enabled: t.enabled, diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index cca4579f79..d8aefe9b60 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0); @@ -44,7 +44,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2,3); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index 0e6786ef59..134864b017 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1308,11 +1308,11 @@ def migration41(tdset): return tdset.apply_doc_actions(doc_actions) -@migration(schema_version=42) -def migration42(tdset): +@migration(schema_version=43) +def migration43(tdset): """ Adds columns for register witch table columns are triggered in webhooks. """ return tdset.apply_doc_actions([ - add_column('_grist_Triggers_column', 'columnRefList', 'RefList:_grist_Tables_column'), + add_column('_grist_Triggers', 'columnRefList', 'RefList:_grist_Tables_column'), ]) diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index f499577f79..e44408fad3 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -15,7 +15,7 @@ import actions -SCHEMA_VERSION = 42 +SCHEMA_VERSION = 43 def make_column(col_id, col_type, formula='', isFormula=False): return { diff --git a/test/nbrowser/WebhookOverflow.ts b/test/nbrowser/WebhookOverflow.ts index df52ca1527..ee4cb45f3a 100644 --- a/test/nbrowser/WebhookOverflow.ts +++ b/test/nbrowser/WebhookOverflow.ts @@ -34,6 +34,7 @@ describe('WebhookOverflow', function () { enabled: true, name: 'test webhook', tableId: 'Table2', + columnIds: '' }; await docApi.addWebhook(webhookDetails); await docApi.addWebhook(webhookDetails); diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 22401c6fa9..92f4d04f1b 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -4427,6 +4427,7 @@ function testDocApi() { tableId: 'Table1', name: '', memo: '', + columnIds: '', }, usage : { status: 'idle', numWaiting: 0, @@ -4444,6 +4445,7 @@ function testDocApi() { tableId: 'Table1', name: '', memo: '', + columnIds: '', }, usage : { status: 'idle', numWaiting: 0, @@ -4801,6 +4803,7 @@ function testDocApi() { enabled: true, name: 'My Webhook', memo: 'Sync store', + columnIds: '', }; let stats = await readStats(docId); From f736afc7d35139597b9385af06a1b07faae17e42 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Thu, 1 Feb 2024 16:06:30 +0100 Subject: [PATCH 04/31] take in account the columns to check before trigger action --- app/client/ui/WebhookPage.ts | 8 ++++++- app/server/lib/DocApi.ts | 10 +++----- app/server/lib/Triggers.ts | 44 ++++++++++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index 399553e642..09b2490447 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -163,6 +163,8 @@ class WebhookExternalTable implements IExternalTable { } } const delta = editor.delta; + console.log("---------------------------Before Edit"); + console.log(delta); for (const recId of delta.removeRows) { const rec = editor.getRecord(recId); if (!rec) { @@ -185,7 +187,7 @@ class WebhookExternalTable implements IExternalTable { const {delta} = editor; const updates = new Set(delta.updateRows); const addsAndUpdates = new Set([...delta.addRows, ...delta.updateRows]); -// if table change :remove columnIds list + // TODO : if table change :remove columnIds list for (const recId of addsAndUpdates) { const rec = editor.getRecord(recId); if (!rec) { @@ -235,6 +237,10 @@ class WebhookExternalTable implements IExternalTable { for (const webhook of webhooks) { const values = _mapWebhookValues(webhook); const rowId = rowMap.get(webhook.id); + console.log("sync"); + console.log(values); + console.log(rowMap); + if (rowId) { toRemove.delete(rowId); actions.push( diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 5d682aba84..168b5e6298 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -416,10 +416,6 @@ export class DocWorkerApi { currentTableId = tableId; } - console.log("HERE 6666 DOC API SAVING TRIGGER"); - console.log(columnIds); - console.log(tableId); - if (columnIds !== undefined) { if (columnIds !== null && columnIds !== '') { if (!currentTableId) { @@ -427,10 +423,10 @@ export class DocWorkerApi { } // columnIds have to be of shape "columnId; columnId; columnId" fields.columnRefList = [GristObjCode.List, ...columnIds.split(";").map( - columnId => { return colIdToReference(metaTables, currentTableId!, columnId.trim()); } + columnId => { return colIdToReference(metaTables, currentTableId!, columnId.trim().replace(/^\$/, '')); } )]; } else { - fields.columnRefList = [GristObjCode.List, 0]; + fields.columnRefList = [GristObjCode.List]; } } @@ -950,7 +946,7 @@ export class DocWorkerApi { await activeDoc.sendWebhookNotification(); - res.json({success: false}); + res.json({success: true}); }) ); diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 3426908cf3..f85d221517 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -206,10 +206,9 @@ export class DocTriggers { // ...if the monitored table was modified by the summarized actions, // fetch the modified/created records and note the work that needs to be done. if (tableDelta) { - console.log("---------------------------------------"); + console.log("--------------------------------------- handle() triggers.ts"); console.log(tableDelta); console.log(tableDelta.columnDeltas); - console.log(tableDelta.columnDeltas.Region); const recordDeltas = this._getRecordDeltas(tableDelta); const filters = {id: [...recordDeltas.keys()]}; @@ -228,6 +227,8 @@ export class DocTriggers { for (const task of tasks) { events.push(...this._handleTask(task, await task.tableDataAction)); } + console.log("----------- events list"); + console.log(events); if (!events.length) { return summary; } @@ -509,7 +510,6 @@ export class DocTriggers { for (const action of webhookActions) { const colId = this._getColId(trigger.isReadyColRef); // no validation const tableId = this._getTableId(trigger.tableRef); - // TODO copy that for columns !! 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)); } @@ -517,9 +517,27 @@ export class DocTriggers { } } + if (trigger.columnRefList) { + for (const colRef of trigger.columnRefList.slice(1)) { + if (!this._validateColId(colRef as number, trigger.tableRef)) { + // column does not belong to table, let's ignore trigger and log stats + for (const action of webhookActions) { + const colId = this._getColId(colRef as number); // no validation + const tableId = this._getTableId(trigger.tableRef); + const error = `column 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. + console.log("bulkColValues"); + console.log(bulkColValues); + const rowIndexesToSend: number[] = _.range(bulkColValues.id.length).filter(rowIndex => { const rowId = bulkColValues.id[rowIndex]; return this._shouldTriggerActions( @@ -552,7 +570,13 @@ export class DocTriggers { recordDelta: RecordDelta, tableDelta: TableDelta, ): boolean { + console.log("_shouldTriggerActions"); + console.log(rowId); + console.log(recordDelta); + console.log(trigger); let readyBefore: boolean; + console.log("tableDelta.columnDeltas"); + console.log(tableDelta.columnDeltas); if (!trigger.isReadyColRef) { // User hasn't configured a column, so all records are considered ready immediately readyBefore = recordDelta.existedBefore; @@ -593,9 +617,21 @@ export class DocTriggers { } } + const colIdsToCheck: Array = []; + if (trigger.columnRefList) { + for (const colRef of trigger.columnRefList.slice(1)) { + colIdsToCheck.push(this._getColId(colRef as number)!); + } + } + let eventType: EventType; if (readyBefore) { - eventType = "update"; + // check if any of the columns to check were changed to consider this an update + if (colIdsToCheck.length === 0 || colIdsToCheck.some(colId => tableDelta.columnDeltas[colId]?.[rowId])) { + eventType = "update"; + } else { + return false; + } // If we allow subscribing to deletion in the future // if (recordDelta.existedAfter) { // eventType = "update"; From d4d6fe83adf304cae45d30285450c475c82cc93d Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Thu, 1 Feb 2024 16:50:35 +0100 Subject: [PATCH 05/31] remove columnIds if change tableID --- app/client/ui/WebhookPage.ts | 5 ----- app/server/lib/DocApi.ts | 31 ++++++++++++++----------------- app/server/lib/Triggers.ts | 16 ---------------- 3 files changed, 14 insertions(+), 38 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index 09b2490447..ae2823864e 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -163,8 +163,6 @@ class WebhookExternalTable implements IExternalTable { } } const delta = editor.delta; - console.log("---------------------------Before Edit"); - console.log(delta); for (const recId of delta.removeRows) { const rec = editor.getRecord(recId); if (!rec) { @@ -237,9 +235,6 @@ class WebhookExternalTable implements IExternalTable { for (const webhook of webhooks) { const values = _mapWebhookValues(webhook); const rowId = rowMap.get(webhook.id); - console.log("sync"); - console.log(values); - console.log(rowMap); if (rowId) { toRemove.delete(rowId); diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 168b5e6298..a30ef22577 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -336,7 +336,6 @@ export class DocWorkerApi { if (!fields.tableRef) { throw new ApiError(`tableId is required`, 400); } - console.log(" JE SUIS DANS LE REGISTER WEBHOOK DU FICHIER DOC API"); const unsubscribeKey = uuidv4(); const webhookSecret: WebHookSecret = {unsubscribeKey, url}; @@ -412,22 +411,24 @@ export class DocWorkerApi { } if (tableId !== undefined) { - fields.tableRef = tableIdToRef(metaTables, tableId); - currentTableId = tableId; - } - - if (columnIds !== undefined) { - if (columnIds !== null && columnIds !== '') { - if (!currentTableId) { - throw new ApiError(`Cannot find columns "${columnIds}" because table is not known`, 404); + if (columnIds !== undefined && columnIds !== null && columnIds !== '') { + if (tableId !== currentTableId) { + // if the tableId changed, we need to reset the columnIds + fields.columnRefList = [GristObjCode.List]; + } else { + if (!currentTableId) { + throw new ApiError(`Cannot find columns "${columnIds}" because table is not known`, 404); + } + // columnIds have to be of shape "columnId; columnId; columnId" + fields.columnRefList = [GristObjCode.List, ...columnIds.split(";").map( + columnId => { return colIdToReference(metaTables, currentTableId!, columnId.trim().replace(/^\$/, '')); } + )]; } - // columnIds have to be of shape "columnId; columnId; columnId" - fields.columnRefList = [GristObjCode.List, ...columnIds.split(";").map( - columnId => { return colIdToReference(metaTables, currentTableId!, columnId.trim().replace(/^\$/, '')); } - )]; } else { fields.columnRefList = [GristObjCode.List]; } + fields.tableRef = tableIdToRef(metaTables, tableId); + currentTableId = tableId; } if (isReadyColumn !== undefined) { @@ -918,13 +919,9 @@ export class DocWorkerApi { '/api/docs/:docId/webhooks/:webhookId', isOwner, validate(WebhookPatch), withDocTriggersLock(async (activeDoc, req, res) => { - console.log("----- INSIDE DOCAPI - api call update webhook"); - const docId = activeDoc.docName; const webhookId = req.params.webhookId; const {fields, url} = await getWebhookSettings(activeDoc, req, webhookId, req.body); - console.log(fields); - console.log(req.body); if (fields.enabled === false) { await activeDoc.triggers.clearSingleWebhookQueue(webhookId); } diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index f85d221517..23c62bb7ca 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -182,8 +182,6 @@ export class DocTriggers { for (const tableRef of Object.keys(triggersByTableRef).sort()) { const triggers = triggersByTableRef[tableRef]; const tableId = getTableId(Number(tableRef))!; // groupBy makes tableRef a string - console.log(triggersByTableId); - console.log(triggers); triggersByTableId.push([tableId, triggers]); for (const trigger of triggers) { if (trigger.isReadyColRef) { @@ -206,9 +204,6 @@ export class DocTriggers { // ...if the monitored table was modified by the summarized actions, // fetch the modified/created records and note the work that needs to be done. if (tableDelta) { - console.log("--------------------------------------- handle() triggers.ts"); - console.log(tableDelta); - console.log(tableDelta.columnDeltas); const recordDeltas = this._getRecordDeltas(tableDelta); const filters = {id: [...recordDeltas.keys()]}; @@ -227,8 +222,6 @@ export class DocTriggers { for (const task of tasks) { events.push(...this._handleTask(task, await task.tableDataAction)); } - console.log("----------- events list"); - console.log(events); if (!events.length) { return summary; } @@ -535,9 +528,6 @@ export class DocTriggers { // TODO: would be worth checking that the trigger's fields are valid (ie: eventTypes, url, // ...) as there's no guarantee that they are. - console.log("bulkColValues"); - console.log(bulkColValues); - const rowIndexesToSend: number[] = _.range(bulkColValues.id.length).filter(rowIndex => { const rowId = bulkColValues.id[rowIndex]; return this._shouldTriggerActions( @@ -570,13 +560,7 @@ export class DocTriggers { recordDelta: RecordDelta, tableDelta: TableDelta, ): boolean { - console.log("_shouldTriggerActions"); - console.log(rowId); - console.log(recordDelta); - console.log(trigger); let readyBefore: boolean; - console.log("tableDelta.columnDeltas"); - console.log(tableDelta.columnDeltas); if (!trigger.isReadyColRef) { // User hasn't configured a column, so all records are considered ready immediately readyBefore = recordDelta.existedBefore; From 62836eb19b2f62ca493e32bb5ca8fac67986f5b7 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Fri, 2 Feb 2024 12:44:32 +0100 Subject: [PATCH 06/31] webhook translatable --- app/client/ui/WebhookPage.ts | 32 +++++++++++++++----------------- static/locales/en.client.json | 14 +++++++++++++- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index ae2823864e..91e5fd4a4f 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -45,13 +45,13 @@ const WEBHOOK_COLUMNS = [ id: 'vt_webhook_fc2', colId: 'url', type: 'Text', - label: 'URL', + label: t('URL'), }, { id: 'vt_webhook_fc3', colId: 'eventTypes', type: 'ChoiceList', - label: 'Event Types', + label: t('Event Types'), widgetOptions: JSON.stringify({ widget: 'TextBox', alignment: 'left', @@ -63,13 +63,13 @@ const WEBHOOK_COLUMNS = [ id: 'vt_webhook_fc10', colId: 'columnIds', type: 'Text', - label: 'Columns (séparé par des ;)', + label: t('Columns to check when update (separated by ;)'), }, { id: 'vt_webhook_fc4', colId: 'enabled', type: 'Bool', - label: 'Enabled', + label: t('Enabled'), widgetOptions: JSON.stringify({ widget: 'Switch', }), @@ -78,31 +78,31 @@ const WEBHOOK_COLUMNS = [ id: 'vt_webhook_fc5', colId: 'isReadyColumn', type: 'Text', - label: 'Ready Column', + label: t('Ready Column'), }, { id: 'vt_webhook_fc6', colId: 'webhookId', type: 'Text', - label: 'Webhook Id', + label: t('Webhook Id'), }, { id: 'vt_webhook_fc7', colId: 'name', type: 'Text', - label: 'Name', + label: t('Name'), }, { id: 'vt_webhook_fc8', colId: 'memo', type: 'Text', - label: 'Memo', + label: t('Memo'), }, { id: 'vt_webhook_fc9', colId: 'status', type: 'Text', - label: 'Status', + label: t('Status'), }, ] as const; @@ -113,9 +113,8 @@ const WEBHOOK_VIEW_FIELDS: Array<(typeof WEBHOOK_COLUMNS)[number]['colId']> = [ 'name', 'memo', 'eventTypes', 'url', 'tableId', 'isReadyColumn', - 'columnIds', - 'webhookId', 'enabled', - 'status' + 'columnIds', 'webhookId', + 'enabled', 'status' ]; /** @@ -158,7 +157,7 @@ class WebhookExternalTable implements IExternalTable { } const colIds = new Set(getColIdsFromDocAction(d) || []); if (colIds.has('webhookId') || colIds.has('status')) { - throw new Error(`Sorry, not all fields can be edited.`); + throw new Error(t(`Sorry, not all fields can be edited.`)); } } } @@ -169,7 +168,7 @@ class WebhookExternalTable implements IExternalTable { continue; } await this._removeWebhook(rec); - reportMessage(`Removed webhook.`); + reportMessage(t(`Removed webhook.`)); } const updates = new Set(delta.updateRows); const t2 = editor; @@ -185,7 +184,6 @@ class WebhookExternalTable implements IExternalTable { const {delta} = editor; const updates = new Set(delta.updateRows); const addsAndUpdates = new Set([...delta.addRows, ...delta.updateRows]); - // TODO : if table change :remove columnIds list for (const recId of addsAndUpdates) { const rec = editor.getRecord(recId); if (!rec) { @@ -364,12 +362,12 @@ export class WebhookPage extends DisposableWithEvents { public async reset() { await this.docApi.flushWebhooks(); - reportSuccess('Cleared webhook queue.'); + reportSuccess(t('Cleared webhook queue.')); } public async resetSelected(id: string) { await this.docApi.flushWebhook(id); - reportSuccess(`Cleared webhook ${id} queue.`); + reportSuccess(t(`Cleared webhook ${id} queue.`)); } } diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 906d0fd344..703aa03903 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -1125,7 +1125,19 @@ }, "WebhookPage": { "Clear Queue": "Clear Queue", - "Webhook Settings": "Webhook Settings" + "Webhook Settings": "Webhook Settings", + "Cleared webhook queue.": "Cleared webhook queue.", + "Columns to check when update (separated by ;)": "Columns to check when update (separated by ;)", + "Enabled": "Enabled", + "Event Types": "Event Types", + "Memo": "Memo", + "Name": "Name", + "Ready Column": "Ready Column", + "Removed webhook.": "Removed webhook.", + "Sorry, not all fields can be edited.": "Sorry, not all fields can be edited.", + "Status": "Status", + "URL": "URL", + "Webhook Id": "Webhook Id" }, "FormulaAssistant": { "Ask the bot.": "Ask the bot.", From 37976490a3b855ff089646516111f4e038aa8c15 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 5 Feb 2024 18:17:00 +0100 Subject: [PATCH 07/31] add test for columnIds in webhooks --- app/server/lib/DocApi.ts | 7 +- samples/2LsCmXef7x7HjzAntB5koT.grist | Bin 0 -> 176128 bytes samples/4zwU6YdxxHUK7UwfTNUHAy.grist | Bin 0 -> 176128 bytes samples/k6ZHoVqLCUm62MX1LDXhKV.grist | Bin 0 -> 176128 bytes samples/mR78N8U6FEzydHF7dKLB4t.grist | Bin 0 -> 176128 bytes samples/og9fjmYEqzpfL2xJexuZa8.grist | Bin 0 -> 176128 bytes samples/ooPSddPQJMXFvYzvv8eL4X.grist | Bin 0 -> 176128 bytes test/server/lib/DocApi.ts | 114 ++++++++++++++++++++++----- 8 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 samples/2LsCmXef7x7HjzAntB5koT.grist create mode 100644 samples/4zwU6YdxxHUK7UwfTNUHAy.grist create mode 100644 samples/k6ZHoVqLCUm62MX1LDXhKV.grist create mode 100644 samples/mR78N8U6FEzydHF7dKLB4t.grist create mode 100644 samples/og9fjmYEqzpfL2xJexuZa8.grist create mode 100644 samples/ooPSddPQJMXFvYzvv8eL4X.grist diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index a30ef22577..3d4c99afe4 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -336,6 +336,7 @@ export class DocWorkerApi { if (!fields.tableRef) { throw new ApiError(`tableId is required`, 400); } + console.log("In the subscribe function of DOC ID app: ", fields); const unsubscribeKey = uuidv4(); const webhookSecret: WebHookSecret = {unsubscribeKey, url}; @@ -412,16 +413,16 @@ export class DocWorkerApi { if (tableId !== undefined) { if (columnIds !== undefined && columnIds !== null && columnIds !== '') { - if (tableId !== currentTableId) { + if (tableId !== currentTableId && currentTableId !== undefined && currentTableId !== null) { // if the tableId changed, we need to reset the columnIds fields.columnRefList = [GristObjCode.List]; } else { - if (!currentTableId) { + if (!tableId) { throw new ApiError(`Cannot find columns "${columnIds}" because table is not known`, 404); } // columnIds have to be of shape "columnId; columnId; columnId" fields.columnRefList = [GristObjCode.List, ...columnIds.split(";").map( - columnId => { return colIdToReference(metaTables, currentTableId!, columnId.trim().replace(/^\$/, '')); } + columnId => { return colIdToReference(metaTables, tableId, columnId.trim().replace(/^\$/, '')); } )]; } } else { diff --git a/samples/2LsCmXef7x7HjzAntB5koT.grist b/samples/2LsCmXef7x7HjzAntB5koT.grist new file mode 100644 index 0000000000000000000000000000000000000000..168c711b370afa8488e995d54c5039a1194a09bf GIT binary patch literal 176128 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 RI4rtX`9WXD;KvO~`u}rbAPfKi literal 0 HcmV?d00001 diff --git a/samples/4zwU6YdxxHUK7UwfTNUHAy.grist b/samples/4zwU6YdxxHUK7UwfTNUHAy.grist new file mode 100644 index 0000000000000000000000000000000000000000..8245ffaf41c0fd79b37aa7b7190337f99bdd90ba GIT binary patch literal 176128 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 O!VkJS20yMz(*Fn0X%RyJ literal 0 HcmV?d00001 diff --git a/samples/og9fjmYEqzpfL2xJexuZa8.grist b/samples/og9fjmYEqzpfL2xJexuZa8.grist new file mode 100644 index 0000000000000000000000000000000000000000..670a47cf118d5d96fcf8657ef22b2ffaf3085ebf GIT binary patch literal 176128 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 aaW|+-h%HmNfy1MFMI7{W3~^jjl>ZM?z!ygV literal 0 HcmV?d00001 diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 92f4d04f1b..c8160290eb 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -3350,6 +3350,14 @@ function testDocApi() { /* Regression test for old _subscribe endpoint. /docs/{did}/webhooks should be used instead to subscribe */ + const serving: Serving = await serveSomething(app => { + app.use(express.json()); + app.post('/200', ({ body }, res) => { + res.sendStatus(200); + res.end(); + }); + }, webhooksTestPort); + async function oldSubscribeCheck(requestBody: any, status: number, ...errors: RegExp[]) { const resp = await axios.post( `${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/_subscribe`, @@ -3430,7 +3438,15 @@ function testDocApi() { await postWebhookCheck({webhooks:[{fields: {eventTypes: ["add"], url: "https://example.com"}}]}, 400, /tableId is missing/); await postWebhookCheck({}, 400, /webhooks is missing/); - + await postWebhookCheck({ + webhooks: [{ + fields: { + tableId: "Table1", eventTypes: ["update"], columnIds: "notExisting", + url: `${serving.url}/200` + } + }] + }, + 403, /Column not found notExisting/); }); @@ -3855,6 +3871,7 @@ function testDocApi() { tableId?: string, isReadyColumn?: string | null, eventTypes?: string[] + columnIds?: string, }) { // Subscribe helper that returns a method to unsubscribe. const data = await subscribe(endpoint, docId, options); @@ -3872,6 +3889,7 @@ function testDocApi() { tableId?: string, isReadyColumn?: string|null, eventTypes?: string[], + columnIds?: string, name?: string, memo?: string, enabled?: boolean, @@ -3883,7 +3901,7 @@ function testDocApi() { eventTypes: options?.eventTypes ?? ['add', 'update'], url: `${serving.url}/${endpoint}`, isReadyColumn: options?.isReadyColumn === undefined ? 'B' : options?.isReadyColumn, - ...pick(options, 'name', 'memo', 'enabled'), + ...pick(options, 'name', 'memo', 'enabled', 'columnIds'), }, chimpy ); assert.equal(status, 200); @@ -4407,6 +4425,49 @@ function testDocApi() { await webhook1(); }); + it("should not call to a webhook when columns updated are not in columnIds", async () => { // eslint-disable-line max-len + // Create a test document. + const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; + const docId = await userApi.newDoc({ name: 'testdoc5' }, ws1); + const doc = userApi.getDocAPI(docId); + await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ + ['ModifyColumn', 'Table1', 'B', { type: 'Bool' }], + ], chimpy); + + const webhook = await autoSubscribe('200', docId, { + columnIds: 'A', eventTypes: ['add', 'update'] + }); + successCalled.reset(); + + // Create record, that will call the webhook. + const newRowIds = await doc.addRows("Table1", { + A: [2], + B: [true], + C: ['c1'] + }); + assert.isTrue(successCalled.called()); + await successCalled.waitAndReset(); + + // Modify the value of column that is not in columnIds. + await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ + ['UpdateRecord', 'Table1', newRowIds[0], { C: 'c2' }], + ], chimpy); + await delay(100); + assert.isFalse(successCalled.called()); + successCalled.reset(); + + // Modify the value of column that is in columnIds. + await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ + ['UpdateRecord', 'Table1', newRowIds[0], { A: 19 }], + ], chimpy); + await delay(100); + assert.isTrue(successCalled.called()); + await successCalled.waitAndReset(); + + // Unsubscribe. + await webhook(); + }); + it("should return statistics", async () => { await clearQueue(docId); // Read stats, it should be empty. @@ -4777,43 +4838,52 @@ function testDocApi() { describe('webhook update', function () { it('should work correctly', async function () { - - async function check(fields: any, status: number, error?: RegExp | string, expectedFieldsCallback?: (fields: any) => any) { - let savedTableId = 'Table1'; const origFields = { tableId: 'Table1', eventTypes: ['add'], isReadyColumn: 'B', name: 'My Webhook', memo: 'Sync store', + columnIds: 'A' }; // subscribe - const webhook = await subscribe('foo', docId, origFields); + const { data } = await axios.post( + `${serverUrl}/api/docs/${docId}/webhooks`, + { + webhooks: [{ + fields: { + ...origFields, + url: `${serving.url}/foo` + } + }] + }, chimpy + ); + const webhooks = data; const expectedFields = { url: `${serving.url}/foo`, - unsubscribeKey: webhook.unsubscribeKey, eventTypes: ['add'], isReadyColumn: 'B', tableId: 'Table1', enabled: true, name: 'My Webhook', memo: 'Sync store', - columnIds: '', + columnIds: 'A', }; let stats = await readStats(docId); assert.equal(stats.length, 1, 'stats=' + JSON.stringify(stats)); - assert.equal(stats[0].id, webhook.webhookId); - assert.deepEqual(stats[0].fields, expectedFields); + assert.equal(stats[0].id, webhooks.webhooks[0].id); + const {unsubscribeKey, ...fieldsWithoutUnsubscribeKey} = stats[0].fields; + assert.deepEqual(fieldsWithoutUnsubscribeKey, expectedFields); // update const resp = await axios.patch( - `${serverUrl}/api/docs/${docId}/webhooks/${webhook.webhookId}`, fields, chimpy + `${serverUrl}/api/docs/${docId}/webhooks/${webhooks.webhooks[0].id}`, fields, chimpy ); // check resp @@ -4821,24 +4891,24 @@ function testDocApi() { if (resp.status === 200) { stats = await readStats(docId); assert.equal(stats.length, 1); - assert.equal(stats[0].id, webhook.webhookId); + assert.equal(stats[0].id, webhooks.webhooks[0].id); if (expectedFieldsCallback) { expectedFieldsCallback(expectedFields); } - assert.deepEqual(stats[0].fields, {...expectedFields, ...fields}); - if (fields.tableId) { - savedTableId = fields.tableId; - } + const {unsubscribeKey, ...fieldsWithoutUnsubscribeKey} = stats[0].fields; + assert.deepEqual(fieldsWithoutUnsubscribeKey, { ...expectedFields, ...fields }); } else { if (error instanceof RegExp) { assert.match(resp.data.details?.userError || resp.data.error, error); } else { - assert.deepEqual(resp.data, {error}); + assert.deepEqual(resp.data, { error }); } } // finally unsubscribe - const unsubscribeResp = await unsubscribe(docId, webhook, savedTableId); + const unsubscribeResp = await axios.delete( + `${serverUrl}/api/docs/${docId}/webhooks/${webhooks.webhooks[0].id}`, chimpy + ); assert.equal(unsubscribeResp.status, 200, JSON.stringify(pick(unsubscribeResp, ['data', 'status']))); stats = await readStats(docId); assert.equal(stats.length, 0, 'stats=' + JSON.stringify(stats)); @@ -4849,11 +4919,13 @@ function testDocApi() { await check({url: "http://example.com"}, 403, "Provided url is forbidden"); // not https // changing table without changing the ready column should reset the latter - await check({tableId: 'Table2'}, 200, '', expectedFields => expectedFields.isReadyColumn = null); - + await check({tableId: 'Table2'}, 200, '', expectedFields => { + expectedFields.isReadyColumn = null; + expectedFields.columnIds = ""; + }); await check({tableId: 'Santa'}, 404, `Table not found "Santa"`); - await check({tableId: 'Table2', isReadyColumn: 'Foo'}, 200); + await check({tableId: 'Table2', isReadyColumn: 'Foo', columnIds: ""}, 200); await check({eventTypes: ['add', 'update']}, 200); await check({eventTypes: []}, 400, "eventTypes must be a non-empty array"); From 5819a57111372d0b438c913dafa67efe0e571460 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 5 Feb 2024 18:23:06 +0100 Subject: [PATCH 08/31] delete samples bd --- samples/2LsCmXef7x7HjzAntB5koT.grist | Bin 176128 -> 0 bytes samples/4zwU6YdxxHUK7UwfTNUHAy.grist | Bin 176128 -> 0 bytes samples/k6ZHoVqLCUm62MX1LDXhKV.grist | Bin 176128 -> 0 bytes samples/mR78N8U6FEzydHF7dKLB4t.grist | Bin 176128 -> 0 bytes samples/og9fjmYEqzpfL2xJexuZa8.grist | Bin 176128 -> 0 bytes samples/ooPSddPQJMXFvYzvv8eL4X.grist | Bin 176128 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 samples/2LsCmXef7x7HjzAntB5koT.grist delete mode 100644 samples/4zwU6YdxxHUK7UwfTNUHAy.grist delete mode 100644 samples/k6ZHoVqLCUm62MX1LDXhKV.grist delete mode 100644 samples/mR78N8U6FEzydHF7dKLB4t.grist delete mode 100644 samples/og9fjmYEqzpfL2xJexuZa8.grist delete mode 100644 samples/ooPSddPQJMXFvYzvv8eL4X.grist diff --git a/samples/2LsCmXef7x7HjzAntB5koT.grist b/samples/2LsCmXef7x7HjzAntB5koT.grist deleted file mode 100644 index 168c711b370afa8488e995d54c5039a1194a09bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176128 zcmeI5U2Gd!cA!}zB}%d>y1QqFcK?>?^vaCQohtqn*}aW4CDCogwB(kl?lz1b&8@0S zsm8@BcKukb@iulP+nq)dBmolazT{yR8(^QZPszh#c3!eOK!DwcBv>R1WFH0yki{a4 zY!EC4y)!R+?k$p{$RcGm$KAI35VFLobML8hzjN=o=iZ`=H{Y!r9u=Fm(;}XjkDQ4J zLgZ~xj6@>y@b41*>%K0+n^^Y%{tEDKw(mIX?R?~)YLRJ>OeKHHaDju!?r;0a7Dwt0=>gGNm^W5U$57yYdyuxCf0ShaJZ&PRBDE))TygE2ESyO*ta}` zF0+PQaqLz-&=PiS*Kf6mvv9gS58r=}Zj+a!A<<#!F&0I!aeJ3hF4z|OVJdL0_? zP>q<{`#aPMzjPp9q3y8xnS){?o?2WK9%MqgSbu+87}Kf7^(L-1ygRfVgQ0uRpu3}b z+Y!3F;@+Wr=njtrSK9$Z??Fvq5DvgI zTJ-z26*@-27{3K^;U@Jw!`dFcd}jZ}Q}NWbqOe~zES-KFKydfmtsADlZCESBBU`>@ zeB{%uJ9IBhe7Zwwb8YqA%^HNCmD&elSiwF$qHTp1x!C4cUDYp(aJ9_A&3PV5AFTHI zYrr_(-H)C-b;NgJrid5k#m;L~MCIaaR=i;n!x9b4qubOG1N%fBu!x4t?G0-oHU?wXclp(HbYXG8+IJh=I^@?HA!;!$4cF)X@Alj85%TrvwT3f!x=dZU!DRa5k zE`&a`wQ2rgZo`i49c;=7F@WC>(=Z{s`ZW#;X~g2$b?L8i};=$gn+Stw&bzmx%+QV z$5U^|5ideT2r_n442rRL-j3mRywg(>SOh&~loCis_%INgklculZF^jmLn(x( zjyAWaPPO&DAYSzMjqZD}NwXoQSzJeO=}y(m^_iIzdGmz5V`RUG-(QHuQ;H%yd^5C1 zY`Gq75B3Lsa(ZlU;6;w_3q2lv8S;@RDjmk6PO5uE3^Vkh3N{EKSveYn`{cZVZ1x{r zL}S@0%)_HgI5s3!V@vQjKeoj8T$U{iT%+sXgIm#fDw`D^&~S(JX4Ig&$HuzhBp&GX z@B!0~xB#-5F=F4P&Vcxchu8vPoZNJKux$p#F4V{f+o;-FYkzSYOi3;ln-7}fDre;#_UOIks2L|^=dv*fW)z7Ek z*K>&kTVkJKzwm_wkN^@u0!RP}AOR$R1dsp{Kmter2|TL=*iC+1|39nV#daY9B!C2v z01`j~NB{{S0VIF~kN^@02;lvH>o5Rf(-UVO`rW@4-&i$ek-8pljc>Ws%Np<~E`J8f z1&>MLRs1*{cOI+~oM2|L$I@*_e(>Gq@Bh`uOI#Iuc8d>^h~%>ehXhYU3Z603gH+1p z^pdKmL?()sFXnP`xva>!60MZwd@fHkMa`Eg1)?gsV!l92`Eo9=Q%M8-sv@ZcZX0{p z7u7j{!-n1Fz!<}-u5a*%2Kq@j3ZBWb)o_e$c)*)$FcSB8lLG&_+zMMAn^c(47NhvDe`fQ#SRs zI3*}FczO0vRsPr+c7|0B9PC@V9l+qFPJ>{Bw(Ps~7z+L=D9M}__$cT+kBD*Y%RtB* z)uF`t;A4gzYi^Y7gq7tQ0DV57*_^ruJ2rHT_HRgV#F6l_+`h4Pk(ia#fUlKHy0-Dr&FOJ zVp6RGA%abJe0F}K(}>@>b+QnlaDmG`K~kX}a80Nr6_VMBq@?N)?@y@;NFKDoawn1bZ!tlq=>+ zQh^qTq!;yOsgNtl1yzp+TFSJn!A&zoFHt35RtYJ{IR#WHdahK-(~7Q?N(H4-R0~bg z)bo0wT%=7!hJBoJu~blJxRwHeOR5U2^veZJFDqKPoY#n|mt+cem~=%VN~NqSvILt$ zxxCiYs3zxRMJ-bWwvH;r=|Ibbw*+Hc%cQ&eQLbgu4Zx`t@X_3)TOWz!UqqtGA4ifu zPX3?d|M+@zZfqqIKmter2_OL^fCP{L5E{;9d>xw&Awfc<|~A^Eci{NM`-AOR$R z1dsp{Kmter2_OL^fCP{L5_ska#3HdsB7x8Uf94#E4MGA)00|%gB!C2v01`j~NB{{S z0VFU)fPMZy!XD#|FC>5jkN^@u0!RP}AOR$R1dsp{KmthMnI#Z>{{JVDo z01`j~NB{{S0VIF~kN^@u0!RP}Jo5xzg>PPjPx%Wk$d@)7%lFd!X@C^I@S0lL&70{@ zg7yF3N0NX4%%i{tA^{|T1dsp{Kmter2_OL^fCP{L68M@BI0L%@=jLW-1))Jylgd-k zMDkxpVo5cU{4dErOsdIGz9t~BnMeQ$AOR$R1dsp{Kmter2_OL^fCPTQ1kOdDkIV_l zZzg6F$;2$YBD0a`(voePOJ`$T^g?3pP!wCPT6@c2!?&Q)5lM9E0h2dd+M)ee{1eX@gL66=|7GAar6`6MJVz$ zd?i1h(o)5u@R{bZ&q{1xAo!>R|ZR)MJJ;Sy@?{G!Ky<$5p-y~^qX??w3 ztFHAFGn<-S;c!ipsMHKosZ&>T41UQlv2S?>U1klr;@GWvpe5|uuHR}AXYZ!%I~uDH zPE9x5wn_Fz%I_Mk0UjPHcYKo$l3ClV*P-za)rhIRzeBC?O9%25+77FqIVdLLsl`R% zK_;Y&_4l`hF`a5$Z{ljhyF=SC7`pcix;v`39ihuRet08oYlXs&BJ{d2A30=~UF0U! zSP$Ev&2S|^%69Z+;^@bg&K#UM9ZzL4!u{L*+*WLD)oR+E5!X3Ngp%tB$Dz|fTj)4y zQ|b(QlMgVJxOXTYy2B&E)ph{Udr%V?gah!57X7|$g^p1$1~z#u?@eB{%uJ9IBhe7Zwwb8YqA%^HNCmD&el zSiwF$qHTp1x!C4cUDYp(aJ9_A&G|SBr4Ls7{54=4@9sy>ojT&XFjK^f^J3>UDxz|6 zHY?sRiD8L`<A(?`q6_@JkOT8evxESacMKF_<56TeNwKaoyR2O#*Z--0zHD&g1%*9i(EF4@LpnAnH z>ETFVOuJ|1M-c7GkL4*YU#%@)BqLjJZYZo6bLz72OZ0}msS+!h`SQ=f`2Mcz} zWezUK%oww=8t2ej-Q_JK=qaO=Ksv&Qf!KuPMs#f34KdB)I)Y1g zs%Eaw%%sShC+r;~`$hczLM)zA6yf2Up*>>D^=NyrKk$>&V|xQHa(rLt@#xEtk3>=F zFcx)E-6LX{p$}ECK?upp(IDI>=M7}D|L7tb%T8e)9$mt*A+Z`;g2(x>CBEmfY+>LU zUH=~3ipEpftnh$_JES+G2Hib2)(t1|K(B`nn0CYkkj;z{`!00`#78{D76{|yHq4XG zkcavWFb(U@*aBnO?!P9)Q2V$-Ff+lX~~k@uNF1xG&oC6H!>u&$2~*Vty$yA6<{k|IO^bi~ripAI&UGTd}{0 z{=evY;>zh?JM~{dY(&Up%YS~M`K779Wkerr_7UA`IL0>QV;!Qs!`F@w?Jc4^g~z4s zT=~dF2E7Y}8?X=_%%D5VIm6wcL}x3-kt%}DKUkkH07fo%TeM||t#hfxb{0mgn;^o# z76aH(hxuZlHZwOfza;!#s#+BuX8Vb<)swsB2CJ>Dra?_T=&oBKl9xg`hZG!#{1>rh zF%mcIPIx=v3#MZ?E+$>~Hx1KcVQP3eH)=;9v%mPQcT)oE*fX)UFd})eag~P z%Oh+%Al$QUuwv{cm%9VoWa0WV`{%yFH&ymO>Tg(ss7u7@jD+AI7232w_J!L9*Botm zX8)BJf@x*YwpEX|dO?t={P0_&?BmjL7J?Dx2t2T7HQG0s{R_{>Q{Vpf(P=N(^9+JQ z-=-8l@y$^-nv}~QkCiu``16ac$eNhh`Z*skPy=gW;o;<#3}S zl5P+arl6s)1Ksb?m;neNW6OIO`$N?-_s=Hzj^D#O{d`ti%)0Kl=^UL2Wzi9iBfvF> z2Nf{QhU9~#PNDt-In07C?QN6~H%+Ie(Sikf=zOBvK8)CpeA{D5bI2w;q{9TlIE1|+ zHgIiTag@-#W3&y+@W3N(n`omhU}?4vTZ>`pcZhox(jva?Ib>W0a>IYawxJ#t`(0D@ zJfiKeJ>Y}~^d`y6r}i}88S#+m|cRkVEB3uwis?Y z+HrTb*iHl#8@H%rMH<9``>^9m(UwZ}@S9Sj79^|@ELR?NdpCoh|DSAc18XAzB!C2v z01`j~NB{{S0VIF~kN^^R8VKO}|7q|N+K2>@01`j~NB{{S0VIF~kN^@u0!Uyo0=WL4 zj33rU0!RP}AOR$R1dsp{Kmter2_OL^@H7y>_5ai0CA1LZ; zO{%{<6)JV8W;?q6ssPft!@AD}bvx4AAF}oTT+)jq|1tSD$^Vx8&&fYZ{z3Bhk`Le% zzK{SCKmter2_OL^fCP{L5e@_~=}0VYdHxHo6e+KaNi= z%=8^k&BPX_`;Vui3$gy=*i<5VDl*s8A4^R29Zp403vf8sx%w1a|3}VD&={npu5EUQ+B!C2v01`j~NB{{S0VIF~kN^^R;t6!u|H+l)$`fxa8i)jt01`j~NB{{S z0VIF~kN^@u0!ZMiCGa2Tvcf(*n{O_1J56*weL3y7;qm%3{bBU>y|lO2rs+4*yHwq= z?K>bebT}-Ql%i6QwQQaib` z&;0q!h3RXt=c8AqeiwxJI-bDP??(P41;2ijx^O`_c++Fwvbg27+FQZ*E%v>=xV%xT zHfmy{x>T=;=|G+qFB-bIy4I+@Q`-=4Y^+|dZrl>D)oxuD(=B58#Jp)cURqq+yk6T_ zT^3hrSE`%!hA7iYr2=KD!y*YzFQvt$_4RtKy4I_~PA|jh`ubAu+}dWnp4p#%J)Ww` z!v1-~(&@)r+m7LSTUE~^+D?mFp1ajFOuDMW+hJLz)2P+8mD&g5u>7#DxV{!5zu42A z*_VDPo;r7~3o+mm{C;B5tzL+y78iwscRlX+0Nu?U;!wB4A~;w)>iZ+Iab3VPtUIfE zT5QxlX!N|cxCpMa+n!-tZkWg$;V##93^S}ZWcELKEuJbCh5N4zSbM`UEX`<>UN;6q4$oZ}W$R&)ai#`6X`n}R zI!moZjL>1odeRzc(J*&xJGS2*B{VG8BbElSc&KV-|H7;BR6Z~4w+7n3L0#K-v|d1h z=J!seN45U2#F+VsUnsAx@?<%%L>2tVdWddaoNBd z?mT_=HR#8o!N>suH*8=qDlcGt45o0$P<@ZF8oo-4w(OzvnS=G0xq}{T_B*KI7~9*2 z(X4y;TBw7%(s2&r%11^rdKY4F18$xT8qV?+@POfNP@?Yv=aDK1kClV<`OGVardO%4y#sK<>hc2b8@jg>F87QUU7;rR zm;+WIe{QT^uk}))PKRKHHru96^pT}9`{ftosccp_xX|y1joOX+)>W49243hLzY^+$ zo_L%GG+PgaX!~B<_eKqA>NvI&D$MNvvva%)9X0(uVj6I-nT4^A@qK4r4mH28z_CSma`d<@(*TmvG|NI8tZ5!oG^CkBHck4nM)|DQ#Y zKRbSjizSc%5#8vzbF3_ zyulX|Kmter2_OL^fCP{L5X> z;n|M+{1iWVE+|>mTkx5|=Y{@r>$?_IX6ygRnXkB6czGm%1dsp{Kmter2_OL^fCP{L z52Z2d1J zKaaoZ6Ur697Ch*z$*lc7!AqcD1NMI#So;t?@0FdRh235YL~1a;9P3 zS=D(JKMu#Xw?m!o31*gOwCHx^2j6Y}{$G8(#8vq&b$pOSB%f^@wuu8KRgXd{<#KvS zRa7DqMavg+Ik{X`OO*mqm0U4jprw2{m)EJJ0e)4H)B?B7g)cIp zItOsru-hCMV_4Po4XW9W?kC}BYuhngZ>!-L+uQKHCI%yMk2fjspUa(h`(E1HYt!@_ z>0PSs*!G?D<+P#0VX>qX6>xnvPYZIkpb{;smWo=oS0JQQfXleIqfp#u9<2N9Ezxz}GQX`8k8)*(yFrP*w``lB z(BS1~p`^$3u`{f4;9%dw}LOru5nQXg5Yo+E1sVp)q1owF5DN`}>KH5s~klEJoy-MJZR7%W9q|Ia<`!oG$Bn zf#^*lm7DoWQ!f_t#fnOlLZwtLm*KZ2HzBNOvXqx)b@CVygjUw&9w|mh6l|49K1(5> zWedfEnk|Ff*;1)kg7~IMa=!c&#t25?%Zd?S7~z*4BR>7rDHbEWg7IY{*SLj)r zDTOK7xY=dqyI88KR@$h_=7JbfCRoo0-r9$gSE(S{Bzim&@^3c zs-!?GR3dOFYo&@#O8Fd>36&)&UxKMkk#facNh;6+k@TY8EERGkxuEJZ@zCHnsoH_y z$g<@pKhgMsH4r70?luA)8 zG)Ys>>xFWWHWe9?jdHP67`GNV;bNUn{=JnxKl;ixJD9H@^I|+a*Ekt7>C>f|-XNOL zk<&BbgJ?pv1A~a|ygm7Y=pR}q8$<;HaY|Jn8!Hzyy{u^Ea$X~3yZxpTDpitS`}i^bXNW-VV3R9&r${nkU%qdeo)tQ!t2F z%cb9Nr-MQCuf8*>BV*H{gJ@E<1A~aA!%zMo64et9q6zO>^baD|_4)=89|y;+;HrHK zR8}AEQBcRwCRp_bJMrw|d>wt9(sAo?Fpf=zhI_?!TE0oHOl;O8s2gVxY!EzxhLfzu zk5R|Ohc`ahJQ8eI@cRV!_@+gbzYq2=f*TSoV)?|pX*=HF?F8OKPTViL8wDrbZy4wa z!)nH%9`t?V#M>Rg<&OaFS2FJ6gMRL>_l3O+O2X&00k?+dNZe~|`M%_vcfz^^-!g?8 RI4rtX`9WXD;KvO~`u}rbAPfKi diff --git a/samples/4zwU6YdxxHUK7UwfTNUHAy.grist b/samples/4zwU6YdxxHUK7UwfTNUHAy.grist deleted file mode 100644 index 8245ffaf41c0fd79b37aa7b7190337f99bdd90ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176128 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 O!VkJS20yMz(*Fn0X%RyJ diff --git a/samples/og9fjmYEqzpfL2xJexuZa8.grist b/samples/og9fjmYEqzpfL2xJexuZa8.grist deleted file mode 100644 index 670a47cf118d5d96fcf8657ef22b2ffaf3085ebf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176128 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 aaW|+-h%HmNfy1MFMI7{W3~^jjl>ZM?z!ygV From fd0da6db0b0b6c7300ba36de37f77effba091ba9 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Tue, 6 Feb 2024 11:32:26 +0100 Subject: [PATCH 09/31] add ignore eslint error --- test/server/lib/DocApi.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index c8160290eb..26420bf3d6 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -4878,6 +4878,7 @@ function testDocApi() { let stats = await readStats(docId); assert.equal(stats.length, 1, 'stats=' + JSON.stringify(stats)); assert.equal(stats[0].id, webhooks.webhooks[0].id); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const {unsubscribeKey, ...fieldsWithoutUnsubscribeKey} = stats[0].fields; assert.deepEqual(fieldsWithoutUnsubscribeKey, expectedFields); @@ -4895,6 +4896,7 @@ function testDocApi() { if (expectedFieldsCallback) { expectedFieldsCallback(expectedFields); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars const {unsubscribeKey, ...fieldsWithoutUnsubscribeKey} = stats[0].fields; assert.deepEqual(fieldsWithoutUnsubscribeKey, { ...expectedFields, ...fields }); } else { From 4c813e062adc643b8113dc8e2ecd1315a83b6444 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Tue, 6 Feb 2024 15:32:41 +0100 Subject: [PATCH 10/31] correct tests for webhookPage --- test/nbrowser/WebhookPage.ts | 45 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/test/nbrowser/WebhookPage.ts b/test/nbrowser/WebhookPage.ts index 940a813918..14ccd905b7 100644 --- a/test/nbrowser/WebhookPage.ts +++ b/test/nbrowser/WebhookPage.ts @@ -1,9 +1,9 @@ -import {DocCreationInfo} from 'app/common/DocListAPI'; -import {DocAPI} from 'app/common/UserAPI'; -import {assert, driver, Key} from 'mocha-webdriver'; +import { DocCreationInfo } from 'app/common/DocListAPI'; +import { DocAPI } from 'app/common/UserAPI'; +import { assert, driver, Key } from 'mocha-webdriver'; import * as gu from 'test/nbrowser/gristUtils'; -import {server, setupTestSuite} from 'test/nbrowser/testUtils'; -import {EnvironmentSnapshot} from 'test/server/testUtils'; +import { server, setupTestSuite } from 'test/nbrowser/testUtils'; +import { EnvironmentSnapshot } from 'test/server/testUtils'; describe('WebhookPage', function () { this.timeout(60000); @@ -26,10 +26,10 @@ describe('WebhookPage', function () { doc = await session.tempDoc(cleanup, 'Hello.grist'); docApi = api.getDocAPI(doc.id); await api.applyUserActions(doc.id, [ - ['AddTable', 'Table2', [{id: 'A'}, {id: 'B'}, {id: 'C'}, {id: 'D'}, {id: 'E'}]], + ['AddTable', 'Table2', [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }, { id: 'E' }]], ]); await api.applyUserActions(doc.id, [ - ['AddTable', 'Table3', [{id: 'A'}, {id: 'B'}, {id: 'C'}, {id: 'D'}, {id: 'E'}]], + ['AddTable', 'Table3', [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }, { id: 'E' }]], ]); await api.updateDocPermissions(doc.id, { users: { @@ -55,6 +55,7 @@ describe('WebhookPage', function () { 'URL', 'Table', 'Ready Column', + 'Columns to check when update (separated by ;)', 'Webhook Id', 'Enabled', 'Status', @@ -91,7 +92,7 @@ describe('WebhookPage', function () { assert.equal(await getField(1, 'Memo'), 'Test Memo'); }); // Make sure the webhook is actually working. - await docApi.addRows('Table1', {A: ['zig'], B: ['zag']}); + await docApi.addRows('Table1', { A: ['zig'], B: ['zag'] }); // Make sure the data gets delivered, and that the webhook status is updated. await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 1); @@ -100,7 +101,7 @@ describe('WebhookPage', function () { }); // Remove the webhook and make sure it is no longer listed. assert.equal(await gu.getCardListCount(), 2); - await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); + await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await gu.waitForServer(); @@ -122,14 +123,14 @@ describe('WebhookPage', function () { await setField(2, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table3/records?flat=1`); await setField(2, 'Table', 'Table1'); await gu.waitForServer(); - await docApi.addRows('Table1', {A: ['zig2'], B: ['zag2']}); + await docApi.addRows('Table1', { A: ['zig2'], B: ['zag2'] }); await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 1); assert.lengthOf((await docApi.getRows('Table3')).A, 1); assert.match(await getField(1, 'Status'), /status...success/); assert.match(await getField(2, 'Status'), /status...success/); }); - await docApi.updateRows('Table1', {id: [1], A: ['zig3'], B: ['zag3']}); + await docApi.updateRows('Table1', { id: [1], A: ['zig3'], B: ['zag3'] }); await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 2); assert.lengthOf((await docApi.getRows('Table3')).A, 1); @@ -139,11 +140,11 @@ describe('WebhookPage', function () { // confirm that nothing shows up to Table3. assert.lengthOf((await docApi.getRows('Table3')).A, 1); // Break everything down. - await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); + await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await gu.waitForServer(); - await gu.getDetailCell({col: 'Memo', rowNum: 1}).click(); + await gu.getDetailCell({ col: 'Memo', rowNum: 1 }).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.waitForServer(); assert.equal(await gu.getCardListCount(), 1); @@ -162,7 +163,7 @@ describe('WebhookPage', function () { await setField(1, 'URL', `http://${host}/notathing`); await setField(1, 'Table', 'Table1'); await gu.waitForServer(); - await docApi.addRows('Table1', {A: ['dud1']}); + await docApi.addRows('Table1', { A: ['dud1'] }); await gu.waitToPass(async () => { assert.match(await getField(1, 'Status'), /status...failure/); assert.match(await getField(1, 'Status'), /numWaiting..1/); @@ -174,14 +175,14 @@ describe('WebhookPage', function () { assert.match(await getField(1, 'Status'), /numWaiting..0/); }); assert.lengthOf((await docApi.getRows('Table2')).A, 0); - await docApi.addRows('Table1', {A: ['dud2']}); + await docApi.addRows('Table1', { A: ['dud2'] }); await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 1); assert.match(await getField(1, 'Status'), /status...success/); }); // Break everything down. - await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); + await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await gu.waitForServer(); @@ -237,7 +238,7 @@ describe('WebhookPage', function () { assert.match(await getField(1, 'Memo'), /multiple memo/); }); - await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); + await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await driver.switchTo().window(ownerTab); @@ -253,13 +254,13 @@ describe('WebhookPage', function () { * Checks that a particular route to modifying cells in a virtual table * is in place (previously it was not). */ - it('can paste into a cell without clicking into it', async function() { + it('can paste into a cell without clicking into it', async function () { await openWebhookPage(); await setField(1, 'Name', '1234'); await gu.waitForServer(); await clipboard.lockAndPerform(async (cb) => { await cb.copy(); - await gu.getDetailCell({col: 'Memo', rowNum: 1}).click(); + await gu.getDetailCell({ col: 'Memo', rowNum: 1 }).click(); await cb.paste(); }); await gu.waitForServer(); @@ -268,12 +269,12 @@ describe('WebhookPage', function () { }); async function setField(rowNum: number, col: string, text: string) { - await gu.getDetailCell({col, rowNum}).click(); + await gu.getDetailCell({ col, rowNum }).click(); await gu.enterCell(text); } async function getField(rowNum: number, col: string) { - const cell = await gu.getDetailCell({col, rowNum}); + const cell = await gu.getDetailCell({ col, rowNum }); return cell.getText(); } @@ -287,5 +288,5 @@ async function openWebhookPage() { async function waitForWebhookPage() { await driver.findContentWait('button', /Clear Queue/, 3000); // No section, so no easy utility for setting focus. Click on a random cell. - await gu.getDetailCell({col: 'Webhook Id', rowNum: 1}).click(); + await gu.getDetailCell({ col: 'Webhook Id', rowNum: 1 }).click(); } From dbc8498a91c0e272ec9eb1301895cbf830217a8a Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Tue, 6 Feb 2024 16:12:40 +0100 Subject: [PATCH 11/31] add a delay before testing --- test/server/lib/DocApi.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 26420bf3d6..fcab639926 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -4445,6 +4445,7 @@ function testDocApi() { B: [true], C: ['c1'] }); + await delay(100); assert.isTrue(successCalled.called()); await successCalled.waitAndReset(); From 706c8f79d16b04ddbd151cd157125cc1c5d1f636 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 14 Feb 2024 16:52:32 +0100 Subject: [PATCH 12/31] fix forgotten log + typo + simplify if statement --- app/server/lib/DocApi.ts | 5 ++--- sandbox/grist/migrations.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 285986c0bb..17d2dae353 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -337,7 +337,6 @@ export class DocWorkerApi { if (!fields.tableRef) { throw new ApiError(`tableId is required`, 400); } - console.log("In the subscribe function of DOC ID app: ", fields); const unsubscribeKey = uuidv4(); const webhookSecret: WebHookSecret = {unsubscribeKey, url}; @@ -413,8 +412,8 @@ export class DocWorkerApi { } if (tableId !== undefined) { - if (columnIds !== undefined && columnIds !== null && columnIds !== '') { - if (tableId !== currentTableId && currentTableId !== undefined && currentTableId !== null) { + if (columnIds) { + if (tableId !== currentTableId && currentTableId) { // if the tableId changed, we need to reset the columnIds fields.columnRefList = [GristObjCode.List]; } else { diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index 134864b017..df54c1aad1 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1311,7 +1311,7 @@ def migration41(tdset): @migration(schema_version=43) def migration43(tdset): """ - Adds columns for register witch table columns are triggered in webhooks. + Adds columns to register which table columns are triggered in webhooks. """ return tdset.apply_doc_actions([ add_column('_grist_Triggers', 'columnRefList', 'RefList:_grist_Tables_column'), From 9d802e01a56aee0a9c0cc965acfb83484e88e0bc Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 14 Feb 2024 16:55:13 +0100 Subject: [PATCH 13/31] update Triggers-ti.ts for APIs body validator --- app/common/Triggers-ti.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/common/Triggers-ti.ts b/app/common/Triggers-ti.ts index 2489181d36..d741682a8e 100644 --- a/app/common/Triggers-ti.ts +++ b/app/common/Triggers-ti.ts @@ -16,6 +16,7 @@ export const WebhookFields = t.iface([], { "url": "string", "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), "tableId": "string", + "columnIds": "string", "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), @@ -29,6 +30,7 @@ export const WebhookStatus = t.union(t.lit('idle'), t.lit('sending'), t.lit('ret export const WebhookSubscribe = t.iface([], { "url": "string", "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), + "columnIds": t.opt("string"), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), @@ -47,6 +49,7 @@ export const WebhookSummary = t.iface([], { "eventTypes": t.array("string"), "isReadyColumn": t.union("string", "null"), "tableId": "string", + "columnIds": "string", "enabled": "boolean", "name": "string", "memo": "string", @@ -63,6 +66,7 @@ export const WebhookPatch = t.iface([], { "url": t.opt("string"), "eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update")))), "tableId": t.opt("string"), + "columnIds": t.opt("string"), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), From 8a75bb94c1741e3bd802e5f55cc98192929a503d Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 14 Feb 2024 17:29:14 +0100 Subject: [PATCH 14/31] add forgotten translation --- app/client/ui/WebhookPage.ts | 2 +- static/locales/en.client.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index 91e5fd4a4f..05502fb913 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -37,7 +37,7 @@ const WEBHOOK_COLUMNS = [ id: 'vt_webhook_fc1', colId: 'tableId', type: 'Choice', - label: 'Table', + label: t('Table'), // widgetOptions are configured later, since the choices depend // on the user tables in the document. }, diff --git a/static/locales/en.client.json b/static/locales/en.client.json index de6b806b93..97dfa8bb55 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -1165,7 +1165,8 @@ "Sorry, not all fields can be edited.": "Sorry, not all fields can be edited.", "Status": "Status", "URL": "URL", - "Webhook Id": "Webhook Id" + "Webhook Id": "Webhook Id", + "Table": "Table" }, "FormulaAssistant": { "Ask the bot.": "Ask the bot.", From 3f5ebb33716fe847b94a3b6656e25042184e006b Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 21 Feb 2024 09:47:54 +0100 Subject: [PATCH 15/31] Add test + fix to ignore empty columnId string --- app/server/lib/DocApi.ts | 8 ++-- samples/qM2x4FcBJUDv6vky8Dicpe.grist | Bin 0 -> 176128 bytes samples/tT6AwLN8c3BrzG3CKfj4TW.grist | Bin 0 -> 176128 bytes test/server/lib/DocApi.ts | 56 ++++++++++++++++++--------- 4 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 samples/qM2x4FcBJUDv6vky8Dicpe.grist create mode 100644 samples/tT6AwLN8c3BrzG3CKfj4TW.grist diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 17d2dae353..ee345a844a 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -421,9 +421,11 @@ export class DocWorkerApi { throw new ApiError(`Cannot find columns "${columnIds}" because table is not known`, 404); } // columnIds have to be of shape "columnId; columnId; columnId" - fields.columnRefList = [GristObjCode.List, ...columnIds.split(";").map( - columnId => { return colIdToReference(metaTables, tableId, columnId.trim().replace(/^\$/, '')); } - )]; + fields.columnRefList = [GristObjCode.List, ...columnIds.split(";") + .filter(columnId => columnId.trim() !== "") + .map( + columnId => { return colIdToReference(metaTables, tableId, columnId.trim().replace(/^\$/, '')); } + )]; } } else { fields.columnRefList = [GristObjCode.List]; diff --git a/samples/qM2x4FcBJUDv6vky8Dicpe.grist b/samples/qM2x4FcBJUDv6vky8Dicpe.grist new file mode 100644 index 0000000000000000000000000000000000000000..744abca4c9d65040504a3f08f5e0c237d4760d47 GIT binary patch literal 176128 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 XU4m_y!VMf6-K*@Nr(>|=x*+_241@$A literal 0 HcmV?d00001 diff --git a/samples/tT6AwLN8c3BrzG3CKfj4TW.grist b/samples/tT6AwLN8c3BrzG3CKfj4TW.grist new file mode 100644 index 0000000000000000000000000000000000000000..124313a2aae510876342941ba98422eff923a498 GIT binary patch literal 176128 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 M=;;{zxGqWmA7E%JtpET3 literal 0 HcmV?d00001 diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index fcab639926..fda5d1484c 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -4425,7 +4425,7 @@ function testDocApi() { await webhook1(); }); - it("should not call to a webhook when columns updated are not in columnIds", async () => { // eslint-disable-line max-len + it("should call to a webhook only when columns updated are in columnIds if not empty", async () => { // eslint-disable-line max-len // Create a test document. const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; const docId = await userApi.newDoc({ name: 'testdoc5' }, ws1); @@ -4434,11 +4434,28 @@ function testDocApi() { ['ModifyColumn', 'Table1', 'B', { type: 'Bool' }], ], chimpy); - const webhook = await autoSubscribe('200', docId, { + const modifyColumnNotInColumnIds = async (newValues: { [key: string]: any; } ) => { + await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ + ['UpdateRecord', 'Table1', newRowIds[0], newValues], + ], chimpy); + await delay(100); + assert.isFalse(successCalled.called()); + successCalled.reset(); + }; + const modifyColumnInColumnIds = async (newValues: { [key: string]: any; }) => { + await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ + ['UpdateRecord', 'Table1', newRowIds[0], newValues], + ], chimpy); + await delay(100); + assert.isTrue(successCalled.called()); + await successCalled.waitAndReset(); + }; + + // Webhook with only one columnId. + const webhook1 = await autoSubscribe('200', docId, { columnIds: 'A', eventTypes: ['add', 'update'] }); successCalled.reset(); - // Create record, that will call the webhook. const newRowIds = await doc.addRows("Table1", { A: [2], @@ -4448,25 +4465,26 @@ function testDocApi() { await delay(100); assert.isTrue(successCalled.called()); await successCalled.waitAndReset(); + await modifyColumnNotInColumnIds({ C: 'c2' }); + await modifyColumnInColumnIds({ A: 19 }); + await webhook1(); // Unsubscribe. - // Modify the value of column that is not in columnIds. - await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ - ['UpdateRecord', 'Table1', newRowIds[0], { C: 'c2' }], - ], chimpy); - await delay(100); - assert.isFalse(successCalled.called()); + // Webhook with multiple columnIds (check the shape of the columnIds string) + const webhook2 = await autoSubscribe('200', docId, { + columnIds: 'A; B', eventTypes: ['update'] + }); successCalled.reset(); + await modifyColumnNotInColumnIds({ C: 'c3' }); + await modifyColumnInColumnIds({ A: 20 }); + await webhook2(); - // Modify the value of column that is in columnIds. - await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ - ['UpdateRecord', 'Table1', newRowIds[0], { A: 19 }], - ], chimpy); - await delay(100); - assert.isTrue(successCalled.called()); - await successCalled.waitAndReset(); - - // Unsubscribe. - await webhook(); + // Check that string terminating with ";" not breaking the webhook + const webhook3 = await autoSubscribe('200', docId, { + columnIds: 'A;', eventTypes: ['update'] + }); + await modifyColumnNotInColumnIds({ C: 'c4' }); + await modifyColumnInColumnIds({ A: 21 }); + await webhook3(); }); it("should return statistics", async () => { From bd38d9bdd5f4f79c0685c201f3b6daad1cb9588a Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 21 Feb 2024 09:51:39 +0100 Subject: [PATCH 16/31] delete accidently added samples --- samples/qM2x4FcBJUDv6vky8Dicpe.grist | Bin 176128 -> 0 bytes samples/tT6AwLN8c3BrzG3CKfj4TW.grist | Bin 176128 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 samples/qM2x4FcBJUDv6vky8Dicpe.grist delete mode 100644 samples/tT6AwLN8c3BrzG3CKfj4TW.grist diff --git a/samples/qM2x4FcBJUDv6vky8Dicpe.grist b/samples/qM2x4FcBJUDv6vky8Dicpe.grist deleted file mode 100644 index 744abca4c9d65040504a3f08f5e0c237d4760d47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176128 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 XU4m_y!VMf6-K*@Nr(>|=x*+_241@$A diff --git a/samples/tT6AwLN8c3BrzG3CKfj4TW.grist b/samples/tT6AwLN8c3BrzG3CKfj4TW.grist deleted file mode 100644 index 124313a2aae510876342941ba98422eff923a498..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176128 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 M=;;{zxGqWmA7E%JtpET3 From 1a71169bf10e6dfad112778e9b83938326318446 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 21 Feb 2024 09:59:43 +0100 Subject: [PATCH 17/31] rename migration 43 to 42 --- app/common/schema.ts | 2 +- app/server/lib/initialDocSql.ts | 4 ++-- sandbox/grist/migrations.py | 4 ++-- sandbox/grist/schema.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/common/schema.ts b/app/common/schema.ts index 77f4baf304..7bf8d4483f 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData"; // tslint:disable:object-literal-key-quotes -export const SCHEMA_VERSION = 43; +export const SCHEMA_VERSION = 42; export const schema = { diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index d8aefe9b60..cca4579f79 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0); @@ -44,7 +44,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2,3); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index df54c1aad1..bc29df2e6b 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1308,8 +1308,8 @@ def migration41(tdset): return tdset.apply_doc_actions(doc_actions) -@migration(schema_version=43) -def migration43(tdset): +@migration(schema_version=42) +def migration42(tdset): """ Adds columns to register which table columns are triggered in webhooks. """ diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index e44408fad3..f499577f79 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -15,7 +15,7 @@ import actions -SCHEMA_VERSION = 43 +SCHEMA_VERSION = 42 def make_column(col_id, col_type, formula='', isFormula=False): return { From 24c1870039c315e485beae56f72bf627edbce029 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 26 Feb 2024 16:12:28 +0100 Subject: [PATCH 18/31] make test clearer --- test/server/lib/DocApi.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index fda5d1484c..bdf69383eb 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -4434,19 +4434,17 @@ function testDocApi() { ['ModifyColumn', 'Table1', 'B', { type: 'Bool' }], ], chimpy); - const modifyColumnNotInColumnIds = async (newValues: { [key: string]: any; } ) => { + const modifyColumn = async (newValues: { [key: string]: any; } ) => { await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ ['UpdateRecord', 'Table1', newRowIds[0], newValues], ], chimpy); await delay(100); + }; + const assertSuccessNotCalled = async () => { assert.isFalse(successCalled.called()); successCalled.reset(); }; - const modifyColumnInColumnIds = async (newValues: { [key: string]: any; }) => { - await axios.post(`${serverUrl}/api/docs/${docId}/apply`, [ - ['UpdateRecord', 'Table1', newRowIds[0], newValues], - ], chimpy); - await delay(100); + const assertSuccessCalled = async () => { assert.isTrue(successCalled.called()); await successCalled.waitAndReset(); }; @@ -4465,8 +4463,10 @@ function testDocApi() { await delay(100); assert.isTrue(successCalled.called()); await successCalled.waitAndReset(); - await modifyColumnNotInColumnIds({ C: 'c2' }); - await modifyColumnInColumnIds({ A: 19 }); + await modifyColumn({ C: 'c2' }); + await assertSuccessNotCalled(); + await modifyColumn({ A: 19 }); + await assertSuccessCalled(); await webhook1(); // Unsubscribe. // Webhook with multiple columnIds (check the shape of the columnIds string) @@ -4474,16 +4474,20 @@ function testDocApi() { columnIds: 'A; B', eventTypes: ['update'] }); successCalled.reset(); - await modifyColumnNotInColumnIds({ C: 'c3' }); - await modifyColumnInColumnIds({ A: 20 }); + await modifyColumn({ C: 'c3' }); + await assertSuccessNotCalled(); + await modifyColumn({ A: 20 }); + await assertSuccessCalled(); await webhook2(); // Check that string terminating with ";" not breaking the webhook const webhook3 = await autoSubscribe('200', docId, { columnIds: 'A;', eventTypes: ['update'] }); - await modifyColumnNotInColumnIds({ C: 'c4' }); - await modifyColumnInColumnIds({ A: 21 }); + await modifyColumn({ C: 'c4' }); + await assertSuccessNotCalled(); + await modifyColumn({ A: 21 }); + await assertSuccessCalled(); await webhook3(); }); From 22ad13a723f5aeed71c4517ff2f3e1e6b4febb6c Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 10:28:37 +0100 Subject: [PATCH 19/31] rename label for webhook columnIds --- app/client/ui/WebhookPage.ts | 2 +- test/server/lib/DocApi.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index 05502fb913..f2140c3453 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -63,7 +63,7 @@ const WEBHOOK_COLUMNS = [ id: 'vt_webhook_fc10', colId: 'columnIds', type: 'Text', - label: t('Columns to check when update (separated by ;)'), + label: t('Filter for changes in these columns (semicolon-separated ids)'), }, { id: 'vt_webhook_fc4', diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 20b48ffc00..c4071641ae 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -3352,7 +3352,7 @@ function testDocApi() { */ const serving: Serving = await serveSomething(app => { app.use(express.json()); - app.post('/200', ({ body }, res) => { + app.post('/200', ({body}, res) => { res.sendStatus(200); res.end(); }); From 3226372a5461ac96e3cb98b74253e9ed70caa9ba Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 10:33:41 +0100 Subject: [PATCH 20/31] rename columnRefList to watchedColRefList --- app/common/schema.ts | 4 ++-- app/server/lib/DocApi.ts | 6 +++--- app/server/lib/Triggers.ts | 10 +++++----- app/server/lib/initialDocSql.ts | 4 ++-- sandbox/grist/migrations.py | 4 ++-- sandbox/grist/schema.py | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/common/schema.ts b/app/common/schema.ts index 7bf8d4483f..e432baf613 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -167,7 +167,7 @@ export const schema = { label : "Text", memo : "Text", enabled : "Bool", - columnRefList : "RefList:_grist_Tables_column", + watchedColRefList : "RefList:_grist_Tables_column", }, "_grist_ACLRules": { @@ -389,7 +389,7 @@ export interface SchemaTypes { label: string; memo: string; enabled: boolean; - columnRefList: [GristObjCode.List, ...number[]]|null; + watchedColRefList: [GristObjCode.List, ...number[]]|null; }; "_grist_ACLRules": { diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index b7ca2c6c2d..48f56f739f 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -400,20 +400,20 @@ export class DocWorkerApi { if (columnIds) { if (tableId !== currentTableId && currentTableId) { // if the tableId changed, we need to reset the columnIds - fields.columnRefList = [GristObjCode.List]; + fields.watchedColRefList = [GristObjCode.List]; } else { if (!tableId) { throw new ApiError(`Cannot find columns "${columnIds}" because table is not known`, 404); } // columnIds have to be of shape "columnId; columnId; columnId" - fields.columnRefList = [GristObjCode.List, ...columnIds.split(";") + fields.watchedColRefList = [GristObjCode.List, ...columnIds.split(";") .filter(columnId => columnId.trim() !== "") .map( columnId => { return colIdToReference(metaTables, tableId, columnId.trim().replace(/^\$/, '')); } )]; } } else { - fields.columnRefList = [GristObjCode.List]; + fields.watchedColRefList = [GristObjCode.List]; } fields.tableRef = tableIdToRef(metaTables, tableId); currentTableId = tableId; diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 23c62bb7ca..512f5974b5 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -288,7 +288,7 @@ export class DocTriggers { // Other fields used to register this webhook. eventTypes: decodeObject(t.eventTypes) as string[], isReadyColumn: getColId(t.isReadyColRef) ?? null, - columnIds: t.columnRefList?.slice(1).map(columnRef => getColId(columnRef as number)).join("; ") || "", + columnIds: t.watchedColRefList?.slice(1).map(columnRef => getColId(columnRef as number)).join("; ") || "", tableId: getTableId(t.tableRef) ?? null, // For future use - for now every webhook is enabled. enabled: t.enabled, @@ -510,8 +510,8 @@ export class DocTriggers { } } - if (trigger.columnRefList) { - for (const colRef of trigger.columnRefList.slice(1)) { + if (trigger.watchedColRefList) { + for (const colRef of trigger.watchedColRefList.slice(1)) { if (!this._validateColId(colRef as number, trigger.tableRef)) { // column does not belong to table, let's ignore trigger and log stats for (const action of webhookActions) { @@ -602,8 +602,8 @@ export class DocTriggers { } const colIdsToCheck: Array = []; - if (trigger.columnRefList) { - for (const colRef of trigger.columnRefList.slice(1)) { + if (trigger.watchedColRefList) { + for (const colRef of trigger.watchedColRefList.slice(1)) { colIdsToCheck.push(this._getColId(colRef as number)!); } } diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index cca4579f79..5feab82d96 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS "_grist_Views_section_field" (id INTEGER PRIMARY KEY, CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "fileExt" TEXT DEFAULT '', "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL); -CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "columnRefList" TEXT DEFAULT NULL); +CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "watchedColRefList" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT ''); INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'',''); CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT ''); @@ -80,7 +80,7 @@ INSERT INTO _grist_Views_section_field VALUES(9,3,9,4,0,'',0,0,'',NULL); CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "fileExt" TEXT DEFAULT '', "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL); -CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "columnRefList" TEXT DEFAULT NULL); +CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "watchedColRefList" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT ''); INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'',''); CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT ''); diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index bc29df2e6b..26096365d5 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1311,8 +1311,8 @@ def migration41(tdset): @migration(schema_version=42) def migration42(tdset): """ - Adds columns to register which table columns are triggered in webhooks. + Adds column to register which table columns are triggered in webhooks. """ return tdset.apply_doc_actions([ - add_column('_grist_Triggers', 'columnRefList', 'RefList:_grist_Tables_column'), + add_column('_grist_Triggers', 'watchedColRefList', 'RefList:_grist_Tables_column'), ]) diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index ab3f31bafe..4fd8b169d7 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -261,7 +261,7 @@ def schema_create_actions(): make_column("label", "Text"), make_column("memo", "Text"), make_column("enabled", "Bool"), - make_column("columnRefList", "RefList:_grist_Tables_column") + make_column("watchedColRefList", "RefList:_grist_Tables_column") ]), # All of the ACL rules. From cca6541a20778f69580f64d786de3424543f2d98 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 10:43:07 +0100 Subject: [PATCH 21/31] remove white spaces and bring together comment and function --- test/server/lib/DocApi.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index c4071641ae..89d5dbd3aa 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -3347,21 +3347,21 @@ function testDocApi() { }); describe('webhooks related endpoints', async function () { - /* - Regression test for old _subscribe endpoint. /docs/{did}/webhooks should be used instead to subscribe - */ - const serving: Serving = await serveSomething(app => { - app.use(express.json()); - app.post('/200', ({body}, res) => { - res.sendStatus(200); - res.end(); - }); - }, webhooksTestPort); + const serving: Serving = await serveSomething(app => { + app.use(express.json()); + app.post('/200', ({body}, res) => { + res.sendStatus(200); + res.end(); + }); + }, webhooksTestPort); - async function oldSubscribeCheck(requestBody: any, status: number, ...errors: RegExp[]) { - const resp = await axios.post( - `${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/_subscribe`, - requestBody, chimpy + /* + Regression test for old _subscribe endpoint. /docs/{did}/webhooks should be used instead to subscribe + */ + async function oldSubscribeCheck(requestBody: any, status: number, ...errors: RegExp[]) { + const resp = await axios.post( + `${serverUrl}/api/docs/${docIds.Timesheets}/tables/Table1/_subscribe`, + requestBody, chimpy ); assert.equal(resp.status, status); for (const error of errors) { @@ -4874,7 +4874,7 @@ function testDocApi() { }; // subscribe - const { data } = await axios.post( + const {data} = await axios.post( `${serverUrl}/api/docs/${docId}/webhooks`, { webhooks: [{ From c73e6957f41df5ae2fa4415088245751be0a1e57 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 16:52:57 +0100 Subject: [PATCH 22/31] isolate watchedColIds string in frontend --- app/client/ui/WebhookPage.ts | 34 +++++++++++++++++++++------- app/common/Triggers-ti.ts | 8 +++---- app/common/Triggers.ts | 8 +++---- app/server/lib/DocApi.ts | 15 ++++++------- app/server/lib/Triggers.ts | 3 ++- test/nbrowser/WebhookOverflow.ts | 2 +- test/server/lib/DocApi.ts | 38 ++++++++++++++++---------------- 7 files changed, 63 insertions(+), 45 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index f2140c3453..9af587d9aa 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -61,7 +61,7 @@ const WEBHOOK_COLUMNS = [ }, { id: 'vt_webhook_fc10', - colId: 'columnIds', + colId: 'watchedColIdsText', type: 'Text', label: t('Filter for changes in these columns (semicolon-separated ids)'), }, @@ -113,7 +113,7 @@ const WEBHOOK_VIEW_FIELDS: Array<(typeof WEBHOOK_COLUMNS)[number]['colId']> = [ 'name', 'memo', 'eventTypes', 'url', 'tableId', 'isReadyColumn', - 'columnIds', 'webhookId', + 'watchedColIdsText', 'webhookId', 'enabled', 'status' ]; @@ -133,9 +133,9 @@ class WebhookExternalTable implements IExternalTable { public name = 'GristHidden_WebhookTable'; public initialActions = _prepareWebhookInitialActions(this.name); public saveableFields = [ - 'tableId', 'columnIds', 'url', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn', + 'tableId', 'watchedColIdsText', 'url', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn', ]; - public webhooks: ObservableArray = observableArray([]); + public webhooks: ObservableArray = observableArray([]); public constructor(private _docApi: DocAPI) { } @@ -276,7 +276,12 @@ class WebhookExternalTable implements IExternalTable { private _initalizeWebhookList(webhooks: WebhookSummary[]){ this.webhooks.removeAll(); - this.webhooks.push(...webhooks); + this.webhooks.push( + ...webhooks.map(webhook => { + const uiWebhook: WebhookPageSummary = {...webhook}; + uiWebhook.fields.watchedColIdsText = webhook.fields.watchedColIds ? webhook.fields.watchedColIds.join(";") : ""; + return uiWebhook; + })); } private _getErrorString(e: ApiError): string { @@ -315,6 +320,9 @@ class WebhookExternalTable implements IExternalTable { if (fields.eventTypes) { fields.eventTypes = without(fields.eventTypes, 'L'); } + fields.watchedColIds = fields.watchedColIdsText + ? fields.watchedColIdsText.split(";").filter((colId: string) => colId.trim() !== "") + : []; return fields; } } @@ -447,16 +455,21 @@ function _prepareWebhookInitialActions(tableId: string): DocAction[] { /** * Map a webhook summary to a webhook table raw record. The main * difference is that `eventTypes` is tweaked to be in a cell format, - * and `status` is converted to a string. + * `status` is converted to a string, + * and `watchedColIdsText` is converted to list in a cell format. */ -function _mapWebhookValues(webhookSummary: WebhookSummary): Partial { +function _mapWebhookValues(webhookSummary: WebhookPageSummary): Partial { const fields = webhookSummary.fields; - const {eventTypes} = fields; + const {eventTypes, watchedColIdsText} = fields; + const watchedColIds = watchedColIdsText + ? watchedColIdsText.split(";").filter(colId => colId.trim() !== "") + : []; return { ...fields, webhookId: webhookSummary.id, status: JSON.stringify(webhookSummary.usage), eventTypes: [GristObjCode.List, ...eventTypes], + watchedColIds: [GristObjCode.List, ...watchedColIds], }; } @@ -464,6 +477,11 @@ type WebhookSchemaType = { [prop in keyof WebhookSummary['fields']]: WebhookSummary['fields'][prop] } & { eventTypes: [GristObjCode, ...unknown[]]; + watchedColIds: [GristObjCode, ...unknown[]]; status: string; webhookId: string; } + +type WebhookPageSummary = WebhookSummary & { + fields: {watchedColIdsText?: string;} +} diff --git a/app/common/Triggers-ti.ts b/app/common/Triggers-ti.ts index d741682a8e..ed48744a62 100644 --- a/app/common/Triggers-ti.ts +++ b/app/common/Triggers-ti.ts @@ -16,7 +16,7 @@ export const WebhookFields = t.iface([], { "url": "string", "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), "tableId": "string", - "columnIds": "string", + "watchedColIds": t.array("string"), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), @@ -30,7 +30,7 @@ export const WebhookStatus = t.union(t.lit('idle'), t.lit('sending'), t.lit('ret export const WebhookSubscribe = t.iface([], { "url": "string", "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), - "columnIds": t.opt("string"), + "watchedColIds": t.array("string"), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), @@ -49,7 +49,7 @@ export const WebhookSummary = t.iface([], { "eventTypes": t.array("string"), "isReadyColumn": t.union("string", "null"), "tableId": "string", - "columnIds": "string", + "watchedColIds": t.array("string"), "enabled": "boolean", "name": "string", "memo": "string", @@ -66,7 +66,7 @@ export const WebhookPatch = t.iface([], { "url": t.opt("string"), "eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update")))), "tableId": t.opt("string"), - "columnIds": t.opt("string"), + "watchedColIds": t.array("string"), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), diff --git a/app/common/Triggers.ts b/app/common/Triggers.ts index 5232211228..1716ddc9c2 100644 --- a/app/common/Triggers.ts +++ b/app/common/Triggers.ts @@ -10,7 +10,7 @@ export interface WebhookFields { url: string; eventTypes: Array<"add"|"update">; tableId: string; - columnIds: string; + watchedColIds: string[]; enabled?: boolean; isReadyColumn?: string|null; name?: string; @@ -27,7 +27,7 @@ export type WebhookStatus = 'idle'|'sending'|'retrying'|'postponed'|'error'|'inv export interface WebhookSubscribe { url: string; eventTypes: Array<"add"|"update">; - columnIds?: string; + watchedColIds: string[]; enabled?: boolean; isReadyColumn?: string|null; name?: string; @@ -46,7 +46,7 @@ export interface WebhookSummary { eventTypes: string[]; isReadyColumn: string|null; tableId: string; - columnIds: string; + watchedColIds: string[]; enabled: boolean; name: string; memo: string; @@ -66,7 +66,7 @@ export interface WebhookPatch { url?: string; eventTypes?: Array<"add"|"update">; tableId?: string; - columnIds?: string; + watchedColIds: string[]; enabled?: boolean; isReadyColumn?: string|null; name?: string; diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 48f56f739f..6661361fca 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -380,7 +380,7 @@ export class DocWorkerApi { const tablesTable = activeDoc.docData!.getMetaTable("_grist_Tables"); const trigger = webhookId ? activeDoc.triggers.getWebhookTriggerRecord(webhookId) : undefined; let currentTableId = trigger ? tablesTable.getValue(trigger.tableRef, 'tableId')! : undefined; - const {url, eventTypes, columnIds, isReadyColumn, name} = webhook; + const {url, eventTypes, watchedColIds, isReadyColumn, name} = webhook; const tableId = await getRealTableId(req.params.tableId || webhook.tableId, {metaTables}); const fields: Partial = {}; @@ -397,19 +397,18 @@ export class DocWorkerApi { } if (tableId !== undefined) { - if (columnIds) { + if (watchedColIds) { if (tableId !== currentTableId && currentTableId) { - // if the tableId changed, we need to reset the columnIds + // if the tableId changed, we need to reset the watchedColIds fields.watchedColRefList = [GristObjCode.List]; } else { if (!tableId) { - throw new ApiError(`Cannot find columns "${columnIds}" because table is not known`, 404); + throw new ApiError(`Cannot find columns "${watchedColIds}" because table is not known`, 404); } - // columnIds have to be of shape "columnId; columnId; columnId" - fields.watchedColRefList = [GristObjCode.List, ...columnIds.split(";") - .filter(columnId => columnId.trim() !== "") + fields.watchedColRefList = [GristObjCode.List, ...watchedColIds + .filter(colId => colId.trim() !== "") .map( - columnId => { return colIdToReference(metaTables, tableId, columnId.trim().replace(/^\$/, '')); } + colId => { return colIdToReference(metaTables, tableId, colId.trim().replace(/^\$/, '')); } )]; } } else { diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 512f5974b5..c90ee54812 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -277,6 +277,7 @@ export class DocTriggers { // Webhook might have been deleted in the mean time. continue; } + const decodedWatchedColRefList = decodeObject(t.watchedColRefList) as number[] || []; // Report some basic info and usage stats. const entry: WebhookSummary = { // Id of the webhook @@ -288,7 +289,7 @@ export class DocTriggers { // Other fields used to register this webhook. eventTypes: decodeObject(t.eventTypes) as string[], isReadyColumn: getColId(t.isReadyColRef) ?? null, - columnIds: t.watchedColRefList?.slice(1).map(columnRef => getColId(columnRef as number)).join("; ") || "", + watchedColIds: decodedWatchedColRefList.map((columnRef) => getColId(columnRef)), tableId: getTableId(t.tableRef) ?? null, // For future use - for now every webhook is enabled. enabled: t.enabled, diff --git a/test/nbrowser/WebhookOverflow.ts b/test/nbrowser/WebhookOverflow.ts index ee4cb45f3a..2d2ee11e67 100644 --- a/test/nbrowser/WebhookOverflow.ts +++ b/test/nbrowser/WebhookOverflow.ts @@ -34,7 +34,7 @@ describe('WebhookOverflow', function () { enabled: true, name: 'test webhook', tableId: 'Table2', - columnIds: '' + watchedColIds: [] }; await docApi.addWebhook(webhookDetails); await docApi.addWebhook(webhookDetails); diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 89d5dbd3aa..054adc2f63 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -1226,8 +1226,8 @@ function testDocApi() { const listColResp = await axios.get(url, { ...chimpy, params: { hidden: true } }); assert.equal(listColResp.status, 200, "Should succeed in listing columns"); - const columnIds = listColResp.data.columns.map(({id}: {id: string}) => id).sort(); - assert.deepEqual(columnIds, ["B", "C", "manualSort"]); + const watchedColIds = listColResp.data.columns.map(({id}: {id: string}) => id).sort(); + assert.deepEqual(watchedColIds, ["B", "C", "manualSort"]); }); it('should return 404 if table not found', async function() { @@ -3441,7 +3441,7 @@ function testDocApi() { await postWebhookCheck({ webhooks: [{ fields: { - tableId: "Table1", eventTypes: ["update"], columnIds: "notExisting", + tableId: "Table1", eventTypes: ["update"], watchedColIds: ["notExisting"], url: `${serving.url}/200` } }] @@ -3871,7 +3871,7 @@ function testDocApi() { tableId?: string, isReadyColumn?: string | null, eventTypes?: string[] - columnIds?: string, + watchedColIds?: string[], }) { // Subscribe helper that returns a method to unsubscribe. const data = await subscribe(endpoint, docId, options); @@ -3889,7 +3889,7 @@ function testDocApi() { tableId?: string, isReadyColumn?: string|null, eventTypes?: string[], - columnIds?: string, + watchedColIds?: string[], name?: string, memo?: string, enabled?: boolean, @@ -3901,7 +3901,7 @@ function testDocApi() { eventTypes: options?.eventTypes ?? ['add', 'update'], url: `${serving.url}/${endpoint}`, isReadyColumn: options?.isReadyColumn === undefined ? 'B' : options?.isReadyColumn, - ...pick(options, 'name', 'memo', 'enabled', 'columnIds'), + ...pick(options, 'name', 'memo', 'enabled', 'watchedColIds'), }, chimpy ); assert.equal(status, 200); @@ -4425,7 +4425,7 @@ function testDocApi() { await webhook1(); }); - it("should call to a webhook only when columns updated are in columnIds if not empty", async () => { // eslint-disable-line max-len + it("should call to a webhook only when columns updated are in watchedColIds if not empty", async () => { // eslint-disable-line max-len // Create a test document. const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; const docId = await userApi.newDoc({ name: 'testdoc5' }, ws1); @@ -4449,9 +4449,9 @@ function testDocApi() { await successCalled.waitAndReset(); }; - // Webhook with only one columnId. + // Webhook with only one watchedColId. const webhook1 = await autoSubscribe('200', docId, { - columnIds: 'A', eventTypes: ['add', 'update'] + watchedColIds: ['A'], eventTypes: ['add', 'update'] }); successCalled.reset(); // Create record, that will call the webhook. @@ -4469,9 +4469,9 @@ function testDocApi() { await assertSuccessCalled(); await webhook1(); // Unsubscribe. - // Webhook with multiple columnIds (check the shape of the columnIds string) + // Webhook with multiple watchedColIds const webhook2 = await autoSubscribe('200', docId, { - columnIds: 'A; B', eventTypes: ['update'] + watchedColIds: ['A', 'B'], eventTypes: ['update'] }); successCalled.reset(); await modifyColumn({ C: 'c3' }); @@ -4480,9 +4480,9 @@ function testDocApi() { await assertSuccessCalled(); await webhook2(); - // Check that string terminating with ";" not breaking the webhook + // Check that empty string in watchedColIds are ignored const webhook3 = await autoSubscribe('200', docId, { - columnIds: 'A;', eventTypes: ['update'] + watchedColIds: ['A', ""], eventTypes: ['update'] }); await modifyColumn({ C: 'c4' }); await assertSuccessNotCalled(); @@ -4511,7 +4511,7 @@ function testDocApi() { tableId: 'Table1', name: '', memo: '', - columnIds: '', + watchedColIds: [], }, usage : { status: 'idle', numWaiting: 0, @@ -4529,7 +4529,7 @@ function testDocApi() { tableId: 'Table1', name: '', memo: '', - columnIds: '', + watchedColIds: [], }, usage : { status: 'idle', numWaiting: 0, @@ -4870,7 +4870,7 @@ function testDocApi() { isReadyColumn: 'B', name: 'My Webhook', memo: 'Sync store', - columnIds: 'A' + watchedColIds: ['A'] }; // subscribe @@ -4895,7 +4895,7 @@ function testDocApi() { enabled: true, name: 'My Webhook', memo: 'Sync store', - columnIds: 'A', + watchedColIds: ['A'], }; let stats = await readStats(docId); @@ -4946,11 +4946,11 @@ function testDocApi() { // changing table without changing the ready column should reset the latter await check({tableId: 'Table2'}, 200, '', expectedFields => { expectedFields.isReadyColumn = null; - expectedFields.columnIds = ""; + expectedFields.watchedColIds = []; }); await check({tableId: 'Santa'}, 404, `Table not found "Santa"`); - await check({tableId: 'Table2', isReadyColumn: 'Foo', columnIds: ""}, 200); + await check({tableId: 'Table2', isReadyColumn: 'Foo', watchedColIds: [""]}, 200); await check({eventTypes: ['add', 'update']}, 200); await check({eventTypes: []}, 400, "eventTypes must be a non-empty array"); From 35e5b2ed00a1a78117ec8f1bcfaa2452be1f8fb9 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 16:57:20 +0100 Subject: [PATCH 23/31] rename columnIds --- test/server/lib/DocApi.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 054adc2f63..0fba3990f0 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -1226,8 +1226,8 @@ function testDocApi() { const listColResp = await axios.get(url, { ...chimpy, params: { hidden: true } }); assert.equal(listColResp.status, 200, "Should succeed in listing columns"); - const watchedColIds = listColResp.data.columns.map(({id}: {id: string}) => id).sort(); - assert.deepEqual(watchedColIds, ["B", "C", "manualSort"]); + const columnIds = listColResp.data.columns.map(({id}: {id: string}) => id).sort(); + assert.deepEqual(columnIds, ["B", "C", "manualSort"]); }); it('should return 404 if table not found', async function() { @@ -4926,7 +4926,7 @@ function testDocApi() { if (error instanceof RegExp) { assert.match(resp.data.details?.userError || resp.data.error, error); } else { - assert.deepEqual(resp.data, { error }); + assert.deepEqual(resp.data, {error}); } } From b19d169405e63c51e53dab9947cba9e8b20553ea Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 17:00:55 +0100 Subject: [PATCH 24/31] adapt WebhookPage test --- test/nbrowser/WebhookPage.ts | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/nbrowser/WebhookPage.ts b/test/nbrowser/WebhookPage.ts index 14ccd905b7..8157610b4c 100644 --- a/test/nbrowser/WebhookPage.ts +++ b/test/nbrowser/WebhookPage.ts @@ -1,9 +1,9 @@ -import { DocCreationInfo } from 'app/common/DocListAPI'; -import { DocAPI } from 'app/common/UserAPI'; -import { assert, driver, Key } from 'mocha-webdriver'; +import {DocCreationInfo} from 'app/common/DocListAPI'; +import {DocAPI} from 'app/common/UserAPI'; +import {assert, driver, Key} from 'mocha-webdriver'; import * as gu from 'test/nbrowser/gristUtils'; -import { server, setupTestSuite } from 'test/nbrowser/testUtils'; -import { EnvironmentSnapshot } from 'test/server/testUtils'; +import {server, setupTestSuite} from 'test/nbrowser/testUtils'; +import {EnvironmentSnapshot} from 'test/server/testUtils'; describe('WebhookPage', function () { this.timeout(60000); @@ -26,10 +26,10 @@ describe('WebhookPage', function () { doc = await session.tempDoc(cleanup, 'Hello.grist'); docApi = api.getDocAPI(doc.id); await api.applyUserActions(doc.id, [ - ['AddTable', 'Table2', [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }, { id: 'E' }]], + ['AddTable', 'Table2', [{id: 'A'}, {id: 'B'}, {id: 'C'}, {id: 'D'}, {id: 'E'}]], ]); await api.applyUserActions(doc.id, [ - ['AddTable', 'Table3', [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }, { id: 'E' }]], + ['AddTable', 'Table3', [{id: 'A'}, {id: 'B'}, {id: 'C'}, {id: 'D'}, {id: 'E'}]], ]); await api.updateDocPermissions(doc.id, { users: { @@ -55,7 +55,7 @@ describe('WebhookPage', function () { 'URL', 'Table', 'Ready Column', - 'Columns to check when update (separated by ;)', + 'Filter for changes in these columns (semicolon-separated ids)', 'Webhook Id', 'Enabled', 'Status', @@ -92,7 +92,7 @@ describe('WebhookPage', function () { assert.equal(await getField(1, 'Memo'), 'Test Memo'); }); // Make sure the webhook is actually working. - await docApi.addRows('Table1', { A: ['zig'], B: ['zag'] }); + await docApi.addRows('Table1', {A: ['zig'], B: ['zag']}); // Make sure the data gets delivered, and that the webhook status is updated. await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 1); @@ -101,7 +101,7 @@ describe('WebhookPage', function () { }); // Remove the webhook and make sure it is no longer listed. assert.equal(await gu.getCardListCount(), 2); - await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); + await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await gu.waitForServer(); @@ -123,14 +123,14 @@ describe('WebhookPage', function () { await setField(2, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table3/records?flat=1`); await setField(2, 'Table', 'Table1'); await gu.waitForServer(); - await docApi.addRows('Table1', { A: ['zig2'], B: ['zag2'] }); + await docApi.addRows('Table1', {A: ['zig2'], B: ['zag2']}); await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 1); assert.lengthOf((await docApi.getRows('Table3')).A, 1); assert.match(await getField(1, 'Status'), /status...success/); assert.match(await getField(2, 'Status'), /status...success/); }); - await docApi.updateRows('Table1', { id: [1], A: ['zig3'], B: ['zag3'] }); + await docApi.updateRows('Table1', {id: [1], A: ['zig3'], B: ['zag3']}); await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 2); assert.lengthOf((await docApi.getRows('Table3')).A, 1); @@ -140,11 +140,11 @@ describe('WebhookPage', function () { // confirm that nothing shows up to Table3. assert.lengthOf((await docApi.getRows('Table3')).A, 1); // Break everything down. - await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); + await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await gu.waitForServer(); - await gu.getDetailCell({ col: 'Memo', rowNum: 1 }).click(); + await gu.getDetailCell({col: 'Memo', rowNum: 1}).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.waitForServer(); assert.equal(await gu.getCardListCount(), 1); @@ -163,7 +163,7 @@ describe('WebhookPage', function () { await setField(1, 'URL', `http://${host}/notathing`); await setField(1, 'Table', 'Table1'); await gu.waitForServer(); - await docApi.addRows('Table1', { A: ['dud1'] }); + await docApi.addRows('Table1', {A: ['dud1']}); await gu.waitToPass(async () => { assert.match(await getField(1, 'Status'), /status...failure/); assert.match(await getField(1, 'Status'), /numWaiting..1/); @@ -175,14 +175,14 @@ describe('WebhookPage', function () { assert.match(await getField(1, 'Status'), /numWaiting..0/); }); assert.lengthOf((await docApi.getRows('Table2')).A, 0); - await docApi.addRows('Table1', { A: ['dud2'] }); + await docApi.addRows('Table1', {A: ['dud2']}); await gu.waitToPass(async () => { assert.lengthOf((await docApi.getRows('Table2')).A, 1); assert.match(await getField(1, 'Status'), /status...success/); }); // Break everything down. - await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); + await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await gu.waitForServer(); @@ -238,7 +238,7 @@ describe('WebhookPage', function () { assert.match(await getField(1, 'Memo'), /multiple memo/); }); - await gu.getDetailCell({ col: 'Name', rowNum: 1 }).click(); + await gu.getDetailCell({col: 'Name', rowNum: 1}).click(); await gu.sendKeys(Key.chord(await gu.modKey(), Key.DELETE)); await gu.confirm(true, true); await driver.switchTo().window(ownerTab); @@ -260,7 +260,7 @@ describe('WebhookPage', function () { await gu.waitForServer(); await clipboard.lockAndPerform(async (cb) => { await cb.copy(); - await gu.getDetailCell({ col: 'Memo', rowNum: 1 }).click(); + await gu.getDetailCell({col: 'Memo', rowNum: 1}).click(); await cb.paste(); }); await gu.waitForServer(); @@ -269,12 +269,12 @@ describe('WebhookPage', function () { }); async function setField(rowNum: number, col: string, text: string) { - await gu.getDetailCell({ col, rowNum }).click(); + await gu.getDetailCell({col, rowNum}).click(); await gu.enterCell(text); } async function getField(rowNum: number, col: string) { - const cell = await gu.getDetailCell({ col, rowNum }); + const cell = await gu.getDetailCell({col, rowNum}); return cell.getText(); } @@ -288,5 +288,5 @@ async function openWebhookPage() { async function waitForWebhookPage() { await driver.findContentWait('button', /Clear Queue/, 3000); // No section, so no easy utility for setting focus. Click on a random cell. - await gu.getDetailCell({ col: 'Webhook Id', rowNum: 1 }).click(); + await gu.getDetailCell({col: 'Webhook Id', rowNum: 1}).click(); } From f6a9199c4a1e4dc8afb34bbf7dfec080a73521b2 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 17:06:34 +0100 Subject: [PATCH 25/31] rename UIWebhookSummary type --- app/client/ui/WebhookPage.ts | 8 ++++---- test/nbrowser/WebhookPage.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/client/ui/WebhookPage.ts b/app/client/ui/WebhookPage.ts index 9af587d9aa..09a6a0694c 100644 --- a/app/client/ui/WebhookPage.ts +++ b/app/client/ui/WebhookPage.ts @@ -135,7 +135,7 @@ class WebhookExternalTable implements IExternalTable { public saveableFields = [ 'tableId', 'watchedColIdsText', 'url', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn', ]; - public webhooks: ObservableArray = observableArray([]); + public webhooks: ObservableArray = observableArray([]); public constructor(private _docApi: DocAPI) { } @@ -278,7 +278,7 @@ class WebhookExternalTable implements IExternalTable { this.webhooks.removeAll(); this.webhooks.push( ...webhooks.map(webhook => { - const uiWebhook: WebhookPageSummary = {...webhook}; + const uiWebhook: UIWebhookSummary = {...webhook}; uiWebhook.fields.watchedColIdsText = webhook.fields.watchedColIds ? webhook.fields.watchedColIds.join(";") : ""; return uiWebhook; })); @@ -458,7 +458,7 @@ function _prepareWebhookInitialActions(tableId: string): DocAction[] { * `status` is converted to a string, * and `watchedColIdsText` is converted to list in a cell format. */ -function _mapWebhookValues(webhookSummary: WebhookPageSummary): Partial { +function _mapWebhookValues(webhookSummary: UIWebhookSummary): Partial { const fields = webhookSummary.fields; const {eventTypes, watchedColIdsText} = fields; const watchedColIds = watchedColIdsText @@ -482,6 +482,6 @@ type WebhookSchemaType = { webhookId: string; } -type WebhookPageSummary = WebhookSummary & { +type UIWebhookSummary = WebhookSummary & { fields: {watchedColIdsText?: string;} } diff --git a/test/nbrowser/WebhookPage.ts b/test/nbrowser/WebhookPage.ts index 8157610b4c..8ec511445d 100644 --- a/test/nbrowser/WebhookPage.ts +++ b/test/nbrowser/WebhookPage.ts @@ -254,7 +254,7 @@ describe('WebhookPage', function () { * Checks that a particular route to modifying cells in a virtual table * is in place (previously it was not). */ - it('can paste into a cell without clicking into it', async function () { + it('can paste into a cell without clicking into it', async function() { await openWebhookPage(); await setField(1, 'Name', '1234'); await gu.waitForServer(); From f2e8ee8c013a177dc97c4280ea417f3afce41852 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 25 Mar 2024 17:14:03 +0100 Subject: [PATCH 26/31] add webhookPage test --- test/nbrowser/WebhookPage.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/nbrowser/WebhookPage.ts b/test/nbrowser/WebhookPage.ts index 8ec511445d..8c96913e5f 100644 --- a/test/nbrowser/WebhookPage.ts +++ b/test/nbrowser/WebhookPage.ts @@ -81,15 +81,17 @@ describe('WebhookPage', function () { await gu.waitToPass(async () => { assert.equal(await getField(1, 'Webhook Id'), id); }); - // Now other fields like name and memo are persisted. + // Now other fields like name, memo and watchColIds are persisted. await setField(1, 'Name', 'Test Webhook'); await setField(1, 'Memo', 'Test Memo'); + await setField(1, 'Filter for changes in these columns (semicolon-separated ids)', 'A; B'); await gu.waitForServer(); await driver.navigate().refresh(); await waitForWebhookPage(); await gu.waitToPass(async () => { assert.equal(await getField(1, 'Name'), 'Test Webhook'); assert.equal(await getField(1, 'Memo'), 'Test Memo'); + assert.equal(await getField(1, 'Filter for changes in these columns (semicolon-separated ids)'), 'A;B'); }); // Make sure the webhook is actually working. await docApi.addRows('Table1', {A: ['zig'], B: ['zag']}); From b20d459ac8f69e5512c2b309986c805f4d175a81 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Tue, 26 Mar 2024 10:35:45 +0100 Subject: [PATCH 27/31] make watchedColIds optional --- app/common/Triggers-ti.ts | 8 ++++---- app/common/Triggers.ts | 8 ++++---- test/server/lib/DocApi.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/common/Triggers-ti.ts b/app/common/Triggers-ti.ts index ed48744a62..bb04bbaec6 100644 --- a/app/common/Triggers-ti.ts +++ b/app/common/Triggers-ti.ts @@ -16,7 +16,7 @@ export const WebhookFields = t.iface([], { "url": "string", "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), "tableId": "string", - "watchedColIds": t.array("string"), + "watchedColIds": t.opt(t.array("string")), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), @@ -30,7 +30,7 @@ export const WebhookStatus = t.union(t.lit('idle'), t.lit('sending'), t.lit('ret export const WebhookSubscribe = t.iface([], { "url": "string", "eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))), - "watchedColIds": t.array("string"), + "watchedColIds": t.opt(t.array("string")), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), @@ -49,7 +49,7 @@ export const WebhookSummary = t.iface([], { "eventTypes": t.array("string"), "isReadyColumn": t.union("string", "null"), "tableId": "string", - "watchedColIds": t.array("string"), + "watchedColIds": t.opt(t.array("string")), "enabled": "boolean", "name": "string", "memo": "string", @@ -66,7 +66,7 @@ export const WebhookPatch = t.iface([], { "url": t.opt("string"), "eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update")))), "tableId": t.opt("string"), - "watchedColIds": t.array("string"), + "watchedColIds": t.opt(t.array("string")), "enabled": t.opt("boolean"), "isReadyColumn": t.opt(t.union("string", "null")), "name": t.opt("string"), diff --git a/app/common/Triggers.ts b/app/common/Triggers.ts index 1716ddc9c2..d3b492d610 100644 --- a/app/common/Triggers.ts +++ b/app/common/Triggers.ts @@ -10,7 +10,7 @@ export interface WebhookFields { url: string; eventTypes: Array<"add"|"update">; tableId: string; - watchedColIds: string[]; + watchedColIds?: string[]; enabled?: boolean; isReadyColumn?: string|null; name?: string; @@ -27,7 +27,7 @@ export type WebhookStatus = 'idle'|'sending'|'retrying'|'postponed'|'error'|'inv export interface WebhookSubscribe { url: string; eventTypes: Array<"add"|"update">; - watchedColIds: string[]; + watchedColIds?: string[]; enabled?: boolean; isReadyColumn?: string|null; name?: string; @@ -46,7 +46,7 @@ export interface WebhookSummary { eventTypes: string[]; isReadyColumn: string|null; tableId: string; - watchedColIds: string[]; + watchedColIds?: string[]; enabled: boolean; name: string; memo: string; @@ -66,7 +66,7 @@ export interface WebhookPatch { url?: string; eventTypes?: Array<"add"|"update">; tableId?: string; - watchedColIds: string[]; + watchedColIds?: string[]; enabled?: boolean; isReadyColumn?: string|null; name?: string; diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 0fba3990f0..e1a6813bcc 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -4950,7 +4950,7 @@ function testDocApi() { }); await check({tableId: 'Santa'}, 404, `Table not found "Santa"`); - await check({tableId: 'Table2', isReadyColumn: 'Foo', watchedColIds: [""]}, 200); + await check({tableId: 'Table2', isReadyColumn: 'Foo', watchedColIds: []}, 200); await check({eventTypes: ['add', 'update']}, 200); await check({eventTypes: []}, 400, "eventTypes must be a non-empty array"); From b4ab5ee851a7fa29ac6f05a1f8ab7b14cea059b7 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Tue, 26 Mar 2024 12:01:31 +0100 Subject: [PATCH 28/31] add translation --- static/locales/en.client.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 8daa60c10f..a486ab5e4d 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -1181,7 +1181,8 @@ "Status": "Status", "URL": "URL", "Webhook Id": "Webhook Id", - "Table": "Table" + "Table": "Table", + "Filter for changes in these columns (semicolon-separated ids)": "Filter for changes in these columns (semicolon-separated ids)" }, "FormulaAssistant": { "Ask the bot.": "Ask the bot.", From 410dd28bb5d045ba1e94300ab047a3468f972e0a Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Tue, 26 Mar 2024 12:15:50 +0100 Subject: [PATCH 29/31] add generic options in _grist_triggers table --- app/common/schema.ts | 2 ++ app/server/lib/initialDocSql.ts | 4 ++-- sandbox/grist/migrations.py | 1 + sandbox/grist/schema.py | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/common/schema.ts b/app/common/schema.ts index e432baf613..dc1fb56252 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -168,6 +168,7 @@ export const schema = { memo : "Text", enabled : "Bool", watchedColRefList : "RefList:_grist_Tables_column", + options : "Text", }, "_grist_ACLRules": { @@ -390,6 +391,7 @@ export interface SchemaTypes { memo: string; enabled: boolean; watchedColRefList: [GristObjCode.List, ...number[]]|null; + options: string; }; "_grist_ACLRules": { diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index 5feab82d96..48c22a3b11 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS "_grist_Views_section_field" (id INTEGER PRIMARY KEY, CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "fileExt" TEXT DEFAULT '', "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL); -CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "watchedColRefList" TEXT DEFAULT NULL); +CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "watchedColRefList" TEXT DEFAULT NULL, "options" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT ''); INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'',''); CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT ''); @@ -80,7 +80,7 @@ INSERT INTO _grist_Views_section_field VALUES(9,3,9,4,0,'',0,0,'',NULL); CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formula" TEXT DEFAULT '', "name" TEXT DEFAULT '', "tableRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "fileExt" TEXT DEFAULT '', "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL); -CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "watchedColRefList" TEXT DEFAULT NULL); +CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '', "label" TEXT DEFAULT '', "memo" TEXT DEFAULT '', "enabled" BOOLEAN DEFAULT 0, "watchedColRefList" TEXT DEFAULT NULL, "options" TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT ''); INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'',''); CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT ''); diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index 26096365d5..517b84bd76 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1315,4 +1315,5 @@ def migration42(tdset): """ return tdset.apply_doc_actions([ add_column('_grist_Triggers', 'watchedColRefList', 'RefList:_grist_Tables_column'), + add_column('_grist_Triggers', 'options', 'Text'), ]) diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index 4fd8b169d7..413e0cfccc 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -261,7 +261,8 @@ def schema_create_actions(): make_column("label", "Text"), make_column("memo", "Text"), make_column("enabled", "Bool"), - make_column("watchedColRefList", "RefList:_grist_Tables_column") + make_column("watchedColRefList", "RefList:_grist_Tables_column"), + make_column("options", "Text"), ]), # All of the ACL rules. From e62a84414102602e031ecb1c7c5cc89aba10e16e Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 27 Mar 2024 11:22:33 +0100 Subject: [PATCH 30/31] add migration --- app/common/schema.ts | 2 +- app/server/lib/initialDocSql.ts | 4 ++-- sandbox/grist/migrations.py | 8 ++++++++ sandbox/grist/schema.py | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/common/schema.ts b/app/common/schema.ts index dc1fb56252..3600b91756 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData"; // tslint:disable:object-literal-key-quotes -export const SCHEMA_VERSION = 42; +export const SCHEMA_VERSION = 43; export const schema = { diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index 48c22a3b11..1be8ffd62a 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0); @@ -44,7 +44,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2,3); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index 517b84bd76..9e39c9c8b6 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1315,5 +1315,13 @@ def migration42(tdset): """ return tdset.apply_doc_actions([ add_column('_grist_Triggers', 'watchedColRefList', 'RefList:_grist_Tables_column'), + ]) + +@migration(schema_version=43) +def migration43(tdset): + """ + Adds column to register which table columns are triggered in webhooks. + """ + return tdset.apply_doc_actions([ add_column('_grist_Triggers', 'options', 'Text'), ]) diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index 413e0cfccc..5c55aaa856 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -15,7 +15,7 @@ import actions -SCHEMA_VERSION = 42 +SCHEMA_VERSION = 43 def make_column(col_id, col_type, formula='', isFormula=False): return { From 1aeca1bd9bcdd85abfd6528294e537a4b555178e Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Wed, 27 Mar 2024 11:25:45 +0100 Subject: [PATCH 31/31] remove migration --- app/common/schema.ts | 2 +- app/server/lib/initialDocSql.ts | 4 ++-- sandbox/grist/migrations.py | 8 -------- sandbox/grist/schema.py | 2 +- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/app/common/schema.ts b/app/common/schema.ts index 3600b91756..dc1fb56252 100644 --- a/app/common/schema.ts +++ b/app/common/schema.ts @@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData"; // tslint:disable:object-literal-key-quotes -export const SCHEMA_VERSION = 43; +export const SCHEMA_VERSION = 42; export const schema = { diff --git a/app/server/lib/initialDocSql.ts b/app/server/lib/initialDocSql.ts index 1be8ffd62a..48c22a3b11 100644 --- a/app/server/lib/initialDocSql.ts +++ b/app/server/lib/initialDocSql.ts @@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0); @@ -44,7 +44,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = ` PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT ''); -INSERT INTO _grist_DocInfo VALUES(1,'','','',43,'',''); +INSERT INTO _grist_DocInfo VALUES(1,'','','',42,'',''); CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0, "recordCardViewSectionRef" INTEGER DEFAULT 0); INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2,3); CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "description" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL); diff --git a/sandbox/grist/migrations.py b/sandbox/grist/migrations.py index 9e39c9c8b6..517b84bd76 100644 --- a/sandbox/grist/migrations.py +++ b/sandbox/grist/migrations.py @@ -1315,13 +1315,5 @@ def migration42(tdset): """ return tdset.apply_doc_actions([ add_column('_grist_Triggers', 'watchedColRefList', 'RefList:_grist_Tables_column'), - ]) - -@migration(schema_version=43) -def migration43(tdset): - """ - Adds column to register which table columns are triggered in webhooks. - """ - return tdset.apply_doc_actions([ add_column('_grist_Triggers', 'options', 'Text'), ]) diff --git a/sandbox/grist/schema.py b/sandbox/grist/schema.py index 5c55aaa856..413e0cfccc 100644 --- a/sandbox/grist/schema.py +++ b/sandbox/grist/schema.py @@ -15,7 +15,7 @@ import actions -SCHEMA_VERSION = 43 +SCHEMA_VERSION = 42 def make_column(col_id, col_type, formula='', isFormula=False): return {