@prismatic-io/components-airtable 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -2
- package/dist/icon.png +0 -0
- package/dist/index.js +218 -0
- package/npm-dist/icon.png +0 -0
- package/npm-dist/index.cjs +1547 -0
- package/npm-dist/index.d.cts +1005 -0
- package/package.json +50 -4
|
@@ -0,0 +1,1547 @@
|
|
|
1
|
+
let _prismatic_io_spectral = require("@prismatic-io/spectral");
|
|
2
|
+
let _prismatic_io_spectral_http = require("@prismatic-io/spectral/http");
|
|
3
|
+
let node_crypto = require("node:crypto");
|
|
4
|
+
var connections_default = [
|
|
5
|
+
(0, _prismatic_io_spectral.oauth2Connection)({
|
|
6
|
+
oauth2Type: _prismatic_io_spectral.OAuth2Type.AuthorizationCode,
|
|
7
|
+
oauth2PkceMethod: _prismatic_io_spectral.OAuth2PkceMethod.S256,
|
|
8
|
+
key: "oauth",
|
|
9
|
+
display: {
|
|
10
|
+
label: "OAuth 2.0",
|
|
11
|
+
description: "Authenticate requests to Airtable using OAuth 2.0."
|
|
12
|
+
},
|
|
13
|
+
inputs: {
|
|
14
|
+
authorizeUrl: {
|
|
15
|
+
label: "Authorize URL",
|
|
16
|
+
placeholder: "Enter Authorize URL",
|
|
17
|
+
type: "string",
|
|
18
|
+
required: true,
|
|
19
|
+
shown: false,
|
|
20
|
+
comments: "The OAuth 2.0 Authorization URL for Airtable",
|
|
21
|
+
default: "https://airtable.com/oauth2/v1/authorize"
|
|
22
|
+
},
|
|
23
|
+
tokenUrl: {
|
|
24
|
+
label: "Token URL",
|
|
25
|
+
placeholder: "Enter Token URL",
|
|
26
|
+
type: "string",
|
|
27
|
+
required: true,
|
|
28
|
+
shown: false,
|
|
29
|
+
comments: "The OAuth 2.0 Token URL for Airtable",
|
|
30
|
+
default: "https://airtable.com/oauth2/v1/token"
|
|
31
|
+
},
|
|
32
|
+
scopes: {
|
|
33
|
+
label: "Scopes",
|
|
34
|
+
placeholder: "Enter scopes",
|
|
35
|
+
type: "string",
|
|
36
|
+
required: true,
|
|
37
|
+
shown: true,
|
|
38
|
+
comments: "A space-delimited set of one or more scopes to get the user's permission to access.",
|
|
39
|
+
default: "data.records:read data.records:write data.recordComments:read data.recordComments:write schema.bases:read schema.bases:write webhook:manage"
|
|
40
|
+
},
|
|
41
|
+
clientId: {
|
|
42
|
+
label: "Client ID",
|
|
43
|
+
placeholder: "Enter Client ID",
|
|
44
|
+
type: "string",
|
|
45
|
+
required: true,
|
|
46
|
+
shown: true,
|
|
47
|
+
comments: "Provide the Client ID you received from https://airtable.com/create/oauth.",
|
|
48
|
+
example: "cli1234567890abcd"
|
|
49
|
+
},
|
|
50
|
+
clientSecret: {
|
|
51
|
+
label: "Client Secret",
|
|
52
|
+
placeholder: "Enter Client Secret",
|
|
53
|
+
type: "password",
|
|
54
|
+
required: true,
|
|
55
|
+
shown: true,
|
|
56
|
+
comments: "Provide the Client Secret you received from https://airtable.com/create/oauth."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
(0, _prismatic_io_spectral.connection)({
|
|
61
|
+
key: "personalAccessToken",
|
|
62
|
+
display: {
|
|
63
|
+
label: "Personal Access Token",
|
|
64
|
+
description: "Authenticate requests to Airtable using a personal access token."
|
|
65
|
+
},
|
|
66
|
+
inputs: { apiKey: {
|
|
67
|
+
label: "API Key",
|
|
68
|
+
placeholder: "Enter Personal Access Token",
|
|
69
|
+
type: "password",
|
|
70
|
+
required: true,
|
|
71
|
+
shown: true,
|
|
72
|
+
comments: "You can generate an API key from https://airtable.com/create/tokens",
|
|
73
|
+
example: "patAbCdEfGh1234567.1234567890abcdefghijklmnopqr"
|
|
74
|
+
} }
|
|
75
|
+
}),
|
|
76
|
+
(0, _prismatic_io_spectral.connection)({
|
|
77
|
+
key: "apiKey",
|
|
78
|
+
display: {
|
|
79
|
+
label: "API Key and Base ID (Deprecated)",
|
|
80
|
+
description: "Authenticate requests to Airtable using an API key and base ID. This connection is deprecated as of February 1, 2024."
|
|
81
|
+
},
|
|
82
|
+
inputs: {
|
|
83
|
+
base: {
|
|
84
|
+
label: "Airtable Base ID",
|
|
85
|
+
placeholder: "Enter Airtable Base ID",
|
|
86
|
+
type: "string",
|
|
87
|
+
required: true,
|
|
88
|
+
shown: true,
|
|
89
|
+
comments: "Visit https://airtable.com/api and select your workspace. The ID of your base will be printed for you in green.",
|
|
90
|
+
example: "appLkNDICXNqxSDhG"
|
|
91
|
+
},
|
|
92
|
+
apiKey: {
|
|
93
|
+
label: "API Key",
|
|
94
|
+
placeholder: "Enter API Key",
|
|
95
|
+
type: "password",
|
|
96
|
+
required: true,
|
|
97
|
+
shown: true,
|
|
98
|
+
comments: "You can generate an API key from https://airtable.com/account.",
|
|
99
|
+
example: "keyvTlNCTqEXAMPLE"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
];
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/client.ts
|
|
106
|
+
const createAirtableClient = (airtableConnection, debug = false) => {
|
|
107
|
+
return (0, _prismatic_io_spectral_http.createClient)({
|
|
108
|
+
baseUrl: "https://api.airtable.com",
|
|
109
|
+
headers: {
|
|
110
|
+
Authorization: `Bearer ${airtableConnection.token?.access_token || airtableConnection.fields.apiKey}`,
|
|
111
|
+
"Content-Type": "application/json"
|
|
112
|
+
},
|
|
113
|
+
debug
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/examplePayloads/bases.ts
|
|
118
|
+
const listBasesExamplePayload = { data: [
|
|
119
|
+
{
|
|
120
|
+
id: "appLkNDICXNqxSDhG",
|
|
121
|
+
name: "Apartment Hunting",
|
|
122
|
+
permissionLevel: "create"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "appSW9R5uCNmRmfl6",
|
|
126
|
+
name: "Project Tracker",
|
|
127
|
+
permissionLevel: "edit"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "appVZ3mDrP8fK4xyL",
|
|
131
|
+
name: "Marketing Campaigns",
|
|
132
|
+
permissionLevel: "read"
|
|
133
|
+
}
|
|
134
|
+
] };
|
|
135
|
+
const getBaseSchemaExamplePayload = { data: { tables: [{
|
|
136
|
+
description: "Apartments to track.",
|
|
137
|
+
fields: [
|
|
138
|
+
{
|
|
139
|
+
description: "Name of the apartment",
|
|
140
|
+
id: "fld1VnoyuotSTyxW1",
|
|
141
|
+
name: "Name",
|
|
142
|
+
type: "singleLineText"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "fldoaIqdn5szURHpw",
|
|
146
|
+
name: "Pictures",
|
|
147
|
+
type: "multipleAttachments"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "fldumZe00w09RYTW6",
|
|
151
|
+
name: "District",
|
|
152
|
+
options: {
|
|
153
|
+
inverseLinkFieldId: "fldWnCJlo2z6ttT8Y",
|
|
154
|
+
isReversed: false,
|
|
155
|
+
linkedTableId: "tblK6MZHez0ZvBChZ",
|
|
156
|
+
prefersSingleRecordLink: true
|
|
157
|
+
},
|
|
158
|
+
type: "multipleRecordLinks"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "fldPrQNxGqKXLbR4w",
|
|
162
|
+
name: "Price",
|
|
163
|
+
type: "currency",
|
|
164
|
+
options: {
|
|
165
|
+
precision: 2,
|
|
166
|
+
symbol: "$"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
id: "tbltp8DGLhqbUmjK1",
|
|
171
|
+
name: "Apartments",
|
|
172
|
+
primaryFieldId: "fld1VnoyuotSTyxW1",
|
|
173
|
+
views: [{
|
|
174
|
+
id: "viwQpsuEDqHFqegkp",
|
|
175
|
+
name: "Grid view",
|
|
176
|
+
type: "grid"
|
|
177
|
+
}, {
|
|
178
|
+
id: "viwKLb2UxT9mQpP4d",
|
|
179
|
+
name: "Gallery view",
|
|
180
|
+
type: "gallery"
|
|
181
|
+
}]
|
|
182
|
+
}, {
|
|
183
|
+
fields: [{
|
|
184
|
+
id: "fldEVzvQOoULO38yl",
|
|
185
|
+
name: "Name",
|
|
186
|
+
type: "singleLineText"
|
|
187
|
+
}, {
|
|
188
|
+
description: "Apartments that belong to this district",
|
|
189
|
+
id: "fldWnCJlo2z6ttT8Y",
|
|
190
|
+
name: "Apartments",
|
|
191
|
+
options: {
|
|
192
|
+
inverseLinkFieldId: "fldumZe00w09RYTW6",
|
|
193
|
+
isReversed: false,
|
|
194
|
+
linkedTableId: "tbltp8DGLhqbUmjK1",
|
|
195
|
+
prefersSingleRecordLink: false
|
|
196
|
+
},
|
|
197
|
+
type: "multipleRecordLinks"
|
|
198
|
+
}],
|
|
199
|
+
id: "tblK6MZHez0ZvBChZ",
|
|
200
|
+
name: "Districts",
|
|
201
|
+
primaryFieldId: "fldEVzvQOoULO38yl",
|
|
202
|
+
views: [{
|
|
203
|
+
id: "viwi3KXvrKug2mIBS",
|
|
204
|
+
name: "Grid view",
|
|
205
|
+
type: "grid"
|
|
206
|
+
}]
|
|
207
|
+
}] } };
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/examplePayloads/misc.ts
|
|
210
|
+
const rawRequestExamplePayload = { data: {
|
|
211
|
+
data: { tables: [{
|
|
212
|
+
id: "tbltp8DGLhqbUmjK1",
|
|
213
|
+
name: "Apartments",
|
|
214
|
+
primaryFieldId: "fld1VnoyuotSTyxW1",
|
|
215
|
+
fields: [{
|
|
216
|
+
id: "fld1VnoyuotSTyxW1",
|
|
217
|
+
name: "Name",
|
|
218
|
+
type: "singleLineText"
|
|
219
|
+
}],
|
|
220
|
+
views: [{
|
|
221
|
+
id: "viwQpsuEDqHFqegkp",
|
|
222
|
+
name: "Grid view",
|
|
223
|
+
type: "grid"
|
|
224
|
+
}]
|
|
225
|
+
}] },
|
|
226
|
+
headers: {}
|
|
227
|
+
} };
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/examplePayloads/records.ts
|
|
230
|
+
const recordExamplePayload = { data: {
|
|
231
|
+
id: "recZ6qSLw0OCA6Xul",
|
|
232
|
+
createdTime: "2022-06-01T17:50:40.000Z",
|
|
233
|
+
fields: {
|
|
234
|
+
Notes: "We finished this and we're ready to move on to our backlog",
|
|
235
|
+
Status: "Complete",
|
|
236
|
+
"Start date": "2022-05-31",
|
|
237
|
+
Projects: "Updated sales process",
|
|
238
|
+
Priority: "High",
|
|
239
|
+
"Assigned to": "Jane Doe",
|
|
240
|
+
Count: 42
|
|
241
|
+
}
|
|
242
|
+
} };
|
|
243
|
+
const createRecordExamplePayload = recordExamplePayload;
|
|
244
|
+
const getRecordExamplePayload = recordExamplePayload;
|
|
245
|
+
const updateRecordExamplePayload = recordExamplePayload;
|
|
246
|
+
const listRecordsExamplePayload = { data: [{
|
|
247
|
+
id: "recZ6qSLw0OCA6Xul",
|
|
248
|
+
createdTime: "2022-06-01T17:50:40.000Z",
|
|
249
|
+
fields: {
|
|
250
|
+
Notes: "We finished this and we're ready to move on to our backlog",
|
|
251
|
+
Status: "Complete",
|
|
252
|
+
"Start date": "2022-05-31",
|
|
253
|
+
Projects: "Updated sales process",
|
|
254
|
+
Priority: "High",
|
|
255
|
+
"Assigned to": "Jane Doe",
|
|
256
|
+
Count: 42
|
|
257
|
+
}
|
|
258
|
+
}, {
|
|
259
|
+
id: "rec3EKPqD9kLdNPx2",
|
|
260
|
+
createdTime: "2022-05-15T14:23:10.000Z",
|
|
261
|
+
fields: {
|
|
262
|
+
Notes: "Initial setup and configuration",
|
|
263
|
+
Status: "In Progress",
|
|
264
|
+
"Start date": "2022-05-15",
|
|
265
|
+
Projects: "New feature development",
|
|
266
|
+
Priority: "Medium",
|
|
267
|
+
"Assigned to": "John Smith",
|
|
268
|
+
Count: 15
|
|
269
|
+
}
|
|
270
|
+
}] };
|
|
271
|
+
const deleteRecordExamplePayload = { data: {
|
|
272
|
+
deleted: true,
|
|
273
|
+
id: "recZ6qSLw0OCA6Xul"
|
|
274
|
+
} };
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/examplePayloads/triggers.ts
|
|
277
|
+
const baseChangesTriggerExamplePayload = {
|
|
278
|
+
payload: {
|
|
279
|
+
headers: {
|
|
280
|
+
Accept: "*/*",
|
|
281
|
+
"Content-Type": "application/json",
|
|
282
|
+
Host: "hooks.example.com",
|
|
283
|
+
"User-Agent": "Airtable Webhooks",
|
|
284
|
+
"X-Airtable-Content-MAC": "hmacSHA256=abc123def456..."
|
|
285
|
+
},
|
|
286
|
+
queryParameters: {},
|
|
287
|
+
rawBody: { data: "<data (1024 bytes)>" },
|
|
288
|
+
body: { data: {
|
|
289
|
+
notification: {
|
|
290
|
+
base: { id: "appABCDEFGHIJKLMN" },
|
|
291
|
+
webhook: { id: "achABCDEFGHIJKLMN" },
|
|
292
|
+
timestamp: "2023-01-15T10:30:00.000Z"
|
|
293
|
+
},
|
|
294
|
+
payloads: [{
|
|
295
|
+
actionMetadata: {
|
|
296
|
+
source: "client",
|
|
297
|
+
sourceMetadata: { user: {
|
|
298
|
+
email: "user@example.com",
|
|
299
|
+
id: "usrABCDEFGHIJKLMN",
|
|
300
|
+
permissionLevel: "create"
|
|
301
|
+
} }
|
|
302
|
+
},
|
|
303
|
+
baseTransactionNumber: 42,
|
|
304
|
+
payloadFormat: "v0",
|
|
305
|
+
timestamp: "2023-01-15T10:30:00.000Z"
|
|
306
|
+
}],
|
|
307
|
+
payloadCount: 1,
|
|
308
|
+
cursor: 43
|
|
309
|
+
} },
|
|
310
|
+
pathFragment: "",
|
|
311
|
+
webhookUrls: { "Base Changes Flow": "https://hooks.example.com/trigger/BASE_CHANGES_TRIGGER" },
|
|
312
|
+
webhookApiKeys: { "Base Changes Flow": ["api-key-base-changes"] },
|
|
313
|
+
invokeUrl: "https://hooks.example.com/trigger/BASE_CHANGES_TRIGGER",
|
|
314
|
+
executionId: "ExecutionResult:base-changes-execution-456",
|
|
315
|
+
customer: {
|
|
316
|
+
id: "Q3VzdG9tZXI6ZXhhbXBsZS1jdXN0b21lci1pZA==",
|
|
317
|
+
name: "Example Customer",
|
|
318
|
+
externalId: "customer-12345"
|
|
319
|
+
},
|
|
320
|
+
instance: {
|
|
321
|
+
id: "SW5zdGFuY2U6ZXhhbXBsZS1pbnN0YW5jZS1pZA==",
|
|
322
|
+
name: "Airtable Base Monitor Instance"
|
|
323
|
+
},
|
|
324
|
+
user: {
|
|
325
|
+
id: "VXNlcjpleGFtcGxlLXVzZXItaWQ=",
|
|
326
|
+
email: "user@example.com",
|
|
327
|
+
name: "Jane Doe",
|
|
328
|
+
externalId: "user-67890"
|
|
329
|
+
},
|
|
330
|
+
integration: {
|
|
331
|
+
name: "Airtable Base Changes",
|
|
332
|
+
id: "SW50ZWdyYXRpb246YWlydGFibGUtYmFzZS1jaGFuZ2Vz",
|
|
333
|
+
versionSequenceId: "1",
|
|
334
|
+
externalVersion: "1.0.0"
|
|
335
|
+
},
|
|
336
|
+
flow: {
|
|
337
|
+
name: "Monitor Base Changes",
|
|
338
|
+
id: "RmxvdzpiYXNlLWNoYW5nZXM=",
|
|
339
|
+
stableId: "YmFzZS1jaGFuZ2VzLXN0YWJsZQ=="
|
|
340
|
+
},
|
|
341
|
+
startedAt: "2023-01-15T10:30:00.000Z",
|
|
342
|
+
globalDebug: false
|
|
343
|
+
},
|
|
344
|
+
branch: "Notification"
|
|
345
|
+
};
|
|
346
|
+
const pollChangesTriggerExamplePayload = { payload: {
|
|
347
|
+
headers: {},
|
|
348
|
+
queryParameters: {},
|
|
349
|
+
rawBody: { data: null },
|
|
350
|
+
body: { data: {
|
|
351
|
+
created: [{
|
|
352
|
+
id: "recABCDEFGHIJ1234",
|
|
353
|
+
createdTime: "2026-05-25T14:30:00.000Z",
|
|
354
|
+
fields: {
|
|
355
|
+
Name: "Acme Spring Launch",
|
|
356
|
+
Status: "Active",
|
|
357
|
+
Notes: "Newly created campaign record."
|
|
358
|
+
}
|
|
359
|
+
}],
|
|
360
|
+
updated: [{
|
|
361
|
+
id: "recXYZWVUTSR5678",
|
|
362
|
+
createdTime: "2026-04-10T09:15:00.000Z",
|
|
363
|
+
fields: {
|
|
364
|
+
Name: "Q1 Holiday Campaign",
|
|
365
|
+
Status: "Completed",
|
|
366
|
+
Notes: "Existing record, modified after last poll."
|
|
367
|
+
}
|
|
368
|
+
}]
|
|
369
|
+
} },
|
|
370
|
+
pathFragment: "",
|
|
371
|
+
webhookUrls: {},
|
|
372
|
+
webhookApiKeys: {},
|
|
373
|
+
invokeUrl: "",
|
|
374
|
+
executionId: "RXhhbXBsZUV4ZWN1dGlvblJlc3VsdElk",
|
|
375
|
+
customer: {
|
|
376
|
+
id: "testCustomerId",
|
|
377
|
+
name: "Test Customer",
|
|
378
|
+
externalId: "testExternalId"
|
|
379
|
+
},
|
|
380
|
+
instance: {
|
|
381
|
+
id: "testInstanceId",
|
|
382
|
+
name: "Test Instance"
|
|
383
|
+
},
|
|
384
|
+
user: {
|
|
385
|
+
id: "testUserId",
|
|
386
|
+
email: "user@example.com",
|
|
387
|
+
name: "Test User",
|
|
388
|
+
externalId: "testUserExternalId"
|
|
389
|
+
},
|
|
390
|
+
integration: {
|
|
391
|
+
id: "testIntegrationId",
|
|
392
|
+
name: "Test Integration",
|
|
393
|
+
versionSequenceId: "1",
|
|
394
|
+
externalVersion: ""
|
|
395
|
+
},
|
|
396
|
+
flow: {
|
|
397
|
+
id: "testFlowId",
|
|
398
|
+
name: "Test Flow",
|
|
399
|
+
stableId: "testFlowStableId"
|
|
400
|
+
},
|
|
401
|
+
startedAt: "2024-01-15T00:00:00.000Z",
|
|
402
|
+
globalDebug: false
|
|
403
|
+
} };
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/examplePayloads/users.ts
|
|
406
|
+
const getCurrentUserExamplePayload = { data: {
|
|
407
|
+
id: "usrL2PNC5o3H4lBEi",
|
|
408
|
+
email: "jane.doe@example.com",
|
|
409
|
+
scopes: [
|
|
410
|
+
"data.records:read",
|
|
411
|
+
"data.records:write",
|
|
412
|
+
"data.recordComments:read",
|
|
413
|
+
"data.recordComments:write",
|
|
414
|
+
"schema.bases:read",
|
|
415
|
+
"schema.bases:write",
|
|
416
|
+
"webhook:manage"
|
|
417
|
+
]
|
|
418
|
+
} };
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/examplePayloads/webhooks.ts
|
|
421
|
+
const listWebhooksExamplePayload = { data: [{
|
|
422
|
+
id: "ach00000000000001",
|
|
423
|
+
specification: { options: { filters: {
|
|
424
|
+
dataTypes: ["tableData"],
|
|
425
|
+
recordChangeScope: "tbltp8DGLhqbUmjK1"
|
|
426
|
+
} } },
|
|
427
|
+
notificationUrl: "https://example.com/receive-webhook",
|
|
428
|
+
cursorForNextPayload: 5,
|
|
429
|
+
areNotificationsEnabled: true,
|
|
430
|
+
lastNotificationResult: {
|
|
431
|
+
success: true,
|
|
432
|
+
completionTimestamp: "2022-02-01T21:25:05.663Z",
|
|
433
|
+
durationMs: 2.603,
|
|
434
|
+
retryNumber: 0
|
|
435
|
+
},
|
|
436
|
+
lastSuccessfulNotificationTime: "2022-02-01T21:25:05.663Z",
|
|
437
|
+
isHookEnabled: true,
|
|
438
|
+
expirationTime: "2023-01-20T00:00:00.000Z"
|
|
439
|
+
}, {
|
|
440
|
+
id: "ach00000000000002",
|
|
441
|
+
specification: { options: { filters: { dataTypes: ["tableData", "tableMetadata"] } } },
|
|
442
|
+
notificationUrl: "https://hooks.example.io/airtable",
|
|
443
|
+
cursorForNextPayload: 12,
|
|
444
|
+
areNotificationsEnabled: true,
|
|
445
|
+
lastNotificationResult: {
|
|
446
|
+
success: true,
|
|
447
|
+
completionTimestamp: "2022-02-15T14:30:22.145Z",
|
|
448
|
+
durationMs: 1.854,
|
|
449
|
+
retryNumber: 0
|
|
450
|
+
},
|
|
451
|
+
lastSuccessfulNotificationTime: "2022-02-15T14:30:22.145Z",
|
|
452
|
+
isHookEnabled: true,
|
|
453
|
+
expirationTime: "2023-02-10T00:00:00.000Z"
|
|
454
|
+
}] };
|
|
455
|
+
const createWebhookExamplePayload = { data: {
|
|
456
|
+
id: "ach00000000000001",
|
|
457
|
+
macSecretBase64: "c3VwZXIgZHVwZXIgc2VjcmV0IGtleSBmb3Igd2ViaG9vaw==",
|
|
458
|
+
expirationTime: "2023-01-20T00:00:00.000Z"
|
|
459
|
+
} };
|
|
460
|
+
const deleteWebhookExamplePayload = { data: {} };
|
|
461
|
+
const refreshWebhookExamplePayload = { data: { expirationTime: "2023-01-30T00:00:00.000Z" } };
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/inputs/common.ts
|
|
464
|
+
const connectionInput = (0, _prismatic_io_spectral.input)({
|
|
465
|
+
label: "Connection",
|
|
466
|
+
type: "connection",
|
|
467
|
+
required: true,
|
|
468
|
+
comments: "The Airtable connection to use."
|
|
469
|
+
});
|
|
470
|
+
const baseIdInput = (0, _prismatic_io_spectral.input)({
|
|
471
|
+
label: "Base ID",
|
|
472
|
+
type: "string",
|
|
473
|
+
required: false,
|
|
474
|
+
example: "appLkNDICXNqxSDhG",
|
|
475
|
+
placeholder: "Enter base ID",
|
|
476
|
+
comments: "The ID of the base to interact with. Required if you use an OAuth connection, and optional if you specify base ID with a legacy API Key connection.",
|
|
477
|
+
clean: _prismatic_io_spectral.util.types.toString,
|
|
478
|
+
dataSource: "selectBase"
|
|
479
|
+
});
|
|
480
|
+
const tableName = (0, _prismatic_io_spectral.input)({
|
|
481
|
+
label: "Table Name",
|
|
482
|
+
type: "string",
|
|
483
|
+
required: true,
|
|
484
|
+
example: "Marketing Campaigns",
|
|
485
|
+
placeholder: "Enter table name",
|
|
486
|
+
comments: "The name of the table to access.",
|
|
487
|
+
dataSource: "selectTable",
|
|
488
|
+
clean: _prismatic_io_spectral.util.types.toString
|
|
489
|
+
});
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/inputs/bases.ts
|
|
492
|
+
const listBasesInputs = { airtableConnection: connectionInput };
|
|
493
|
+
const getBaseSchemaInputs = {
|
|
494
|
+
airtableConnection: connectionInput,
|
|
495
|
+
baseId: baseIdInput
|
|
496
|
+
};
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/inputs/dataSources.ts
|
|
499
|
+
const includeIdInput = (0, _prismatic_io_spectral.input)({
|
|
500
|
+
label: "Include ID in Dropdown",
|
|
501
|
+
type: "boolean",
|
|
502
|
+
required: false,
|
|
503
|
+
default: "false",
|
|
504
|
+
clean: _prismatic_io_spectral.util.types.toBool,
|
|
505
|
+
comments: "When true, the base or table ID is shown next to each dropdown menu option."
|
|
506
|
+
});
|
|
507
|
+
const selectBaseInputs = {
|
|
508
|
+
airtableConnection: connectionInput,
|
|
509
|
+
includeId: includeIdInput
|
|
510
|
+
};
|
|
511
|
+
const selectTableInputs = {
|
|
512
|
+
airtableConnection: connectionInput,
|
|
513
|
+
baseId: {
|
|
514
|
+
...baseIdInput,
|
|
515
|
+
required: true,
|
|
516
|
+
dataSource: void 0
|
|
517
|
+
},
|
|
518
|
+
includeId: includeIdInput
|
|
519
|
+
};
|
|
520
|
+
const selectRecordInputs = {
|
|
521
|
+
airtableConnection: connectionInput,
|
|
522
|
+
baseId: {
|
|
523
|
+
...baseIdInput,
|
|
524
|
+
dataSource: void 0
|
|
525
|
+
},
|
|
526
|
+
tableName: {
|
|
527
|
+
...tableName,
|
|
528
|
+
dataSource: void 0
|
|
529
|
+
},
|
|
530
|
+
includeId: includeIdInput
|
|
531
|
+
};
|
|
532
|
+
const selectWebhookInputs = {
|
|
533
|
+
airtableConnection: connectionInput,
|
|
534
|
+
baseId: {
|
|
535
|
+
...baseIdInput,
|
|
536
|
+
dataSource: void 0
|
|
537
|
+
},
|
|
538
|
+
includeId: includeIdInput
|
|
539
|
+
};
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/inputs/misc.ts
|
|
542
|
+
const { debugRequest: _, ...httpInputs } = _prismatic_io_spectral_http.inputs;
|
|
543
|
+
const rawRequestInputs = {
|
|
544
|
+
connection: connectionInput,
|
|
545
|
+
...httpInputs,
|
|
546
|
+
url: {
|
|
547
|
+
...httpInputs.url,
|
|
548
|
+
comments: "The request path only (e.g., `/v0/meta/bases/{BASE_ID}/tables`). The base URL [https://api.airtable.com](https://api.airtable.com) is already included. For example, to call `https://api.airtable.com/v0/meta/bases/{BASE_ID}/tables`, enter only `/v0/meta/bases/{BASE_ID}/tables` in this field.",
|
|
549
|
+
example: "/v0/meta/bases/{BASE_ID}/tables"
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
//#endregion
|
|
553
|
+
//#region src/util/index.ts
|
|
554
|
+
const getBaseId = (airtableConnection, baseId) => {
|
|
555
|
+
if (!airtableConnection && !baseId) throw new Error("You must specify a base ID");
|
|
556
|
+
return baseId || airtableConnection.fields.base;
|
|
557
|
+
};
|
|
558
|
+
const cleanDynamicValues = (values) => {
|
|
559
|
+
if (values) {
|
|
560
|
+
const parsedJson = _prismatic_io_spectral.util.types.toObject(values);
|
|
561
|
+
if (!Array.isArray(parsedJson)) throw new Error("Dynamic Fields should be an array");
|
|
562
|
+
for (const pair of parsedJson) if (!("key" in pair) || !("value" in pair)) throw new Error("Each item in Dynamic Fields should be a key-value pair");
|
|
563
|
+
return _prismatic_io_spectral.util.types.keyValPairListToObject(parsedJson);
|
|
564
|
+
}
|
|
565
|
+
return {};
|
|
566
|
+
};
|
|
567
|
+
const paginateData = async (client, url, recordArrayName, params, fetchAll) => {
|
|
568
|
+
const records = [];
|
|
569
|
+
let offset = "";
|
|
570
|
+
do {
|
|
571
|
+
const { data } = await client.get(url, { params: {
|
|
572
|
+
...params,
|
|
573
|
+
offset
|
|
574
|
+
} });
|
|
575
|
+
records.push(...data[recordArrayName]);
|
|
576
|
+
offset = data.offset;
|
|
577
|
+
} while (offset && fetchAll);
|
|
578
|
+
return records;
|
|
579
|
+
};
|
|
580
|
+
const needsRefresh = (expirationTime) => {
|
|
581
|
+
const expiration = new Date(expirationTime);
|
|
582
|
+
const now = /* @__PURE__ */ new Date();
|
|
583
|
+
return (expiration.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24) < 7;
|
|
584
|
+
};
|
|
585
|
+
const toStringList = (value) => {
|
|
586
|
+
if (value && Array.isArray(value)) return value;
|
|
587
|
+
return [];
|
|
588
|
+
};
|
|
589
|
+
const toOptionalString = (value) => value ? _prismatic_io_spectral.util.types.toString(value) : void 0;
|
|
590
|
+
function getBase64FromUrl(url) {
|
|
591
|
+
const lastPathSegmentMatch = url.match(/\/([^/]+)$/);
|
|
592
|
+
return lastPathSegmentMatch ? lastPathSegmentMatch[1] : "";
|
|
593
|
+
}
|
|
594
|
+
const resolvePollingRecordChanges = (data) => {
|
|
595
|
+
const changesObject = data ?? {};
|
|
596
|
+
return [...(changesObject.created ?? []).map((record) => ({
|
|
597
|
+
changeType: "created",
|
|
598
|
+
record
|
|
599
|
+
})), ...(changesObject.updated ?? []).map((record) => ({
|
|
600
|
+
changeType: "updated",
|
|
601
|
+
record
|
|
602
|
+
}))];
|
|
603
|
+
};
|
|
604
|
+
const LOOK_BACK_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
605
|
+
const lookBackDateClean = (value) => {
|
|
606
|
+
if (value === void 0 || value === null || typeof value === "string" && value.trim() === "") return "";
|
|
607
|
+
const raw = typeof value === "string" ? value.trim() : String(value);
|
|
608
|
+
const match = typeof value === "string" ? raw.match(LOOK_BACK_DATE_PATTERN) : null;
|
|
609
|
+
if (!match) throw new Error(`Look-back Date must be a date in YYYY-MM-DD format. Received: ${raw}`);
|
|
610
|
+
const [, yearStr, monthStr, dayStr] = match;
|
|
611
|
+
const year = Number(yearStr);
|
|
612
|
+
const month = Number(monthStr);
|
|
613
|
+
const day = Number(dayStr);
|
|
614
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
615
|
+
if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) throw new Error(`Look-back Date must be a date in YYYY-MM-DD format. Received: ${raw}`);
|
|
616
|
+
if (parsed.getTime() > Date.now()) throw new Error(`Look-back Date cannot be a future date. Received: ${raw}`);
|
|
617
|
+
return parsed.toISOString();
|
|
618
|
+
};
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/inputs/records.ts
|
|
621
|
+
const recordIdInput = (0, _prismatic_io_spectral.input)({
|
|
622
|
+
label: "Record ID",
|
|
623
|
+
type: "string",
|
|
624
|
+
required: true,
|
|
625
|
+
example: "rec6r4kNmGDk5D52F",
|
|
626
|
+
placeholder: "Enter record ID",
|
|
627
|
+
comments: "The unique identifier for the record. Functions as the primary key for the row in the table.",
|
|
628
|
+
dataSource: "selectRecord",
|
|
629
|
+
clean: _prismatic_io_spectral.util.types.toString
|
|
630
|
+
});
|
|
631
|
+
const recordFields = (0, _prismatic_io_spectral.input)({
|
|
632
|
+
label: "Record Fields",
|
|
633
|
+
type: "string",
|
|
634
|
+
collection: "keyvaluelist",
|
|
635
|
+
required: false,
|
|
636
|
+
placeholder: "Add field key-value pairs",
|
|
637
|
+
comments: "The key-value pairs to set on the record. Each key maps to a column name in the table.",
|
|
638
|
+
clean: (values) => _prismatic_io_spectral.util.types.keyValPairListToObject(values)
|
|
639
|
+
});
|
|
640
|
+
const dynamicFields = (0, _prismatic_io_spectral.input)({
|
|
641
|
+
label: "Dynamic Fields",
|
|
642
|
+
type: "code",
|
|
643
|
+
language: "json",
|
|
644
|
+
required: false,
|
|
645
|
+
comments: "A JSON array of key-value pairs that can be configured at deploy time with a key-value config variable. When provided, the default 'Record Fields' input is ignored.",
|
|
646
|
+
example: JSON.stringify([{
|
|
647
|
+
key: "name",
|
|
648
|
+
value: "My Example Account"
|
|
649
|
+
}, {
|
|
650
|
+
key: "phone",
|
|
651
|
+
value: "5551234567"
|
|
652
|
+
}], null, 2),
|
|
653
|
+
clean: cleanDynamicValues
|
|
654
|
+
});
|
|
655
|
+
const listRecordsInputs = {
|
|
656
|
+
airtableConnection: connectionInput,
|
|
657
|
+
baseId: baseIdInput,
|
|
658
|
+
tableName,
|
|
659
|
+
view: (0, _prismatic_io_spectral.input)({
|
|
660
|
+
label: "View",
|
|
661
|
+
type: "string",
|
|
662
|
+
example: "Grid view",
|
|
663
|
+
placeholder: "Enter view name or ID",
|
|
664
|
+
required: false,
|
|
665
|
+
comments: "The name or ID of a view in the table. When set, only records in that view are returned, sorted in the order defined by the view.",
|
|
666
|
+
clean: (value) => _prismatic_io_spectral.util.types.toString(value) || void 0
|
|
667
|
+
}),
|
|
668
|
+
fields: (0, _prismatic_io_spectral.input)({
|
|
669
|
+
label: "Fields",
|
|
670
|
+
type: "string",
|
|
671
|
+
example: "Name, Email, Status",
|
|
672
|
+
placeholder: "Enter field names",
|
|
673
|
+
required: false,
|
|
674
|
+
collection: "valuelist",
|
|
675
|
+
comments: "The names (or IDs) of the fields to return. When omitted, all fields are returned.",
|
|
676
|
+
clean: (values) => Array.isArray(values) && values.length ? values.map((field) => _prismatic_io_spectral.util.types.toString(field)) : void 0
|
|
677
|
+
}),
|
|
678
|
+
filterByFormula: (0, _prismatic_io_spectral.input)({
|
|
679
|
+
label: "Filter By Formula",
|
|
680
|
+
type: "string",
|
|
681
|
+
example: "AND({Quantity} * {Price} > 100, NOT({Shipped?}))",
|
|
682
|
+
placeholder: "Enter Airtable formula",
|
|
683
|
+
required: false,
|
|
684
|
+
comments: "An Airtable formula used to limit results to records matching specific criteria.",
|
|
685
|
+
clean: (value) => _prismatic_io_spectral.util.types.toString(value) || void 0
|
|
686
|
+
})
|
|
687
|
+
};
|
|
688
|
+
const getRecordInputs = {
|
|
689
|
+
airtableConnection: connectionInput,
|
|
690
|
+
baseId: baseIdInput,
|
|
691
|
+
tableName,
|
|
692
|
+
record: recordIdInput
|
|
693
|
+
};
|
|
694
|
+
const createRecordInputs = {
|
|
695
|
+
airtableConnection: connectionInput,
|
|
696
|
+
baseId: baseIdInput,
|
|
697
|
+
tableName,
|
|
698
|
+
recordFields,
|
|
699
|
+
dynamicFields
|
|
700
|
+
};
|
|
701
|
+
const updateRecordInputs = {
|
|
702
|
+
airtableConnection: connectionInput,
|
|
703
|
+
baseId: baseIdInput,
|
|
704
|
+
tableName,
|
|
705
|
+
record: recordIdInput,
|
|
706
|
+
recordFields,
|
|
707
|
+
dynamicFields
|
|
708
|
+
};
|
|
709
|
+
const deleteRecordInputs = {
|
|
710
|
+
airtableConnection: connectionInput,
|
|
711
|
+
baseId: baseIdInput,
|
|
712
|
+
tableName,
|
|
713
|
+
recordId: recordIdInput
|
|
714
|
+
};
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region src/inputs/triggers.ts
|
|
717
|
+
const dataTypes = (0, _prismatic_io_spectral.input)({
|
|
718
|
+
label: "Data Types",
|
|
719
|
+
type: "string",
|
|
720
|
+
collection: "valuelist",
|
|
721
|
+
required: true,
|
|
722
|
+
default: ["tableData"],
|
|
723
|
+
model: [
|
|
724
|
+
{
|
|
725
|
+
label: "Table Data (Records)",
|
|
726
|
+
value: "tableData"
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
label: "Table Fields (Schema)",
|
|
730
|
+
value: "tableFields"
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
label: "Table Metadata",
|
|
734
|
+
value: "tableMetadata"
|
|
735
|
+
}
|
|
736
|
+
],
|
|
737
|
+
comments: "Types of changes to monitor.",
|
|
738
|
+
clean: toStringList
|
|
739
|
+
});
|
|
740
|
+
const recordChangeScope = (0, _prismatic_io_spectral.input)({
|
|
741
|
+
label: "Record Change Scope",
|
|
742
|
+
type: "string",
|
|
743
|
+
required: false,
|
|
744
|
+
comments: "Optional table ID or view ID to limit webhook to specific table/view. Leave empty to monitor all tables in base.",
|
|
745
|
+
example: "tblMwMnUJpKoJUDzo",
|
|
746
|
+
placeholder: "Enter table ID or view ID",
|
|
747
|
+
clean: toOptionalString
|
|
748
|
+
});
|
|
749
|
+
const pollView = (0, _prismatic_io_spectral.input)({
|
|
750
|
+
label: "View",
|
|
751
|
+
type: "string",
|
|
752
|
+
example: "Grid view",
|
|
753
|
+
placeholder: "Enter view name or ID",
|
|
754
|
+
required: false,
|
|
755
|
+
comments: "Optional view (name or ID) to scope the poll to. When set, only records in that view are considered.",
|
|
756
|
+
clean: toOptionalString
|
|
757
|
+
});
|
|
758
|
+
const additionalFilter = (0, _prismatic_io_spectral.input)({
|
|
759
|
+
label: "Additional Filter Formula",
|
|
760
|
+
type: "string",
|
|
761
|
+
required: false,
|
|
762
|
+
example: "{Status} = 'Active'",
|
|
763
|
+
placeholder: "Enter Airtable formula",
|
|
764
|
+
comments: "Optional Airtable formula combined (AND) with the modification-time filter applied by the trigger.",
|
|
765
|
+
clean: toOptionalString
|
|
766
|
+
});
|
|
767
|
+
const lookBackDate = (0, _prismatic_io_spectral.input)({
|
|
768
|
+
label: "Look-back Date",
|
|
769
|
+
placeholder: "Enter look-back date (YYYY-MM-DD)",
|
|
770
|
+
type: "string",
|
|
771
|
+
required: false,
|
|
772
|
+
comments: "The date the initial sync starts from, in YYYY-MM-DD format. Cannot be a future date. Leave empty to start from the first recurrence with no backfill. When set, the initial sync seeds each record modified on or after this date once, ignoring the visibility filters.",
|
|
773
|
+
example: "2026-01-01",
|
|
774
|
+
clean: lookBackDateClean
|
|
775
|
+
});
|
|
776
|
+
const showNewRecords = (0, _prismatic_io_spectral.input)({
|
|
777
|
+
label: "Show New Records",
|
|
778
|
+
type: "boolean",
|
|
779
|
+
required: false,
|
|
780
|
+
default: "true",
|
|
781
|
+
comments: "When true, records with a `createdTime` after the last poll are included in the trigger results.",
|
|
782
|
+
clean: _prismatic_io_spectral.util.types.toBool
|
|
783
|
+
});
|
|
784
|
+
const showUpdatedRecords = (0, _prismatic_io_spectral.input)({
|
|
785
|
+
label: "Show Updated Records",
|
|
786
|
+
type: "boolean",
|
|
787
|
+
required: false,
|
|
788
|
+
default: "true",
|
|
789
|
+
comments: "When true, records modified after the last poll (but created earlier) are included in the trigger results.",
|
|
790
|
+
clean: _prismatic_io_spectral.util.types.toBool
|
|
791
|
+
});
|
|
792
|
+
const webhookInputs = {};
|
|
793
|
+
const baseChangesInputs = {
|
|
794
|
+
airtableConnection: connectionInput,
|
|
795
|
+
baseId: {
|
|
796
|
+
...baseIdInput,
|
|
797
|
+
required: true,
|
|
798
|
+
comments: "The ID of the base to interact with."
|
|
799
|
+
},
|
|
800
|
+
dataTypes,
|
|
801
|
+
recordChangeScope
|
|
802
|
+
};
|
|
803
|
+
const pollChangesInputs = {
|
|
804
|
+
airtableConnection: connectionInput,
|
|
805
|
+
baseId: {
|
|
806
|
+
...baseIdInput,
|
|
807
|
+
required: true,
|
|
808
|
+
comments: "The ID of the base containing the table to poll."
|
|
809
|
+
},
|
|
810
|
+
tableName,
|
|
811
|
+
lookBackDate,
|
|
812
|
+
pollView,
|
|
813
|
+
additionalFilter,
|
|
814
|
+
showNewRecords,
|
|
815
|
+
showUpdatedRecords
|
|
816
|
+
};
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/inputs/users.ts
|
|
819
|
+
const getCurrentUserInputs = { airtableConnection: connectionInput };
|
|
820
|
+
//#endregion
|
|
821
|
+
//#region src/inputs/webhooks.ts
|
|
822
|
+
const notificationUrlInput = (0, _prismatic_io_spectral.input)({
|
|
823
|
+
label: "Notification URL",
|
|
824
|
+
type: "string",
|
|
825
|
+
required: false,
|
|
826
|
+
placeholder: "Enter notification URL",
|
|
827
|
+
example: "https://your-webhook-endpoint.com/airtable/notifications",
|
|
828
|
+
comments: "The URL that receives notification pings when the webhook fires.",
|
|
829
|
+
clean: toOptionalString
|
|
830
|
+
});
|
|
831
|
+
const specificationInput = (0, _prismatic_io_spectral.input)({
|
|
832
|
+
label: "Specification",
|
|
833
|
+
type: "code",
|
|
834
|
+
language: "json",
|
|
835
|
+
required: true,
|
|
836
|
+
comments: "A JSON object describing the types of changes the webhook listens for.",
|
|
837
|
+
example: JSON.stringify({ options: { filters: {
|
|
838
|
+
fromSources: ["publicApi"],
|
|
839
|
+
dataTypes: ["tableData"],
|
|
840
|
+
recordChangeScope: "tblMwMnUJpKoJUDzo"
|
|
841
|
+
} } }, null, 2),
|
|
842
|
+
clean: _prismatic_io_spectral.util.types.toObject
|
|
843
|
+
});
|
|
844
|
+
const webhookIdInput = (0, _prismatic_io_spectral.input)({
|
|
845
|
+
label: "Webhook ID",
|
|
846
|
+
type: "string",
|
|
847
|
+
required: true,
|
|
848
|
+
placeholder: "Enter webhook ID",
|
|
849
|
+
example: "ach00000000000001",
|
|
850
|
+
comments: "The unique identifier for the webhook.",
|
|
851
|
+
dataSource: "selectWebhook",
|
|
852
|
+
clean: _prismatic_io_spectral.util.types.toString
|
|
853
|
+
});
|
|
854
|
+
const listWebhooksInputs = {
|
|
855
|
+
airtableConnection: connectionInput,
|
|
856
|
+
baseId: baseIdInput
|
|
857
|
+
};
|
|
858
|
+
const createWebhookInputs = {
|
|
859
|
+
airtableConnection: connectionInput,
|
|
860
|
+
baseId: baseIdInput,
|
|
861
|
+
notificationUrl: notificationUrlInput,
|
|
862
|
+
specification: specificationInput
|
|
863
|
+
};
|
|
864
|
+
const deleteWebhookInputs = {
|
|
865
|
+
airtableConnection: connectionInput,
|
|
866
|
+
baseId: baseIdInput,
|
|
867
|
+
webhookId: webhookIdInput
|
|
868
|
+
};
|
|
869
|
+
const refreshWebhookInputs = {
|
|
870
|
+
airtableConnection: connectionInput,
|
|
871
|
+
baseId: baseIdInput,
|
|
872
|
+
webhookId: webhookIdInput
|
|
873
|
+
};
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/actions/bases/index.ts
|
|
876
|
+
var bases_default = {
|
|
877
|
+
getBaseSchema: (0, _prismatic_io_spectral.action)({
|
|
878
|
+
display: {
|
|
879
|
+
label: "Get Base Schema",
|
|
880
|
+
description: "Retrieve the schema of all tables within a base."
|
|
881
|
+
},
|
|
882
|
+
inputs: getBaseSchemaInputs,
|
|
883
|
+
perform: async (context, params) => {
|
|
884
|
+
const { data } = await createAirtableClient(params.airtableConnection, context.debug.enabled).get(`/v0/meta/bases/${params.baseId}/tables`);
|
|
885
|
+
return { data };
|
|
886
|
+
},
|
|
887
|
+
examplePayload: getBaseSchemaExamplePayload
|
|
888
|
+
}),
|
|
889
|
+
listBases: (0, _prismatic_io_spectral.action)({
|
|
890
|
+
display: {
|
|
891
|
+
label: "List Bases",
|
|
892
|
+
description: "List all bases within the Airtable account."
|
|
893
|
+
},
|
|
894
|
+
inputs: listBasesInputs,
|
|
895
|
+
perform: async (context, params) => {
|
|
896
|
+
return { data: await paginateData(createAirtableClient(params.airtableConnection, context.debug.enabled), "/v0/meta/bases", "bases", {}, true) };
|
|
897
|
+
},
|
|
898
|
+
examplePayload: listBasesExamplePayload
|
|
899
|
+
})
|
|
900
|
+
};
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/actions/misc/index.ts
|
|
903
|
+
var misc_default = { rawRequest: (0, _prismatic_io_spectral.action)({
|
|
904
|
+
display: {
|
|
905
|
+
label: "Raw Request",
|
|
906
|
+
description: "Send a raw HTTP request to Airtable."
|
|
907
|
+
},
|
|
908
|
+
inputs: rawRequestInputs,
|
|
909
|
+
perform: async (context, { connection, ...rest }) => {
|
|
910
|
+
const apiKey = connection.token?.access_token || connection.fields.apiKey;
|
|
911
|
+
const { data, headers } = await (0, _prismatic_io_spectral_http.sendRawRequest)("https://api.airtable.com", {
|
|
912
|
+
...rest,
|
|
913
|
+
debugRequest: context.debug.enabled
|
|
914
|
+
}, { Authorization: `Bearer ${apiKey}` });
|
|
915
|
+
return { data: {
|
|
916
|
+
data,
|
|
917
|
+
headers
|
|
918
|
+
} };
|
|
919
|
+
},
|
|
920
|
+
examplePayload: rawRequestExamplePayload
|
|
921
|
+
}) };
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/actions/records/index.ts
|
|
924
|
+
var records_default = {
|
|
925
|
+
createRecord: (0, _prismatic_io_spectral.action)({
|
|
926
|
+
display: {
|
|
927
|
+
label: "Create Record",
|
|
928
|
+
description: "Create a new record in the specified table."
|
|
929
|
+
},
|
|
930
|
+
perform: async (context, { recordFields, dynamicFields, airtableConnection, baseId, tableName }) => {
|
|
931
|
+
const NO_RECORDS = 0;
|
|
932
|
+
const client = createAirtableClient(airtableConnection, context.debug.enabled);
|
|
933
|
+
const retrievedBaseId = getBaseId(airtableConnection, baseId);
|
|
934
|
+
const fields = Object.keys(dynamicFields)?.length > NO_RECORDS ? dynamicFields : recordFields;
|
|
935
|
+
const { data } = await client.post(`/v0/${retrievedBaseId}/${tableName}`, {
|
|
936
|
+
fields,
|
|
937
|
+
typecast: true
|
|
938
|
+
});
|
|
939
|
+
return { data };
|
|
940
|
+
},
|
|
941
|
+
inputs: createRecordInputs,
|
|
942
|
+
examplePayload: createRecordExamplePayload
|
|
943
|
+
}),
|
|
944
|
+
deleteRecord: (0, _prismatic_io_spectral.action)({
|
|
945
|
+
display: {
|
|
946
|
+
label: "Delete Record",
|
|
947
|
+
description: "Delete a record from the specified table."
|
|
948
|
+
},
|
|
949
|
+
perform: async (context, params) => {
|
|
950
|
+
const client = createAirtableClient(params.airtableConnection);
|
|
951
|
+
const baseId = getBaseId(params.airtableConnection, params.baseId);
|
|
952
|
+
const { data } = await client.delete(`/v0/${baseId}/${params.tableName}/${params.recordId}`);
|
|
953
|
+
return { data };
|
|
954
|
+
},
|
|
955
|
+
inputs: deleteRecordInputs,
|
|
956
|
+
examplePayload: deleteRecordExamplePayload
|
|
957
|
+
}),
|
|
958
|
+
getRecord: (0, _prismatic_io_spectral.action)({
|
|
959
|
+
display: {
|
|
960
|
+
label: "Get Record",
|
|
961
|
+
description: "Retrieve a record by ID from the specified table."
|
|
962
|
+
},
|
|
963
|
+
perform: async (context, params) => {
|
|
964
|
+
const client = createAirtableClient(params.airtableConnection, context.debug.enabled);
|
|
965
|
+
const baseId = getBaseId(params.airtableConnection, params.baseId);
|
|
966
|
+
const { data } = await client.get(`/v0/${baseId}/${params.tableName}/${params.record}`);
|
|
967
|
+
return { data };
|
|
968
|
+
},
|
|
969
|
+
inputs: getRecordInputs,
|
|
970
|
+
examplePayload: getRecordExamplePayload
|
|
971
|
+
}),
|
|
972
|
+
listRecords: (0, _prismatic_io_spectral.action)({
|
|
973
|
+
display: {
|
|
974
|
+
label: "List Records",
|
|
975
|
+
description: "List all records within the specified table."
|
|
976
|
+
},
|
|
977
|
+
perform: async (context, params) => {
|
|
978
|
+
return { data: await paginateData(createAirtableClient(params.airtableConnection, context.debug.enabled), `/v0/${getBaseId(params.airtableConnection, params.baseId)}/${params.tableName}`, "records", {
|
|
979
|
+
view: params.view,
|
|
980
|
+
fields: params.fields,
|
|
981
|
+
filterByFormula: params.filterByFormula
|
|
982
|
+
}, true) };
|
|
983
|
+
},
|
|
984
|
+
inputs: listRecordsInputs,
|
|
985
|
+
examplePayload: listRecordsExamplePayload
|
|
986
|
+
}),
|
|
987
|
+
updateRecord: (0, _prismatic_io_spectral.action)({
|
|
988
|
+
display: {
|
|
989
|
+
label: "Update Record",
|
|
990
|
+
description: "Update the field values of a record in the specified table."
|
|
991
|
+
},
|
|
992
|
+
perform: async (context, { recordFields, record, baseId, tableName, airtableConnection, dynamicFields }) => {
|
|
993
|
+
const NO_RECORDS = 0;
|
|
994
|
+
const client = createAirtableClient(airtableConnection, context.debug.enabled);
|
|
995
|
+
const retrievedBaseId = getBaseId(airtableConnection, baseId);
|
|
996
|
+
const fields = Object.keys(dynamicFields)?.length > NO_RECORDS ? dynamicFields : recordFields;
|
|
997
|
+
const { data } = await client.patch(`/v0/${retrievedBaseId}/${tableName}/${record}`, {
|
|
998
|
+
fields,
|
|
999
|
+
typecast: true
|
|
1000
|
+
});
|
|
1001
|
+
return { data };
|
|
1002
|
+
},
|
|
1003
|
+
inputs: updateRecordInputs,
|
|
1004
|
+
examplePayload: updateRecordExamplePayload
|
|
1005
|
+
})
|
|
1006
|
+
};
|
|
1007
|
+
//#endregion
|
|
1008
|
+
//#region src/actions/users/index.ts
|
|
1009
|
+
var users_default = { getCurrentUser: (0, _prismatic_io_spectral.action)({
|
|
1010
|
+
display: {
|
|
1011
|
+
label: "Get Current User",
|
|
1012
|
+
description: "Retrieve the user ID and OAuth scopes for the current user."
|
|
1013
|
+
},
|
|
1014
|
+
inputs: getCurrentUserInputs,
|
|
1015
|
+
perform: async (context, params) => {
|
|
1016
|
+
const { data } = await createAirtableClient(params.airtableConnection, context.debug.enabled).get("/v0/meta/whoami");
|
|
1017
|
+
return { data };
|
|
1018
|
+
},
|
|
1019
|
+
examplePayload: getCurrentUserExamplePayload
|
|
1020
|
+
}) };
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/actions/webhooks/createWebhook.ts
|
|
1023
|
+
const createWebhook = (0, _prismatic_io_spectral.action)({
|
|
1024
|
+
display: {
|
|
1025
|
+
label: "Create Webhook",
|
|
1026
|
+
description: "Create a new webhook subscription for a base."
|
|
1027
|
+
},
|
|
1028
|
+
perform: async (context, params) => {
|
|
1029
|
+
const client = createAirtableClient(params.airtableConnection, context.debug.enabled);
|
|
1030
|
+
const baseId = getBaseId(params.airtableConnection, params.baseId);
|
|
1031
|
+
const payload = {
|
|
1032
|
+
notificationUrl: params.notificationUrl,
|
|
1033
|
+
specification: params.specification
|
|
1034
|
+
};
|
|
1035
|
+
const { data } = await client.post(`/v0/bases/${baseId}/webhooks`, payload);
|
|
1036
|
+
return { data };
|
|
1037
|
+
},
|
|
1038
|
+
inputs: createWebhookInputs,
|
|
1039
|
+
examplePayload: createWebhookExamplePayload
|
|
1040
|
+
});
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/actions/webhooks/index.ts
|
|
1043
|
+
var webhooks_default = {
|
|
1044
|
+
deleteWebhook: (0, _prismatic_io_spectral.action)({
|
|
1045
|
+
display: {
|
|
1046
|
+
label: "Delete Webhook",
|
|
1047
|
+
description: "Delete a webhook subscription from a base."
|
|
1048
|
+
},
|
|
1049
|
+
perform: async (context, params) => {
|
|
1050
|
+
const client = createAirtableClient(params.airtableConnection, context.debug.enabled);
|
|
1051
|
+
const baseId = getBaseId(params.airtableConnection, params.baseId);
|
|
1052
|
+
await client.delete(`/v0/bases/${baseId}/webhooks/${params.webhookId}`);
|
|
1053
|
+
return { data: {} };
|
|
1054
|
+
},
|
|
1055
|
+
inputs: deleteWebhookInputs,
|
|
1056
|
+
examplePayload: deleteWebhookExamplePayload
|
|
1057
|
+
}),
|
|
1058
|
+
listWebhooks: (0, _prismatic_io_spectral.action)({
|
|
1059
|
+
display: {
|
|
1060
|
+
label: "List Webhooks",
|
|
1061
|
+
description: "List all webhook subscriptions registered for a base."
|
|
1062
|
+
},
|
|
1063
|
+
perform: async (context, params) => {
|
|
1064
|
+
const client = createAirtableClient(params.airtableConnection, context.debug.enabled);
|
|
1065
|
+
const baseId = getBaseId(params.airtableConnection, params.baseId);
|
|
1066
|
+
const { data } = await client.get(`/v0/bases/${baseId}/webhooks`);
|
|
1067
|
+
return { data: data.webhooks };
|
|
1068
|
+
},
|
|
1069
|
+
inputs: listWebhooksInputs,
|
|
1070
|
+
examplePayload: listWebhooksExamplePayload
|
|
1071
|
+
}),
|
|
1072
|
+
createWebhook,
|
|
1073
|
+
refreshWebhook: (0, _prismatic_io_spectral.action)({
|
|
1074
|
+
display: {
|
|
1075
|
+
label: "Refresh Webhook",
|
|
1076
|
+
description: "Extend the expiration of an existing webhook subscription."
|
|
1077
|
+
},
|
|
1078
|
+
perform: async (context, params) => {
|
|
1079
|
+
const client = createAirtableClient(params.airtableConnection, context.debug.enabled);
|
|
1080
|
+
const baseId = getBaseId(params.airtableConnection, params.baseId);
|
|
1081
|
+
const { data } = await client.post(`/v0/bases/${baseId}/webhooks/${params.webhookId}/refresh`);
|
|
1082
|
+
return { data };
|
|
1083
|
+
},
|
|
1084
|
+
inputs: refreshWebhookInputs,
|
|
1085
|
+
examplePayload: refreshWebhookExamplePayload
|
|
1086
|
+
})
|
|
1087
|
+
};
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region src/actions/index.ts
|
|
1090
|
+
var actions_default = {
|
|
1091
|
+
...bases_default,
|
|
1092
|
+
...records_default,
|
|
1093
|
+
...users_default,
|
|
1094
|
+
...misc_default,
|
|
1095
|
+
...webhooks_default
|
|
1096
|
+
};
|
|
1097
|
+
//#endregion
|
|
1098
|
+
//#region src/dataSources/selectBase.ts
|
|
1099
|
+
const selectBase = (0, _prismatic_io_spectral.dataSource)({
|
|
1100
|
+
dataSourceType: "picklist",
|
|
1101
|
+
display: {
|
|
1102
|
+
label: "Select Base",
|
|
1103
|
+
description: "Select an Airtable base from a dropdown."
|
|
1104
|
+
},
|
|
1105
|
+
inputs: selectBaseInputs,
|
|
1106
|
+
perform: async (context, params) => {
|
|
1107
|
+
return { result: (await paginateData(createAirtableClient(params.airtableConnection), "/v0/meta/bases", "bases", {}, true)).map((base) => ({
|
|
1108
|
+
key: base.id,
|
|
1109
|
+
label: params.includeId ? `${base.name} (id: ${base.id})` : base.name
|
|
1110
|
+
})) };
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
//#endregion
|
|
1114
|
+
//#region src/dataSources/selectRecord.ts
|
|
1115
|
+
const selectRecord = (0, _prismatic_io_spectral.dataSource)({
|
|
1116
|
+
dataSourceType: "picklist",
|
|
1117
|
+
display: {
|
|
1118
|
+
label: "Select Record",
|
|
1119
|
+
description: "Select a record from a table."
|
|
1120
|
+
},
|
|
1121
|
+
inputs: selectRecordInputs,
|
|
1122
|
+
perform: async (context, { airtableConnection, baseId, tableName, includeId }) => {
|
|
1123
|
+
return { result: (await paginateData(createAirtableClient(airtableConnection), `/v0/${baseId}/${tableName}`, "records", {}, true)).map((record) => {
|
|
1124
|
+
const label = includeId ? `${record.fields.Name} (id: ${record.id})` : record.fields.Name;
|
|
1125
|
+
return {
|
|
1126
|
+
key: record.id,
|
|
1127
|
+
label: _prismatic_io_spectral.util.types.toString(label)
|
|
1128
|
+
};
|
|
1129
|
+
}) };
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/dataSources/index.ts
|
|
1134
|
+
var dataSources_default = {
|
|
1135
|
+
selectBase,
|
|
1136
|
+
selectTable: (0, _prismatic_io_spectral.dataSource)({
|
|
1137
|
+
dataSourceType: "picklist",
|
|
1138
|
+
display: {
|
|
1139
|
+
label: "Select Table",
|
|
1140
|
+
description: "Select a table from an Airtable base."
|
|
1141
|
+
},
|
|
1142
|
+
inputs: selectTableInputs,
|
|
1143
|
+
perform: async (context, params) => {
|
|
1144
|
+
return { result: (await paginateData(createAirtableClient(params.airtableConnection), `/v0/meta/bases/${params.baseId}/tables`, "tables", {}, true)).map((table) => ({
|
|
1145
|
+
key: table.id,
|
|
1146
|
+
label: params.includeId ? `${table.name} (id: ${table.id})` : table.name
|
|
1147
|
+
})) };
|
|
1148
|
+
}
|
|
1149
|
+
}),
|
|
1150
|
+
selectWebhook: (0, _prismatic_io_spectral.dataSource)({
|
|
1151
|
+
dataSourceType: "picklist",
|
|
1152
|
+
display: {
|
|
1153
|
+
label: "Select Webhook",
|
|
1154
|
+
description: "Select a webhook subscription from an Airtable base."
|
|
1155
|
+
},
|
|
1156
|
+
inputs: selectWebhookInputs,
|
|
1157
|
+
perform: async (context, { airtableConnection, baseId, includeId }) => {
|
|
1158
|
+
const { data } = await createAirtableClient(airtableConnection).get(`/v0/bases/${baseId}/webhooks`);
|
|
1159
|
+
return { result: data.webhooks.map((webhook) => {
|
|
1160
|
+
const label = includeId ? `${webhook.notificationUrl} (id: ${webhook.id})` : webhook.notificationUrl;
|
|
1161
|
+
return {
|
|
1162
|
+
key: webhook.id,
|
|
1163
|
+
label: _prismatic_io_spectral.util.types.toString(label)
|
|
1164
|
+
};
|
|
1165
|
+
}) };
|
|
1166
|
+
}
|
|
1167
|
+
}),
|
|
1168
|
+
selectRecord
|
|
1169
|
+
};
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region src/helpers/triggers/webhookExists.ts
|
|
1172
|
+
const webhookExists = async ({ client, baseId, webhookId }) => {
|
|
1173
|
+
try {
|
|
1174
|
+
await client.get(`/v0/bases/${baseId}/webhooks/${webhookId}/payloads`);
|
|
1175
|
+
return true;
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
const error = e;
|
|
1178
|
+
if (error.message.includes("404")) return false;
|
|
1179
|
+
throw new Error(`Failed to check if webhook exists: ${error.message}`);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
//#endregion
|
|
1183
|
+
//#region src/helpers/triggers/baseChangesCreate.ts
|
|
1184
|
+
const baseChangesCreate = async ({ flow, webhookUrls, crossFlowState, logger, debug }, { airtableConnection, baseId, dataTypes, recordChangeScope }) => {
|
|
1185
|
+
const integrationFlowName = flow.name;
|
|
1186
|
+
const encodedId = getBase64FromUrl(webhookUrls[integrationFlowName]);
|
|
1187
|
+
const baseChangesState = crossFlowState?.[encodedId] || {};
|
|
1188
|
+
let stateWebhookId = baseChangesState.webhookId || "";
|
|
1189
|
+
const stateBaseId = baseChangesState.baseId || "";
|
|
1190
|
+
const stateDataTypes = baseChangesState.dataTypes || [];
|
|
1191
|
+
const stateRecordChangeScope = baseChangesState.recordChangeScope || "";
|
|
1192
|
+
const sameConfig = `${stateBaseId}${stateDataTypes.join(",")}${stateRecordChangeScope}` === `${baseId}${dataTypes.join(",")}${recordChangeScope || ""}`;
|
|
1193
|
+
const client = createAirtableClient(airtableConnection, debug.enabled);
|
|
1194
|
+
if (stateWebhookId) {
|
|
1195
|
+
if (!await webhookExists({
|
|
1196
|
+
client,
|
|
1197
|
+
baseId,
|
|
1198
|
+
webhookId: stateWebhookId
|
|
1199
|
+
})) {
|
|
1200
|
+
logger.info(`Webhook ${stateWebhookId} from state is deleted or expired, resetting`);
|
|
1201
|
+
stateWebhookId = "";
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (stateWebhookId && sameConfig) {
|
|
1205
|
+
logger.info("Webhook already created with this configuration, skipping");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (stateWebhookId && !sameConfig) {
|
|
1209
|
+
logger.info(`Config changed, deleting previous webhook ${stateWebhookId}`);
|
|
1210
|
+
try {
|
|
1211
|
+
await client.delete(`/v0/bases/${stateBaseId}/webhooks/${stateWebhookId}`);
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
if (e.message.includes("404")) logger.info("Webhook already deleted (404), skipping");
|
|
1214
|
+
else throw e;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const webhookUrl = webhookUrls[flow.name];
|
|
1218
|
+
logger.info(`Creating Airtable webhook for base ${baseId} at ${webhookUrl}`);
|
|
1219
|
+
const specification = { options: { filters: { dataTypes: dataTypes || ["tableData"] } } };
|
|
1220
|
+
if (recordChangeScope) specification.options.filters.recordChangeScope = recordChangeScope;
|
|
1221
|
+
const { data: webhook } = await client.post(`/v0/bases/${baseId}/webhooks`, {
|
|
1222
|
+
notificationUrl: webhookUrl,
|
|
1223
|
+
specification
|
|
1224
|
+
});
|
|
1225
|
+
crossFlowState[encodedId] = {
|
|
1226
|
+
webhookId: webhook.id,
|
|
1227
|
+
macSecret: webhook.macSecretBase64,
|
|
1228
|
+
expirationTime: webhook.expirationTime,
|
|
1229
|
+
baseId,
|
|
1230
|
+
cursor: null,
|
|
1231
|
+
dataTypes,
|
|
1232
|
+
recordChangeScope,
|
|
1233
|
+
webhookUrl,
|
|
1234
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1235
|
+
};
|
|
1236
|
+
if (debug.enabled) logger.debug("Stored state", JSON.stringify(crossFlowState[encodedId], null, 2));
|
|
1237
|
+
logger.info(`Airtable webhook created successfully (ID: ${webhook.id}, expires: ${webhook.expirationTime})`);
|
|
1238
|
+
};
|
|
1239
|
+
//#endregion
|
|
1240
|
+
//#region src/helpers/triggers/baseChangesDelete.ts
|
|
1241
|
+
const baseChangesDelete = async ({ flow, webhookUrls, crossFlowState, logger, debug }, { airtableConnection }) => {
|
|
1242
|
+
const integrationFlowName = flow.name;
|
|
1243
|
+
const encodedId = getBase64FromUrl(webhookUrls[integrationFlowName]);
|
|
1244
|
+
const { webhookId, baseId } = crossFlowState?.[encodedId] || {};
|
|
1245
|
+
if (!webhookId) {
|
|
1246
|
+
logger.warn("No webhook ID found in instance state, skipping deletion");
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const client = createAirtableClient(airtableConnection, debug.enabled);
|
|
1250
|
+
logger.info(`Deleting Airtable webhook ${webhookId} from base ${baseId}`);
|
|
1251
|
+
try {
|
|
1252
|
+
await client.delete(`/v0/bases/${baseId}/webhooks/${webhookId}`);
|
|
1253
|
+
logger.info("Airtable webhook deleted successfully");
|
|
1254
|
+
} catch (e) {
|
|
1255
|
+
if (e.message.includes("404")) logger.info("Webhook already deleted (404), skipping");
|
|
1256
|
+
else throw e;
|
|
1257
|
+
} finally {
|
|
1258
|
+
if (crossFlowState[encodedId]) delete crossFlowState[encodedId];
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
//#endregion
|
|
1262
|
+
//#region src/helpers/triggers/autoRefreshWebhook.ts
|
|
1263
|
+
const autoRefreshWebhook = async ({ client, baseId, webhookId, expirationTime, logger, debug }) => {
|
|
1264
|
+
if (needsRefresh(expirationTime)) {
|
|
1265
|
+
if (debug) logger.info(`Webhook expiring soon (${expirationTime}), refreshing...`);
|
|
1266
|
+
try {
|
|
1267
|
+
const { data: refreshData } = await client.post(`/v0/bases/${baseId}/webhooks/${webhookId}/refresh`);
|
|
1268
|
+
if (debug) logger.info(`Webhook expiration refreshed (new expiration: ${refreshData.expirationTime})`);
|
|
1269
|
+
return refreshData.expirationTime;
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
const error = e;
|
|
1272
|
+
if (debug) logger.error(`Failed to refresh webhook: ${error.message}`);
|
|
1273
|
+
return expirationTime;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return expirationTime;
|
|
1277
|
+
};
|
|
1278
|
+
//#endregion
|
|
1279
|
+
//#region src/helpers/triggers/listWebhookPayloads.ts
|
|
1280
|
+
const listWebhookPayloads = async ({ client, baseId, webhookId, lastCursor, logger, debug }) => {
|
|
1281
|
+
const allPayloads = [];
|
|
1282
|
+
let currentCursor = lastCursor;
|
|
1283
|
+
let mightHaveMore = true;
|
|
1284
|
+
if (debug) logger.info(`Fetching webhook payloads (starting cursor: ${currentCursor || "null"})`);
|
|
1285
|
+
while (mightHaveMore) {
|
|
1286
|
+
const url = currentCursor ? `/v0/bases/${baseId}/webhooks/${webhookId}/payloads?cursor=${currentCursor}` : `/v0/bases/${baseId}/webhooks/${webhookId}/payloads`;
|
|
1287
|
+
const { data: payloadData } = await client.get(url);
|
|
1288
|
+
allPayloads.push(...payloadData.payloads);
|
|
1289
|
+
currentCursor = payloadData.cursor;
|
|
1290
|
+
mightHaveMore = payloadData.mightHaveMore;
|
|
1291
|
+
if (debug) logger.debug(`Fetched ${payloadData.payloads.length} payloads (cursor: ${currentCursor}, mightHaveMore: ${mightHaveMore})`);
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
allPayloads,
|
|
1295
|
+
currentCursor
|
|
1296
|
+
};
|
|
1297
|
+
};
|
|
1298
|
+
//#endregion
|
|
1299
|
+
//#region src/helpers/triggers/validateMacSignature.ts
|
|
1300
|
+
const validateMacSignature = ({ macSecret, payload, logger, debug }) => {
|
|
1301
|
+
const macHeader = _prismatic_io_spectral.util.types.lowerCaseHeaders(payload.headers)["x-airtable-content-mac"];
|
|
1302
|
+
if (!macHeader) throw new Error("No MAC header found in request");
|
|
1303
|
+
const macSecretDecoded = Buffer.from(macSecret, "base64");
|
|
1304
|
+
const bodyString = Buffer.isBuffer(payload.rawBody.data) ? payload.rawBody.data.toString("utf8") : JSON.stringify(payload.rawBody.data);
|
|
1305
|
+
const hmac = (0, node_crypto.createHmac)("sha256", macSecretDecoded);
|
|
1306
|
+
hmac.update(bodyString, "utf8");
|
|
1307
|
+
const expectedContentHmac = `hmac-sha256=${hmac.digest("hex")}`;
|
|
1308
|
+
if (debug) logger.debug("MAC validation details", {
|
|
1309
|
+
macHeader,
|
|
1310
|
+
expectedContentHmac,
|
|
1311
|
+
bodyPreview: bodyString.substring(0, 100)
|
|
1312
|
+
});
|
|
1313
|
+
if (macHeader !== expectedContentHmac) throw new Error("Invalid MAC signature - request not from Airtable or using incorrect secret");
|
|
1314
|
+
if (debug) logger.debug("MAC signature validated successfully");
|
|
1315
|
+
};
|
|
1316
|
+
//#endregion
|
|
1317
|
+
//#region src/helpers/triggers/baseChangesPerform.ts
|
|
1318
|
+
const baseChangesPerform = async ({ flow, webhookUrls, crossFlowState, logger, debug }, payload, { airtableConnection, baseId }) => {
|
|
1319
|
+
const integrationFlowName = flow.name;
|
|
1320
|
+
const encodedId = getBase64FromUrl(webhookUrls[integrationFlowName]);
|
|
1321
|
+
const baseChangesState = crossFlowState?.[encodedId] || {};
|
|
1322
|
+
if (debug) logger.debug("Base changes state", JSON.stringify(baseChangesState, null, 2));
|
|
1323
|
+
const { webhookId, expirationTime, cursor: lastCursor, macSecret } = baseChangesState;
|
|
1324
|
+
if (!webhookId || !expirationTime || !macSecret) throw new Error("Missing required instance state properties, cannot process payload");
|
|
1325
|
+
const client = createAirtableClient(airtableConnection, debug.enabled);
|
|
1326
|
+
if (!await webhookExists({
|
|
1327
|
+
client,
|
|
1328
|
+
baseId,
|
|
1329
|
+
webhookId
|
|
1330
|
+
})) throw new Error(`Webhook ${webhookId} is deleted or expired, cannot process payload`);
|
|
1331
|
+
if (_prismatic_io_spectral.util.types.lowerCaseHeaders(payload.headers)["prismatic-invoke-type"] === "Scheduled") {
|
|
1332
|
+
logger.info(`${integrationFlowName} Trigger invoked by schedule - refreshing webhook`);
|
|
1333
|
+
const newExpirationTime = await autoRefreshWebhook({
|
|
1334
|
+
client,
|
|
1335
|
+
baseId,
|
|
1336
|
+
webhookId,
|
|
1337
|
+
expirationTime,
|
|
1338
|
+
logger,
|
|
1339
|
+
debug: debug.enabled
|
|
1340
|
+
});
|
|
1341
|
+
crossFlowState[encodedId] = {
|
|
1342
|
+
...baseChangesState,
|
|
1343
|
+
expirationTime: newExpirationTime
|
|
1344
|
+
};
|
|
1345
|
+
return {
|
|
1346
|
+
payload: {
|
|
1347
|
+
...payload,
|
|
1348
|
+
body: { data: {
|
|
1349
|
+
expirationTime: newExpirationTime,
|
|
1350
|
+
message: "Webhook refreshed"
|
|
1351
|
+
} }
|
|
1352
|
+
},
|
|
1353
|
+
branch: "Refresh"
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
validateMacSignature({
|
|
1357
|
+
macSecret,
|
|
1358
|
+
payload,
|
|
1359
|
+
logger,
|
|
1360
|
+
debug: debug.enabled
|
|
1361
|
+
});
|
|
1362
|
+
const { allPayloads, currentCursor } = await listWebhookPayloads({
|
|
1363
|
+
client,
|
|
1364
|
+
baseId,
|
|
1365
|
+
webhookId,
|
|
1366
|
+
lastCursor,
|
|
1367
|
+
logger,
|
|
1368
|
+
debug: debug.enabled
|
|
1369
|
+
});
|
|
1370
|
+
const newExpirationTime = await autoRefreshWebhook({
|
|
1371
|
+
client,
|
|
1372
|
+
baseId,
|
|
1373
|
+
webhookId,
|
|
1374
|
+
expirationTime,
|
|
1375
|
+
logger,
|
|
1376
|
+
debug: debug.enabled
|
|
1377
|
+
});
|
|
1378
|
+
crossFlowState[encodedId] = {
|
|
1379
|
+
...baseChangesState,
|
|
1380
|
+
cursor: currentCursor,
|
|
1381
|
+
expirationTime: newExpirationTime
|
|
1382
|
+
};
|
|
1383
|
+
if (debug) logger.info(`Retrieved ${allPayloads.length} total payloads, stored cursor: ${currentCursor}`);
|
|
1384
|
+
return {
|
|
1385
|
+
payload: {
|
|
1386
|
+
...payload,
|
|
1387
|
+
body: { data: {
|
|
1388
|
+
notification: payload.body.data,
|
|
1389
|
+
payloads: allPayloads,
|
|
1390
|
+
payloadCount: allPayloads.length,
|
|
1391
|
+
cursor: currentCursor
|
|
1392
|
+
} }
|
|
1393
|
+
},
|
|
1394
|
+
branch: "Notification"
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/triggers/baseChanges.ts
|
|
1399
|
+
const baseChanges = (0, _prismatic_io_spectral.trigger)({
|
|
1400
|
+
display: {
|
|
1401
|
+
label: "Base Change Notifications",
|
|
1402
|
+
description: "Receive base change notifications from Airtable. Automatically creates and manages a webhook subscription when the instance is deployed, and removes the subscription when the instance is deleted."
|
|
1403
|
+
},
|
|
1404
|
+
webhookLifecycleHandlers: {
|
|
1405
|
+
create: baseChangesCreate,
|
|
1406
|
+
delete: baseChangesDelete
|
|
1407
|
+
},
|
|
1408
|
+
perform: baseChangesPerform,
|
|
1409
|
+
inputs: baseChangesInputs,
|
|
1410
|
+
synchronousResponseSupport: "invalid",
|
|
1411
|
+
scheduleSupport: "valid",
|
|
1412
|
+
allowsBranching: true,
|
|
1413
|
+
staticBranchNames: ["Notification", "Refresh"],
|
|
1414
|
+
examplePayload: baseChangesTriggerExamplePayload
|
|
1415
|
+
});
|
|
1416
|
+
const fetchAirtableRecordsSince = async (connection, baseId, tableNameOrId, lastPolledAtIso, debug, maxPages, view, additionalFilter, resumeOffset) => {
|
|
1417
|
+
const client = createAirtableClient(connection, debug);
|
|
1418
|
+
const modifiedFilter = `IS_AFTER(LAST_MODIFIED_TIME(), DATETIME_PARSE("${lastPolledAtIso}"))`;
|
|
1419
|
+
const filterByFormula = additionalFilter ? `AND(${additionalFilter}, ${modifiedFilter})` : modifiedFilter;
|
|
1420
|
+
const records = [];
|
|
1421
|
+
let offset = resumeOffset;
|
|
1422
|
+
let pages = 0;
|
|
1423
|
+
do {
|
|
1424
|
+
const { data } = await client.get(`/v0/${baseId}/${tableNameOrId}`, { params: {
|
|
1425
|
+
filterByFormula,
|
|
1426
|
+
pageSize: 100,
|
|
1427
|
+
...view && { view },
|
|
1428
|
+
...offset && { offset }
|
|
1429
|
+
} });
|
|
1430
|
+
records.push(...data.records ?? []);
|
|
1431
|
+
offset = data.offset;
|
|
1432
|
+
pages++;
|
|
1433
|
+
} while (offset && pages < maxPages);
|
|
1434
|
+
return {
|
|
1435
|
+
records,
|
|
1436
|
+
offset
|
|
1437
|
+
};
|
|
1438
|
+
};
|
|
1439
|
+
const partitionRecordsByTimestamp = (records, boundaryIso, opts) => {
|
|
1440
|
+
const boundary = new Date(boundaryIso);
|
|
1441
|
+
const created = [];
|
|
1442
|
+
const updated = [];
|
|
1443
|
+
for (const record of records) {
|
|
1444
|
+
const createdAt = typeof record.createdTime === "string" ? new Date(record.createdTime) : null;
|
|
1445
|
+
const isNew = createdAt !== null && createdAt > boundary;
|
|
1446
|
+
if (isNew && (opts.isBackfill || opts.showNewRecords)) created.push(record);
|
|
1447
|
+
else if (!isNew && (opts.isBackfill || opts.showUpdatedRecords)) updated.push(record);
|
|
1448
|
+
}
|
|
1449
|
+
return {
|
|
1450
|
+
created,
|
|
1451
|
+
updated
|
|
1452
|
+
};
|
|
1453
|
+
};
|
|
1454
|
+
//#endregion
|
|
1455
|
+
//#region src/triggers/pollChangesTrigger.ts
|
|
1456
|
+
const pollChangesTrigger = (0, _prismatic_io_spectral.pollingTrigger)({
|
|
1457
|
+
display: {
|
|
1458
|
+
label: "New and Updated Records",
|
|
1459
|
+
description: "Retrieves existing and ongoing records in a specified Airtable table. Load history once, check for changes on a schedule, or both."
|
|
1460
|
+
},
|
|
1461
|
+
examplePayload: pollChangesTriggerExamplePayload,
|
|
1462
|
+
inputs: pollChangesInputs,
|
|
1463
|
+
triggerResolverSupport: "valid",
|
|
1464
|
+
batchConfig: { batchSize: 50 },
|
|
1465
|
+
triggerResolver: {
|
|
1466
|
+
resolveItems: (_context, { payload }) => resolvePollingRecordChanges(payload.body.data),
|
|
1467
|
+
getNextPaginationState: (_context, { payload }) => payload.paginationState ?? null
|
|
1468
|
+
},
|
|
1469
|
+
perform: async (context, payload, params) => {
|
|
1470
|
+
const incoming = payload.paginationState;
|
|
1471
|
+
const now = incoming?.drainStartedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1472
|
+
const state = context.polling.getState() ?? {};
|
|
1473
|
+
const seedSince = params.lookBackDate ? new Date(params.lookBackDate).toISOString() : now;
|
|
1474
|
+
const lastPolledAt = state.lastPolledAt ?? seedSince;
|
|
1475
|
+
const isBackfill = state.backfillActive === true || !state.lastPolledAt && Boolean(params.lookBackDate);
|
|
1476
|
+
const maxPages = context.batch?.enabled === true ? 1 : 100;
|
|
1477
|
+
const { records, offset } = await fetchAirtableRecordsSince(params.airtableConnection, params.baseId, params.tableName, lastPolledAt, context.debug.enabled, maxPages, params.pollView, params.additionalFilter, incoming?.offset);
|
|
1478
|
+
const endOfStream = !offset;
|
|
1479
|
+
const result = partitionRecordsByTimestamp(records, lastPolledAt, {
|
|
1480
|
+
isBackfill,
|
|
1481
|
+
showNewRecords: params.showNewRecords !== false,
|
|
1482
|
+
showUpdatedRecords: params.showUpdatedRecords !== false
|
|
1483
|
+
});
|
|
1484
|
+
const nextCursor = endOfStream ? null : {
|
|
1485
|
+
offset,
|
|
1486
|
+
drainStartedAt: now
|
|
1487
|
+
};
|
|
1488
|
+
let advancedCursor = lastPolledAt;
|
|
1489
|
+
if (!endOfStream && context.batch?.enabled !== true) {
|
|
1490
|
+
advancedCursor = records.map((r) => r.createdTime).filter((t) => typeof t === "string").sort().pop() ?? lastPolledAt;
|
|
1491
|
+
context.logger.warn(`Polling truncated at the page cap for table ${params.tableName}. Advancing cursor to ${advancedCursor}; next poll resumes from there.`);
|
|
1492
|
+
}
|
|
1493
|
+
context.polling.setState({
|
|
1494
|
+
lastPolledAt: endOfStream ? now : advancedCursor,
|
|
1495
|
+
...isBackfill && !endOfStream ? { backfillActive: true } : {}
|
|
1496
|
+
});
|
|
1497
|
+
return {
|
|
1498
|
+
payload: {
|
|
1499
|
+
...payload,
|
|
1500
|
+
paginationState: nextCursor ?? void 0,
|
|
1501
|
+
body: { data: result }
|
|
1502
|
+
},
|
|
1503
|
+
polledNoChanges: (result.created?.length ?? 0) === 0 && (result.updated?.length ?? 0) === 0 && nextCursor === null && !incoming
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
//#endregion
|
|
1508
|
+
//#region src/triggers/index.ts
|
|
1509
|
+
var triggers_default = {
|
|
1510
|
+
webhook: (0, _prismatic_io_spectral.trigger)({
|
|
1511
|
+
display: {
|
|
1512
|
+
label: "Webhook",
|
|
1513
|
+
description: "Receive and validate webhook requests from Airtable for manually configured webhook subscriptions."
|
|
1514
|
+
},
|
|
1515
|
+
perform: async (context, payload) => {
|
|
1516
|
+
return Promise.resolve({ payload });
|
|
1517
|
+
},
|
|
1518
|
+
inputs: webhookInputs,
|
|
1519
|
+
synchronousResponseSupport: "invalid",
|
|
1520
|
+
scheduleSupport: "invalid"
|
|
1521
|
+
}),
|
|
1522
|
+
baseChanges,
|
|
1523
|
+
pollChangesTrigger
|
|
1524
|
+
};
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/index.ts
|
|
1527
|
+
var src_default = (0, _prismatic_io_spectral.component)({
|
|
1528
|
+
key: "airtable",
|
|
1529
|
+
documentationUrl: "https://prismatic.io/docs/components/airtable/",
|
|
1530
|
+
public: true,
|
|
1531
|
+
display: {
|
|
1532
|
+
label: "Airtable",
|
|
1533
|
+
description: "Manage records, tables, and bases in Airtable.",
|
|
1534
|
+
iconPath: "icon.png",
|
|
1535
|
+
category: "Data Platforms"
|
|
1536
|
+
},
|
|
1537
|
+
actions: actions_default,
|
|
1538
|
+
connections: connections_default,
|
|
1539
|
+
dataSources: dataSources_default,
|
|
1540
|
+
triggers: triggers_default,
|
|
1541
|
+
hooks: { error: _prismatic_io_spectral_http.handleErrors }
|
|
1542
|
+
});
|
|
1543
|
+
//#endregion
|
|
1544
|
+
//#region npm-dist/.entry.ts
|
|
1545
|
+
var _entry_default = (0, _prismatic_io_spectral.callableComponent)(src_default);
|
|
1546
|
+
//#endregion
|
|
1547
|
+
module.exports = _entry_default;
|