diff --git a/packages/backend/src/apps/appwrite/assets/favicon.svg b/packages/backend/src/apps/appwrite/assets/favicon.svg new file mode 100644 index 00000000..63bf0f23 --- /dev/null +++ b/packages/backend/src/apps/appwrite/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/appwrite/auth/index.js b/packages/backend/src/apps/appwrite/auth/index.js new file mode 100644 index 00000000..dfdd374b --- /dev/null +++ b/packages/backend/src/apps/appwrite/auth/index.js @@ -0,0 +1,65 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'projectId', + label: 'Project ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Project ID of your Appwrite project.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API key of your Appwrite project.', + clickToCopy: false, + }, + { + key: 'instanceUrl', + label: 'Appwrite instance URL', + type: 'string', + required: false, + readOnly: false, + placeholder: '', + description: '', + clickToCopy: true, + }, + { + key: 'host', + label: 'Host Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Host name of your Appwrite project.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/appwrite/auth/is-still-verified.js b/packages/backend/src/apps/appwrite/auth/is-still-verified.js new file mode 100644 index 00000000..6663679a --- /dev/null +++ b/packages/backend/src/apps/appwrite/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/appwrite/auth/verify-credentials.js b/packages/backend/src/apps/appwrite/auth/verify-credentials.js new file mode 100644 index 00000000..3cd61698 --- /dev/null +++ b/packages/backend/src/apps/appwrite/auth/verify-credentials.js @@ -0,0 +1,5 @@ +const verifyCredentials = async ($) => { + await $.http.get('/v1/users'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/appwrite/common/add-auth-header.js b/packages/backend/src/apps/appwrite/common/add-auth-header.js new file mode 100644 index 00000000..1bec6104 --- /dev/null +++ b/packages/backend/src/apps/appwrite/common/add-auth-header.js @@ -0,0 +1,16 @@ +const addAuthHeader = ($, requestConfig) => { + requestConfig.headers['Content-Type'] = 'application/json'; + + if ($.auth.data?.apiKey && $.auth.data?.projectId) { + requestConfig.headers['X-Appwrite-Project'] = $.auth.data.projectId; + requestConfig.headers['X-Appwrite-Key'] = $.auth.data.apiKey; + } + + if ($.auth.data?.host) { + requestConfig.headers['Host'] = $.auth.data.host; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/appwrite/common/set-base-url.js b/packages/backend/src/apps/appwrite/common/set-base-url.js new file mode 100644 index 00000000..35a7a957 --- /dev/null +++ b/packages/backend/src/apps/appwrite/common/set-base-url.js @@ -0,0 +1,13 @@ +const setBaseUrl = ($, requestConfig) => { + const instanceUrl = $.auth.data.instanceUrl; + + if (instanceUrl) { + requestConfig.baseURL = instanceUrl; + } else if ($.app.apiBaseUrl) { + requestConfig.baseURL = $.app.apiBaseUrl; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/appwrite/dynamic-data/index.js b/packages/backend/src/apps/appwrite/dynamic-data/index.js new file mode 100644 index 00000000..45eecdb0 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listCollections from './list-collections/index.js'; +import listDatabases from './list-databases/index.js'; + +export default [listCollections, listDatabases]; diff --git a/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js b/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js new file mode 100644 index 00000000..aa4b1a17 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js @@ -0,0 +1,44 @@ +export default { + name: 'List collections', + key: 'listCollections', + + async run($) { + const collections = { + data: [], + }; + const databaseId = $.step.parameters.databaseId; + + if (!databaseId) { + return collections; + } + + const params = { + queries: [ + JSON.stringify({ + method: 'orderAsc', + atttribute: 'name' + }), + JSON.stringify({ + method: 'limit', + values: [100] + }), + ], + }; + + const { data } = await $.http.get( + `/v1/databases/${databaseId}/collections`, + { params } + ); + + if (data?.collections) { + for (const collection of data.collections) { + collections.data.push({ + value: collection.$id, + name: collection.name, + }); + } + } + + return collections; + }, +}; diff --git a/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js b/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js new file mode 100644 index 00000000..5a815412 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js @@ -0,0 +1,36 @@ +export default { + name: 'List databases', + key: 'listDatabases', + + async run($) { + const databases = { + data: [], + }; + + const params = { + queries: [ + JSON.stringify({ + method: 'orderAsc', + atttribute: 'name' + }), + JSON.stringify({ + method: 'limit', + values: [100] + }), + ], + }; + + const { data } = await $.http.get('/v1/databases', { params }); + + if (data?.databases) { + for (const database of data.databases) { + databases.data.push({ + value: database.$id, + name: database.name, + }); + } + } + + return databases; + }, +}; diff --git a/packages/backend/src/apps/appwrite/index.js b/packages/backend/src/apps/appwrite/index.js new file mode 100644 index 00000000..735c2491 --- /dev/null +++ b/packages/backend/src/apps/appwrite/index.js @@ -0,0 +1,21 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Appwrite', + key: 'appwrite', + baseUrl: 'https://appwrite.io', + apiBaseUrl: 'https://cloud.appwrite.io', + iconUrl: '{BASE_URL}/apps/appwrite/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/appwrite/connection', + primaryColor: 'FD366E', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/appwrite/triggers/index.js b/packages/backend/src/apps/appwrite/triggers/index.js new file mode 100644 index 00000000..30d4b6cc --- /dev/null +++ b/packages/backend/src/apps/appwrite/triggers/index.js @@ -0,0 +1,3 @@ +import newDocuments from './new-documents/index.js'; + +export default [newDocuments]; diff --git a/packages/backend/src/apps/appwrite/triggers/new-documents/index.js b/packages/backend/src/apps/appwrite/triggers/new-documents/index.js new file mode 100644 index 00000000..6c5a7e5f --- /dev/null +++ b/packages/backend/src/apps/appwrite/triggers/new-documents/index.js @@ -0,0 +1,103 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New documents', + key: 'newDocuments', + pollInterval: 15, + description: 'Triggers when a new document is created.', + arguments: [ + { + label: 'Database', + key: 'databaseId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDatabases', + }, + ], + }, + }, + { + label: 'Collection', + key: 'collectionId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.databaseId'], + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCollections', + }, + { + name: 'parameters.databaseId', + value: '{parameters.databaseId}', + }, + ], + }, + }, + ], + + async run($) { + const { databaseId, collectionId } = $.step.parameters; + + const limit = 1; + let lastDocumentId = undefined; + let offset = 0; + let documentCount = 0; + + do { + const params = { + queries: [ + JSON.stringify({ + method: 'orderDesc', + atttribute: '$createdAt' + }), + JSON.stringify({ + method: 'limit', + values: [limit] + }), + // An invalid cursor shouldn't be sent. + lastDocumentId && JSON.stringify({ + method: 'cursorAfter', + values: [lastDocumentId] + }) + ].filter(Boolean), + }; + + const { data } = await $.http.get( + `/v1/databases/${databaseId}/collections/${collectionId}/documents`, + { params }, + ); + + const documents = data?.documents; + documentCount = documents?.length; + offset = offset + limit; + lastDocumentId = documents[documentCount - 1]?.$id; + + if (!documentCount) { + return; + } + + for (const document of documents) { + $.pushTriggerItem({ + raw: document, + meta: { + internalId: document.$id, + }, + }); + } + } while (documentCount === limit); + }, +}); diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 462fec41..219a1d51 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -41,6 +41,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/airtable/connection' }, ], }, + { + text: 'Appwrite', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/appwrite/triggers' }, + { text: 'Connection', link: '/apps/appwrite/connection' }, + ], + }, { text: 'Carbone', collapsible: true, diff --git a/packages/docs/pages/apps/appwrite/connection.md b/packages/docs/pages/apps/appwrite/connection.md new file mode 100644 index 00000000..1a0d91c4 --- /dev/null +++ b/packages/docs/pages/apps/appwrite/connection.md @@ -0,0 +1,20 @@ +# Appwrite + +:::info +This page explains the steps you need to follow to set up the Appwrite +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Login to your Appwrite account: [https://appwrite.io/](https://appwrite.io/). +2. Go to your project's **Settings**. +3. In the Settings, click on the **View API Keys** button in **API credentials** section. +4. Click on the **Create API Key** button. +5. Fill the name field and select **Never** for the expiration date. +6. Click on the **Next** button. +7. Click on the **Select all** and then click on the **Create** button. +8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch. +9. Write any screen name to be displayed in Automatisch. +10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatsich. +11. If you are using self-hosted Appwrite project, you can paste the instace url into **Appwrite instance URL** field in Automatisch. +12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL. +13. Start using Appwrite integration with Automatisch! diff --git a/packages/docs/pages/apps/appwrite/triggers.md b/packages/docs/pages/apps/appwrite/triggers.md new file mode 100644 index 00000000..2177a8a9 --- /dev/null +++ b/packages/docs/pages/apps/appwrite/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/appwrite.svg +items: + - name: New documets + desc: Triggers when a new document is created. +--- + + + + diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md index 40d2ad18..ad2b4c0c 100644 --- a/packages/docs/pages/guide/available-apps.md +++ b/packages/docs/pages/guide/available-apps.md @@ -3,6 +3,7 @@ The following integrations are currently supported by Automatisch. - [Airtable](/apps/airtable/actions) +- [Appwrite](/apps/appwrite/triggers) - [Carbone](/apps/carbone/actions) - [Datastore](/apps/datastore/actions) - [DeepL](/apps/deepl/actions) diff --git a/packages/docs/pages/public/favicons/appwrite.svg b/packages/docs/pages/public/favicons/appwrite.svg new file mode 100644 index 00000000..63bf0f23 --- /dev/null +++ b/packages/docs/pages/public/favicons/appwrite.svg @@ -0,0 +1 @@ + \ No newline at end of file