diff --git a/node/sync-with-algolia/src/main.js b/node/sync-with-algolia/src/main.js index d2793585..631fee59 100644 --- a/node/sync-with-algolia/src/main.js +++ b/node/sync-with-algolia/src/main.js @@ -60,7 +60,7 @@ export default async ({ req, res, log }) => { break; } - log(`Syncing chunk of ${response.documents.length} documents ...`); + log(`Syncing chunk of ${response.documents.length} documents...`); const records = response.documents.map(({ $id, ...document }) => ({ ...document, diff --git a/php/sync-with-algolia/.gitignore b/php/sync-with-algolia/.gitignore new file mode 100644 index 00000000..65a21c66 --- /dev/null +++ b/php/sync-with-algolia/.gitignore @@ -0,0 +1,470 @@ +##### Windows +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +##### Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +##### MacOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +##### Backup +*.bak +*.gho +*.ori +*.orig +*.tmp + +##### GPG +secring.* + +##### Dropbox +# Dropbox settings and caches +.dropbox +.dropbox.attr +.dropbox.cache + +##### SynopsysVCS +# Waveform formats +*.vcd +*.vpd +*.evcd +*.fsdb + +# Default name of the simulation executable. A different name can be +# specified with this switch (the associated daidir database name is +# also taken from here): -o / +simv + +# Generated for Verilog and VHDL top configs +simv.daidir/ +simv.db.dir/ + +# Infrastructure necessary to co-simulate SystemC models with +# Verilog/VHDL models. An alternate directory may be specified with this +# switch: -Mdir= +csrc/ + +# Log file - the following switch allows to specify the file that will be +# used to write all messages from simulation: -l +*.log + +# Coverage results (generated with urg) and database location. The +# following switch can also be used: urg -dir .vdb +simv.vdb/ +urgReport/ + +# DVE and UCLI related files. +DVEfiles/ +ucli.key + +# When the design is elaborated for DirectC, the following file is created +# with declarations for C/C++ functions. +vc_hdrs.h + +##### SVN +.svn/ + +##### Mercurial +.hg/ +.hgignore +.hgsigs +.hgsub +.hgsubstate +.hgtags + +##### Bazaar +.bzr/ +.bzrignore + +##### CVS +/CVS/* +**/CVS/* +.cvsignore +*/.cvsignore + +##### TortoiseGit +# Project-level settings +/.tgitconfig + +##### PuTTY +# Private key +*.ppk + +##### Vim +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +##### Emacs +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + +##### SublimeText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +##### Notepad++ +# Notepad++ backups # +*.bak + +##### TextMate +*.tmproj +*.tmproject +tmtags + +##### VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +##### NetBeans +**/nbproject/private/ +**/nbproject/Makefile-*.mk +**/nbproject/Package-*.bash +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +##### JetBrains +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +##### Eclipse +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +##### Dreamweaver +# DW Dreamweaver added files +_notes +_compareTemp +configs/ +dwsync.xml +dw_php_codehinting.config +*.mno + +##### CodeKit +# General CodeKit files to ignore +config.codekit +config.codekit3 +/min + +##### Gradle +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +##### Composer +composer.phar +/vendor/ + +##### PHP CodeSniffer +# gitignore for the PHP Codesniffer framework +# website: https://github.com/squizlabs/PHP_CodeSniffer +# +# Recommended template: PHP.gitignore + +/wpcs/* + +##### SASS +.sass-cache/ +*.css.map +*.sass.map +*.scss.map \ No newline at end of file diff --git a/php/sync-with-algolia/README.md b/php/sync-with-algolia/README.md new file mode 100644 index 00000000..e6e2e417 --- /dev/null +++ b/php/sync-with-algolia/README.md @@ -0,0 +1,108 @@ +# 🔍 PHP Sync with Algolia Function + +Intuitive search bar for any data in Appwrite Databases. + +## 🧰 Usage + +### `GET /` + +HTML form for interacting with the function. + +### `POST /` + +Triggers indexing of the Appwrite database collection to Algolia. + +No parameters required. + +**Response** + +Sample `204` Response: No content. + +## ⚙️ Configuration + +| Setting | Value | +| ----------------- | ------------------ | +| Runtime | PHP (8.0) | +| Entrypoint | `src/index.php` | +| Build Commands | `composer install` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | + +## 🔒 Environment Variables + +### APPWRITE_API_KEY + +The API Key to talk to Appwrite backend APIs. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `d1efb...aec35` | +| Documentation | [Appwrite: Getting Started for Server](https://appwrite.io/docs/getting-started-for-server#apiKey) | + +### APPWRITE_DATABASE_ID + +The ID of the Appwrite database that contains the collection to sync. + +| Question | Answer | +| ------------- | --------------------------------------------------------- | +| Required | Yes | +| Sample Value | `64a55...7b912` | +| Documentation | [Appwrite: Databases](https://appwrite.io/docs/databases) | + +### APPWRITE_COLLECTION_ID + +The ID of the collection in the Appwrite database to sync. + +| Question | Answer | +| ------------- | ------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `7c3e8...2a9f1` | +| Documentation | [Appwrite: Collections](https://appwrite.io/docs/collections) | + +### APPWRITE_ENDPOINT + +The URL endpoint of the Appwrite server. If not provided, it defaults to the Appwrite Cloud server: `https://cloud.appwrite.io/v1`. + +| Question | Answer | +| ------------ | ------------------------------ | +| Required | No | +| Sample Value | `https://cloud.appwrite.io/v1` | + +### ALGOLIA_APP_ID + +The application ID for your Algolia service. + +| Question | Answer | +| ------------ | ----------- | +| Required | Yes | +| Sample Value | `EG6...VJJ` | + +### ALGOLIA_ADMIN_API_KEY + +The admin API Key for your Algolia service. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `fd0aa...136a8` | +| Documentation | [Algolia: API Keys](https://www.algolia.com/doc/guides/security/api-keys/) | + +### ALGOLIA_INDEX_ID + +The ID of the index in Algolia where the documents are to be synced. + +| Question | Answer | +| ------------ | ---------------- | +| Required | Yes | +| Sample Value | `appwrite_index` | + +### ALGOLIA_SEARCH_API_KEY + +The search API Key for your Algolia service. This key is used for searching the synced index. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `bf2f5...df733` | +| Documentation | [Algolia: API Keys](https://www.algolia.com/doc/guides/security/api-keys/) | diff --git a/php/sync-with-algolia/composer.json b/php/sync-with-algolia/composer.json new file mode 100644 index 00000000..f61d92e5 --- /dev/null +++ b/php/sync-with-algolia/composer.json @@ -0,0 +1,9 @@ +{ + "name": "templates/starter", + "type": "library", + "require": { + "php": ">=8.0.0", + "appwrite/appwrite": "^8.0", + "algolia/algoliasearch-client-php": "^3.4" + } +} diff --git a/php/sync-with-algolia/src/index.php b/php/sync-with-algolia/src/index.php new file mode 100644 index 00000000..e3997f66 --- /dev/null +++ b/php/sync-with-algolia/src/index.php @@ -0,0 +1,83 @@ +req->method === 'GET') { + $html = interpolate(get_static_file('index.html'), [ + 'ALGOLIA_APP_ID' => $_ENV['ALGOLIA_APP_ID'], + 'ALGOLIA_INDEX_ID' => $_ENV['ALGOLIA_INDEX_ID'], + 'ALGOLIA_SEARCH_API_KEY' => $_ENV['ALGOLIA_SEARCH_API_KEY'], + ]); + return $context->res->send($html, 200, [ + 'Content-Type' => 'text/html', + ]); + } + + $client = new Client(); + $client + ->setEndpoint('https://cloud.appwrite.io/v1') + ->setProject($_ENV['APPWRITE_FUNCTION_PROJECT_ID']) + ->setKey($_ENV['APPWRITE_API_KEY']); + + $databases = new Databases($client); + + $algolia = SearchClient::create( + $_ENV['ALGOLIA_APP_ID'], + $_ENV['ALGOLIA_ADMIN_API_KEY'] + ); + $index = $algolia->initIndex($_ENV['ALGOLIA_INDEX_ID']); + + $cursor = null; + do { + $queries = [Query::limit(100)]; + + if ($cursor) { + array_push($queries, Query::cursorAfter($cursor)); + } + + $response = $databases->listDocuments( + $_ENV['APPWRITE_DATABASE_ID'], + $_ENV['APPWRITE_COLLECTION_ID'], + $queries + ); + + if (count($response['documents']) > 0) { + $cursor = $response['documents'][count($response['documents']) - 1]['$id']; + } else { + $context->log('No more documents found.'); + $cursor = null; + break; + } + + $context->log('Syncing chunk of ' . count($response['documents']) . ' documents...'); + + $documentsWithObjectId = array_map(function ($document) { + $document['objectID'] = $document['$id']; + unset($document['$id']); + return $document; + }, $response['documents']); + + $index->saveObjects($documentsWithObjectId); + } while ($cursor !== null); + + $context->log('Sync finished.'); + + return $context->res->send('Sync finished.', 200); +}; diff --git a/php/sync-with-algolia/src/utils.php b/php/sync-with-algolia/src/utils.php new file mode 100644 index 00000000..6d4e7498 --- /dev/null +++ b/php/sync-with-algolia/src/utils.php @@ -0,0 +1,48 @@ + 0) { + throw new Exception('Missing required fields: ' . implode(', ', $missing)); + } +} + +/** + * Interpolates values into a template string. + * @param string $template The template string containing placeholders like "{{key}}". + * @param array $values An associative array with keys and values to replace in the template. + * @return string The interpolated string with placeholders replaced by corresponding values. + */ +function interpolate(string $template, array $values): string +{ + return preg_replace_callback('/{{([^}]+)}}/', function ($matches) use ($values) { + $key = $matches[1]; + return isset($values[$key]) ? $values[$key] : ''; + }, $template); +} diff --git a/php/sync-with-algolia/static/index.html b/php/sync-with-algolia/static/index.html new file mode 100644 index 00000000..9ea7ddba --- /dev/null +++ b/php/sync-with-algolia/static/index.html @@ -0,0 +1,78 @@ + + + + + + + Algolia Search Demo + + + + + + + + +
+
+
+
+

Algolia Search Demo

+ +
+

+ Use this demo to verify that the sync between Appwrite Databases and + Algolia was successful. Search your Algolia index using the input + below. +

+
+
+
+
+ +
+
+
+
+ + + + diff --git a/python/sync-with-algolia/.gitignore b/python/sync-with-algolia/.gitignore new file mode 100644 index 00000000..68bc17f9 --- /dev/null +++ b/python/sync-with-algolia/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/python/sync-with-algolia/README.md b/python/sync-with-algolia/README.md new file mode 100644 index 00000000..ce257610 --- /dev/null +++ b/python/sync-with-algolia/README.md @@ -0,0 +1,108 @@ +# 🔍 Python Sync with Algolia Function + +Intuitive search bar for any data in Appwrite Databases. + +## 🧰 Usage + +### `GET /` + +HTML form for interacting with the function. + +### `POST /` + +Triggers indexing of the Appwrite database collection to Algolia. + +No parameters required. + +**Response** + +Sample `204` Response: No content. + +## ⚙️ Configuration + +| Setting | Value | +| ----------------- | --------------------------------- | +| Runtime | Python (3.9) | +| Entrypoint | `src/main.py` | +| Build Commands | `pip install -r requirements.txt` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | + +## 🔒 Environment Variables + +### APPWRITE_API_KEY + +The API Key to talk to Appwrite backend APIs. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `d1efb...aec35` | +| Documentation | [Appwrite: Getting Started for Server](https://appwrite.io/docs/getting-started-for-server#apiKey) | + +### APPWRITE_DATABASE_ID + +The ID of the Appwrite database that contains the collection to sync. + +| Question | Answer | +| ------------- | --------------------------------------------------------- | +| Required | Yes | +| Sample Value | `64a55...7b912` | +| Documentation | [Appwrite: Databases](https://appwrite.io/docs/databases) | + +### APPWRITE_COLLECTION_ID + +The ID of the collection in the Appwrite database to sync. + +| Question | Answer | +| ------------- | ------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `7c3e8...2a9f1` | +| Documentation | [Appwrite: Collections](https://appwrite.io/docs/collections) | + +### APPWRITE_ENDPOINT + +The URL endpoint of the Appwrite server. If not provided, it defaults to the Appwrite Cloud server: `https://cloud.appwrite.io/v1`. + +| Question | Answer | +| ------------ | ------------------------------ | +| Required | No | +| Sample Value | `https://cloud.appwrite.io/v1` | + +### ALGOLIA_APP_ID + +The application ID for your Algolia service. + +| Question | Answer | +| ------------ | ----------- | +| Required | Yes | +| Sample Value | `EG6...VJJ` | + +### ALGOLIA_ADMIN_API_KEY + +The admin API Key for your Algolia service. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `fd0aa...136a8` | +| Documentation | [Algolia: API Keys](https://www.algolia.com/doc/guides/security/api-keys/) | + +### ALGOLIA_INDEX_ID + +The ID of the index in Algolia where the documents are to be synced. + +| Question | Answer | +| ------------ | ---------------- | +| Required | Yes | +| Sample Value | `appwrite_index` | + +### ALGOLIA_SEARCH_API_KEY + +The search API Key for your Algolia service. This key is used for searching the synced index. + +| Question | Answer | +| ------------- | -------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `bf2f5...df733` | +| Documentation | [Algolia: API Keys](https://www.algolia.com/doc/guides/security/api-keys/) | diff --git a/python/sync-with-algolia/requirements.txt b/python/sync-with-algolia/requirements.txt new file mode 100644 index 00000000..dc70171f --- /dev/null +++ b/python/sync-with-algolia/requirements.txt @@ -0,0 +1,2 @@ +appwrite +algoliasearch \ No newline at end of file diff --git a/python/sync-with-algolia/src/main.py b/python/sync-with-algolia/src/main.py new file mode 100644 index 00000000..1dc0b7a6 --- /dev/null +++ b/python/sync-with-algolia/src/main.py @@ -0,0 +1,77 @@ +import os +from algoliasearch.search_client import SearchClient +from appwrite.client import Client +from appwrite.services.databases import Databases +from appwrite.query import Query +from utils import get_static_file, throw_if_missing, interpolate + + +def main(context): + throw_if_missing( + os.environ, + [ + "APPWRITE_API_KEY", + "APPWRITE_DATABASE_ID", + "APPWRITE_COLLECTION_ID", + "ALGOLIA_APP_ID", + "ALGOLIA_INDEX_ID", + "ALGOLIA_ADMIN_API_KEY", + "ALGOLIA_SEARCH_API_KEY", + ], + ) + + if context.req.method == "GET": + html = interpolate( + get_static_file("index.html"), + { + "ALGOLIA_APP_ID": os.environ["ALGOLIA_APP_ID"], + "ALGOLIA_INDEX_ID": os.environ["ALGOLIA_INDEX_ID"], + "ALGOLIA_SEARCH_API_KEY": os.environ["ALGOLIA_SEARCH_API_KEY"], + }, + ) + return context.res.send(html, 200, {"Content-Type": "text/html; charset=utf-8"}) + + client = ( + Client() + .set_endpoint(os.environ["APPWRITE_ENDPOINT"] or "https://cloud.appwrite.io/v1") + .set_project(os.environ["APPWRITE_FUNCTION_PROJECT_ID"]) + .set_key(os.environ["APPWRITE_API_KEY"]) + ) + database = Databases(client) + + algolia = SearchClient.create( + os.environ["ALGOLIA_APP_ID"], os.environ["ALGOLIA_ADMIN_API_KEY"] + ) + index = algolia.init_index(os.environ["ALGOLIA_INDEX_ID"]) + + cursor = None + while True: + queries = [Query.limit(100)] + if cursor is not None: + queries.append(Query.cursor_after(cursor)) + + response = database.list_documents( + os.environ["APPWRITE_DATABASE_ID"], + os.environ["APPWRITE_COLLECTION_ID"], + queries, + ) + + if len(response.documents) > 0: + cursor = response.documents[len(response.documents) - 1]["$id"] + else: + context.log("No more documents found.") + cursor = None + break + + context.log(f"Syncing chunk of {len(response.documents)} documents...") + documents_with_object_ids = [ + {"objectID": document["$id"], **document} for document in response.documents + ] + index.save_objects(documents_with_object_ids) + + if cursor is None: + break + + context.log("Sync finished.") + + return context.res.send("Sync finished.", 200) diff --git a/python/sync-with-algolia/src/utils.py b/python/sync-with-algolia/src/utils.py new file mode 100644 index 00000000..fafb8006 --- /dev/null +++ b/python/sync-with-algolia/src/utils.py @@ -0,0 +1,54 @@ +import os, re + +__dirname = os.path.dirname(os.path.abspath(__file__)) +static_folder = os.path.join(__dirname, "../static") + + +def get_static_file(file_name: str) -> str: + """ + Returns the contents of a file in the static folder + + Parameters: + file_name (str): Name of the file to read + + Returns: + (str): Contents of static/{file_name} + """ + file_path = os.path.join(static_folder, file_name) + with open(file_path, "r") as file: + return file.read() + + +def throw_if_missing(obj: object, keys: list[str]) -> None: + """ + Throws an error if any of the keys are missing from the object + + Parameters: + obj (object): Object to check + keys (list[str]): List of keys to check + + Raises: + ValueError: If any keys are missing + """ + missing = [key for key in keys if key not in obj or not obj[key]] + if missing: + raise ValueError(f"Missing required fields: {', '.join(missing)}") + + +def interpolate(template: str, values: dict[str, str]) -> str: + """ + Interpolates a template string with the given values + + Parameters: + template(str): Template string to interpolate + values(dict): Dictionary of values to interpolate + + Returns: + (str): Interpolated string + """ + + def replace_match(match): + key = match.group(1) + return values.get(key, "") + + return re.sub(r"{{([^}]+)}}", replace_match, template) diff --git a/python/sync-with-algolia/static/index.html b/python/sync-with-algolia/static/index.html new file mode 100644 index 00000000..9ea7ddba --- /dev/null +++ b/python/sync-with-algolia/static/index.html @@ -0,0 +1,78 @@ + + + + + + + Algolia Search Demo + + + + + + + + +
+
+
+
+

Algolia Search Demo

+ +
+

+ Use this demo to verify that the sync between Appwrite Databases and + Algolia was successful. Search your Algolia index using the input + below. +

+
+
+
+
+ +
+
+
+
+ + + +