Skip to content

Commit

Permalink
Merge pull request #1551 from automatisch/AUT-599
Browse files Browse the repository at this point in the history
feat(airtable): add airtable integration
  • Loading branch information
barinali committed May 15, 2024
2 parents 9548c93 + d19a455 commit 88468c4
Show file tree
Hide file tree
Showing 25 changed files with 810 additions and 1 deletion.
92 changes: 92 additions & 0 deletions packages/backend/src/apps/airtable/actions/create-record/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import defineAction from '../../../../helpers/define-action.js';

export default defineAction({
name: 'Create record',
key: 'createRecord',
description: 'Creates a new record with fields that automatically populate.',
arguments: [
{
label: 'Base',
key: 'baseId',
type: 'dropdown',
required: true,
description: 'Base in which to create the record.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listBases',
},
],
},
},
{
label: 'Table',
key: 'tableId',
type: 'dropdown',
required: true,
dependsOn: ['parameters.baseId'],
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTables',
},
{
name: 'parameters.baseId',
value: '{parameters.baseId}',
},
],
},
additionalFields: {
type: 'query',
name: 'getDynamicFields',
arguments: [
{
name: 'key',
value: 'listFields',
},
{
name: 'parameters.baseId',
value: '{parameters.baseId}',
},
{
name: 'parameters.tableId',
value: '{parameters.tableId}',
},
],
},
},
],

async run($) {
const { baseId, tableId, ...rest } = $.step.parameters;

const fields = Object.entries(rest).reduce((result, [key, value]) => {
if (Array.isArray(value)) {
result[key] = value.map((item) => item.value);
} else if (value !== '') {
result[key] = value;
}
return result;
}, {});

const body = {
typecast: true,
fields,
};

const { data } = await $.http.post(`/v0/${baseId}/${tableId}`, body);

$.setActionItem({
raw: data,
});
},
});
174 changes: 174 additions & 0 deletions packages/backend/src/apps/airtable/actions/find-record/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import defineAction from '../../../../helpers/define-action.js';
import { URLSearchParams } from 'url';

export default defineAction({
name: 'Find record',
key: 'findRecord',
description:
"Finds a record using simple field search or use Airtable's formula syntax to find a matching record.",
arguments: [
{
label: 'Base',
key: 'baseId',
type: 'dropdown',
required: true,
description: 'Base in which to create the record.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listBases',
},
],
},
},
{
label: 'Table',
key: 'tableId',
type: 'dropdown',
required: true,
dependsOn: ['parameters.baseId'],
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTables',
},
{
name: 'parameters.baseId',
value: '{parameters.baseId}',
},
],
},
},
{
label: 'Search by field',
key: 'tableField',
type: 'dropdown',
required: false,
dependsOn: ['parameters.baseId', 'parameters.tableId'],
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTableFields',
},
{
name: 'parameters.baseId',
value: '{parameters.baseId}',
},
{
name: 'parameters.tableId',
value: '{parameters.tableId}',
},
],
},
},
{
label: 'Search Value',
key: 'searchValue',
type: 'string',
required: false,
variables: true,
description:
'The value of unique identifier for the record. For date values, please use the ISO format (e.g., "YYYY-MM-DD").',
},
{
label: 'Search for exact match?',
key: 'exactMatch',
type: 'dropdown',
required: true,
description: '',
variables: true,
options: [
{ label: 'Yes', value: 'true' },
{ label: 'No', value: 'false' },
],
},
{
label: 'Search Formula',
key: 'searchFormula',
type: 'string',
required: false,
variables: true,
description:
'Instead, you have the option to use an Airtable search formula for locating records according to sophisticated criteria and across various fields.',
},
{
label: 'Limit to View',
key: 'limitToView',
type: 'dropdown',
required: false,
dependsOn: ['parameters.baseId', 'parameters.tableId'],
description:
'You have the choice to restrict the search to a particular view ID if desired.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTableViews',
},
{
name: 'parameters.baseId',
value: '{parameters.baseId}',
},
{
name: 'parameters.tableId',
value: '{parameters.tableId}',
},
],
},
},
],

async run($) {
const {
baseId,
tableId,
tableField,
searchValue,
exactMatch,
searchFormula,
limitToView,
} = $.step.parameters;

let filterByFormula;

if (tableField && searchValue) {
filterByFormula =
exactMatch === 'true'
? `{${tableField}} = '${searchValue}'`
: `LOWER({${tableField}}) = LOWER('${searchValue}')`;
} else {
filterByFormula = searchFormula;
}

const body = new URLSearchParams({
filterByFormula,
view: limitToView,
});

const { data } = await $.http.post(
`/v0/${baseId}/${tableId}/listRecords`,
body
);

$.setActionItem({
raw: data,
});
},
});
4 changes: 4 additions & 0 deletions packages/backend/src/apps/airtable/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import createRecord from './create-record/index.js';
import findRecord from './find-record/index.js';

export default [createRecord, findRecord];
9 changes: 9 additions & 0 deletions packages/backend/src/apps/airtable/assets/favicon.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions packages/backend/src/apps/airtable/auth/generate-auth-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import crypto from 'crypto';
import { URLSearchParams } from 'url';
import authScope from '../common/auth-scope.js';

export default async function generateAuthUrl($) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const state = crypto.randomBytes(100).toString('base64url');
const codeVerifier = crypto.randomBytes(96).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');

const searchParams = new URLSearchParams({
client_id: $.auth.data.clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: authScope.join(' '),
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});

const url = `https://airtable.com/oauth2/v1/authorize?${searchParams.toString()}`;

await $.auth.set({
url,
originalCodeChallenge: codeChallenge,
originalState: state,
codeVerifier,
});
}
48 changes: 48 additions & 0 deletions packages/backend/src/apps/airtable/auth/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import generateAuthUrl from './generate-auth-url.js';
import verifyCredentials from './verify-credentials.js';
import refreshToken from './refresh-token.js';
import isStillVerified from './is-still-verified.js';

export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string',
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/airtable/connections/add',
placeholder: null,
description:
'When asked to input a redirect URL in Airtable, enter the URL above.',
clickToCopy: true,
},
{
key: 'clientId',
label: 'Client ID',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'clientSecret',
label: 'Client Secret',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
],

generateAuthUrl,
verifyCredentials,
isStillVerified,
refreshToken,
};
8 changes: 8 additions & 0 deletions packages/backend/src/apps/airtable/auth/is-still-verified.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import getCurrentUser from '../common/get-current-user.js';

const isStillVerified = async ($) => {
const currentUser = await getCurrentUser($);
return !!currentUser.id;
};

export default isStillVerified;

0 comments on commit 88468c4

Please sign in to comment.