From 97d9dbc5dd728bd629280e3d996de87e4a0da60e Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Mon, 17 Jul 2023 22:07:37 +0100
Subject: [PATCH 1/9] feat: Sync with Algolia function

---
 sync-with-algolia/.gitignore         |   1 +
 sync-with-algolia/.prettierrc.json   |   6 +
 sync-with-algolia/README.md          |  39 ++++
 sync-with-algolia/package-lock.json  | 270 +++++++++++++++++++++++++++
 sync-with-algolia/package.json       |  20 ++
 sync-with-algolia/src/environment.js |  30 +++
 sync-with-algolia/src/main.js        |  94 ++++++++++
 sync-with-algolia/static/index.html  |  99 ++++++++++
 8 files changed, 559 insertions(+)
 create mode 100644 sync-with-algolia/.gitignore
 create mode 100644 sync-with-algolia/.prettierrc.json
 create mode 100644 sync-with-algolia/README.md
 create mode 100644 sync-with-algolia/package-lock.json
 create mode 100644 sync-with-algolia/package.json
 create mode 100644 sync-with-algolia/src/environment.js
 create mode 100644 sync-with-algolia/src/main.js
 create mode 100644 sync-with-algolia/static/index.html

diff --git a/sync-with-algolia/.gitignore b/sync-with-algolia/.gitignore
new file mode 100644
index 00000000..b512c09d
--- /dev/null
+++ b/sync-with-algolia/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/sync-with-algolia/.prettierrc.json b/sync-with-algolia/.prettierrc.json
new file mode 100644
index 00000000..fa51da29
--- /dev/null
+++ b/sync-with-algolia/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+  "trailingComma": "es5",
+  "tabWidth": 2,
+  "semi": false,
+  "singleQuote": true
+}
diff --git a/sync-with-algolia/README.md b/sync-with-algolia/README.md
new file mode 100644
index 00000000..b2b5c18b
--- /dev/null
+++ b/sync-with-algolia/README.md
@@ -0,0 +1,39 @@
+# Appwrite - Algolia Sync Function
+
+This function helps in syncing the data between Appwrite's database and Algolia's search index. It fetches the documents from Appwrite database and updates the Algolia search index.
+
+## Environment Variables
+
+To ensure the function operates as intended, ensure the following variables are set:
+
+- **ALGOLIA_APP_ID**: This is your Algolia application's ID.
+- **ALGOLIA_ADMIN_API_KEY**: This is your Algolia application's Admin API key.
+- **ALGOLIA_INDEX_ID**: This is the ID of the Algolia index where the data is stored.
+- **ALGOLIA_SEARCH_API_KEY**: This is the Search API key of your Algolia application.
+- **APPWRITE_API_KEY**: This is your Appwrite project's API key.
+- **APPWRITE_DATABASE_ID**: This is the ID of your Appwrite database where the data is stored.
+- **APPWRITE_COLLECTION_ID**: This is the ID of the collection within the Appwrite database.
+- **APPWRITE_ENDPOINT**: This is the endpoint where your Appwrite server is located.
+- **APPWRITE_PROJECT_ID**: This refers to the specific ID of your Appwrite project.
+
+## Usage
+
+This function supports two types of requests:
+
+1. **Retrieving the Search Interface**
+
+   - **Request Type:** GET
+   - **Response:** 
+     - On success, the function will respond with an HTML interface for Algolia search. The response content type is `text/html`.
+
+2. **Syncing Appwrite Database with Algolia Index**
+
+   - **Request Type:** POST
+   - **Response:** 
+     - On success, the function will synchronize the Appwrite database with the specified Algolia index and return an empty response. The sync process operates in chunks, retrieving up to 100 documents per request from the Appwrite database and updating them on the Algolia index.
+
+## Limitations
+
+This function synchronizes the Appwrite database with the Algolia index in one direction only - from Appwrite to Algolia. If any changes are made directly on the Algolia index, they will not be reflected in the Appwrite database. 
+
+Additionally, if any changes are made in the Appwrite database after the sync, they will not be reflected in the Algolia index until the sync function is run again. To achieve real-time sync, consider triggering this function whenever a document is added, updated, or deleted in your Appwrite database.
diff --git a/sync-with-algolia/package-lock.json b/sync-with-algolia/package-lock.json
new file mode 100644
index 00000000..84384a06
--- /dev/null
+++ b/sync-with-algolia/package-lock.json
@@ -0,0 +1,270 @@
+{
+  "name": "sync-with-algolia",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "sync-with-algolia",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "algoliasearch": "^4.17.2",
+        "node-appwrite": "^9.0.0"
+      },
+      "devDependencies": {
+        "prettier": "^3.0.0"
+      }
+    },
+    "node_modules/@algolia/cache-browser-local-storage": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.17.2.tgz",
+      "integrity": "sha512-ZkVN7K/JE+qMQbpR6h3gQOGR6yCJpmucSBCmH5YDxnrYbp2CbrVCu0Nr+FGVoWzMJNznj1waShkfQ9awERulLw==",
+      "dependencies": {
+        "@algolia/cache-common": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/cache-common": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.17.2.tgz",
+      "integrity": "sha512-fojbhYIS8ovfYs6hwZpy1O4mBfVRxNgAaZRqsdVQd54hU4MxYDYFCxagYX28lOBz7btcDHld6BMoWXvjzkx6iQ=="
+    },
+    "node_modules/@algolia/cache-in-memory": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.17.2.tgz",
+      "integrity": "sha512-UYQcMzPurNi+cPYkuPemTZkjKAjdgAS1hagC5irujKbrYnN4yscK4TkOI5tX+O8/KegtJt3kOK07OIrJ2QDAAw==",
+      "dependencies": {
+        "@algolia/cache-common": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/client-account": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.17.2.tgz",
+      "integrity": "sha512-doSk89pBPDpDyKJSHFADIGa2XSGrBCj3QwPvqtRJXDADpN+OjW+eTR8r4hEs/7X4GGfjfAOAES8JgDx+fZntYw==",
+      "dependencies": {
+        "@algolia/client-common": "4.17.2",
+        "@algolia/client-search": "4.17.2",
+        "@algolia/transporter": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/client-analytics": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.17.2.tgz",
+      "integrity": "sha512-V+DcXbOtD/hKwAR3qGQrtlrJ3q2f9OKfx843q744o4m3xHv5ueCAvGXB1znPsdaUrVDNAImcgEgqwI9x7EJbDw==",
+      "dependencies": {
+        "@algolia/client-common": "4.17.2",
+        "@algolia/client-search": "4.17.2",
+        "@algolia/requester-common": "4.17.2",
+        "@algolia/transporter": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/client-common": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.17.2.tgz",
+      "integrity": "sha512-gKBUnjxi0ukJYIJxVREYGt1Dmj1B3RBYbfGWi0dIPp1BC1VvQm+BOuNwsIwmq/x3MPO+sGuK978eKiP3tZDvag==",
+      "dependencies": {
+        "@algolia/requester-common": "4.17.2",
+        "@algolia/transporter": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/client-personalization": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.17.2.tgz",
+      "integrity": "sha512-wc4UgOWxSYWz5wpuelNmlt895jA9twjZWM2ms17Ws8qCvBHF7OVGdMGgbysPB8790YnfvvDnSsWOv3CEj26Eow==",
+      "dependencies": {
+        "@algolia/client-common": "4.17.2",
+        "@algolia/requester-common": "4.17.2",
+        "@algolia/transporter": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/client-search": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.17.2.tgz",
+      "integrity": "sha512-FUjIs+gRe0upJC++uVs4sdxMw15JxfkT86Gr/kqVwi9kcqaZhXntSbW/Fw959bIYXczjmeVQsilYvBWW4YvSZA==",
+      "dependencies": {
+        "@algolia/client-common": "4.17.2",
+        "@algolia/requester-common": "4.17.2",
+        "@algolia/transporter": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/logger-common": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.17.2.tgz",
+      "integrity": "sha512-EfXuweUE+1HiSMsQidaDWA5Lv4NnStYIlh7PO5pLkI+sdhbMX0e5AO5nUAMIFM1VkEANes70RA8fzhP6OqCqQQ=="
+    },
+    "node_modules/@algolia/logger-console": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.17.2.tgz",
+      "integrity": "sha512-JuG8HGVlJ+l/UEDK4h2Y8q/IEmRjQz1J0aS9tf6GPNbGYiSvMr1DDdZ+hqV3bb1XE6wU8Ypex56HisWMSpnG0A==",
+      "dependencies": {
+        "@algolia/logger-common": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/requester-browser-xhr": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.17.2.tgz",
+      "integrity": "sha512-FKI2lYWwksALfRt2OETFmGb5+P7WVc4py2Ai3H7k8FSfTLwVvs9WVVmtlx6oANQ8RFEK4B85h8DQJTJ29TDfmA==",
+      "dependencies": {
+        "@algolia/requester-common": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/requester-common": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.17.2.tgz",
+      "integrity": "sha512-Rfim23ztAhYpE9qm+KCfCRo+YLJCjiiTG+IpDdzUjMpYPhUtirQT0A35YEd/gKn86YNyydxS9w8iRSjwKh+L0A=="
+    },
+    "node_modules/@algolia/requester-node-http": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.17.2.tgz",
+      "integrity": "sha512-E0b0kyCDMvUIhQmDNd/mH4fsKJdEEX6PkMKrYJjzm6moo+rP22tqpq4Rfe7DZD8OB6/LsDD3zs3Kvd+L+M5wwQ==",
+      "dependencies": {
+        "@algolia/requester-common": "4.17.2"
+      }
+    },
+    "node_modules/@algolia/transporter": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.17.2.tgz",
+      "integrity": "sha512-m8pXlz5OnNzjD1rcw+duCN4jG4yEzkJBsvKYMoN22Oq6rQwy1AY5muZ+IQUs4dL+A364CYkRMLRWhvXpCZ1x+g==",
+      "dependencies": {
+        "@algolia/cache-common": "4.17.2",
+        "@algolia/logger-common": "4.17.2",
+        "@algolia/requester-common": "4.17.2"
+      }
+    },
+    "node_modules/algoliasearch": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.17.2.tgz",
+      "integrity": "sha512-VFu43JJNYIW74awp7oeQcQsPcxOhd8psqBDTfyNO2Zt6L1NqnNMTVnaIdQ+8dtKqUDBqQZp0szPxECvX8CK2Fg==",
+      "dependencies": {
+        "@algolia/cache-browser-local-storage": "4.17.2",
+        "@algolia/cache-common": "4.17.2",
+        "@algolia/cache-in-memory": "4.17.2",
+        "@algolia/client-account": "4.17.2",
+        "@algolia/client-analytics": "4.17.2",
+        "@algolia/client-common": "4.17.2",
+        "@algolia/client-personalization": "4.17.2",
+        "@algolia/client-search": "4.17.2",
+        "@algolia/logger-common": "4.17.2",
+        "@algolia/logger-console": "4.17.2",
+        "@algolia/requester-browser-xhr": "4.17.2",
+        "@algolia/requester-common": "4.17.2",
+        "@algolia/requester-node-http": "4.17.2",
+        "@algolia/transporter": "4.17.2"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/axios": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+      "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+      "dependencies": {
+        "follow-redirects": "^1.15.0",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-appwrite": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-9.0.0.tgz",
+      "integrity": "sha512-iTcHbuaJfr6bP/HFkRVV+FcaumKkbINqZyypQdl+tYxv6Dx0bkB/YKUXGYfTkgP18TLPWQQB++OGQhi98dlo2w==",
+      "dependencies": {
+        "axios": "^1.3.6",
+        "form-data": "^4.0.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
+      "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    }
+  }
+}
diff --git a/sync-with-algolia/package.json b/sync-with-algolia/package.json
new file mode 100644
index 00000000..b2eb4d19
--- /dev/null
+++ b/sync-with-algolia/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "sync-with-algolia",
+  "version": "1.0.0",
+  "description": "",
+  "main": "src/main.js",
+  "type": "module",
+  "scripts": {
+    "format": "prettier --write src/**/*.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "algoliasearch": "^4.17.2",
+    "node-appwrite": "^9.0.0"
+  },
+  "devDependencies": {
+    "prettier": "^3.0.0"
+  }
+}
diff --git a/sync-with-algolia/src/environment.js b/sync-with-algolia/src/environment.js
new file mode 100644
index 00000000..e83fd3f6
--- /dev/null
+++ b/sync-with-algolia/src/environment.js
@@ -0,0 +1,30 @@
+/**
+ * @param {string} key
+ * @return {string}
+ */
+function getRequiredEnv(key) {
+  const value = process.env[key]
+  if (value === undefined) {
+    throw new Error(`Environment variable ${key} is not set`)
+  }
+  return value
+}
+
+function EnvironmentService() {
+  return {
+    ALGOLIA_APP_ID: getRequiredEnv('ALGOLIA_APP_ID'),
+    ALGOLIA_ADMIN_API_KEY: getRequiredEnv('ALGOLIA_ADMIN_API_KEY'),
+    ALGOLIA_INDEX_ID: getRequiredEnv('ALGOLIA_INDEX_ID'),
+    ALGOLIA_SEARCH_API_KEY: getRequiredEnv('ALGOLIA_SEARCH_API_KEY'),
+    APPWRITE_API_KEY: getRequiredEnv('APPWRITE_API_KEY'),
+    APPWRITE_DATABASE_ID: getRequiredEnv('APPWRITE_DATABASE_ID'),
+    APPWRITE_COLLECTION_ID: getRequiredEnv('APPWRITE_COLLECTION_ID'),
+    APPWRITE_ENDPOINT:
+      process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1',
+    APPWRITE_PROJECT_ID:
+      process.env.APPWRITE_PROJECT_ID ??
+      getRequiredEnv('APPWRITE_FUNCTION_PROJECT_ID'),
+  }
+}
+
+export default EnvironmentService
diff --git a/sync-with-algolia/src/main.js b/sync-with-algolia/src/main.js
new file mode 100644
index 00000000..d33112b0
--- /dev/null
+++ b/sync-with-algolia/src/main.js
@@ -0,0 +1,94 @@
+import { Client, Databases, Query } from 'node-appwrite'
+import algoliasearch from 'algoliasearch'
+import fs from 'fs'
+import path from 'path'
+import { fileURLToPath } from 'url'
+import EnvironmentService from './environment.js'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+const staticFolder = path.join(__dirname, '../static')
+
+/**
+ * @param {string} template
+ * @param {Record<string, string>} values
+ * @returns {string}
+ */
+function interpolate(template, values) {
+  return template.replace(/{{([^}]+)}}/g, (_, key) => values[key])
+}
+
+export default async ({ req, res, log }) => {
+  const {
+    ALGOLIA_APP_ID,
+    ALGOLIA_ADMIN_API_KEY,
+    ALGOLIA_INDEX_ID,
+    ALGOLIA_SEARCH_API_KEY,
+    APPWRITE_API_KEY,
+    APPWRITE_DATABASE_ID,
+    APPWRITE_COLLECTION_ID,
+    APPWRITE_ENDPOINT,
+    APPWRITE_PROJECT_ID,
+  } = EnvironmentService()
+
+  if (req.method === 'GET') {
+    const template = fs
+      .readFileSync(path.join(staticFolder, 'index.html'))
+      .toString()
+
+    const html = interpolate(template, {
+      ALGOLIA_APP_ID,
+      ALGOLIA_INDEX_ID,
+      ALGOLIA_SEARCH_API_KEY,
+    })
+
+    return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' })
+  }
+
+  const client = new Client()
+    .setEndpoint(APPWRITE_ENDPOINT)
+    .setProject(APPWRITE_PROJECT_ID)
+    .setKey(APPWRITE_API_KEY)
+
+  const databases = new Databases(client)
+
+  const algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY)
+  const algoliaIndex = algolia.initIndex(ALGOLIA_INDEX_ID)
+
+  let cursor = null
+
+  do {
+    const queries = [Query.limit(100)]
+
+    if (cursor) {
+      queries.push(Query.cursorAfter(cursor))
+    }
+
+    const response = await databases.listDocuments(
+      APPWRITE_DATABASE_ID,
+      APPWRITE_COLLECTION_ID,
+      queries
+    )
+
+    if (response.documents.length > 0) {
+      cursor = response.documents[response.documents.length - 1].$id
+    } else {
+      log(`No more documents found.`)
+      cursor = null
+      break
+    }
+
+    log(`Syncing chunk of ${response.documents.length} documents ...`)
+
+    const records = response.documents.map((document) => ({
+      ...document,
+      objectID: document.$id,
+    }))
+    await algoliaIndex.saveObjects(records)
+  } while (cursor !== null)
+
+  log('Sync finished.')
+
+  return res.empty()
+}
diff --git a/sync-with-algolia/static/index.html b/sync-with-algolia/static/index.html
new file mode 100644
index 00000000..31dd82ef
--- /dev/null
+++ b/sync-with-algolia/static/index.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Algolia Search Demo</title>
+
+    <script
+      src="https://cdn.jsdelivr.net/npm/algoliasearch@4.17.2/dist/algoliasearch-lite.umd.js"
+      integrity="sha256-X2G+vjV8i6Jj2NSGFTvy6Z4q2AlmF5aH0HiIpWJ8eU4="
+      crossorigin="anonymous"
+    ></script>
+    <script
+      src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.56.3/dist/instantsearch.production.min.js"
+      integrity="sha256-mWvjWAfZylKMOg+S3HLq+wng1HHRnl2Idr2r8NsUzIU="
+      crossorigin="anonymous"
+    ></script>
+    <link
+      rel="stylesheet"
+      href="https://cdn.jsdelivr.net/npm/instantsearch.css@8.0.0/themes/satellite-min.css"
+      integrity="sha256-p/rGN4RGy6EDumyxF9t7LKxWGg6/MZfGhJM/asKkqvA="
+      crossorigin="anonymous"
+    />
+
+    <link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink" />
+    <link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink-icons" />
+  </head>
+  <body>
+    <main class="main-content">
+      <div class="top-cover u-padding-block-end-56">
+        <div class="container">
+          <div
+            class="u-flex u-gap-16 u-flex-justify-center u-margin-block-start-16"
+          >
+            <h1 class="heading-level-1">Algolia Search Demo</h1>
+            <code class="u-un-break-text"></code>
+          </div>
+          <p
+            class="body-text-1 u-normal u-margin-block-start-8"
+            style="max-width: 50rem"
+          >
+            This is demo application. You can ue this app to ensure sync between
+            Appwrite Databases and Algolia Seach was successful. Use search
+            input below to search your data.
+          </p>
+        </div>
+      </div>
+      <div class="container u-margin-block-start-negative-56">
+        <div class="card u-flex u-gap-24 u-flex-vertical">
+          <div id="searchbox"></div>
+          <div id="hits"></div>
+        </div>
+      </div>
+    </main>
+
+    <script>
+      const searchClient = algoliasearch(
+        "{{ALGOLIA_APP_ID}}",
+        "{{ALGOLIA_SEARCH_API_KEY}}"
+      );
+
+      const search = instantsearch({
+        indexName: "{{ALGOLIA_INDEX_ID}}",
+        searchClient,
+      });
+
+      search.addWidgets([
+        instantsearch.widgets.searchBox({
+          container: "#searchbox",
+        }),
+
+        instantsearch.widgets.hits({
+          container: "#hits",
+          templates: {
+            item(hit, { html, components }) {
+              return html`
+                <div class="u-flex u-flex-vertical u-gap-12">
+                  <div class="u-flex u-gap-12">
+                    <h5 class="eyebrow-heading-2">
+                      ${hit[Object.keys(hit)[0]]}
+                    </h5>
+                    <div><code class="inline-code">${hit.$id}</code></div>
+                  </div>
+
+                  <div style="overflow-x: hidden; line-break: anywhere;">
+                    <p class="u-color-text-gray">${JSON.stringify(hit)}</p>
+                  </div>
+                </div>
+              `;
+            },
+          },
+        }),
+      ]);
+
+      search.start();
+    </script>
+  </body>
+</html>

From adf1d6a2dd2f6d223c13dcfa06aa8c9946064920 Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Wed, 19 Jul 2023 11:04:49 +0100
Subject: [PATCH 2/9] chore: add semis, del pjson extras

---
 sync-with-algolia/.gitignore         | 131 ++++++++++++++++++++++++++-
 sync-with-algolia/.prettierrc.json   |   2 +-
 sync-with-algolia/package.json       |   2 -
 sync-with-algolia/src/environment.js |  10 +-
 sync-with-algolia/src/main.js        |  67 +++++++-------
 5 files changed, 169 insertions(+), 43 deletions(-)

diff --git a/sync-with-algolia/.gitignore b/sync-with-algolia/.gitignore
index b512c09d..6a7d6d8e 100644
--- a/sync-with-algolia/.gitignore
+++ b/sync-with-algolia/.gitignore
@@ -1 +1,130 @@
-node_modules
\ No newline at end of file
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
\ No newline at end of file
diff --git a/sync-with-algolia/.prettierrc.json b/sync-with-algolia/.prettierrc.json
index fa51da29..0a725205 100644
--- a/sync-with-algolia/.prettierrc.json
+++ b/sync-with-algolia/.prettierrc.json
@@ -1,6 +1,6 @@
 {
   "trailingComma": "es5",
   "tabWidth": 2,
-  "semi": false,
+  "semi": true,
   "singleQuote": true
 }
diff --git a/sync-with-algolia/package.json b/sync-with-algolia/package.json
index b2eb4d19..d4e7f956 100644
--- a/sync-with-algolia/package.json
+++ b/sync-with-algolia/package.json
@@ -8,8 +8,6 @@
     "format": "prettier --write src/**/*.js"
   },
   "keywords": [],
-  "author": "",
-  "license": "ISC",
   "dependencies": {
     "algoliasearch": "^4.17.2",
     "node-appwrite": "^9.0.0"
diff --git a/sync-with-algolia/src/environment.js b/sync-with-algolia/src/environment.js
index e83fd3f6..d1c68624 100644
--- a/sync-with-algolia/src/environment.js
+++ b/sync-with-algolia/src/environment.js
@@ -3,11 +3,11 @@
  * @return {string}
  */
 function getRequiredEnv(key) {
-  const value = process.env[key]
+  const value = process.env[key];
   if (value === undefined) {
-    throw new Error(`Environment variable ${key} is not set`)
+    throw new Error(`Environment variable ${key} is not set`);
   }
-  return value
+  return value;
 }
 
 function EnvironmentService() {
@@ -24,7 +24,7 @@ function EnvironmentService() {
     APPWRITE_PROJECT_ID:
       process.env.APPWRITE_PROJECT_ID ??
       getRequiredEnv('APPWRITE_FUNCTION_PROJECT_ID'),
-  }
+  };
 }
 
-export default EnvironmentService
+export default EnvironmentService;
diff --git a/sync-with-algolia/src/main.js b/sync-with-algolia/src/main.js
index d33112b0..d9addc72 100644
--- a/sync-with-algolia/src/main.js
+++ b/sync-with-algolia/src/main.js
@@ -1,14 +1,13 @@
-import { Client, Databases, Query } from 'node-appwrite'
-import algoliasearch from 'algoliasearch'
-import fs from 'fs'
-import path from 'path'
-import { fileURLToPath } from 'url'
-import EnvironmentService from './environment.js'
+import { Client, Databases, Query } from 'node-appwrite';
+import algoliasearch from 'algoliasearch';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import EnvironmentService from './environment.js';
 
-const __filename = fileURLToPath(import.meta.url)
-const __dirname = path.dirname(__filename)
-
-const staticFolder = path.join(__dirname, '../static')
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const staticFolder = path.join(__dirname, '../static');
 
 /**
  * @param {string} template
@@ -16,7 +15,7 @@ const staticFolder = path.join(__dirname, '../static')
  * @returns {string}
  */
 function interpolate(template, values) {
-  return template.replace(/{{([^}]+)}}/g, (_, key) => values[key])
+  return template.replace(/{{([^}]+)}}/g, (_, key) => values[key]);
 }
 
 export default async ({ req, res, log }) => {
@@ -30,65 +29,65 @@ export default async ({ req, res, log }) => {
     APPWRITE_COLLECTION_ID,
     APPWRITE_ENDPOINT,
     APPWRITE_PROJECT_ID,
-  } = EnvironmentService()
+  } = EnvironmentService();
 
   if (req.method === 'GET') {
     const template = fs
       .readFileSync(path.join(staticFolder, 'index.html'))
-      .toString()
+      .toString();
 
     const html = interpolate(template, {
       ALGOLIA_APP_ID,
       ALGOLIA_INDEX_ID,
       ALGOLIA_SEARCH_API_KEY,
-    })
+    });
 
-    return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' })
+    return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' });
   }
 
   const client = new Client()
     .setEndpoint(APPWRITE_ENDPOINT)
     .setProject(APPWRITE_PROJECT_ID)
-    .setKey(APPWRITE_API_KEY)
+    .setKey(APPWRITE_API_KEY);
 
-  const databases = new Databases(client)
+  const databases = new Databases(client);
 
-  const algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY)
-  const algoliaIndex = algolia.initIndex(ALGOLIA_INDEX_ID)
+  const algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY);
+  const algoliaIndex = algolia.initIndex(ALGOLIA_INDEX_ID);
 
-  let cursor = null
+  let cursor = null;
 
   do {
-    const queries = [Query.limit(100)]
+    const queries = [Query.limit(100)];
 
     if (cursor) {
-      queries.push(Query.cursorAfter(cursor))
+      queries.push(Query.cursorAfter(cursor));
     }
 
     const response = await databases.listDocuments(
       APPWRITE_DATABASE_ID,
       APPWRITE_COLLECTION_ID,
       queries
-    )
+    );
 
     if (response.documents.length > 0) {
-      cursor = response.documents[response.documents.length - 1].$id
+      cursor = response.documents[response.documents.length - 1].$id;
     } else {
-      log(`No more documents found.`)
-      cursor = null
-      break
+      log(`No more documents found.`);
+      cursor = null;
+      break;
     }
 
-    log(`Syncing chunk of ${response.documents.length} documents ...`)
+    log(`Syncing chunk of ${response.documents.length} documents ...`);
 
     const records = response.documents.map((document) => ({
       ...document,
       objectID: document.$id,
-    }))
-    await algoliaIndex.saveObjects(records)
-  } while (cursor !== null)
+    }));
+    await algoliaIndex.saveObjects(records);
+  } while (cursor !== null);
 
-  log('Sync finished.')
+  log('Sync finished.');
 
-  return res.empty()
-}
+  return res.empty();
+};

From aa109b28bf002cc938efc6dc7cabda0c76081afa Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Wed, 19 Jul 2023 21:34:51 +0100
Subject: [PATCH 3/9] chore: use classes

---
 sync-with-algolia/env.d.ts           | 17 +++++++++++++++
 sync-with-algolia/src/environment.js | 28 +++++++++++-------------
 sync-with-algolia/src/main.js        | 32 ++++++++++------------------
 3 files changed, 41 insertions(+), 36 deletions(-)
 create mode 100644 sync-with-algolia/env.d.ts

diff --git a/sync-with-algolia/env.d.ts b/sync-with-algolia/env.d.ts
new file mode 100644
index 00000000..fe81fdc3
--- /dev/null
+++ b/sync-with-algolia/env.d.ts
@@ -0,0 +1,17 @@
+declare global {
+  namespace NodeJS {
+    interface ProcessEnv {
+      ALGOLIA_APP_ID?: string;
+      ALGOLIA_ADMIN_API_KEY?: string;
+      ALGOLIA_INDEX_NAME?: string;
+      APPWRITE_ENDPOINT?: string;
+      APPWRITE_PROJECT_ID?: string;
+      APPWRITE_API_KEY?: string;
+      APPWRITE_FUNCTION_PROJECT_ID?: string;
+      APPWRITE_DATABASE_ID?: string;
+      APPWRITE_COLLECTION_ID?: string;
+    }
+  }
+}
+
+export {};
diff --git a/sync-with-algolia/src/environment.js b/sync-with-algolia/src/environment.js
index d1c68624..bcc65468 100644
--- a/sync-with-algolia/src/environment.js
+++ b/sync-with-algolia/src/environment.js
@@ -10,21 +10,19 @@ function getRequiredEnv(key) {
   return value;
 }
 
-function EnvironmentService() {
-  return {
-    ALGOLIA_APP_ID: getRequiredEnv('ALGOLIA_APP_ID'),
-    ALGOLIA_ADMIN_API_KEY: getRequiredEnv('ALGOLIA_ADMIN_API_KEY'),
-    ALGOLIA_INDEX_ID: getRequiredEnv('ALGOLIA_INDEX_ID'),
-    ALGOLIA_SEARCH_API_KEY: getRequiredEnv('ALGOLIA_SEARCH_API_KEY'),
-    APPWRITE_API_KEY: getRequiredEnv('APPWRITE_API_KEY'),
-    APPWRITE_DATABASE_ID: getRequiredEnv('APPWRITE_DATABASE_ID'),
-    APPWRITE_COLLECTION_ID: getRequiredEnv('APPWRITE_COLLECTION_ID'),
-    APPWRITE_ENDPOINT:
-      process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1',
-    APPWRITE_PROJECT_ID:
-      process.env.APPWRITE_PROJECT_ID ??
-      getRequiredEnv('APPWRITE_FUNCTION_PROJECT_ID'),
-  };
+class EnvironmentService {
+  ALGOLIA_APP_ID = getRequiredEnv('ALGOLIA_APP_ID');
+  ALGOLIA_ADMIN_API_KEY = getRequiredEnv('ALGOLIA_ADMIN_API_KEY');
+  ALGOLIA_INDEX_ID = getRequiredEnv('ALGOLIA_INDEX_ID');
+  ALGOLIA_SEARCH_API_KEY = getRequiredEnv('ALGOLIA_SEARCH_API_KEY');
+  APPWRITE_API_KEY = getRequiredEnv('APPWRITE_API_KEY');
+  APPWRITE_DATABASE_ID = getRequiredEnv('APPWRITE_DATABASE_ID');
+  APPWRITE_COLLECTION_ID = getRequiredEnv('APPWRITE_COLLECTION_ID');
+  APPWRITE_ENDPOINT =
+    process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1';
+  APPWRITE_PROJECT_ID =
+    process.env.APPWRITE_PROJECT_ID ??
+    getRequiredEnv('APPWRITE_FUNCTION_PROJECT_ID');
 }
 
 export default EnvironmentService;
diff --git a/sync-with-algolia/src/main.js b/sync-with-algolia/src/main.js
index d9addc72..d3de52e9 100644
--- a/sync-with-algolia/src/main.js
+++ b/sync-with-algolia/src/main.js
@@ -19,17 +19,7 @@ function interpolate(template, values) {
 }
 
 export default async ({ req, res, log }) => {
-  const {
-    ALGOLIA_APP_ID,
-    ALGOLIA_ADMIN_API_KEY,
-    ALGOLIA_INDEX_ID,
-    ALGOLIA_SEARCH_API_KEY,
-    APPWRITE_API_KEY,
-    APPWRITE_DATABASE_ID,
-    APPWRITE_COLLECTION_ID,
-    APPWRITE_ENDPOINT,
-    APPWRITE_PROJECT_ID,
-  } = EnvironmentService();
+  const env = new EnvironmentService();
 
   if (req.method === 'GET') {
     const template = fs
@@ -37,23 +27,23 @@ export default async ({ req, res, log }) => {
       .toString();
 
     const html = interpolate(template, {
-      ALGOLIA_APP_ID,
-      ALGOLIA_INDEX_ID,
-      ALGOLIA_SEARCH_API_KEY,
+      ALGOLIA_APP_ID: env.ALGOLIA_APP_ID,
+      ALGOLIA_INDEX_ID: env.ALGOLIA_INDEX_ID,
+      ALGOLIA_SEARCH_API_KEY: env.ALGOLIA_SEARCH_API_KEY,
     });
 
     return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' });
   }
 
   const client = new Client()
-    .setEndpoint(APPWRITE_ENDPOINT)
-    .setProject(APPWRITE_PROJECT_ID)
-    .setKey(APPWRITE_API_KEY);
+    .setEndpoint(env.APPWRITE_ENDPOINT)
+    .setProject(env.APPWRITE_PROJECT_ID)
+    .setKey(env.APPWRITE_API_KEY);
 
   const databases = new Databases(client);
 
-  const algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY);
-  const algoliaIndex = algolia.initIndex(ALGOLIA_INDEX_ID);
+  const algolia = algoliasearch(env.ALGOLIA_APP_ID, env.ALGOLIA_ADMIN_API_KEY);
+  const algoliaIndex = algolia.initIndex(env.ALGOLIA_INDEX_ID);
 
   let cursor = null;
 
@@ -65,8 +55,8 @@ export default async ({ req, res, log }) => {
     }
 
     const response = await databases.listDocuments(
-      APPWRITE_DATABASE_ID,
-      APPWRITE_COLLECTION_ID,
+      env.APPWRITE_DATABASE_ID,
+      env.APPWRITE_COLLECTION_ID,
       queries
     );
 

From 5d135e4c0f18b5128fc9b9510d4fc140029f8ad8 Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Wed, 19 Jul 2023 21:39:24 +0100
Subject: [PATCH 4/9] chore: destructure env

---
 sync-with-algolia/src/main.js | 32 +++++++++++++++++++++-----------
 1 file changed, 21 insertions(+), 11 deletions(-)

diff --git a/sync-with-algolia/src/main.js b/sync-with-algolia/src/main.js
index d3de52e9..45497fc6 100644
--- a/sync-with-algolia/src/main.js
+++ b/sync-with-algolia/src/main.js
@@ -19,7 +19,17 @@ function interpolate(template, values) {
 }
 
 export default async ({ req, res, log }) => {
-  const env = new EnvironmentService();
+  const {
+    APPWRITE_ENDPOINT,
+    APPWRITE_PROJECT_ID,
+    APPWRITE_API_KEY,
+    APPWRITE_DATABASE_ID,
+    APPWRITE_COLLECTION_ID,
+    ALGOLIA_APP_ID,
+    ALGOLIA_ADMIN_API_KEY,
+    ALGOLIA_INDEX_ID,
+    ALGOLIA_SEARCH_API_KEY,
+  } = new EnvironmentService();
 
   if (req.method === 'GET') {
     const template = fs
@@ -27,23 +37,23 @@ export default async ({ req, res, log }) => {
       .toString();
 
     const html = interpolate(template, {
-      ALGOLIA_APP_ID: env.ALGOLIA_APP_ID,
-      ALGOLIA_INDEX_ID: env.ALGOLIA_INDEX_ID,
-      ALGOLIA_SEARCH_API_KEY: env.ALGOLIA_SEARCH_API_KEY,
+      ALGOLIA_APP_ID,
+      ALGOLIA_INDEX_ID,
+      ALGOLIA_SEARCH_API_KEY,
     });
 
     return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' });
   }
 
   const client = new Client()
-    .setEndpoint(env.APPWRITE_ENDPOINT)
-    .setProject(env.APPWRITE_PROJECT_ID)
-    .setKey(env.APPWRITE_API_KEY);
+    .setEndpoint(APPWRITE_ENDPOINT)
+    .setProject(APPWRITE_PROJECT_ID)
+    .setKey(APPWRITE_API_KEY);
 
   const databases = new Databases(client);
 
-  const algolia = algoliasearch(env.ALGOLIA_APP_ID, env.ALGOLIA_ADMIN_API_KEY);
-  const algoliaIndex = algolia.initIndex(env.ALGOLIA_INDEX_ID);
+  const algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY);
+  const algoliaIndex = algolia.initIndex(ALGOLIA_INDEX_ID);
 
   let cursor = null;
 
@@ -55,8 +65,8 @@ export default async ({ req, res, log }) => {
     }
 
     const response = await databases.listDocuments(
-      env.APPWRITE_DATABASE_ID,
-      env.APPWRITE_COLLECTION_ID,
+      APPWRITE_DATABASE_ID,
+      APPWRITE_COLLECTION_ID,
       queries
     );
 

From c5f5ec9df24fec6f12e89ac1ec2b2aa10dc65a35 Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Thu, 20 Jul 2023 10:39:48 +0100
Subject: [PATCH 5/9] chore: new structure

---
 {sync-with-algolia => node/sync-with-algolia}/.gitignore         | 0
 {sync-with-algolia => node/sync-with-algolia}/.prettierrc.json   | 0
 {sync-with-algolia => node/sync-with-algolia}/README.md          | 0
 {sync-with-algolia => node/sync-with-algolia}/env.d.ts           | 0
 {sync-with-algolia => node/sync-with-algolia}/package-lock.json  | 0
 {sync-with-algolia => node/sync-with-algolia}/package.json       | 0
 {sync-with-algolia => node/sync-with-algolia}/src/environment.js | 0
 {sync-with-algolia => node/sync-with-algolia}/src/main.js        | 0
 {sync-with-algolia => node/sync-with-algolia}/static/index.html  | 0
 9 files changed, 0 insertions(+), 0 deletions(-)
 rename {sync-with-algolia => node/sync-with-algolia}/.gitignore (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/.prettierrc.json (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/README.md (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/env.d.ts (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/package-lock.json (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/package.json (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/src/environment.js (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/src/main.js (100%)
 rename {sync-with-algolia => node/sync-with-algolia}/static/index.html (100%)

diff --git a/sync-with-algolia/.gitignore b/node/sync-with-algolia/.gitignore
similarity index 100%
rename from sync-with-algolia/.gitignore
rename to node/sync-with-algolia/.gitignore
diff --git a/sync-with-algolia/.prettierrc.json b/node/sync-with-algolia/.prettierrc.json
similarity index 100%
rename from sync-with-algolia/.prettierrc.json
rename to node/sync-with-algolia/.prettierrc.json
diff --git a/sync-with-algolia/README.md b/node/sync-with-algolia/README.md
similarity index 100%
rename from sync-with-algolia/README.md
rename to node/sync-with-algolia/README.md
diff --git a/sync-with-algolia/env.d.ts b/node/sync-with-algolia/env.d.ts
similarity index 100%
rename from sync-with-algolia/env.d.ts
rename to node/sync-with-algolia/env.d.ts
diff --git a/sync-with-algolia/package-lock.json b/node/sync-with-algolia/package-lock.json
similarity index 100%
rename from sync-with-algolia/package-lock.json
rename to node/sync-with-algolia/package-lock.json
diff --git a/sync-with-algolia/package.json b/node/sync-with-algolia/package.json
similarity index 100%
rename from sync-with-algolia/package.json
rename to node/sync-with-algolia/package.json
diff --git a/sync-with-algolia/src/environment.js b/node/sync-with-algolia/src/environment.js
similarity index 100%
rename from sync-with-algolia/src/environment.js
rename to node/sync-with-algolia/src/environment.js
diff --git a/sync-with-algolia/src/main.js b/node/sync-with-algolia/src/main.js
similarity index 100%
rename from sync-with-algolia/src/main.js
rename to node/sync-with-algolia/src/main.js
diff --git a/sync-with-algolia/static/index.html b/node/sync-with-algolia/static/index.html
similarity index 100%
rename from sync-with-algolia/static/index.html
rename to node/sync-with-algolia/static/index.html

From 1f9cd7c229f70bb582f9816a7cde4e2b036e1186 Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Thu, 20 Jul 2023 12:09:54 +0100
Subject: [PATCH 6/9] chore: prettier script

---
 node/sync-with-algolia/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/node/sync-with-algolia/package.json b/node/sync-with-algolia/package.json
index d4e7f956..04329011 100644
--- a/node/sync-with-algolia/package.json
+++ b/node/sync-with-algolia/package.json
@@ -5,7 +5,7 @@
   "main": "src/main.js",
   "type": "module",
   "scripts": {
-    "format": "prettier --write src/**/*.js"
+    "format": "prettier --write ."
   },
   "keywords": [],
   "dependencies": {

From 08e2ecd5d3f4f95ed829e2722497ef81e68e0943 Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Tue, 25 Jul 2023 13:10:50 +0100
Subject: [PATCH 7/9] docs: update to new template

---
 node/sync-with-algolia/README.md | 129 ++++++++++++++++++++++++-------
 1 file changed, 103 insertions(+), 26 deletions(-)

diff --git a/node/sync-with-algolia/README.md b/node/sync-with-algolia/README.md
index b2b5c18b..e7c72853 100644
--- a/node/sync-with-algolia/README.md
+++ b/node/sync-with-algolia/README.md
@@ -1,39 +1,116 @@
-# Appwrite - Algolia Sync Function
+# ⚡ Sync With Algolia Function
 
-This function helps in syncing the data between Appwrite's database and Algolia's search index. It fetches the documents from Appwrite database and updates the Algolia search index.
+Dyncs documents in an Appwrite database collection to an Algolia index.
 
-## Environment Variables
+## 🧰 Usage
 
-To ensure the function operates as intended, ensure the following variables are set:
+### `GET /`
 
-- **ALGOLIA_APP_ID**: This is your Algolia application's ID.
-- **ALGOLIA_ADMIN_API_KEY**: This is your Algolia application's Admin API key.
-- **ALGOLIA_INDEX_ID**: This is the ID of the Algolia index where the data is stored.
-- **ALGOLIA_SEARCH_API_KEY**: This is the Search API key of your Algolia application.
-- **APPWRITE_API_KEY**: This is your Appwrite project's API key.
-- **APPWRITE_DATABASE_ID**: This is the ID of your Appwrite database where the data is stored.
-- **APPWRITE_COLLECTION_ID**: This is the ID of the collection within the Appwrite database.
-- **APPWRITE_ENDPOINT**: This is the endpoint where your Appwrite server is located.
-- **APPWRITE_PROJECT_ID**: This refers to the specific ID of your Appwrite project.
+Returns a HTML page where search can be performed to test the indexing.
 
-## Usage
 
-This function supports two types of requests:
+### `POST /`
 
-1. **Retrieving the Search Interface**
+Triggers indexing of the Appwrite database collection to Algolia.
 
-   - **Request Type:** GET
-   - **Response:** 
-     - On success, the function will respond with an HTML interface for Algolia search. The response content type is `text/html`.
+**Response**
 
-2. **Syncing Appwrite Database with Algolia Index**
+Sample `204` Response: No content.
 
-   - **Request Type:** POST
-   - **Response:** 
-     - On success, the function will synchronize the Appwrite database with the specified Algolia index and return an empty response. The sync process operates in chunks, retrieving up to 100 documents per request from the Appwrite database and updating them on the Algolia index.
+## ⚙️ Configuration
 
-## Limitations
+| Setting           | Value         |
+| ----------------- | ------------- |
+| Runtime           | Node (18.0)   |
+| Entrypoint        | `src/main.js` |
+| Build Commands    | `npm install` |
+| Permissions       | `any`         |
+| Timeout (Seconds) | 15            |
 
-This function synchronizes the Appwrite database with the Algolia index in one direction only - from Appwrite to Algolia. If any changes are made directly on the Algolia index, they will not be reflected in the Appwrite database. 
+## 🔒 Environment Variables
 
-Additionally, if any changes are made in the Appwrite database after the sync, they will not be reflected in the Algolia index until the sync function is run again. To achieve real-time sync, consider triggering this function whenever a document is added, updated, or deleted in your Appwrite database.
+### 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  | `612a3...5b6c9`                                           |
+| 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` |
+
+### APPWRITE_PROJECT_ID
+
+The ID of the Appwrite project associated with the function.
+
+| Question     | Answer              |
+| ------------ | ------------------- |
+| Required     | Yes                 |
+| Sample Value | `builtWithAppwrite` |
+
+### ALGOLIA_APP_ID
+
+The application ID for your Algolia service.
+
+| Question     | Answer             |
+| ------------ | ------------------ |
+| Required     | Yes                |
+| Sample Value | `YourAlgoliaAppId` |
+
+### ALGOLIA_ADMIN_API_KEY
+
+The admin API Key for your Algolia service.
+
+| Question      | Answer                                                                     |
+| ------------- | -------------------------------------------------------------------------- |
+| Required      | Yes                                                                        |
+| Sample Value  | `YourAlgoliaAdminApiKey`                                                   |
+| 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 | `YourAlgoliaIndexId` |
+
+### 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  | `YourAlgoliaSearchApiKey`                                                  |
+| Documentation | [Algolia: API Keys](https://www.algolia.com/doc/guides/security/api-keys/) |

From 46daeeabf7ff73a273d2f771d1434990ba67c63b Mon Sep 17 00:00:00 2001
From: loks0n <22452787+loks0n@users.noreply.github.com>
Date: Sun, 30 Jul 2023 21:22:48 +0100
Subject: [PATCH 8/9] chore: new utils

---
 node/sync-with-algolia/src/environment.js | 28 ---------
 node/sync-with-algolia/src/main.js        | 74 ++++++++++-------------
 node/sync-with-algolia/src/utils.js       | 43 +++++++++++++
 3 files changed, 74 insertions(+), 71 deletions(-)
 delete mode 100644 node/sync-with-algolia/src/environment.js
 create mode 100644 node/sync-with-algolia/src/utils.js

diff --git a/node/sync-with-algolia/src/environment.js b/node/sync-with-algolia/src/environment.js
deleted file mode 100644
index bcc65468..00000000
--- a/node/sync-with-algolia/src/environment.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @param {string} key
- * @return {string}
- */
-function getRequiredEnv(key) {
-  const value = process.env[key];
-  if (value === undefined) {
-    throw new Error(`Environment variable ${key} is not set`);
-  }
-  return value;
-}
-
-class EnvironmentService {
-  ALGOLIA_APP_ID = getRequiredEnv('ALGOLIA_APP_ID');
-  ALGOLIA_ADMIN_API_KEY = getRequiredEnv('ALGOLIA_ADMIN_API_KEY');
-  ALGOLIA_INDEX_ID = getRequiredEnv('ALGOLIA_INDEX_ID');
-  ALGOLIA_SEARCH_API_KEY = getRequiredEnv('ALGOLIA_SEARCH_API_KEY');
-  APPWRITE_API_KEY = getRequiredEnv('APPWRITE_API_KEY');
-  APPWRITE_DATABASE_ID = getRequiredEnv('APPWRITE_DATABASE_ID');
-  APPWRITE_COLLECTION_ID = getRequiredEnv('APPWRITE_COLLECTION_ID');
-  APPWRITE_ENDPOINT =
-    process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1';
-  APPWRITE_PROJECT_ID =
-    process.env.APPWRITE_PROJECT_ID ??
-    getRequiredEnv('APPWRITE_FUNCTION_PROJECT_ID');
-}
-
-export default EnvironmentService;
diff --git a/node/sync-with-algolia/src/main.js b/node/sync-with-algolia/src/main.js
index 45497fc6..09908fe9 100644
--- a/node/sync-with-algolia/src/main.js
+++ b/node/sync-with-algolia/src/main.js
@@ -1,59 +1,47 @@
 import { Client, Databases, Query } from 'node-appwrite';
 import algoliasearch from 'algoliasearch';
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import EnvironmentService from './environment.js';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-const staticFolder = path.join(__dirname, '../static');
-
-/**
- * @param {string} template
- * @param {Record<string, string>} values
- * @returns {string}
- */
-function interpolate(template, values) {
-  return template.replace(/{{([^}]+)}}/g, (_, key) => values[key]);
-}
+import { getStaticFile, interpolate, throwIfMissing } from './utils.js';
 
 export default async ({ req, res, log }) => {
-  const {
-    APPWRITE_ENDPOINT,
-    APPWRITE_PROJECT_ID,
-    APPWRITE_API_KEY,
-    APPWRITE_DATABASE_ID,
-    APPWRITE_COLLECTION_ID,
-    ALGOLIA_APP_ID,
-    ALGOLIA_ADMIN_API_KEY,
-    ALGOLIA_INDEX_ID,
-    ALGOLIA_SEARCH_API_KEY,
-  } = new EnvironmentService();
+  throwIfMissing(process.env, [
+    'ALGOLIA_APP_ID',
+    'ALGOLIA_ADMIN_API_KEY',
+    'ALGOLIA_INDEX_ID',
+    'ALGOLIA_SEARCH_API_KEY',
+    'APPWRITE_API_KEY',
+    'APPWRITE_DATABASE_ID',
+    'APPWRITE_COLLECTION_ID',
+    'APPWRITE_PROJECT_ID',
+  ]);
 
   if (req.method === 'GET') {
-    const template = fs
-      .readFileSync(path.join(staticFolder, 'index.html'))
-      .toString();
-
-    const html = interpolate(template, {
-      ALGOLIA_APP_ID,
-      ALGOLIA_INDEX_ID,
-      ALGOLIA_SEARCH_API_KEY,
+    const html = interpolate(getStaticFile('index.html'), {
+      ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID,
+      ALGOLIA_INDEX_ID: process.env.ALGOLIA_INDEX_ID,
+      ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY,
     });
 
     return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' });
   }
 
   const client = new Client()
-    .setEndpoint(APPWRITE_ENDPOINT)
-    .setProject(APPWRITE_PROJECT_ID)
-    .setKey(APPWRITE_API_KEY);
+    .setEndpoint(
+      process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1'
+    )
+    .setProject(
+      process.env.APPWRITE_PROJECT_ID ??
+        process.env.APPWRITE_FUNCTION_PROJECT_ID ??
+        ''
+    )
+    .setKey(process.env.APPWRITE_API_KEY ?? '');
 
   const databases = new Databases(client);
 
-  const algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY);
-  const algoliaIndex = algolia.initIndex(ALGOLIA_INDEX_ID);
+  const algolia = algoliasearch(
+    process.env.ALGOLIA_APP_ID ?? '',
+    process.env.ALGOLIA_ADMIN_API_KEY ?? ''
+  );
+  const algoliaIndex = algolia.initIndex(process.env.ALGOLIA_INDEX_ID ?? '');
 
   let cursor = null;
 
@@ -65,8 +53,8 @@ export default async ({ req, res, log }) => {
     }
 
     const response = await databases.listDocuments(
-      APPWRITE_DATABASE_ID,
-      APPWRITE_COLLECTION_ID,
+      process.env.APPWRITE_DATABASE_ID ?? '',
+      process.env.APPWRITE_COLLECTION_ID ?? '',
       queries
     );
 
diff --git a/node/sync-with-algolia/src/utils.js b/node/sync-with-algolia/src/utils.js
new file mode 100644
index 00000000..1a8951bd
--- /dev/null
+++ b/node/sync-with-algolia/src/utils.js
@@ -0,0 +1,43 @@
+import path from 'path';
+import { fileURLToPath } from 'url';
+import fs from 'fs';
+
+/**
+ * Throws an error if any of the keys are missing from the object
+ * @param {*} obj
+ * @param {string[]} keys
+ * @throws {Error}
+ */
+export function throwIfMissing(obj, keys) {
+  const missing = [];
+  for (let key of keys) {
+    if (!(key in obj) || !obj[key]) {
+      missing.push(key);
+    }
+  }
+  if (missing.length > 0) {
+    throw new Error(`Missing required fields: ${missing.join(', ')}`);
+  }
+}
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const staticFolder = path.join(__dirname, '../static');
+
+/**
+ * Returns the contents of a file in the static folder
+ * @param {string} fileName
+ * @returns {string} Contents of static/{fileName}
+ */
+export function getStaticFile(fileName) {
+  return fs.readFileSync(path.join(staticFolder, fileName)).toString();
+}
+
+/**
+ * @param {string} template
+ * @param {Record<string, string | undefined>} values
+ * @returns {string}
+ */
+export function interpolate(template, values) {
+  return template.replace(/{{([^}]+)}}/g, (_, key) => values[key] || '');
+}

From 2898ac0ade99c70e8ec05057fb1e69fe4bedce3d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matej=20Ba=C4=8Do?= <matejbaco2000@gmail.com>
Date: Mon, 31 Jul 2023 07:35:40 +0000
Subject: [PATCH 9/9] PR review changes

---
 node/sync-with-algolia/README.md         | 28 +++++++++---------------
 node/sync-with-algolia/src/main.js       |  9 ++------
 node/sync-with-algolia/static/index.html | 12 +++++-----
 3 files changed, 18 insertions(+), 31 deletions(-)

diff --git a/node/sync-with-algolia/README.md b/node/sync-with-algolia/README.md
index e7c72853..73748376 100644
--- a/node/sync-with-algolia/README.md
+++ b/node/sync-with-algolia/README.md
@@ -1,18 +1,19 @@
-# ⚡ Sync With Algolia Function
+# 🔍 Node.js Sync with Algolia Function
 
-Dyncs documents in an Appwrite database collection to an Algolia index.
+Intuitive search bar for any data in Appwrite Databases.
 
 ## 🧰 Usage
 
 ### `GET /`
 
-Returns a HTML page where search can be performed to test the indexing.
-
+HTML form for interacting with the model.
 
 ### `POST /`
 
 Triggers indexing of the Appwrite database collection to Algolia.
 
+No parameters required.
+
 **Response**
 
 Sample `204` Response: No content.
@@ -46,7 +47,7 @@ The ID of the Appwrite database that contains the collection to sync.
 | Question      | Answer                                                    |
 | ------------- | --------------------------------------------------------- |
 | Required      | Yes                                                       |
-| Sample Value  | `612a3...5b6c9`                                           |
+| Sample Value  | `64a55...7b912`                                           |
 | Documentation | [Appwrite: Databases](https://appwrite.io/docs/databases) |
 
 ### APPWRITE_COLLECTION_ID
@@ -68,15 +69,6 @@ The URL endpoint of the Appwrite server. If not provided, it defaults to the App
 | Required     | No                             |
 | Sample Value | `https://cloud.appwrite.io/v1` |
 
-### APPWRITE_PROJECT_ID
-
-The ID of the Appwrite project associated with the function.
-
-| Question     | Answer              |
-| ------------ | ------------------- |
-| Required     | Yes                 |
-| Sample Value | `builtWithAppwrite` |
-
 ### ALGOLIA_APP_ID
 
 The application ID for your Algolia service.
@@ -84,7 +76,7 @@ The application ID for your Algolia service.
 | Question     | Answer             |
 | ------------ | ------------------ |
 | Required     | Yes                |
-| Sample Value | `YourAlgoliaAppId` |
+| Sample Value | `EG6...VJJ`        |
 
 ### ALGOLIA_ADMIN_API_KEY
 
@@ -93,7 +85,7 @@ The admin API Key for your Algolia service.
 | Question      | Answer                                                                     |
 | ------------- | -------------------------------------------------------------------------- |
 | Required      | Yes                                                                        |
-| Sample Value  | `YourAlgoliaAdminApiKey`                                                   |
+| Sample Value  | `fd0aa...136a8`                                                            |
 | Documentation | [Algolia: API Keys](https://www.algolia.com/doc/guides/security/api-keys/) |
 
 ### ALGOLIA_INDEX_ID
@@ -103,7 +95,7 @@ The ID of the index in Algolia where the documents are to be synced.
 | Question     | Answer               |
 | ------------ | -------------------- |
 | Required     | Yes                  |
-| Sample Value | `YourAlgoliaIndexId` |
+| Sample Value | `appwrite_index`     |
 
 ### ALGOLIA_SEARCH_API_KEY
 
@@ -112,5 +104,5 @@ The search API Key for your Algolia service. This key is used for searching the
 | Question      | Answer                                                                     |
 | ------------- | -------------------------------------------------------------------------- |
 | Required      | Yes                                                                        |
-| Sample Value  | `YourAlgoliaSearchApiKey`                                                  |
+| Sample Value  | `bf2f5...df733`                                                            |
 | Documentation | [Algolia: API Keys](https://www.algolia.com/doc/guides/security/api-keys/) |
diff --git a/node/sync-with-algolia/src/main.js b/node/sync-with-algolia/src/main.js
index 09908fe9..9b200c14 100644
--- a/node/sync-with-algolia/src/main.js
+++ b/node/sync-with-algolia/src/main.js
@@ -11,7 +11,6 @@ export default async ({ req, res, log }) => {
     'APPWRITE_API_KEY',
     'APPWRITE_DATABASE_ID',
     'APPWRITE_COLLECTION_ID',
-    'APPWRITE_PROJECT_ID',
   ]);
 
   if (req.method === 'GET') {
@@ -28,11 +27,7 @@ export default async ({ req, res, log }) => {
     .setEndpoint(
       process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1'
     )
-    .setProject(
-      process.env.APPWRITE_PROJECT_ID ??
-        process.env.APPWRITE_FUNCTION_PROJECT_ID ??
-        ''
-    )
+    .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID ?? '')
     .setKey(process.env.APPWRITE_API_KEY ?? '');
 
   const databases = new Databases(client);
@@ -61,7 +56,7 @@ export default async ({ req, res, log }) => {
     if (response.documents.length > 0) {
       cursor = response.documents[response.documents.length - 1].$id;
     } else {
-      log(`No more documents found.`);
+      log('No more documents found.');
       cursor = null;
       break;
     }
diff --git a/node/sync-with-algolia/static/index.html b/node/sync-with-algolia/static/index.html
index 31dd82ef..c0b519d0 100644
--- a/node/sync-with-algolia/static/index.html
+++ b/node/sync-with-algolia/static/index.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
@@ -56,22 +56,22 @@ <h1 class="heading-level-1">Algolia Search Demo</h1>
 
     <script>
       const searchClient = algoliasearch(
-        "{{ALGOLIA_APP_ID}}",
-        "{{ALGOLIA_SEARCH_API_KEY}}"
+        '{{ALGOLIA_APP_ID}}',
+        '{{ALGOLIA_SEARCH_API_KEY}}'
       );
 
       const search = instantsearch({
-        indexName: "{{ALGOLIA_INDEX_ID}}",
+        indexName: '{{ALGOLIA_INDEX_ID}}',
         searchClient,
       });
 
       search.addWidgets([
         instantsearch.widgets.searchBox({
-          container: "#searchbox",
+          container: '#searchbox',
         }),
 
         instantsearch.widgets.hits({
-          container: "#hits",
+          container: '#hits',
           templates: {
             item(hit, { html, components }) {
               return html`
