Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(deno): add sync with meilisearch template #212

Merged
merged 5 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions deno/sync-with-meilisearch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ⚡ Sync Appwrite to Meilisearch Function

Syncs documents in an Appwrite database collection to a Meilisearch index.

## 🧰 Usage

### GET /

Returns HTML page where search can be performed to test the indexing.

### POST /

Triggers indexing of the Appwrite database collection to Meilisearch.

**Response**

Sample `204` Response: No content.

## ⚙️ Configuration

| Setting | Value |
| ----------------- | ------------- |
| Runtime | Node (18.0) |
| Entrypoint | `src/main.js` |
| Build Commands | `npm install` |
| Permissions | `any` |
| Timeout (Seconds) | 15 |
Aunali321 marked this conversation as resolved.
Show resolved Hide resolved

## 🔒 Environment Variables

### APPWRITE_API_KEY

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/databases#collection) |

### 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` |

### MEILISEARCH_ENDPOINT

The host URL of the Meilisearch server.

| Question | Answer |
| ------------ | ----------------------- |
| Required | Yes |
| Sample Value | `http://127.0.0.1:7700` |

### MEILISEARCH_ADMIN_API_KEY

The admin API key for Meilisearch.

| Question | Answer |
| ------------- | ------------------------------------------------------------------------ |
| Required | Yes |
| Sample Value | `masterKey1234` |
| Documentation | [Meilisearch: API Keys](https://docs.meilisearch.com/reference/api/keys) |

### MEILISEARCH_INDEX_NAME

Name of the Meilisearch index to which the documents will be synchronized.

| Question | Answer |
| ------------ | ---------- |
| Required | Yes |
| Sample Value | `my_index` |

### MEILISEARCH_SEARCH_API_KEY

API Key for Meilisearch search operations.

| Question | Answer |
| ------------- | ------------------------------------------------------------------------ |
| Required | Yes |
| Sample Value | `searchKey1234` |
| Documentation | [Meilisearch: API Keys](https://docs.meilisearch.com/reference/api/keys) |
75 changes: 75 additions & 0 deletions deno/sync-with-meilisearch/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Client, Databases, Query } from "https://deno.land/x/appwrite@9.0.0/mod.ts";
import { getStaticFile, interpolate, throwIfMissing } from './utils.ts';
import { MeiliSearch } from "https://esm.sh/meilisearch@0.35.0";
import { serve } from "https://deno.land/std@0.154.0/http/server.ts";

const handler = async (req : any, log : any) => {
Aunali321 marked this conversation as resolved.
Show resolved Hide resolved
throwIfMissing(Deno.env.toObject(), [
'APPWRITE_API_KEY',
'APPWRITE_DATABASE_ID',
'APPWRITE_COLLECTION_ID',
'MEILISEARCH_ENDPOINT',
'MEILISEARCH_INDEX_NAME',
'MEILISEARCH_ADMIN_API_KEY',
'MEILISEARCH_SEARCH_API_KEY',
]);

if (req.method === 'GET') {
const html = interpolate(await getStaticFile('index.html'), {
MEILISEARCH_ENDPOINT: Deno.env.get('MEILISEARCH_ENDPOINT'),
MEILISEARCH_INDEX_NAME: Deno.env.get('MEILISEARCH_INDEX_NAME'),
MEILISEARCH_SEARCH_API_KEY: Deno.env.get('MEILISEARCH_SEARCH_API_KEY'),
});

return new Response(html,{ status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
Aunali321 marked this conversation as resolved.
Show resolved Hide resolved
Aunali321 marked this conversation as resolved.
Show resolved Hide resolved
}

const client = new Client()
.setEndpoint(
Deno.env.get('APPWRITE_ENDPOINT') ?? 'https://cloud.appwrite.io/v1'
)
.setProject(Deno.env.get('APPWRITE_FUNCTION_PROJECT_ID') ?? '')
.setKey(Deno.env.get('APPWRITE_API_KEY') ?? '');

const databases = new Databases(client);

const meilisearch = new MeiliSearch({
host: Deno.env.get('MEILISEARCH_ENDPOINT'),
apiKey: Deno.env.get('MEILISEARCH_ADMIN_API_KEY'),
});

const index = meilisearch.index(Deno.env.get('MEILISEARCH_INDEX_NAME'));

let cursor = null;

do {
const queries = [Query.limit(100)];

if (cursor) {
queries.push(Query.cursorAfter(cursor));
}

const { documents } = await databases.listDocuments(
Deno.env.get('APPWRITE_DATABASE_ID') ?? '',
Deno.env.get('APPWRITE_COLLECTION_ID') ?? '',
queries
);

if (documents.length > 0) {
cursor = documents[documents.length - 1].$id;
} else {
log(`No more documents found.`);
cursor = null;
break;
}

log(`Syncing chunk of ${documents.length} documents ...`);
await index.addDocuments(documents, { primaryKey: '$id' });
} while (cursor !== null);

log('Sync finished.');

return new Response('Sync finished.', {status: 200});
}

serve(handler);
Aunali321 marked this conversation as resolved.
Show resolved Hide resolved
32 changes: 32 additions & 0 deletions deno/sync-with-meilisearch/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Throws an error if any of the keys are missing from the object
*/
export function throwIfMissing(obj: any, keys: string[]) {
const missing: string[] = [];
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(', ')}`);
}
}

/**
* Returns the contents of a file in the static folder
*/
export async function getStaticFile(fileName: string): Promise<string> {
Aunali321 marked this conversation as resolved.
Show resolved Hide resolved
const staticFolder = "../static/"
const filePath = new URL(fileName, `${import.meta.url}/../${staticFolder}`);
try {
const data = await Deno.readFile(filePath);
return new TextDecoder().decode(data);
} catch (error) {
Aunali321 marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Error reading file: ${error.message}`);
}
}

export function interpolate(template: string, values: Record<string, string | undefined>) : string {
return template.replace(/{{([^}]+)}}/g, (_, key) => values[key] || '');
}
72 changes: 72 additions & 0 deletions deno/sync-with-meilisearch/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!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>Meilisearch Demo</title>

<script src="https://unpkg.com/meilisearch@0.34.1"></script>
<script src="https://unpkg.com/alpinejs" defer></script>

<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">Meilisearch 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"
>
Use this demo to verify that the sync between Appwrite Databases and
Meilisearch was successful. Search your Meilisearch index using the
input below.
</p>
</div>
</div>
<div
class="container u-margin-block-start-negative-56"
x-data="{ search: '', results: [ ] }"
x-init="$watch('search', async (value) => { results = await onSearch(value) })"
>
<div class="card u-flex u-gap-24 u-flex-vertical">
<div id="searchbox">
<div
class="input-text-wrapper is-with-end-button u-width-full-line"
>
<input x-model="search" type="search" placeholder="Search" />
<div class="icon-search" aria-hidden="true"></div>
</div>
</div>
<div id="hits" class="u-flex u-flex-vertical u-gap-12">
<template x-for="result in results">
<div class="card">
<pre x-text="JSON.stringify(result, null, '\t')"></pre>
</div>
</template>
</div>
</div>
</div>
</main>
<script>
const meilisearch = new MeiliSearch({
host: '{{MEILISEARCH_ENDPOINT}}',
apiKey: '{{MEILISEARCH_SEARCH_API_KEY}}',
});

const index = meilisearch.index('{{MEILISEARCH_INDEX_NAME}}');

window.onSearch = async function (prompt) {
return (await index.search(prompt)).hits;
};
</script>
</body>
</html>
12 changes: 12 additions & 0 deletions deno/sync-with-meilisearch/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2016",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}