From 9dc82290b5273fd01269bfce578028306098fd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Thu, 25 Jan 2024 12:40:08 +0300 Subject: [PATCH 1/6] feat(appwrite): add appwrite integration --- .../src/apps/appwrite/assets/favicon.svg | 1 + .../backend/src/apps/appwrite/auth/index.js | 65 +++++++++++++++++++ .../apps/appwrite/auth/is-still-verified.js | 8 +++ .../apps/appwrite/auth/verify-credentials.js | 5 ++ .../apps/appwrite/common/add-auth-header.js | 16 +++++ .../src/apps/appwrite/common/set-base-url.js | 13 ++++ packages/backend/src/apps/appwrite/index.js | 17 +++++ packages/docs/pages/.vitepress/config.js | 6 ++ .../docs/pages/apps/appwrite/connection.md | 20 ++++++ .../docs/pages/public/favicons/appwrite.svg | 1 + 10 files changed, 152 insertions(+) create mode 100644 packages/backend/src/apps/appwrite/assets/favicon.svg create mode 100644 packages/backend/src/apps/appwrite/auth/index.js create mode 100644 packages/backend/src/apps/appwrite/auth/is-still-verified.js create mode 100644 packages/backend/src/apps/appwrite/auth/verify-credentials.js create mode 100644 packages/backend/src/apps/appwrite/common/add-auth-header.js create mode 100644 packages/backend/src/apps/appwrite/common/set-base-url.js create mode 100644 packages/backend/src/apps/appwrite/index.js create mode 100644 packages/docs/pages/apps/appwrite/connection.md create mode 100644 packages/docs/pages/public/favicons/appwrite.svg 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/index.js b/packages/backend/src/apps/appwrite/index.js new file mode 100644 index 00000000..77dd13cb --- /dev/null +++ b/packages/backend/src/apps/appwrite/index.js @@ -0,0 +1,17 @@ +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'; + +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: 'https://automatisch.io/docs/apps/appwrite/connection', + primaryColor: 'FD366E', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, +}); diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 462fec41..2c1208ea 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -41,6 +41,12 @@ export default defineConfig({ { text: 'Connection', link: '/apps/airtable/connection' }, ], }, + { + text: 'Appwrite', + collapsible: true, + collapsed: true, + items: [{ 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..60aefe53 --- /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 **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. +13. Start using Appwrite integration with Automatisch! 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 From 6e5c0cc0c77da1fe21e1363bc4d9cc13c9760119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Thu, 25 Jan 2024 16:02:43 +0300 Subject: [PATCH 2/6] feat(appwrite): add new documents trigger --- .../src/apps/appwrite/dynamic-data/index.js | 4 + .../dynamic-data/list-collections/index.js | 33 ++++++++ .../dynamic-data/list-databases/index.js | 26 +++++++ packages/backend/src/apps/appwrite/index.js | 4 + .../src/apps/appwrite/triggers/index.js | 3 + .../appwrite/triggers/new-documents/index.js | 76 +++++++++++++++++++ packages/docs/pages/.vitepress/config.js | 5 +- packages/docs/pages/apps/appwrite/triggers.md | 12 +++ packages/docs/pages/guide/available-apps.md | 1 + 9 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/apps/appwrite/dynamic-data/index.js create mode 100644 packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js create mode 100644 packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js create mode 100644 packages/backend/src/apps/appwrite/triggers/index.js create mode 100644 packages/backend/src/apps/appwrite/triggers/new-documents/index.js create mode 100644 packages/docs/pages/apps/appwrite/triggers.md 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..fa7eabd2 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js @@ -0,0 +1,33 @@ +export default { + name: 'List collections', + key: 'listCollections', + + async run($) { + const collections = { + data: [], + }; + const databaseId = $.step.parameters.databaseId; + + if (!databaseId) { + return collections; + } + + const { data } = await $.http.get( + `/v1/databases/${databaseId}/collections` + ); + + if (data?.collections) { + const sortedCollections = data.collections.sort((a, b) => + a.$createdAt - b.$createdAt ? 1 : -1 + ); + for (const collection of sortedCollections) { + 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..5cdd6107 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js @@ -0,0 +1,26 @@ +export default { + name: 'List databases', + key: 'listDatabases', + + async run($) { + const databases = { + data: [], + }; + + const { data } = await $.http.get('/v1/databases'); + + if (data?.databases) { + const sortedDatabases = data.databases.sort((a, b) => + a.$createdAt - b.$createdAt ? 1 : -1 + ); + for (const database of sortedDatabases) { + 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 index 77dd13cb..4fa30c75 100644 --- a/packages/backend/src/apps/appwrite/index.js +++ b/packages/backend/src/apps/appwrite/index.js @@ -2,6 +2,8 @@ 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', @@ -14,4 +16,6 @@ export default defineApp({ 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..97ac66d2 --- /dev/null +++ b/packages/backend/src/apps/appwrite/triggers/new-documents/index.js @@ -0,0 +1,76 @@ +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 { data } = await $.http.get( + `/v1/databases/${databaseId}/collections/${collectionId}/documents` + ); + + if (!data?.documents?.length) { + return; + } + + const sortedDocuments = data.documents.sort((a, b) => + a.$createdAt - b.$createdAt ? 1 : -1 + ); + + for (const document of sortedDocuments) { + $.pushTriggerItem({ + raw: document, + meta: { + internalId: document.$id, + }, + }); + } + }, +}); diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 2c1208ea..219a1d51 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -45,7 +45,10 @@ export default defineConfig({ text: 'Appwrite', collapsible: true, collapsed: true, - items: [{ text: 'Connection', link: '/apps/appwrite/connection' }], + items: [ + { text: 'Triggers', link: '/apps/appwrite/triggers' }, + { text: 'Connection', link: '/apps/appwrite/connection' }, + ], }, { text: 'Carbone', 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) From 258d920ff2066ca010080f9d3ff2155538868328 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 15 May 2024 17:50:06 +0000 Subject: [PATCH 3/6] docs(appwrite): describe project settings and hostname --- packages/docs/pages/apps/appwrite/connection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs/pages/apps/appwrite/connection.md b/packages/docs/pages/apps/appwrite/connection.md index 60aefe53..1a0d91c4 100644 --- a/packages/docs/pages/apps/appwrite/connection.md +++ b/packages/docs/pages/apps/appwrite/connection.md @@ -6,7 +6,7 @@ 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 **Settings**. +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. @@ -16,5 +16,5 @@ connection in Automatisch. If any of the steps are outdated, please let us know! 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. +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! From c122708b0b8987a891da4990ebc073383764badc Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 15 May 2024 17:50:25 +0000 Subject: [PATCH 4/6] fix(appwrite): utilize DOCS_URL --- packages/backend/src/apps/appwrite/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/apps/appwrite/index.js b/packages/backend/src/apps/appwrite/index.js index 4fa30c75..735c2491 100644 --- a/packages/backend/src/apps/appwrite/index.js +++ b/packages/backend/src/apps/appwrite/index.js @@ -11,7 +11,7 @@ export default defineApp({ baseUrl: 'https://appwrite.io', apiBaseUrl: 'https://cloud.appwrite.io', iconUrl: '{BASE_URL}/apps/appwrite/assets/favicon.svg', - authDocUrl: 'https://automatisch.io/docs/apps/appwrite/connection', + authDocUrl: '{DOCS_URL}/apps/appwrite/connection', primaryColor: 'FD366E', supportsConnections: true, beforeRequest: [setBaseUrl, addAuthHeader], From 8c83b715fefa945389084d6953cd8383505673d4 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 15 May 2024 17:50:44 +0000 Subject: [PATCH 5/6] fix(appwrite/new-documents): add native order and pagination support --- .../appwrite/triggers/new-documents/index.js | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/apps/appwrite/triggers/new-documents/index.js b/packages/backend/src/apps/appwrite/triggers/new-documents/index.js index 97ac66d2..6c5a7e5f 100644 --- a/packages/backend/src/apps/appwrite/triggers/new-documents/index.js +++ b/packages/backend/src/apps/appwrite/triggers/new-documents/index.js @@ -52,25 +52,52 @@ export default defineTrigger({ async run($) { const { databaseId, collectionId } = $.step.parameters; - const { data } = await $.http.get( - `/v1/databases/${databaseId}/collections/${collectionId}/documents` - ); + const limit = 1; + let lastDocumentId = undefined; + let offset = 0; + let documentCount = 0; - if (!data?.documents?.length) { - return; - } + 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 sortedDocuments = data.documents.sort((a, b) => - a.$createdAt - b.$createdAt ? 1 : -1 - ); + const { data } = await $.http.get( + `/v1/databases/${databaseId}/collections/${collectionId}/documents`, + { params }, + ); - for (const document of sortedDocuments) { - $.pushTriggerItem({ - raw: document, - meta: { - internalId: document.$id, - }, - }); - } + 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); }, }); From 1b34a48a61ea8b20d306dbd239fbf9d317f31555 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 15 May 2024 17:58:22 +0000 Subject: [PATCH 6/6] refactor(appwrite/dynamic-data): use native API ordering --- .../dynamic-data/list-collections/index.js | 21 ++++++++++++++----- .../dynamic-data/list-databases/index.js | 20 +++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) 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 index fa7eabd2..aa4b1a17 100644 --- a/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js @@ -12,15 +12,26 @@ export default { 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` + `/v1/databases/${databaseId}/collections`, + { params } ); if (data?.collections) { - const sortedCollections = data.collections.sort((a, b) => - a.$createdAt - b.$createdAt ? 1 : -1 - ); - for (const collection of sortedCollections) { + for (const collection of data.collections) { collections.data.push({ value: collection.$id, name: collection.name, 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 index 5cdd6107..5a815412 100644 --- a/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js @@ -7,13 +7,23 @@ export default { data: [], }; - const { data } = await $.http.get('/v1/databases'); + 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) { - const sortedDatabases = data.databases.sort((a, b) => - a.$createdAt - b.$createdAt ? 1 : -1 - ); - for (const database of sortedDatabases) { + for (const database of data.databases) { databases.data.push({ value: database.$id, name: database.name,