@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.
@@ -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;