Merge pull request #1551 from automatisch/AUT-599
feat(airtable): add airtable integration
This commit is contained in:
@@ -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
packages/backend/src/apps/airtable/actions/find-record/index.js
Normal file
174
packages/backend/src/apps/airtable/actions/find-record/index.js
Normal file
@@ -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
packages/backend/src/apps/airtable/actions/index.js
Normal file
4
packages/backend/src/apps/airtable/actions/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import createRecord from './create-record/index.js';
|
||||||
|
import findRecord from './find-record/index.js';
|
||||||
|
|
||||||
|
export default [createRecord, findRecord];
|
9
packages/backend/src/apps/airtable/assets/favicon.svg
Normal file
9
packages/backend/src/apps/airtable/assets/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="256px" height="215px" viewBox="0 0 256 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path d="M114.25873,2.70101695 L18.8604023,42.1756384 C13.5552723,44.3711638 13.6102328,51.9065311 18.9486282,54.0225085 L114.746142,92.0117514 C123.163769,95.3498757 132.537419,95.3498757 140.9536,92.0117514 L236.75256,54.0225085 C242.08951,51.9065311 242.145916,44.3711638 236.83934,42.1756384 L141.442459,2.70101695 C132.738459,-0.900338983 122.961284,-0.900338983 114.25873,2.70101695" fill="#FFBF00"></path>
|
||||||
|
<path d="M136.349071,112.756863 L136.349071,207.659101 C136.349071,212.173089 140.900664,215.263892 145.096461,213.600615 L251.844122,172.166219 C254.281184,171.200072 255.879376,168.845451 255.879376,166.224705 L255.879376,71.3224678 C255.879376,66.8084791 251.327783,63.7176768 247.131986,65.3809537 L140.384325,106.815349 C137.94871,107.781496 136.349071,110.136118 136.349071,112.756863" fill="#26B5F8"></path>
|
||||||
|
<path d="M111.422771,117.65355 L79.742409,132.949912 L76.5257763,134.504714 L9.65047684,166.548104 C5.4112904,168.593211 0.000578531073,165.503855 0.000578531073,160.794612 L0.000578531073,71.7210757 C0.000578531073,70.0173017 0.874160452,68.5463864 2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill="#ED3049"></path>
|
||||||
|
<path d="M111.422771,117.65355 L79.742409,132.949912 L2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill-opacity="0.25" fill="#000000"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
38
packages/backend/src/apps/airtable/auth/generate-auth-url.js
Normal file
38
packages/backend/src/apps/airtable/auth/generate-auth-url.js
Normal file
@@ -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
packages/backend/src/apps/airtable/auth/index.js
Normal file
48
packages/backend/src/apps/airtable/auth/index.js
Normal file
@@ -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,
|
||||||
|
};
|
@@ -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;
|
40
packages/backend/src/apps/airtable/auth/refresh-token.js
Normal file
40
packages/backend/src/apps/airtable/auth/refresh-token.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { URLSearchParams } from 'node:url';
|
||||||
|
|
||||||
|
import authScope from '../common/auth-scope.js';
|
||||||
|
|
||||||
|
const refreshToken = async ($) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: $.auth.data.clientId,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: $.auth.data.refreshToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const basicAuthToken = Buffer.from(
|
||||||
|
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||||
|
).toString('base64');
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
'https://airtable.com/oauth2/v1/token',
|
||||||
|
params.toString(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Authorization: `Basic ${basicAuthToken}`,
|
||||||
|
},
|
||||||
|
additionalProperties: {
|
||||||
|
skipAddingAuthHeader: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
refreshExpiresIn: data.refresh_expires_in,
|
||||||
|
scope: authScope.join(' '),
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default refreshToken;
|
@@ -0,0 +1,56 @@
|
|||||||
|
import getCurrentUser from '../common/get-current-user.js';
|
||||||
|
|
||||||
|
const verifyCredentials = async ($) => {
|
||||||
|
if ($.auth.data.originalState !== $.auth.data.state) {
|
||||||
|
throw new Error("The 'state' parameter does not match.");
|
||||||
|
}
|
||||||
|
if ($.auth.data.originalCodeChallenge !== $.auth.data.code_challenge) {
|
||||||
|
throw new Error("The 'code challenge' parameter does not match.");
|
||||||
|
}
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value;
|
||||||
|
const basicAuthToken = Buffer.from(
|
||||||
|
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||||
|
).toString('base64');
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
'https://airtable.com/oauth2/v1/token',
|
||||||
|
{
|
||||||
|
code: $.auth.data.code,
|
||||||
|
client_id: $.auth.data.clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code_verifier: $.auth.data.codeVerifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Authorization: `Basic ${basicAuthToken}`,
|
||||||
|
},
|
||||||
|
additionalProperties: {
|
||||||
|
skipAddingAuthHeader: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
clientId: $.auth.data.clientId,
|
||||||
|
clientSecret: $.auth.data.clientSecret,
|
||||||
|
scope: $.auth.data.scope,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
refreshExpiresIn: data.refresh_expires_in,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
screenName: currentUser.email,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
12
packages/backend/src/apps/airtable/common/add-auth-header.js
Normal file
12
packages/backend/src/apps/airtable/common/add-auth-header.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const addAuthHeader = ($, requestConfig) => {
|
||||||
|
if (
|
||||||
|
!requestConfig.additionalProperties?.skipAddingAuthHeader &&
|
||||||
|
$.auth.data?.accessToken
|
||||||
|
) {
|
||||||
|
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
12
packages/backend/src/apps/airtable/common/auth-scope.js
Normal file
12
packages/backend/src/apps/airtable/common/auth-scope.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const authScope = [
|
||||||
|
'data.records:read',
|
||||||
|
'data.records:write',
|
||||||
|
'data.recordComments:read',
|
||||||
|
'data.recordComments:write',
|
||||||
|
'schema.bases:read',
|
||||||
|
'schema.bases:write',
|
||||||
|
'user.email:read',
|
||||||
|
'webhook:manage',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default authScope;
|
@@ -0,0 +1,6 @@
|
|||||||
|
const getCurrentUser = async ($) => {
|
||||||
|
const { data: currentUser } = await $.http.get('/v0/meta/whoami');
|
||||||
|
return currentUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getCurrentUser;
|
6
packages/backend/src/apps/airtable/dynamic-data/index.js
Normal file
6
packages/backend/src/apps/airtable/dynamic-data/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import listBases from './list-bases/index.js';
|
||||||
|
import listTableFields from './list-table-fields/index.js';
|
||||||
|
import listTableViews from './list-table-views/index.js';
|
||||||
|
import listTables from './list-tables/index.js';
|
||||||
|
|
||||||
|
export default [listBases, listTableFields, listTableViews, listTables];
|
@@ -0,0 +1,28 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List bases',
|
||||||
|
key: 'listBases',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const bases = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get('/v0/meta/bases', { params });
|
||||||
|
params.offset = data.offset;
|
||||||
|
|
||||||
|
if (data?.bases) {
|
||||||
|
for (const base of data.bases) {
|
||||||
|
bases.data.push({
|
||||||
|
value: base.id,
|
||||||
|
name: base.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.offset);
|
||||||
|
|
||||||
|
return bases;
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,39 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List table fields',
|
||||||
|
key: 'listTableFields',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const tableFields = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
const { baseId, tableId } = $.step.parameters;
|
||||||
|
|
||||||
|
if (!baseId) {
|
||||||
|
return tableFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
params.offset = data.offset;
|
||||||
|
|
||||||
|
if (data?.tables) {
|
||||||
|
for (const table of data.tables) {
|
||||||
|
if (table.id === tableId) {
|
||||||
|
table.fields.forEach((field) => {
|
||||||
|
tableFields.data.push({
|
||||||
|
value: field.name,
|
||||||
|
name: field.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.offset);
|
||||||
|
|
||||||
|
return tableFields;
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,39 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List table views',
|
||||||
|
key: 'listTableViews',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const tableViews = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
const { baseId, tableId } = $.step.parameters;
|
||||||
|
|
||||||
|
if (!baseId) {
|
||||||
|
return tableViews;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
params.offset = data.offset;
|
||||||
|
|
||||||
|
if (data?.tables) {
|
||||||
|
for (const table of data.tables) {
|
||||||
|
if (table.id === tableId) {
|
||||||
|
table.views.forEach((view) => {
|
||||||
|
tableViews.data.push({
|
||||||
|
value: view.id,
|
||||||
|
name: view.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.offset);
|
||||||
|
|
||||||
|
return tableViews;
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,35 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List tables',
|
||||||
|
key: 'listTables',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const tables = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
const baseId = $.step.parameters.baseId;
|
||||||
|
|
||||||
|
if (!baseId) {
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
params.offset = data.offset;
|
||||||
|
|
||||||
|
if (data?.tables) {
|
||||||
|
for (const table of data.tables) {
|
||||||
|
tables.data.push({
|
||||||
|
value: table.id,
|
||||||
|
name: table.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.offset);
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,3 @@
|
|||||||
|
import listFields from './list-fields/index.js';
|
||||||
|
|
||||||
|
export default [listFields];
|
@@ -0,0 +1,86 @@
|
|||||||
|
const hasValue = (value) => value !== null && value !== undefined;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'List fields',
|
||||||
|
key: 'listFields',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const options = [];
|
||||||
|
const { baseId, tableId } = $.step.parameters;
|
||||||
|
|
||||||
|
if (!hasValue(baseId) || !hasValue(tableId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`);
|
||||||
|
|
||||||
|
const selectedTable = data.tables.find((table) => table.id === tableId);
|
||||||
|
|
||||||
|
if (!selectedTable) return;
|
||||||
|
|
||||||
|
selectedTable.fields.forEach((field) => {
|
||||||
|
if (field.type === 'singleSelect') {
|
||||||
|
options.push({
|
||||||
|
label: field.name,
|
||||||
|
key: field.name,
|
||||||
|
type: 'dropdown',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
options: field.options.choices.map((choice) => ({
|
||||||
|
label: choice.name,
|
||||||
|
value: choice.id,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} else if (field.type === 'multipleSelects') {
|
||||||
|
options.push({
|
||||||
|
label: field.name,
|
||||||
|
key: field.name,
|
||||||
|
type: 'dynamic',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'dropdown',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
options: field.options.choices.map((choice) => ({
|
||||||
|
label: choice.name,
|
||||||
|
value: choice.id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else if (field.type === 'checkbox') {
|
||||||
|
options.push({
|
||||||
|
label: field.name,
|
||||||
|
key: field.name,
|
||||||
|
type: 'dropdown',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Yes',
|
||||||
|
value: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'No',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
options.push({
|
||||||
|
label: field.name,
|
||||||
|
key: field.name,
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
};
|
22
packages/backend/src/apps/airtable/index.js
Normal file
22
packages/backend/src/apps/airtable/index.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import defineApp from '../../helpers/define-app.js';
|
||||||
|
import addAuthHeader from './common/add-auth-header.js';
|
||||||
|
import auth from './auth/index.js';
|
||||||
|
import actions from './actions/index.js';
|
||||||
|
import dynamicData from './dynamic-data/index.js';
|
||||||
|
import dynamicFields from './dynamic-fields/index.js';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Airtable',
|
||||||
|
key: 'airtable',
|
||||||
|
baseUrl: 'https://airtable.com',
|
||||||
|
apiBaseUrl: 'https://api.airtable.com',
|
||||||
|
iconUrl: '{BASE_URL}/apps/airtable/assets/favicon.svg',
|
||||||
|
authDocUrl: '{DOCS_URL}/apps/airtable/connection',
|
||||||
|
primaryColor: 'FFBF00',
|
||||||
|
supportsConnections: true,
|
||||||
|
beforeRequest: [addAuthHeader],
|
||||||
|
auth,
|
||||||
|
actions,
|
||||||
|
dynamicData,
|
||||||
|
dynamicFields,
|
||||||
|
});
|
@@ -26,12 +26,21 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Apps',
|
text: 'Apps',
|
||||||
link: '/apps/carbone/connection',
|
link: '/apps/airtable/connection',
|
||||||
activeMatch: '/apps/',
|
activeMatch: '/apps/',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sidebar: {
|
sidebar: {
|
||||||
'/apps/': [
|
'/apps/': [
|
||||||
|
{
|
||||||
|
text: 'Airtable',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Actions', link: '/apps/airtable/actions' },
|
||||||
|
{ text: 'Connection', link: '/apps/airtable/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Carbone',
|
text: 'Carbone',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
14
packages/docs/pages/apps/airtable/actions.md
Normal file
14
packages/docs/pages/apps/airtable/actions.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/airtable.svg
|
||||||
|
items:
|
||||||
|
- name: Create record
|
||||||
|
desc: Creates a new record with fields that automatically populate.
|
||||||
|
- name: Find record
|
||||||
|
desc: Finds a record using simple field search or use Airtable's formula syntax to find a matching record.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
19
packages/docs/pages/apps/airtable/connection.md
Normal file
19
packages/docs/pages/apps/airtable/connection.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Airtable
|
||||||
|
|
||||||
|
:::info
|
||||||
|
This page explains the steps you need to follow to set up the Airtable
|
||||||
|
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||||
|
:::
|
||||||
|
|
||||||
|
1. Login to your [Airtable account](https://www.airtable.com/).
|
||||||
|
2. Go to this [link](https://airtable.com/create/oauth) and click on the **Register new OAuth integration**.
|
||||||
|
3. Fill the name field.
|
||||||
|
4. Copy **OAuth Redirect URL** from Automatisch to **OAuth redirect URL** field.
|
||||||
|
5. Click on the **Register integration** button.
|
||||||
|
6. In **Developer Details** section, click on the **Generate client secret**.
|
||||||
|
7. Check the checkboxes of **Scopes** section.
|
||||||
|
8. Click on the **Save changes** button.
|
||||||
|
9. Copy **Client ID** to **Client ID** field on Automatisch.
|
||||||
|
10. Copy **Client secret** to **Client secret** field on Automatisch.
|
||||||
|
11. Click **Submit** button on Automatisch.
|
||||||
|
12. Congrats! Start using your new Airtable connection within the flows.
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
The following integrations are currently supported by Automatisch.
|
The following integrations are currently supported by Automatisch.
|
||||||
|
|
||||||
|
- [Airtable](/apps/airtable/actions)
|
||||||
- [Carbone](/apps/carbone/actions)
|
- [Carbone](/apps/carbone/actions)
|
||||||
- [Datastore](/apps/datastore/actions)
|
- [Datastore](/apps/datastore/actions)
|
||||||
- [DeepL](/apps/deepl/actions)
|
- [DeepL](/apps/deepl/actions)
|
||||||
|
9
packages/docs/pages/public/favicons/airtable.svg
Normal file
9
packages/docs/pages/public/favicons/airtable.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="256px" height="215px" viewBox="0 0 256 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path d="M114.25873,2.70101695 L18.8604023,42.1756384 C13.5552723,44.3711638 13.6102328,51.9065311 18.9486282,54.0225085 L114.746142,92.0117514 C123.163769,95.3498757 132.537419,95.3498757 140.9536,92.0117514 L236.75256,54.0225085 C242.08951,51.9065311 242.145916,44.3711638 236.83934,42.1756384 L141.442459,2.70101695 C132.738459,-0.900338983 122.961284,-0.900338983 114.25873,2.70101695" fill="#FFBF00"></path>
|
||||||
|
<path d="M136.349071,112.756863 L136.349071,207.659101 C136.349071,212.173089 140.900664,215.263892 145.096461,213.600615 L251.844122,172.166219 C254.281184,171.200072 255.879376,168.845451 255.879376,166.224705 L255.879376,71.3224678 C255.879376,66.8084791 251.327783,63.7176768 247.131986,65.3809537 L140.384325,106.815349 C137.94871,107.781496 136.349071,110.136118 136.349071,112.756863" fill="#26B5F8"></path>
|
||||||
|
<path d="M111.422771,117.65355 L79.742409,132.949912 L76.5257763,134.504714 L9.65047684,166.548104 C5.4112904,168.593211 0.000578531073,165.503855 0.000578531073,160.794612 L0.000578531073,71.7210757 C0.000578531073,70.0173017 0.874160452,68.5463864 2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill="#ED3049"></path>
|
||||||
|
<path d="M111.422771,117.65355 L79.742409,132.949912 L2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill-opacity="0.25" fill="#000000"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
Reference in New Issue
Block a user