@relayfile/adapter-airtable 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/dist/__tests__/airtable-adapter.test.d.ts +2 -0
- package/dist/__tests__/airtable-adapter.test.d.ts.map +1 -0
- package/dist/__tests__/airtable-adapter.test.js +201 -0
- package/dist/__tests__/airtable-adapter.test.js.map +1 -0
- package/dist/__tests__/webhook-normalizer.test.d.ts +2 -0
- package/dist/__tests__/webhook-normalizer.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-normalizer.test.js +102 -0
- package/dist/__tests__/webhook-normalizer.test.js.map +1 -0
- package/dist/airtable-adapter.d.ts +77 -0
- package/dist/airtable-adapter.d.ts.map +1 -0
- package/dist/airtable-adapter.js +705 -0
- package/dist/airtable-adapter.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/path-mapper.d.ts +21 -0
- package/dist/path-mapper.d.ts.map +1 -0
- package/dist/path-mapper.js +116 -0
- package/dist/path-mapper.js.map +1 -0
- package/dist/queries.d.ts +6 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +43 -0
- package/dist/queries.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook-normalizer.d.ts +50 -0
- package/dist/webhook-normalizer.d.ts.map +1 -0
- package/dist/webhook-normalizer.js +501 -0
- package/dist/webhook-normalizer.js.map +1 -0
- package/dist/writeback.d.ts +5 -0
- package/dist/writeback.d.ts.map +1 -0
- package/dist/writeback.js +139 -0
- package/dist/writeback.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { airtableBasePath, airtableRecordPath, airtableTablePath, computeAirtablePath, normalizeAirtableObjectType, } from './path-mapper.js';
|
|
2
|
+
import { AIRTABLE_WEBHOOK_OBJECT_TYPES } from './types.js';
|
|
3
|
+
export class IntegrationAdapter {
|
|
4
|
+
client;
|
|
5
|
+
provider;
|
|
6
|
+
constructor(client, provider) {
|
|
7
|
+
this.client = client;
|
|
8
|
+
this.provider = provider;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
|
|
12
|
+
const SUPPORTED_EVENTS = AIRTABLE_WEBHOOK_OBJECT_TYPES;
|
|
13
|
+
const AIRTABLE_PROVIDER_NAME = 'airtable';
|
|
14
|
+
export class AirtableAdapter extends IntegrationAdapter {
|
|
15
|
+
name = AIRTABLE_PROVIDER_NAME;
|
|
16
|
+
version = '0.1.0';
|
|
17
|
+
config;
|
|
18
|
+
constructor(client, provider, config = {}) {
|
|
19
|
+
super(client, provider);
|
|
20
|
+
this.config = config;
|
|
21
|
+
}
|
|
22
|
+
supportedEvents() {
|
|
23
|
+
return SUPPORTED_EVENTS.flatMap((objectType) => [
|
|
24
|
+
`${objectType}.create`,
|
|
25
|
+
`${objectType}.update`,
|
|
26
|
+
`${objectType}.delete`,
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
async ingestWebhook(workspaceId, event) {
|
|
30
|
+
try {
|
|
31
|
+
const normalized = this.normalizeEvent(event);
|
|
32
|
+
const context = this.resolveContext(normalized.payload);
|
|
33
|
+
const path = computeAirtablePath(normalized.objectType, normalized.objectId, context);
|
|
34
|
+
const semantics = this.computeSemantics(normalized.objectType, normalized.objectId, normalized.payload);
|
|
35
|
+
if (this.isDeleteEvent(normalized)) {
|
|
36
|
+
if (this.client.deleteFile) {
|
|
37
|
+
await this.client.deleteFile({ workspaceId, path });
|
|
38
|
+
return {
|
|
39
|
+
errors: [],
|
|
40
|
+
filesDeleted: 1,
|
|
41
|
+
filesUpdated: 0,
|
|
42
|
+
filesWritten: 0,
|
|
43
|
+
paths: [path],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const deleteResult = await this.client.writeFile({
|
|
47
|
+
workspaceId,
|
|
48
|
+
path,
|
|
49
|
+
content: this.renderContent(workspaceId, normalized, true),
|
|
50
|
+
contentType: JSON_CONTENT_TYPE,
|
|
51
|
+
semantics,
|
|
52
|
+
});
|
|
53
|
+
const counts = inferWriteCounts(deleteResult, true);
|
|
54
|
+
return {
|
|
55
|
+
errors: [],
|
|
56
|
+
filesDeleted: counts.filesDeleted,
|
|
57
|
+
filesUpdated: counts.filesUpdated,
|
|
58
|
+
filesWritten: counts.filesWritten,
|
|
59
|
+
paths: [path],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const writeResult = await this.client.writeFile({
|
|
63
|
+
workspaceId,
|
|
64
|
+
path,
|
|
65
|
+
content: this.renderContent(workspaceId, normalized, false),
|
|
66
|
+
contentType: JSON_CONTENT_TYPE,
|
|
67
|
+
semantics,
|
|
68
|
+
});
|
|
69
|
+
const counts = inferWriteCounts(writeResult, false);
|
|
70
|
+
return {
|
|
71
|
+
errors: [],
|
|
72
|
+
filesDeleted: 0,
|
|
73
|
+
filesUpdated: counts.filesUpdated,
|
|
74
|
+
filesWritten: counts.filesWritten,
|
|
75
|
+
paths: [path],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const fallbackPath = inferFallbackPath(event);
|
|
80
|
+
return {
|
|
81
|
+
errors: [
|
|
82
|
+
{
|
|
83
|
+
error: toErrorMessage(error),
|
|
84
|
+
path: fallbackPath,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
filesDeleted: 0,
|
|
88
|
+
filesUpdated: 0,
|
|
89
|
+
filesWritten: 0,
|
|
90
|
+
paths: fallbackPath ? [fallbackPath] : [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
computePath(objectType, objectId, context = {}) {
|
|
95
|
+
return computeAirtablePath(objectType, objectId, mergeContext(context, this.config));
|
|
96
|
+
}
|
|
97
|
+
computeSemantics(objectType, objectId, payload) {
|
|
98
|
+
const normalizedType = normalizeAirtableObjectType(objectType);
|
|
99
|
+
const context = this.resolveContext(payload);
|
|
100
|
+
const properties = {
|
|
101
|
+
provider: AIRTABLE_PROVIDER_NAME,
|
|
102
|
+
'provider.object_id': objectId,
|
|
103
|
+
'provider.object_type': normalizedType,
|
|
104
|
+
'airtable.id': objectId,
|
|
105
|
+
'airtable.object_type': normalizedType,
|
|
106
|
+
};
|
|
107
|
+
const relations = new Set();
|
|
108
|
+
const comments = [];
|
|
109
|
+
addStringProperty(properties, 'airtable.base_id', context.baseId);
|
|
110
|
+
addStringProperty(properties, 'airtable.table_id', context.tableId);
|
|
111
|
+
const webhook = getRecord(payload._webhook);
|
|
112
|
+
if (webhook) {
|
|
113
|
+
addStringProperty(properties, 'airtable.webhook.action', webhook.action);
|
|
114
|
+
addStringProperty(properties, 'airtable.webhook.delivery_id', webhook.deliveryId);
|
|
115
|
+
addStringProperty(properties, 'airtable.webhook.event_type', webhook.eventType);
|
|
116
|
+
addStringProperty(properties, 'airtable.webhook.object_id', webhook.objectId);
|
|
117
|
+
addStringProperty(properties, 'airtable.webhook.object_type', webhook.objectType);
|
|
118
|
+
addNumberProperty(properties, 'airtable.webhook.timestamp', webhook.webhookTimestamp);
|
|
119
|
+
}
|
|
120
|
+
if (context.baseId && normalizedType !== 'base') {
|
|
121
|
+
relations.add(airtableBasePath(context.baseId));
|
|
122
|
+
}
|
|
123
|
+
if (context.baseId && context.tableId && normalizedType === 'record') {
|
|
124
|
+
relations.add(airtableTablePath(context.baseId, context.tableId));
|
|
125
|
+
}
|
|
126
|
+
switch (normalizedType) {
|
|
127
|
+
case 'record':
|
|
128
|
+
applyRecordSemantics(properties, relations, comments, payload, context);
|
|
129
|
+
break;
|
|
130
|
+
case 'table':
|
|
131
|
+
applyTableSemantics(properties, relations, comments, payload, context);
|
|
132
|
+
break;
|
|
133
|
+
case 'base':
|
|
134
|
+
applyBaseSemantics(properties, relations, payload);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const semantics = {
|
|
138
|
+
properties,
|
|
139
|
+
relations: sortStrings(relations),
|
|
140
|
+
};
|
|
141
|
+
if (comments.length > 0) {
|
|
142
|
+
semantics.comments = comments;
|
|
143
|
+
}
|
|
144
|
+
return compactSemantics(semantics);
|
|
145
|
+
}
|
|
146
|
+
normalizeEvent(event) {
|
|
147
|
+
if (isNormalizedWebhook(event)) {
|
|
148
|
+
const normalized = {
|
|
149
|
+
provider: event.provider || this.config.provider || AIRTABLE_PROVIDER_NAME,
|
|
150
|
+
eventType: canonicalEventType(event.eventType, event.objectType),
|
|
151
|
+
objectType: normalizeAirtableObjectType(event.objectType),
|
|
152
|
+
objectId: event.objectId.trim(),
|
|
153
|
+
payload: event.payload,
|
|
154
|
+
};
|
|
155
|
+
const connectionId = event.connectionId || this.config.connectionId;
|
|
156
|
+
if (connectionId) {
|
|
157
|
+
normalized.connectionId = connectionId;
|
|
158
|
+
}
|
|
159
|
+
return normalized;
|
|
160
|
+
}
|
|
161
|
+
const objectType = normalizeAirtableObjectType(readPayloadObjectType(event));
|
|
162
|
+
const payload = mergeAirtablePayload(event, objectType, this.config);
|
|
163
|
+
const objectId = readObjectId(payload, objectType);
|
|
164
|
+
if (!objectId) {
|
|
165
|
+
throw new Error(`Airtable ${objectType} webhook is missing object id`);
|
|
166
|
+
}
|
|
167
|
+
const action = normalizeAction(asString(event.action) ??
|
|
168
|
+
asString(event.eventType)?.split('.').at(-1) ??
|
|
169
|
+
'update');
|
|
170
|
+
const normalized = {
|
|
171
|
+
provider: this.config.provider || AIRTABLE_PROVIDER_NAME,
|
|
172
|
+
eventType: `${objectType}.${action}`,
|
|
173
|
+
objectType,
|
|
174
|
+
objectId,
|
|
175
|
+
payload,
|
|
176
|
+
};
|
|
177
|
+
if (this.config.connectionId) {
|
|
178
|
+
normalized.connectionId = this.config.connectionId;
|
|
179
|
+
}
|
|
180
|
+
return normalized;
|
|
181
|
+
}
|
|
182
|
+
resolveContext(payload) {
|
|
183
|
+
const base = getRecord(payload.base);
|
|
184
|
+
const table = getRecord(payload.table);
|
|
185
|
+
const data = getRecord(payload.data);
|
|
186
|
+
const dataBase = getRecord(data?.base);
|
|
187
|
+
const dataTable = getRecord(data?.table);
|
|
188
|
+
const context = {};
|
|
189
|
+
const baseId = asString(payload.baseId) ??
|
|
190
|
+
asString(payload.base_id) ??
|
|
191
|
+
asString(base?.id) ??
|
|
192
|
+
asString(data?.baseId) ??
|
|
193
|
+
asString(data?.base_id) ??
|
|
194
|
+
asString(dataBase?.id) ??
|
|
195
|
+
this.config.baseId;
|
|
196
|
+
if (baseId) {
|
|
197
|
+
context.baseId = baseId;
|
|
198
|
+
}
|
|
199
|
+
const tableId = asString(payload.tableId) ??
|
|
200
|
+
asString(payload.table_id) ??
|
|
201
|
+
asString(table?.id) ??
|
|
202
|
+
asString(data?.tableId) ??
|
|
203
|
+
asString(data?.table_id) ??
|
|
204
|
+
asString(dataTable?.id) ??
|
|
205
|
+
this.config.tableId;
|
|
206
|
+
if (tableId) {
|
|
207
|
+
context.tableId = tableId;
|
|
208
|
+
}
|
|
209
|
+
return context;
|
|
210
|
+
}
|
|
211
|
+
isDeleteEvent(event) {
|
|
212
|
+
const action = getWebhookAction(event.payload) ?? getEventAction(event.eventType);
|
|
213
|
+
return action === 'delete';
|
|
214
|
+
}
|
|
215
|
+
renderContent(workspaceId, event, deleted) {
|
|
216
|
+
return stableJson({
|
|
217
|
+
provider: event.provider,
|
|
218
|
+
connectionId: event.connectionId ?? null,
|
|
219
|
+
workspaceId,
|
|
220
|
+
eventType: event.eventType,
|
|
221
|
+
objectType: normalizeAirtableObjectType(event.objectType),
|
|
222
|
+
objectId: event.objectId,
|
|
223
|
+
deleted,
|
|
224
|
+
payload: event.payload,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function applyRecordSemantics(properties, relations, comments, payload, context) {
|
|
229
|
+
const record = payload;
|
|
230
|
+
const fields = getRecord(record.fields);
|
|
231
|
+
addFirstStringProperty(properties, 'airtable.created_time', record.createdTime, record.created_time);
|
|
232
|
+
addFirstStringProperty(properties, 'airtable.updated_time', record.updatedTime, record.updated_time);
|
|
233
|
+
addFirstStringProperty(properties, 'airtable.table_name', record.tableName, record.table_name);
|
|
234
|
+
addNumberProperty(properties, 'airtable.comment_count', record.commentCount);
|
|
235
|
+
if (fields) {
|
|
236
|
+
const fieldEntries = Object.entries(fields)
|
|
237
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
238
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
239
|
+
properties['airtable.field_count'] = String(fieldEntries.length);
|
|
240
|
+
for (const [fieldName, value] of fieldEntries) {
|
|
241
|
+
const key = slugPropertyKey(fieldName);
|
|
242
|
+
const propertyValue = stringifyFieldValue(value);
|
|
243
|
+
if (propertyValue) {
|
|
244
|
+
properties[`airtable.field.${key}`] = propertyValue;
|
|
245
|
+
}
|
|
246
|
+
collectFieldRelations(relations, value, context);
|
|
247
|
+
}
|
|
248
|
+
const recordTitle = firstNonEmptyString(fields.Name, fields.name, fields.Title, fields.title, fields.Subject, fields.subject);
|
|
249
|
+
addStringProperty(properties, 'airtable.record_title', recordTitle);
|
|
250
|
+
const notes = firstNonEmptyString(fields.Notes, fields.notes, fields.Description, fields.description);
|
|
251
|
+
if (notes) {
|
|
252
|
+
comments.push(notes);
|
|
253
|
+
properties['airtable.notes_length'] = String(notes.length);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function applyTableSemantics(properties, relations, comments, payload, context) {
|
|
258
|
+
const table = payload;
|
|
259
|
+
addStringProperty(properties, 'airtable.name', table.name);
|
|
260
|
+
addStringProperty(properties, 'airtable.primary_field_id', table.primaryFieldId);
|
|
261
|
+
addFirstStringProperty(properties, 'airtable.description', table.description);
|
|
262
|
+
addFirstStringProperty(properties, 'airtable.created_time', table.createdTime, table.created_time);
|
|
263
|
+
addFirstStringProperty(properties, 'airtable.updated_time', table.updatedTime, table.updated_time);
|
|
264
|
+
const base = table.base;
|
|
265
|
+
const baseId = asString(base?.id) ?? context.baseId;
|
|
266
|
+
if (baseId) {
|
|
267
|
+
relations.add(airtableBasePath(baseId));
|
|
268
|
+
addStringProperty(properties, 'airtable.base_id', baseId);
|
|
269
|
+
}
|
|
270
|
+
addStringProperty(properties, 'airtable.base_name', base?.name);
|
|
271
|
+
const fields = asFields(table.fields);
|
|
272
|
+
if (fields.length > 0) {
|
|
273
|
+
properties['airtable.field_count'] = String(fields.length);
|
|
274
|
+
addStringListProperty(properties, 'airtable.field_ids', fields.map((field) => field.id));
|
|
275
|
+
addStringListProperty(properties, 'airtable.field_names', fields.map((field) => field.name));
|
|
276
|
+
addStringListProperty(properties, 'airtable.field_types', uniqueStrings(fields.map((field) => field.type).filter(isString)));
|
|
277
|
+
for (const field of fields) {
|
|
278
|
+
const key = slugPropertyKey(field.name);
|
|
279
|
+
addStringProperty(properties, `airtable.schema.${key}.id`, field.id);
|
|
280
|
+
addStringProperty(properties, `airtable.schema.${key}.type`, field.type);
|
|
281
|
+
addStringProperty(properties, `airtable.schema.${key}.description`, field.description);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const views = asViews(table.views);
|
|
285
|
+
if (views.length > 0) {
|
|
286
|
+
properties['airtable.view_count'] = String(views.length);
|
|
287
|
+
addStringListProperty(properties, 'airtable.view_ids', views.map((view) => view.id));
|
|
288
|
+
addStringListProperty(properties, 'airtable.view_names', views.map((view) => view.name));
|
|
289
|
+
}
|
|
290
|
+
const description = asString(table.description);
|
|
291
|
+
if (description) {
|
|
292
|
+
comments.push(description);
|
|
293
|
+
properties['airtable.description_length'] = String(description.length);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function applyBaseSemantics(properties, relations, payload) {
|
|
297
|
+
const base = payload;
|
|
298
|
+
addStringProperty(properties, 'airtable.name', base.name);
|
|
299
|
+
addStringProperty(properties, 'airtable.permission_level', base.permissionLevel);
|
|
300
|
+
addFirstStringProperty(properties, 'airtable.created_time', base.createdTime, base.created_time);
|
|
301
|
+
const workspace = base.workspace;
|
|
302
|
+
addStringProperty(properties, 'airtable.workspace_id', workspace?.id);
|
|
303
|
+
addStringProperty(properties, 'airtable.workspace_name', workspace?.name);
|
|
304
|
+
const tables = asTables(base.tables);
|
|
305
|
+
if (tables.length > 0) {
|
|
306
|
+
properties['airtable.table_count'] = String(tables.length);
|
|
307
|
+
addStringListProperty(properties, 'airtable.table_ids', tables.map((table) => table.id));
|
|
308
|
+
addStringListProperty(properties, 'airtable.table_names', tables.map((table) => table.name).filter(isString));
|
|
309
|
+
const baseId = asString(base.id);
|
|
310
|
+
if (baseId) {
|
|
311
|
+
for (const table of tables) {
|
|
312
|
+
relations.add(airtableTablePath(baseId, table.id));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function collectFieldRelations(relations, value, context) {
|
|
318
|
+
if (!context.baseId || !context.tableId) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (typeof value === 'string' && /^rec[a-zA-Z0-9_-]+$/u.test(value)) {
|
|
322
|
+
relations.add(airtableRecordPath(context.baseId, context.tableId, value));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (Array.isArray(value)) {
|
|
326
|
+
for (const item of value) {
|
|
327
|
+
collectFieldRelations(relations, item, context);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function mergeAirtablePayload(event, objectType, config) {
|
|
332
|
+
const data = getRecord(event.data);
|
|
333
|
+
const payload = getRecord(event.payload);
|
|
334
|
+
const typedPayload = objectType === 'record'
|
|
335
|
+
? getRecord(event.record) ?? data
|
|
336
|
+
: objectType === 'table'
|
|
337
|
+
? getRecord(event.table) ?? data
|
|
338
|
+
: getRecord(event.base) ?? data;
|
|
339
|
+
const merged = {
|
|
340
|
+
...(payload ?? {}),
|
|
341
|
+
...(typedPayload ?? {}),
|
|
342
|
+
...(data ?? {}),
|
|
343
|
+
};
|
|
344
|
+
const explicitId = asString(event.objectId) ?? asString(event.object_id);
|
|
345
|
+
if (explicitId && !asString(merged.id)) {
|
|
346
|
+
merged.id = explicitId;
|
|
347
|
+
}
|
|
348
|
+
const base = getRecord(event.base);
|
|
349
|
+
if (base) {
|
|
350
|
+
merged.base = base;
|
|
351
|
+
}
|
|
352
|
+
const table = getRecord(event.table);
|
|
353
|
+
if (table) {
|
|
354
|
+
merged.table = table;
|
|
355
|
+
}
|
|
356
|
+
const baseId = asString(event.baseId) ?? asString(event.base_id) ?? asString(base?.id) ?? config.baseId;
|
|
357
|
+
if (baseId) {
|
|
358
|
+
merged.baseId = baseId;
|
|
359
|
+
}
|
|
360
|
+
const tableId = asString(event.tableId) ?? asString(event.table_id) ?? asString(table?.id) ?? config.tableId;
|
|
361
|
+
if (tableId) {
|
|
362
|
+
merged.tableId = tableId;
|
|
363
|
+
}
|
|
364
|
+
merged.objectType = objectType;
|
|
365
|
+
merged._webhook = compactObject({
|
|
366
|
+
action: normalizeAction(asString(event.action) ?? asString(event.eventType)?.split('.').at(-1) ?? 'update'),
|
|
367
|
+
eventType: asString(event.eventType),
|
|
368
|
+
objectId: explicitId ?? asString(merged.id),
|
|
369
|
+
objectType,
|
|
370
|
+
webhookTimestamp: asNumber(event.webhookTimestamp) ?? asNumber(event.timestamp),
|
|
371
|
+
});
|
|
372
|
+
return merged;
|
|
373
|
+
}
|
|
374
|
+
function readPayloadObjectType(event) {
|
|
375
|
+
const data = getRecord(event.data);
|
|
376
|
+
const explicit = asString(event.objectType) ??
|
|
377
|
+
asString(event.object_type) ??
|
|
378
|
+
asString(event.type) ??
|
|
379
|
+
asString(event.eventType)?.split('.').at(0) ??
|
|
380
|
+
asString(data?.objectType) ??
|
|
381
|
+
asString(data?.object_type) ??
|
|
382
|
+
asString(data?.type);
|
|
383
|
+
if (explicit) {
|
|
384
|
+
return explicit;
|
|
385
|
+
}
|
|
386
|
+
if (getRecord(event.record) || (data && getRecord(data.fields))) {
|
|
387
|
+
return 'record';
|
|
388
|
+
}
|
|
389
|
+
if (getRecord(event.table) || (data && Array.isArray(data.fields))) {
|
|
390
|
+
return 'table';
|
|
391
|
+
}
|
|
392
|
+
if (getRecord(event.base) || (data && Array.isArray(data.tables))) {
|
|
393
|
+
return 'base';
|
|
394
|
+
}
|
|
395
|
+
throw new Error('Airtable webhook payload is missing object type');
|
|
396
|
+
}
|
|
397
|
+
function readObjectId(payload, objectType) {
|
|
398
|
+
const normalizedType = normalizeAirtableObjectType(objectType);
|
|
399
|
+
const data = getRecord(payload.data);
|
|
400
|
+
const typed = normalizedType === 'record'
|
|
401
|
+
? getRecord(payload.record) ?? data
|
|
402
|
+
: normalizedType === 'table'
|
|
403
|
+
? getRecord(payload.table) ?? data
|
|
404
|
+
: getRecord(payload.base) ?? data;
|
|
405
|
+
return (asString(typed?.id) ??
|
|
406
|
+
asString(payload.id) ??
|
|
407
|
+
asString(payload.objectId) ??
|
|
408
|
+
asString(payload.object_id));
|
|
409
|
+
}
|
|
410
|
+
function isNormalizedWebhook(event) {
|
|
411
|
+
return (typeof event.eventType === 'string' &&
|
|
412
|
+
typeof event.objectType === 'string' &&
|
|
413
|
+
typeof event.objectId === 'string' &&
|
|
414
|
+
isRecord(event.payload));
|
|
415
|
+
}
|
|
416
|
+
function canonicalEventType(eventType, objectType) {
|
|
417
|
+
const parts = eventType.trim().toLowerCase().split(/[.:]/u).filter(Boolean);
|
|
418
|
+
if (parts.length >= 2) {
|
|
419
|
+
return `${normalizeAirtableObjectType(parts[0] ?? objectType)}.${normalizeAction(parts[1] ?? 'update')}`;
|
|
420
|
+
}
|
|
421
|
+
return `${normalizeAirtableObjectType(objectType)}.${normalizeAction(eventType)}`;
|
|
422
|
+
}
|
|
423
|
+
function normalizeAction(action) {
|
|
424
|
+
const normalized = action.trim().toLowerCase();
|
|
425
|
+
switch (normalized) {
|
|
426
|
+
case 'add':
|
|
427
|
+
case 'added':
|
|
428
|
+
case 'create':
|
|
429
|
+
case 'created':
|
|
430
|
+
case 'insert':
|
|
431
|
+
return 'create';
|
|
432
|
+
case 'delete':
|
|
433
|
+
case 'deleted':
|
|
434
|
+
case 'destroy':
|
|
435
|
+
case 'remove':
|
|
436
|
+
case 'removed':
|
|
437
|
+
return 'delete';
|
|
438
|
+
case 'change':
|
|
439
|
+
case 'changed':
|
|
440
|
+
case 'modify':
|
|
441
|
+
case 'modified':
|
|
442
|
+
case 'update':
|
|
443
|
+
case 'updated':
|
|
444
|
+
return 'update';
|
|
445
|
+
default:
|
|
446
|
+
return normalized || 'update';
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function getWebhookAction(payload) {
|
|
450
|
+
const webhook = getRecord(payload._webhook);
|
|
451
|
+
return normalizeOptionalAction(webhook?.action) ?? normalizeOptionalAction(payload.action);
|
|
452
|
+
}
|
|
453
|
+
function getEventAction(eventType) {
|
|
454
|
+
const action = eventType.split(/[.:]/u).filter(Boolean).at(-1);
|
|
455
|
+
return action ? normalizeAction(action) : undefined;
|
|
456
|
+
}
|
|
457
|
+
function normalizeOptionalAction(value) {
|
|
458
|
+
const string = asString(value);
|
|
459
|
+
return string ? normalizeAction(string) : undefined;
|
|
460
|
+
}
|
|
461
|
+
function inferWriteCounts(result, deleted) {
|
|
462
|
+
if (deleted) {
|
|
463
|
+
return {
|
|
464
|
+
filesDeleted: 1,
|
|
465
|
+
filesUpdated: 0,
|
|
466
|
+
filesWritten: result ? 1 : 0,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (!result) {
|
|
470
|
+
return {
|
|
471
|
+
filesDeleted: 0,
|
|
472
|
+
filesUpdated: 0,
|
|
473
|
+
filesWritten: 1,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (result.created || result.status === 'created') {
|
|
477
|
+
return {
|
|
478
|
+
filesDeleted: 0,
|
|
479
|
+
filesUpdated: 0,
|
|
480
|
+
filesWritten: 1,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (result.updated || result.status === 'updated') {
|
|
484
|
+
return {
|
|
485
|
+
filesDeleted: 0,
|
|
486
|
+
filesUpdated: 1,
|
|
487
|
+
filesWritten: 0,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
filesDeleted: 0,
|
|
492
|
+
filesUpdated: 0,
|
|
493
|
+
filesWritten: 1,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function inferFallbackPath(event) {
|
|
497
|
+
try {
|
|
498
|
+
if (isNormalizedWebhook(event)) {
|
|
499
|
+
const objectType = normalizeAirtableObjectType(event.objectType);
|
|
500
|
+
return computeAirtablePath(objectType, event.objectId, contextFromPayload(event.payload));
|
|
501
|
+
}
|
|
502
|
+
const objectType = normalizeAirtableObjectType(readPayloadObjectType(event));
|
|
503
|
+
const payload = mergeAirtablePayload(event, objectType, {});
|
|
504
|
+
const objectId = readObjectId(payload, objectType) ?? 'unknown';
|
|
505
|
+
return computeAirtablePath(objectType, objectId, contextFromPayload(payload));
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return '/airtable/unmapped-webhook.json';
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function asFields(value) {
|
|
512
|
+
if (!Array.isArray(value)) {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
return value.filter(isAirtableField);
|
|
516
|
+
}
|
|
517
|
+
function mergeContext(context, config) {
|
|
518
|
+
const merged = {};
|
|
519
|
+
const baseId = context.baseId ?? config.baseId;
|
|
520
|
+
if (baseId) {
|
|
521
|
+
merged.baseId = baseId;
|
|
522
|
+
}
|
|
523
|
+
const tableId = context.tableId ?? config.tableId;
|
|
524
|
+
if (tableId) {
|
|
525
|
+
merged.tableId = tableId;
|
|
526
|
+
}
|
|
527
|
+
return merged;
|
|
528
|
+
}
|
|
529
|
+
function contextFromPayload(payload) {
|
|
530
|
+
const context = {};
|
|
531
|
+
const baseId = asString(payload.baseId);
|
|
532
|
+
if (baseId) {
|
|
533
|
+
context.baseId = baseId;
|
|
534
|
+
}
|
|
535
|
+
const tableId = asString(payload.tableId);
|
|
536
|
+
if (tableId) {
|
|
537
|
+
context.tableId = tableId;
|
|
538
|
+
}
|
|
539
|
+
return context;
|
|
540
|
+
}
|
|
541
|
+
function isAirtableField(value) {
|
|
542
|
+
if (!isRecord(value)) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
return Boolean(asString(value.id) && asString(value.name));
|
|
546
|
+
}
|
|
547
|
+
function asViews(value) {
|
|
548
|
+
if (!Array.isArray(value)) {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
return value.filter(isAirtableView);
|
|
552
|
+
}
|
|
553
|
+
function isAirtableView(value) {
|
|
554
|
+
if (!isRecord(value)) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
return Boolean(asString(value.id) && asString(value.name));
|
|
558
|
+
}
|
|
559
|
+
function asTables(value) {
|
|
560
|
+
if (!Array.isArray(value)) {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
return value.filter(isAirtableTable);
|
|
564
|
+
}
|
|
565
|
+
function isAirtableTable(value) {
|
|
566
|
+
if (!isRecord(value)) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
return Boolean(asString(value.id));
|
|
570
|
+
}
|
|
571
|
+
function addStringProperty(properties, key, value) {
|
|
572
|
+
const string = asString(value);
|
|
573
|
+
if (string) {
|
|
574
|
+
properties[key] = string;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function addFirstStringProperty(properties, key, ...values) {
|
|
578
|
+
for (const value of values) {
|
|
579
|
+
const string = asString(value);
|
|
580
|
+
if (string) {
|
|
581
|
+
properties[key] = string;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function addNumberProperty(properties, key, value) {
|
|
587
|
+
const number = asNumber(value);
|
|
588
|
+
if (number !== undefined) {
|
|
589
|
+
properties[key] = String(number);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function addStringListProperty(properties, key, values) {
|
|
593
|
+
const cleaned = uniqueStrings(values);
|
|
594
|
+
if (cleaned.length > 0) {
|
|
595
|
+
properties[key] = cleaned.join(', ');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function compactSemantics(semantics) {
|
|
599
|
+
const compacted = {};
|
|
600
|
+
if (semantics.properties && Object.keys(semantics.properties).length > 0) {
|
|
601
|
+
compacted.properties = semantics.properties;
|
|
602
|
+
}
|
|
603
|
+
if (semantics.relations && semantics.relations.length > 0) {
|
|
604
|
+
compacted.relations = semantics.relations;
|
|
605
|
+
}
|
|
606
|
+
if (semantics.permissions && semantics.permissions.length > 0) {
|
|
607
|
+
compacted.permissions = semantics.permissions;
|
|
608
|
+
}
|
|
609
|
+
if (semantics.comments && semantics.comments.length > 0) {
|
|
610
|
+
compacted.comments = semantics.comments;
|
|
611
|
+
}
|
|
612
|
+
return compacted;
|
|
613
|
+
}
|
|
614
|
+
function compactObject(object) {
|
|
615
|
+
const compacted = {};
|
|
616
|
+
for (const [key, value] of Object.entries(object)) {
|
|
617
|
+
if (value !== undefined && value !== null) {
|
|
618
|
+
compacted[key] = value;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return compacted;
|
|
622
|
+
}
|
|
623
|
+
function sortStrings(values) {
|
|
624
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
625
|
+
}
|
|
626
|
+
function uniqueStrings(values) {
|
|
627
|
+
return sortStrings(values.filter(isString).map((value) => value.trim()).filter(Boolean));
|
|
628
|
+
}
|
|
629
|
+
function stringifyFieldValue(value) {
|
|
630
|
+
if (value === null || value === undefined) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
if (typeof value === 'string') {
|
|
634
|
+
return value.trim() || undefined;
|
|
635
|
+
}
|
|
636
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
637
|
+
return String(value);
|
|
638
|
+
}
|
|
639
|
+
if (Array.isArray(value)) {
|
|
640
|
+
const simpleValues = value.map(stringifyFieldValue).filter(isString);
|
|
641
|
+
return simpleValues.length > 0 ? simpleValues.join(', ') : undefined;
|
|
642
|
+
}
|
|
643
|
+
if (isRecord(value)) {
|
|
644
|
+
return stableJson(value);
|
|
645
|
+
}
|
|
646
|
+
return String(value);
|
|
647
|
+
}
|
|
648
|
+
function stableJson(value) {
|
|
649
|
+
return JSON.stringify(sortJson(value), null, 2);
|
|
650
|
+
}
|
|
651
|
+
function sortJson(value) {
|
|
652
|
+
if (Array.isArray(value)) {
|
|
653
|
+
return value.map(sortJson);
|
|
654
|
+
}
|
|
655
|
+
if (isRecord(value)) {
|
|
656
|
+
const sorted = {};
|
|
657
|
+
for (const key of Object.keys(value).sort((left, right) => left.localeCompare(right))) {
|
|
658
|
+
sorted[key] = sortJson(value[key]);
|
|
659
|
+
}
|
|
660
|
+
return sorted;
|
|
661
|
+
}
|
|
662
|
+
return value;
|
|
663
|
+
}
|
|
664
|
+
function slugPropertyKey(value) {
|
|
665
|
+
const slug = value
|
|
666
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
667
|
+
.replace(/^_+|_+$/g, '')
|
|
668
|
+
.toLowerCase();
|
|
669
|
+
return slug || 'unnamed';
|
|
670
|
+
}
|
|
671
|
+
function firstNonEmptyString(...values) {
|
|
672
|
+
for (const value of values) {
|
|
673
|
+
const string = asString(value);
|
|
674
|
+
if (string) {
|
|
675
|
+
return string;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
function asString(value) {
|
|
681
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
682
|
+
}
|
|
683
|
+
function asNumber(value) {
|
|
684
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
685
|
+
return value;
|
|
686
|
+
}
|
|
687
|
+
if (typeof value === 'string' && value.trim()) {
|
|
688
|
+
const parsed = Number(value);
|
|
689
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
690
|
+
}
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
function isString(value) {
|
|
694
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
695
|
+
}
|
|
696
|
+
function getRecord(value) {
|
|
697
|
+
return isRecord(value) ? value : undefined;
|
|
698
|
+
}
|
|
699
|
+
function isRecord(value) {
|
|
700
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
701
|
+
}
|
|
702
|
+
function toErrorMessage(error) {
|
|
703
|
+
return error instanceof Error ? error.message : String(error);
|
|
704
|
+
}
|
|
705
|
+
//# sourceMappingURL=airtable-adapter.js.map
|