Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion docs/api-catalog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Compact provider slug catalog for agents. Use the slug to reconstruct provider-specific docs URLs only when needed.

Provider count: 874
Provider count: 875

URL patterns:
- Main provider page: https://nango.dev/docs/integrations/all/{slug}.md or https://nango.dev/docs/api-integrations/{slug}.md
Expand Down Expand Up @@ -470,6 +470,7 @@ URL patterns:
| `maxio` | Maxio | BASIC | [docs](https://nango.dev/docs/api-integrations/maxio.md) | [connect](https://nango.dev/docs/api-integrations/maxio/connect.md) | | payment |
| `mcp-generic` | MCP Server OAuth2 (Generic) | MCP_OAUTH2_GENERIC | [docs](https://nango.dev/docs/integrations/all/mcp-generic.md) | | | mcp |
| `medallia` | Medallia | OAUTH2_CC | [docs](https://nango.dev/docs/integrations/all/medallia.md) | [connect](https://nango.dev/docs/integrations/all/medallia/connect.md) | | crm, support, surveys |
| `meilisearch` | Meilisearch | API_KEY | [docs](https://nango.dev/docs/integrations/all/meilisearch.md) | | | search |
| `mercury` | Mercury | OAUTH2 | [docs](https://nango.dev/docs/api-integrations/mercury.md) | [connect](https://nango.dev/docs/api-integrations/mercury/connect.md) | [setup](https://nango.dev/docs/api-integrations/mercury/how-to-register-your-own-mercury-oauth-app.md) | accounting |
| `meta-marketing-api` | Meta Marketing API | OAUTH2 | [docs](https://nango.dev/docs/api-integrations/meta-marketing-api.md) | | [setup](https://nango.dev/docs/api-integrations/meta-marketing-api/how-to-register-your-own-meta-marketing-api-oauth-app.md) | marketing |
| `metabase` | Metabase | API_KEY | [docs](https://nango.dev/docs/api-integrations/metabase.md) | [connect](https://nango.dev/docs/api-integrations/metabase/connect.md) | | analytics |
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,7 @@
"api-integrations/mattermost",
"integrations/all/mcp-generic",
"integrations/all/medallia",
"integrations/all/meilisearch",
"api-integrations/mercury",
"api-integrations/metabase",
"api-integrations/meta-marketing-api",
Expand Down
61 changes: 61 additions & 0 deletions docs/integrations/all/meilisearch.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: Meilisearch
sidebarTitle: Meilisearch
---

import Overview from "/snippets/overview.mdx"

<Overview />

## Access requirements
| Pre-Requisites | Status | Comment|
| - | - | - |
| Paid dev account | ❌ | Meilisearch is open source; Cloud has a free tier. |
| Paid test account | ❌ | |
| Partnership | ❌ | |
| App review | ❌ | |
| Security audit | ❌ | |

## Connecting to Meilisearch

Meilisearch uses API-key authentication. When creating a connection provide:

- **Instance URL** — your Meilisearch host including scheme, without a trailing slash, e.g. `https://ms-xxxx.meilisearch.io` or `http://localhost:7700`.
- **API Key** — a Meilisearch API key (not the master key). The Default Admin API Key works out of the box. A custom key must cover the actions you plan to use (`search`, `documents.*`, `tasks.get`, plus `version` for connection verification and `keys.get` for automatic tenant-token uid resolution).

## Pre-built actions

| Action | Description |
| - | - |
| `search-documents` | Search documents in an index. |
| `get-documents` | Fetch documents from an index, optionally filtered. |
| `add-documents` | Add or replace documents (batch). |
| `update-documents` | Add or partially update documents (batch). |
| `delete-documents` | Delete documents by ids or filter. |
| `get-task` | Poll the status of an async write task. |
| `generate-tenant-token` | Mint a scoped, signed tenant-token JWT carrying per-index ACL search rules. |

## Tenant tokens & ACL

`generate-tenant-token` signs an HS256 JWT with the connection's API key. The token carries `searchRules` (per-index filters) that scope what an end user can search; the signing key's ACL bounds the token. Example input:

```json
{
"searchRules": { "medical_records": { "filter": "patient_id = 42" } },
"expiresInSeconds": 3600
}
```

Notes:

- Tokens expire after **1 hour** by default. Set `expiresInSeconds` or `expiresAt` (epoch seconds) to change this; `expiresAt` takes precedence when both are provided.
- A rule value can also be `null` (no restriction), e.g. `{ "searchRules": { "*": null } }`.
- If the connection's key lacks the `keys.get` action, pass `apiKeyUid` explicitly (find it via `GET /keys` with an authorized key). Master keys cannot sign tenant tokens.

## Useful links

- [Meilisearch API keys](https://www.meilisearch.com/docs/reference/api/keys)
- [Tenant tokens](https://www.meilisearch.com/docs/learn/security/basic_security)
- [Documents API](https://www.meilisearch.com/docs/reference/api/documents)

<Note>Contribute improvements by [editing this page](https://github.com/nangohq/nango/tree/master/docs/integrations/all/meilisearch.mdx)</Note>
1 change: 1 addition & 0 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14494,6 +14494,7 @@ Use these slugs to construct provider-specific docs URLs only when needed.
| `maxio` | Maxio | BASIC | [docs](https://nango.dev/docs/api-integrations/maxio.md) | [connect](https://nango.dev/docs/api-integrations/maxio/connect.md) | | payment |
| `mcp-generic` | MCP Server OAuth2 (Generic) | MCP_OAUTH2_GENERIC | [docs](https://nango.dev/docs/integrations/all/mcp-generic.md) | | | mcp |
| `medallia` | Medallia | OAUTH2_CC | [docs](https://nango.dev/docs/integrations/all/medallia.md) | [connect](https://nango.dev/docs/integrations/all/medallia/connect.md) | | crm, support, surveys |
| `meilisearch` | Meilisearch | API_KEY | [docs](https://nango.dev/docs/integrations/all/meilisearch.md) | | | search |
| `mercury` | Mercury | OAUTH2 | [docs](https://nango.dev/docs/api-integrations/mercury.md) | [connect](https://nango.dev/docs/api-integrations/mercury/connect.md) | [setup](https://nango.dev/docs/api-integrations/mercury/how-to-register-your-own-mercury-oauth-app.md) | accounting |
| `meta-marketing-api` | Meta Marketing API | OAUTH2 | [docs](https://nango.dev/docs/api-integrations/meta-marketing-api.md) | | [setup](https://nango.dev/docs/api-integrations/meta-marketing-api/how-to-register-your-own-meta-marketing-api-oauth-app.md) | marketing |
| `metabase` | Metabase | API_KEY | [docs](https://nango.dev/docs/api-integrations/metabase.md) | [connect](https://nango.dev/docs/api-integrations/metabase/connect.md) | | analytics |
Expand Down
2 changes: 1 addition & 1 deletion docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Use provider URLs by replacing `{slug}` with a slug from the API catalog:

## APIs and integrations

- [API catalog](https://nango.dev/docs/api-catalog.txt): 874 provider slugs with canonical docs routes, auth modes, setup guides, and connect guides.
- [API catalog](https://nango.dev/docs/api-catalog.txt): 875 provider slugs with canonical docs routes, auth modes, setup guides, and connect guides.
- Provider-specific pages are intentionally not expanded here so core Nango guides remain easy for agents to find.

## Resources
Expand Down
6 changes: 6 additions & 0 deletions integrations/meilisearch/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dist/
build/
node_modules/
.env
.nango/*
!.nango/.gitkeep
Empty file.
7 changes: 7 additions & 0 deletions integrations/meilisearch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import './meilisearch/actions/generate-tenant-token.js';
import './meilisearch/actions/search-documents.js';
import './meilisearch/actions/get-documents.js';
import './meilisearch/actions/add-documents.js';
import './meilisearch/actions/update-documents.js';
import './meilisearch/actions/delete-documents.js';
import './meilisearch/actions/get-task.js';
31 changes: 31 additions & 0 deletions integrations/meilisearch/meilisearch/actions/add-documents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createAction } from 'nango';
import * as z from 'zod';

import { enqueuedTaskSchema, meiliDocumentSchema } from '../lib/schemas.js';

const inputSchema = z.object({
indexUid: z.string(),
documents: z.array(meiliDocumentSchema).min(1),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
primaryKey: z.string().optional()
});

const action = createAction({
description: 'Add or replace documents in a Meilisearch index (batch). Returns the enqueued task.',
version: '1.0.0',
endpoint: { method: 'POST', path: '/meilisearch/documents', group: 'Documents' },
input: inputSchema,
output: enqueuedTaskSchema,

exec: async (nango, rawInput) => {
// Declared input schemas are not enforced at runtime; validate explicitly.
const { data: input } = await nango.zodValidateInput({ zodSchema: inputSchema, input: rawInput });
const res = await nango.post({
endpoint: `/indexes/${encodeURIComponent(input.indexUid)}/documents`,
data: input.documents,
...(input.primaryKey ? { params: { primaryKey: input.primaryKey } } : {})
});
return res.data;
}
});

export default action;
38 changes: 38 additions & 0 deletions integrations/meilisearch/meilisearch/actions/delete-documents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createAction } from 'nango';
import * as z from 'zod';

import { enqueuedTaskSchema, filterSchema } from '../lib/schemas.js';

const inputSchema = z.object({
indexUid: z.string(),
ids: z
.array(z.union([z.string(), z.number()]))
.min(1)
.optional(),
filter: filterSchema.optional()
});

const action = createAction({
description: 'Delete documents from a Meilisearch index by ids or by filter (exactly one must be provided). Returns the enqueued task.',
version: '1.0.0',
endpoint: { method: 'POST', path: '/meilisearch/documents/delete', group: 'Documents' },
input: inputSchema,
output: enqueuedTaskSchema,

exec: async (nango, rawInput) => {
// Declared input schemas are not enforced at runtime; validate explicitly.
const { data: input } = await nango.zodValidateInput({ zodSchema: inputSchema, input: rawInput });
const hasIds = input.ids !== undefined;
const hasFilter = input.filter !== undefined;
if (hasIds === hasFilter) {
throw new nango.ActionError({ message: 'Provide exactly one of "ids" or "filter".' });
}

const res = hasIds
? await nango.post({ endpoint: `/indexes/${encodeURIComponent(input.indexUid)}/documents/delete-batch`, data: input.ids })
: await nango.post({ endpoint: `/indexes/${encodeURIComponent(input.indexUid)}/documents/delete`, data: { filter: input.filter } });
return res.data;
}
});

export default action;
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { createAction } from 'nango';
import * as z from 'zod';

import { searchRulesSchema } from '../lib/schemas.js';
import { generateTenantToken } from '../lib/tenant-token.js';

const DEFAULT_TTL_SECONDS = 3600;
const KEYS_PAGE_SIZE = 100;
const MAX_KEY_PAGES = 20;

const inputSchema = z.object({
searchRules: searchRulesSchema,
expiresAt: z.number().optional().describe('Expiry as epoch seconds. Takes precedence over expiresInSeconds.'),
expiresInSeconds: z.number().optional().describe('Expiry as a duration from now. Defaults to 3600 (1 hour) when neither field is set.'),
apiKeyUid: z
.string()
.optional()
.describe('The uid of the signing API key. When omitted, it is resolved by listing keys, which requires the keys.get action.')
});

const output = z.object({
token: z.string(),
expiresAt: z.number()
});

const action = createAction({
description:
'Generate a Meilisearch tenant token: a scoped, signed search JWT carrying per-index ACL rules. Expires after 1 hour unless expiresAt or expiresInSeconds is set.',
version: '1.0.0',
endpoint: { method: 'POST', path: '/meilisearch/tenant-token', group: 'Tenant Tokens' },
input: inputSchema,
output,

exec: async (nango, rawInput) => {
// Declared input schemas are not enforced at runtime; validate explicitly.
// The strict rule schema matters here: Meilisearch ignores unknown rule keys,
// so a typo'd key would silently broaden a signed token's access.
const { data: input } = await nango.zodValidateInput({ zodSchema: inputSchema, input: rawInput });
if (Object.keys(input.searchRules).length === 0) {
throw new nango.ActionError({ message: 'searchRules must define at least one index rule.' });
}

const credentials = await nango.getToken();
if (typeof credentials === 'string' || !('apiKey' in credentials)) {
throw new nango.ActionError({ message: 'Meilisearch connection must use API_KEY auth to mint tenant tokens.' });
}
const apiKey = credentials.apiKey;

let apiKeyUid = input.apiKeyUid;
if (!apiKeyUid) {
// Resolve the key's uid by listing keys and matching locally: GET /keys/{key}
// would put the raw key in the URL path, which proxy/access logs record unredacted.
try {
for (let page = 0; page < MAX_KEY_PAGES && !apiKeyUid; page++) {
const res = await nango.get<{ results: { key: string; uid: string }[]; total: number }>({
endpoint: '/keys',
params: { limit: String(KEYS_PAGE_SIZE), offset: String(page * KEYS_PAGE_SIZE) }
});
apiKeyUid = res.data.results.find((k) => k.key === apiKey)?.uid;
if ((page + 1) * KEYS_PAGE_SIZE >= res.data.total) {
break;
}
}
} catch {
throw new nango.ActionError({
message:
'Could not list keys to resolve the API key uid. This requires a key with the keys.get action. Either connect with a key that has keys.get, or pass apiKeyUid explicitly.'
});
}
if (!apiKeyUid) {
throw new nango.ActionError({
message:
'The connection API key was not found among the instance keys (the master key cannot sign tenant tokens). Connect with a regular API key, or pass apiKeyUid explicitly.'
});
}
}

const nowSeconds = Math.floor(Date.now() / 1000);
const expiresAt = input.expiresAt ?? nowSeconds + (input.expiresInSeconds ?? DEFAULT_TTL_SECONDS);

const token = generateTenantToken({
apiKey,
apiKeyUid,
searchRules: input.searchRules,
expiresAt
});

return { token, expiresAt };
}
});

export default action;
42 changes: 42 additions & 0 deletions integrations/meilisearch/meilisearch/actions/get-documents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createAction } from 'nango';
import * as z from 'zod';

import { filterSchema, meiliDocumentSchema } from '../lib/schemas.js';

// Loose: extra keys are forwarded to Meilisearch so callers can use any
// supported fetch param without a schema change.
const inputSchema = z.looseObject({
indexUid: z.string(),
ids: z.array(z.union([z.string(), z.number()])).optional(),
filter: filterSchema.optional(),
fields: z.array(z.string()).optional(),
limit: z.number().optional(),
offset: z.number().optional()
});

const output = z
.object({
results: z.array(meiliDocumentSchema),
total: z.number(),
limit: z.number(),
offset: z.number()
})
.catchall(z.unknown());

const action = createAction({
description: 'Fetch documents from a Meilisearch index, optionally filtered.',
version: '1.0.0',
endpoint: { method: 'POST', path: '/meilisearch/documents/fetch', group: 'Documents' },
input: inputSchema,
output,

exec: async (nango, rawInput) => {
// Declared input schemas are not enforced at runtime; validate explicitly.
const { data: input } = await nango.zodValidateInput({ zodSchema: inputSchema, input: rawInput });
const { indexUid, ...body } = input;
const res = await nango.post({ endpoint: `/indexes/${encodeURIComponent(indexUid)}/documents/fetch`, data: body });
return res.data;
}
});

export default action;
36 changes: 36 additions & 0 deletions integrations/meilisearch/meilisearch/actions/get-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createAction } from 'nango';
import * as z from 'zod';

const inputSchema = z.object({
taskUid: z.number()
});

const output = z
.object({
uid: z.number(),
indexUid: z.string().nullable(),
status: z.string(),
type: z.string(),
error: z.unknown().nullable().optional(),
enqueuedAt: z.string(),
startedAt: z.string().nullable().optional(),
finishedAt: z.string().nullable().optional()
})
.catchall(z.unknown());

const action = createAction({
description: 'Fetch the status of a Meilisearch async task by its uid.',
version: '1.0.0',
endpoint: { method: 'GET', path: '/meilisearch/tasks', group: 'Tasks' },
input: inputSchema,
output,

exec: async (nango, rawInput) => {
// Declared input schemas are not enforced at runtime; validate explicitly.
const { data: input } = await nango.zodValidateInput({ zodSchema: inputSchema, input: rawInput });
const res = await nango.get({ endpoint: `/tasks/${input.taskUid}` });
return res.data;
}
});

export default action;
Loading