Skip to content

Commit

Permalink
feat(airtable): [nan-1909] add airtable operations (#68)
Browse files Browse the repository at this point in the history
## Describe your changes
Adds airtable syncs, actions.

## Issue ticket number and link
NAN-1909

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] I added tests, otherwise the reason is:
- [ ] External API requests have `retries`
- [ ] Pagination is used where appropriate
- [ ] The built in `nango.paginate` call is used instead of a `while
(true)` loop
- [ ] Third party requests are NOT parallelized (this can cause issues
with rate limits)
- [ ] If a sync requires metadata the `nango.yaml` has `auto_start:
false`
- [ ] If the sync is a `full` sync then `track_deletes: true` is set
  • Loading branch information
khaliqgant authored Oct 22, 2024
1 parent 258306d commit 5d31fe1
Show file tree
Hide file tree
Showing 42 changed files with 1,252 additions and 70 deletions.
122 changes: 121 additions & 1 deletion flows.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,124 @@
integrations:
airtable:
syncs:
tables:
runs: every day
description: >
Lists all tables with their schema for all bases with a reference to
the base id that
the table belongs to
output: Table
track_deletes: true
sync_type: full
endpoint: GET /tables
scopes:
- schema.bases:read
bases:
runs: every day
description: List all bases
output: Base
track_deletes: true
sync_type: full
endpoint: GET /bases
scopes:
- schema.bases:read
actions:
create-webhook:
description: Create a webhook for a particular base
input: CreateWebhook
output: WebhookCreated
endpoint: POST /webhooks
scopes:
- webhook:manage
list-webhooks:
description: List all the webhooks available for a base
output: WebhookResponse
input: BaseId
endpoint: GET /webhooks
scopes:
- webhook:manage
delete-webhook:
description: Delete a webhook
endpoint: DELETE /webhooks
input: DeleteWebhook
output: SuccessResponse
scopes:
- webhook:manage
models:
SuccessResponse:
success: boolean
Table:
baseId: string
baseName: string
id: string
name: string
views: TableView[]
fields: TableField[]
primaryFieldId: string
TableView:
id: string
name: string
type: string
TableField:
id: string
description: string
name: string
type: string
options?:
__string: any
Base:
id: string
name: string
permissionLevel: none | read | comment | edit | create
BaseId:
baseId: string
WebhookResponse:
webhooks: Webhook[]
Webhook:
id: string
areNotificationsEnabled: boolean
cursorForNextPayload: number
isHookEnabled: boolean
lastSuccessfulNotificationTime: string | null
expirationTime?: string | undefined
specification: WebhookSpecification
lastNotificationResult: NotificationResult | null
NotificationResult:
success: boolean
error?:
message: string
completionTimestamp?: string
durationMs?: number
retryNumber?: number
willBeRetried?: boolean
WebhookSpecification:
options:
filters:
recordChangeScope?: string
dataTypes: string[]
changeTypes?: string[]
fromSources?: string[]
sourceOptions?:
formPageSubmission?:
pageId: string
formSubmission?:
viewId: string
watchDataInFieldIds?: string[]
watchSchemasOfFieldIds?: string[]
includes?:
includeCellValuesInFieldIds?: string[] | all
includePreviousCellValues:?: boolean
includePreviousFieldDefinitions?: boolean
CreateWebhook:
baseId: string
specification: WebhookSpecification
WebhookCreated:
id: string
expirationTime: string
DeleteWebhook:
baseId: string
webhookId: string
algolia:
actions:
create-contacts:
Expand Down Expand Up @@ -4997,7 +5117,7 @@ integrations:
backfillPeriodMs: number
OutlookEmail:
id: string
sender: string
sender?: string
recipients?: string | undefined
date: string
subject: string
Expand Down
58 changes: 58 additions & 0 deletions integrations/airtable/actions/create-webhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { NangoAction, ProxyConfiguration, CreateWebhook, WebhookCreated } from '../../models';
import type { AirtableWebhookCreatedResponse } from '../types';
import { createWebhookSchema } from '../schema.zod.js';

export default async function runAction(nango: NangoAction, input: CreateWebhook): Promise<WebhookCreated> {
const parsedInput = createWebhookSchema.safeParse(input);

if (!parsedInput.success) {
for (const error of parsedInput.error.errors) {
await nango.log(`Invalid input provided to create a webhook: ${error.message} at path ${error.path.join('.')}`, { level: 'error' });
}

throw new nango.ActionError({
message: 'Invalid input provided to create a webhook'
});
}

const { baseId, specification } = parsedInput.data;
const webhookUrl = await nango.getWebhookURL();

const config: ProxyConfiguration = {
// https://airtable.com/developers/web/api/create-a-webhook
endpoint: `/v0/bases/${baseId}/webhooks`,
data: {
notificationUrl: webhookUrl,
specification
},
retries: 10
};

const response = await nango.post<AirtableWebhookCreatedResponse>(config);

const { data } = response;

const { expirationTime, id, macSecretBase64 } = data;

const metadata = await nango.getMetadata();

if (metadata?.['webhooks']) {
await nango.updateMetadata({
webhooks: {
...metadata['webhooks'],
[id]: macSecretBase64
}
});
} else {
await nango.updateMetadata({
webhooks: {
[id]: macSecretBase64
}
});
}

return {
id,
expirationTime
};
}
42 changes: 42 additions & 0 deletions integrations/airtable/actions/delete-webhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { NangoAction, ProxyConfiguration, DeleteWebhook, SuccessResponse } from '../../models';
import { deleteWebhookSchema } from '../schema.zod.js';

interface WebhookMetadata {
webhooks: Record<string, string>;
}

export default async function runAction(nango: NangoAction, input: DeleteWebhook): Promise<SuccessResponse> {
const parsedInput = deleteWebhookSchema.safeParse(input);

if (!parsedInput.success) {
for (const error of parsedInput.error.errors) {
await nango.log(`Invalid input provided to delete a webhook: ${error.message} at path ${error.path.join('.')}`, { level: 'error' });
}

throw new nango.ActionError({
message: 'Invalid input provided to delete a webhook'
});
}

const config: ProxyConfiguration = {
// https://airtable.com/developers/web/api/delete-a-webhook
endpoint: `/v0/bases/${input.baseId}/webhooks/${input.webhookId}`,
retries: 10
};

await nango.delete(config);

const metadata = await nango.getMetadata<WebhookMetadata>();

if (metadata?.['webhooks']) {
const { [input.webhookId]: _, ...rest } = metadata['webhooks'];

await nango.updateMetadata({
webhooks: rest
});
}

return {
success: true
};
}
37 changes: 37 additions & 0 deletions integrations/airtable/actions/list-webhooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { NangoAction, ProxyConfiguration, BaseId, Webhook, WebhookResponse } from '../../models';
import type { AirtableWebhook, AirtableWebhookResponse } from '../types';

export default async function runAction(nango: NangoAction, input: BaseId): Promise<WebhookResponse> {
if (!input.baseId) {
throw new nango.ActionError({
message: 'Base ID is required'
});
}

const config: ProxyConfiguration = {
// https://airtable.com/developers/web/api/list-webhooks
endpoint: `/v0/bases/${input.baseId}/webhooks`,
retries: 10
};

const response = await nango.get<AirtableWebhookResponse>(config);

const { data } = response;

const webhookOutput = data.webhooks.map((aWebhook: AirtableWebhook) => {
const webhook: Webhook = {
id: aWebhook.id,
specification: aWebhook.specification,
cursorForNextPayload: aWebhook.cursorForNextPayload,
lastNotificationResult: aWebhook.lastNotificationResult,
areNotificationsEnabled: aWebhook.areNotificationsEnabled,
lastSuccessfulNotificationTime: aWebhook.lastSuccessfulNotificationTime,
isHookEnabled: aWebhook.isHookEnabled,
expirationTime: aWebhook.expirationTime
};

return webhook;
});

return { webhooks: webhookOutput };
}
11 changes: 11 additions & 0 deletions integrations/airtable/fixtures/create-webhook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"baseId": "appaYNlX7K9HdmkDr",
"specification": {
"options": {
"filters": {
"dataTypes": ["tableData"],
"recordChangeScope": "tblQJ3WoaYyxPxm2F"
}
}
}
}
1 change: 1 addition & 0 deletions integrations/airtable/mocks/bases/Base/batchDelete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
12 changes: 12 additions & 0 deletions integrations/airtable/mocks/bases/Base/batchSave.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"id": "apphIX0IHnt7uTvOp",
"name": "Untitled Base",
"permissionLevel": "create"
},
{
"id": "appaYNlX7K9HdmkDr",
"name": "Nango Base",
"permissionLevel": "create"
}
]
11 changes: 11 additions & 0 deletions integrations/airtable/mocks/create-webhook/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"baseId": "appaYNlX7K9HdmkDr",
"specification": {
"options": {
"filters": {
"dataTypes": ["tableData"],
"recordChangeScope": "tblQJ3WoaYyxPxm2F"
}
}
}
}
4 changes: 4 additions & 0 deletions integrations/airtable/mocks/create-webhook/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"id": "achNJQRW9mCgVdRPG",
"expirationTime": "2024-10-29T19:38:20.793Z"
}
4 changes: 4 additions & 0 deletions integrations/airtable/mocks/delete-webhook/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"baseId": "appaYNlX7K9HdmkDr",
"webhookId": "achNJQRW9mCgVdRPG"
}
3 changes: 3 additions & 0 deletions integrations/airtable/mocks/delete-webhook/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"success": true
}
3 changes: 3 additions & 0 deletions integrations/airtable/mocks/list-webhooks/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"baseId": "appaYNlX7K9HdmkDr"
}
26 changes: 26 additions & 0 deletions integrations/airtable/mocks/list-webhooks/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"webhooks": [
{
"id": "achQfrhelikFY4IKb",
"specification": {
"options": {
"filters": {
"recordChangeScope": "tblQJ3WoaYyxPxm2F",
"dataTypes": ["tableData"]
}
}
},
"cursorForNextPayload": 6,
"lastNotificationResult": {
"success": true,
"completionTimestamp": "2024-10-22T13:18:33.391Z",
"durationMs": 2578.119457,
"retryNumber": 0
},
"areNotificationsEnabled": true,
"lastSuccessfulNotificationTime": "2024-10-22T13:18:33.000Z",
"isHookEnabled": true,
"expirationTime": "2024-10-29T13:18:13.002Z"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"data": {
"unique_key": "airtable",
"provider": "airtable",
"display_name": "Airtable",
"logo": "https://app.nango.dev/images/template-logos/airtable.svg",
"webhook_url": null,
"created_at": "2024-10-22T07:50:33.861Z",
"updated_at": "2024-10-22T07:50:33.861Z"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"webhooks": [
{
"id": "achQfrhelikFY4IKb",
"specification": {
"options": {
"filters": {
"recordChangeScope": "tblQJ3WoaYyxPxm2F",
"dataTypes": ["tableData"]
}
}
},
"notificationUrl": "https://h0xqcc9zj1.sharedwithexpose.com/webhook/f790632d-810e-4cbe-a234-ad656418fc70/airtable",
"cursorForNextPayload": 6,
"lastNotificationResult": {
"success": true,
"completionTimestamp": "2024-10-22T13:18:33.391Z",
"durationMs": 2578.119457,
"retryNumber": 0
},
"areNotificationsEnabled": true,
"lastSuccessfulNotificationTime": "2024-10-22T13:18:33.000Z",
"isHookEnabled": true,
"expirationTime": "2024-10-29T13:18:13.002Z"
}
]
}
Loading

0 comments on commit 5d31fe1

Please sign in to comment.