├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── npm.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── .env.example ├── .gitignore ├── nodemon.json ├── package.json ├── src │ ├── collections │ │ ├── Categories.ts │ │ ├── Media.ts │ │ ├── Posts.ts │ │ ├── Tags.ts │ │ └── Users.ts │ ├── payload.config.ts │ └── server.ts ├── tsconfig.json └── yarn.lock ├── package.json ├── src ├── hooks │ ├── createOrUpdateCollection.ts │ └── deleteCollection.ts ├── index.ts └── types.ts ├── tsconfig.json ├── types.d.ts ├── types.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: "14.x" 15 | registry-url: "https://registry.npmjs.org" 16 | - run: yarn install 17 | - run: yarn build 18 | - run: npm publish --access public 19 | env: 20 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NouanceLabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Payload Meilisearch Plugin (ALPHA) 2 | 3 | **Expect breaking changes.** 4 | 5 | A plugin for [Payload CMS](https://github.com/payloadcms/payload) to connect [Meilisearch](https://meilisearch.com) and Payload. 6 | 7 | Roadmap to stable release: 8 | 9 | - ~~Sync collections on create and update~~ 10 | - ~~Delete collections~~ 11 | - Support all field types of Payload (in progress) 12 | - Support for Payload's draft system (planned) 13 | - Support for nested fields (planned) 14 | - Support Meilisearch index options for filtering and sorting (planned) 15 | 16 | ## Installation 17 | 18 | ```bash 19 | yarn add @nouance/payload-meilisearch 20 | # OR 21 | npm i @nouance/payload-meilisearch 22 | ``` 23 | 24 | ## Basic Usage 25 | 26 | In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options): 27 | 28 | ```js 29 | import { buildConfig } from "payload/config"; 30 | import meilisearchPlugin from "@nouance/payload-meilisearch"; 31 | 32 | const config = buildConfig({ 33 | plugins: [ 34 | meilisearchPlugin({ 35 | host: process.env.MEILISEARCH_HOST, 36 | apiKey: process.env.MEILISEARCH_API_KEY, 37 | }), 38 | ], 39 | }); 40 | 41 | export default config; 42 | ``` 43 | 44 | ### Options 45 | 46 | - `host` 47 | 48 | Required. Your Meilisearch host URL. 49 | 50 | - `apiKey` 51 | 52 | Required. Your Meilisearch API key. Must have permissions to read, create and delete indexes and documents. 53 | 54 | - `sync` 55 | 56 | Required. An array of sync configs. This will automatically configure a sync between Payload collections and Meilisearch indexes. See [sync](#sync) for more details. 57 | 58 | - `logs` 59 | 60 | Optional. When `true`, logs sync events to the console as they happen. 61 | 62 | ## Sync 63 | 64 | This option will sync collection's data and their fields to your Meilisearch instance. Each collection slug maps to a unique index in Meilisearch. 65 | 66 | ```js 67 | import { buildConfig } from "payload/config"; 68 | import meilisearchPlugin from "@nouance/payload-meilisearch"; 69 | 70 | const config = buildConfig({ 71 | plugins: [ 72 | meilisearchPlugin({ 73 | host: process.env.MEILISEARCH_HOST, 74 | apiKey: process.env.MEILISEARCH_API_KEY, 75 | sync: [ 76 | { 77 | collection: "posts", 78 | fields: [ 79 | { 80 | name: "title", 81 | }, 82 | { 83 | name: "publishedDate", 84 | alias: "publicationDate", 85 | }, 86 | ], 87 | }, 88 | ], 89 | }), 90 | ], 91 | }); 92 | 93 | export default config; 94 | ``` 95 | 96 | ### Alias 97 | 98 | You can optionally set an `alias` that maps the field to a different name in Meilisearch's index in case you don't want to expose inner field names or want to simplify it. 99 | 100 | ### Alias the ID 101 | 102 | In Meilisearch, every document must have an ID, by default we're setting this to be the document's ID, however you can change it to any other field by making your alias `id`. Note that this has to be unique and a valid format (eg. no spaces), so we recommend leaving it as default or applying a transformer function to make sure your data format is always correct. 103 | 104 | ### Transformers 105 | 106 | On each field you can apply a transformer function that takes in the field value as it is from the `doc` object and then update operation's full `doc` object so that you can modify the values before the collection is indexed. This is useful for dealing with complex fields like RichText where you might want to serialise it into a string so it's easily searchable. 107 | 108 | Example transformer to set the name of a category as the ID of the document: 109 | 110 | ```ts 111 | { 112 | name: "name", 113 | alias: "id", 114 | transformer: (doc) => { 115 | const name: string = doc.name; 116 | return name.replaceAll(" ", "-").toLocaleLowerCase(); 117 | }, 118 | }, 119 | ``` 120 | 121 | ### Curently supported field types 122 | 123 | - **Code** 124 | - **Date** 125 | - **Email** 126 | - **JSON** 127 | - **Number** 128 | - **Point** 129 | - **Radio group** 130 | - **Select** 131 | - **Text** 132 | - **Textarea** 133 | 134 | ## Development 135 | 136 | For development purposes, there is a full working example of how this plugin might be used in the [demo](./demo) of this repo. This demo can be developed locally using any [Meilisearch Cloud](https://cloud.meilisearch.com/) account, you just need a working API key. Then: 137 | 138 | ```bash 139 | git clone git@github.com:NouanceLabs/payload-meilisearch.git \ 140 | cd payload-meilisearch && yarn \ 141 | cd demo && yarn \ 142 | cp .env.example .env \ 143 | vim .env \ # add your Meilisearch creds to this file 144 | yarn dev 145 | ``` 146 | -------------------------------------------------------------------------------- /demo/.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI= 2 | PAYLOAD_SECRET= 3 | 4 | MEILISEARCH_HOST= 5 | MEILISEARCH_API_KEY= 6 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | media 3 | 4 | .env 5 | -------------------------------------------------------------------------------- /demo/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "exec": "ts-node src/server.ts" 4 | } 5 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-meilisearch", 3 | "description": "Payload project created from blog template", 4 | "version": "1.0.0", 5 | "main": "dist/server.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", 9 | "build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", 10 | "build:server": "tsc", 11 | "build": "yarn copyfiles && yarn build:payload && yarn build:server", 12 | "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js", 13 | "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", 14 | "generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types", 15 | "generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema" 16 | }, 17 | "dependencies": { 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "meilisearch": "^0.31.1", 21 | "payload": "^1.6.15" 22 | }, 23 | "devDependencies": { 24 | "@types/express": "^4.17.9", 25 | "copyfiles": "^2.4.1", 26 | "cross-env": "^7.0.3", 27 | "nodemon": "^2.0.6", 28 | "ts-node": "^9.1.1", 29 | "typescript": "^4.8.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/collections/Categories.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | 3 | const Categories: CollectionConfig = { 4 | slug: 'categories', 5 | admin: { 6 | useAsTitle: 'name', 7 | }, 8 | access: { 9 | read: () => true, 10 | }, 11 | fields: [ 12 | { 13 | name: 'name', 14 | type: 'text', 15 | }, 16 | ], 17 | timestamps: false, 18 | } 19 | 20 | export default Categories; -------------------------------------------------------------------------------- /demo/src/collections/Media.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { CollectionConfig } from 'payload/types'; 3 | 4 | const Media: CollectionConfig = { 5 | slug: 'media', 6 | upload: { 7 | staticDir: path.resolve(__dirname, '../../media'), 8 | // Specify the size name that you'd like to use as admin thumbnail 9 | adminThumbnail: 'thumbnail', 10 | imageSizes: [ 11 | { 12 | height: 400, 13 | width: 400, 14 | crop: 'center', 15 | name: 'thumbnail', 16 | }, 17 | { 18 | width: 900, 19 | height: 450, 20 | crop: 'center', 21 | name: 'sixteenByNineMedium', 22 | }, 23 | ], 24 | }, 25 | fields: [], 26 | }; 27 | 28 | export default Media; 29 | -------------------------------------------------------------------------------- /demo/src/collections/Posts.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types' 2 | 3 | const Posts: CollectionConfig = { 4 | slug: 'posts', 5 | admin: { 6 | defaultColumns: ['title', 'author', 'category', 'tags', 'status'], 7 | useAsTitle: 'title', 8 | }, 9 | access: { 10 | read: () => true, 11 | }, 12 | fields: [ 13 | { 14 | name: 'title', 15 | type: 'text', 16 | }, 17 | { 18 | name: 'author', 19 | type: 'relationship', 20 | relationTo: 'users', 21 | }, 22 | { 23 | name: 'publishedDate', 24 | type: 'date', 25 | }, 26 | { 27 | name: 'category', 28 | type: 'relationship', 29 | relationTo: 'categories', 30 | }, 31 | { 32 | name: 'featuredImage', 33 | type: 'upload', 34 | relationTo: 'media', 35 | }, 36 | { 37 | name: 'tags', 38 | type: 'relationship', 39 | relationTo: 'tags', 40 | hasMany: true, 41 | }, 42 | { 43 | name: 'views', 44 | type: 'number', 45 | defaultValue: 0, 46 | }, 47 | { 48 | name: 'content', 49 | type: 'richText', 50 | }, 51 | { 52 | name: 'status', 53 | type: 'select', 54 | options: [ 55 | { 56 | value: 'draft', 57 | label: 'Draft', 58 | }, 59 | { 60 | value: 'published', 61 | label: 'Published', 62 | }, 63 | ], 64 | defaultValue: 'draft', 65 | admin: { 66 | position: 'sidebar', 67 | }, 68 | }, 69 | ], 70 | } 71 | 72 | export default Posts 73 | -------------------------------------------------------------------------------- /demo/src/collections/Tags.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from "payload/types"; 2 | 3 | const Tags: CollectionConfig = { 4 | slug: "tags", 5 | admin: { 6 | useAsTitle: "name", 7 | }, 8 | access: { 9 | read: () => true, 10 | }, 11 | fields: [ 12 | { 13 | name: "name", 14 | type: "text", 15 | }, 16 | { 17 | name: "metadata", 18 | type: "json", 19 | }, 20 | ], 21 | timestamps: false, 22 | }; 23 | 24 | export default Tags; 25 | -------------------------------------------------------------------------------- /demo/src/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | 3 | const Users: CollectionConfig = { 4 | slug: 'users', 5 | auth: true, 6 | admin: { 7 | useAsTitle: 'email', 8 | }, 9 | access: { 10 | read: () => true, 11 | }, 12 | fields: [ 13 | // Email added by default 14 | { 15 | name: 'name', 16 | type: 'text', 17 | } 18 | ], 19 | }; 20 | 21 | export default Users; -------------------------------------------------------------------------------- /demo/src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { buildConfig } from "payload/config"; 2 | import path from "path"; 3 | import Categories from "./collections/Categories"; 4 | import Posts from "./collections/Posts"; 5 | import Tags from "./collections/Tags"; 6 | import Users from "./collections/Users"; 7 | import Media from "./collections/Media"; 8 | import payloadMeilisearch from "../../src/index"; 9 | 10 | export default buildConfig({ 11 | serverURL: "http://localhost:3000", 12 | admin: { 13 | user: Users.slug, 14 | }, 15 | collections: [Categories, Posts, Tags, Users, Media], 16 | typescript: { 17 | outputFile: path.resolve(__dirname, "payload-types.ts"), 18 | }, 19 | graphQL: { 20 | schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"), 21 | }, 22 | plugins: [ 23 | payloadMeilisearch({ 24 | host: process.env.MEILISEARCH_HOST, 25 | apiKey: process.env.MEILISEARCH_API_KEY, 26 | logs: true, 27 | sync: [ 28 | { 29 | collection: "posts", 30 | fields: [ 31 | { 32 | name: "title", 33 | alias: "title", 34 | }, 35 | { 36 | name: "publishedDate", 37 | alias: "publishedDate", 38 | }, 39 | { 40 | name: "views", 41 | alias: "views", 42 | }, 43 | { 44 | name: "author", 45 | alias: "author", 46 | }, 47 | { 48 | name: "tags", 49 | alias: "tags", 50 | }, 51 | { 52 | name: "category", 53 | alias: "category", 54 | }, 55 | { 56 | name: "featuredImage", 57 | }, 58 | { 59 | name: "content", 60 | }, 61 | ], 62 | }, 63 | { 64 | collection: "categories", 65 | fields: [ 66 | { 67 | name: "name", 68 | alias: "id", 69 | transformer: (field) => { 70 | const name = String(field); 71 | return name.replaceAll(" ", "-").toLocaleLowerCase(); 72 | }, 73 | }, 74 | { 75 | name: "name", 76 | alias: "name", 77 | }, 78 | ], 79 | }, 80 | { 81 | collection: "tags", 82 | fields: [ 83 | { 84 | name: "name", 85 | alias: "name", 86 | }, 87 | ], 88 | }, 89 | ], 90 | }), 91 | ], 92 | }); 93 | -------------------------------------------------------------------------------- /demo/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import payload from "payload"; 3 | 4 | require("dotenv").config(); 5 | const app = express(); 6 | 7 | // Redirect root to Admin panel 8 | app.get("/", (_, res) => { 9 | res.redirect("/admin"); 10 | }); 11 | 12 | const start = async () => { 13 | // Initialize Payload 14 | await payload.init({ 15 | secret: process.env.PAYLOAD_SECRET, 16 | mongoURL: process.env.MONGODB_URI, 17 | express: app, 18 | onInit: async () => { 19 | payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`); 20 | }, 21 | }); 22 | 23 | // Add your own express routes here 24 | 25 | app.listen(3000); 26 | }; 27 | 28 | start(); 29 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": false, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "rootDir": "../", 11 | "jsx": "react", 12 | "paths": { 13 | "payload/generated-types": ["./src/payload-types.ts"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules", "dist", "build"], 18 | "ts-node": { 19 | "transpileOnly": true, 20 | "swc": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nouance/payload-meilisearch", 3 | "version": "0.1.3", 4 | "homepage:": "https://nouance.io", 5 | "repository": "git@github.com:NouanceLabs/payload-meilisearch.git", 6 | "description": "Meilisearch integration plugin for Payload CMS", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "build": "tsc", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "payload", 15 | "meilisearch", 16 | "cms", 17 | "plugin", 18 | "typescript", 19 | "search", 20 | "algolia" 21 | ], 22 | "author": "dev@nouance.io", 23 | "license": "MIT", 24 | "peerDependencies": { 25 | "payload": "^1.1.8" 26 | }, 27 | "dependencies": { 28 | "meilisearch": "^0.31.1" 29 | }, 30 | "devDependencies": { 31 | "payload": "^1.1.8", 32 | "typescript": "^4.5.5" 33 | }, 34 | "files": [ 35 | "dist", 36 | "types.js", 37 | "types.d.ts" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/hooks/createOrUpdateCollection.ts: -------------------------------------------------------------------------------- 1 | import MeiliSearch from "meilisearch"; 2 | import type { 3 | CollectionAfterChangeHook, 4 | CollectionConfig, 5 | } from "payload/types"; 6 | import { SanitizedMeilisearchConfig } from "../types"; 7 | 8 | export type CollectionAfterChangeHookWithArgs = ( 9 | args: Parameters[0] & { 10 | collection: CollectionConfig; 11 | meilisearchConfig: SanitizedMeilisearchConfig; 12 | } 13 | ) => void; 14 | 15 | export const createOrUpdateCollection: CollectionAfterChangeHookWithArgs = 16 | async (args) => { 17 | const { req, operation, doc, collection, meilisearchConfig } = args; 18 | 19 | const payload = req?.payload; 20 | 21 | const dataRef = doc || {}; 22 | 23 | const { host, apiKey, sync, logs } = meilisearchConfig || {}; 24 | 25 | if (!host || !apiKey) { 26 | return dataRef; 27 | } 28 | 29 | const client = new MeiliSearch({ 30 | host: host, 31 | apiKey: apiKey, 32 | }); 33 | 34 | if (logs) payload.logger.info(`Syncing collection: ${dataRef.id}`); 35 | 36 | if (payload) { 37 | if (dataRef.id) { 38 | const syncConfig = meilisearchConfig.sync.find( 39 | (item) => item.collection === collection.slug 40 | ); 41 | 42 | if (!syncConfig) return dataRef; 43 | 44 | const targetFields = syncConfig.fields; 45 | 46 | const aliasedId = targetFields.find((field) => field.alias === "id"); 47 | 48 | const syncedFields = {}; 49 | 50 | targetFields.forEach((field) => { 51 | const callback = field.transformer; 52 | const key = field.alias ?? field.name; 53 | 54 | const keyValuePair = { 55 | [key]: callback 56 | ? callback(dataRef[field.name]) 57 | : dataRef[field.name], 58 | }; 59 | 60 | Object.assign(syncedFields, keyValuePair); 61 | }); 62 | 63 | const index = client.index(collection.slug); 64 | 65 | const id = aliasedId?.alias 66 | ? { 67 | [aliasedId.alias]: aliasedId?.transformer 68 | ? aliasedId?.transformer(dataRef[aliasedId.name]) 69 | : dataRef[aliasedId.name], 70 | } 71 | : { 72 | id: dataRef.id, 73 | }; 74 | 75 | const collectionSyncData = { 76 | ...id, 77 | ...syncedFields, 78 | }; 79 | 80 | let response = await index.addDocuments([collectionSyncData]); 81 | 82 | if (logs && response.taskUid) payload.logger.info(`Synced!`); 83 | } 84 | } 85 | 86 | return dataRef; 87 | }; 88 | -------------------------------------------------------------------------------- /src/hooks/deleteCollection.ts: -------------------------------------------------------------------------------- 1 | import MeiliSearch from "meilisearch"; 2 | import type { 3 | CollectionAfterDeleteHook, 4 | CollectionConfig, 5 | } from "payload/types"; 6 | import { SanitizedMeilisearchConfig } from "../types"; 7 | 8 | export type CollectionAfterDeleteHookWithArgs = ( 9 | args: Parameters[0] & { 10 | collection: CollectionConfig; 11 | meilisearchConfig: SanitizedMeilisearchConfig; 12 | } 13 | ) => void; 14 | 15 | export const deleteCollection: CollectionAfterDeleteHookWithArgs = async ( 16 | args 17 | ) => { 18 | const { req, doc, collection, meilisearchConfig } = args; 19 | 20 | const payload = req?.payload; 21 | 22 | const dataRef = doc || {}; 23 | 24 | const { host, apiKey, sync, logs } = meilisearchConfig || {}; 25 | 26 | if (!host || !apiKey) { 27 | return dataRef; 28 | } 29 | 30 | const client = new MeiliSearch({ 31 | host: host, 32 | apiKey: apiKey, 33 | }); 34 | 35 | if (logs) payload.logger.info(`Deleting collection: ${dataRef.id}`); 36 | 37 | if (payload) { 38 | if (dataRef.id) { 39 | const syncConfig = meilisearchConfig.sync.find( 40 | (item) => item.collection === collection.slug 41 | ); 42 | 43 | if (!syncConfig) return dataRef; 44 | 45 | const targetFields = syncConfig.fields; 46 | 47 | const aliasedId = targetFields.find((field) => field.alias === "id"); 48 | 49 | const index = client.index(collection.slug); 50 | 51 | const id = aliasedId?.alias 52 | ? aliasedId.transformer 53 | ? aliasedId.transformer(dataRef[aliasedId.name], doc) 54 | : dataRef[aliasedId.name] 55 | : dataRef.id; 56 | 57 | let response = await index.deleteDocument(id); 58 | 59 | if (logs && response.taskUid) 60 | payload.logger.info(`Successfully deleted!`); 61 | } 62 | } 63 | 64 | return dataRef; 65 | }; 66 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Config as PayloadcConfig, Plugin } from "payload/config"; 2 | import { MeiliSearch } from "meilisearch"; 3 | import { SanitizedMeilisearchConfig } from "./types"; 4 | import { MeilisearchConfig } from "./types"; 5 | import { createOrUpdateCollection } from "./hooks/createOrUpdateCollection"; 6 | import { deleteCollection } from "./hooks/deleteCollection"; 7 | 8 | const payloadMeilisearch = 9 | (incomingConfig: MeilisearchConfig) => 10 | (config: PayloadcConfig): PayloadcConfig => { 11 | const { collections } = config; 12 | 13 | const pluginConfig: SanitizedMeilisearchConfig = { 14 | ...incomingConfig, 15 | sync: incomingConfig?.sync || [], 16 | }; 17 | 18 | if (!collections) return config; 19 | 20 | const processedConfig: PayloadcConfig = { 21 | ...config, 22 | collections: collections.map((collection) => { 23 | const sync = pluginConfig.sync?.find( 24 | (sync) => sync.collection === collection.slug 25 | ); 26 | 27 | const { hooks: existingHooks } = collection; 28 | 29 | if (sync) { 30 | return { 31 | ...collection, 32 | hooks: { 33 | ...collection.hooks, 34 | afterChange: [ 35 | ...(existingHooks?.afterChange || []), 36 | async (args) => 37 | createOrUpdateCollection({ 38 | ...args, 39 | collection, 40 | meilisearchConfig: pluginConfig, 41 | }), 42 | ], 43 | afterDelete: [ 44 | ...(existingHooks?.afterDelete || []), 45 | async (args) => 46 | deleteCollection({ 47 | ...args, 48 | collection, 49 | meilisearchConfig: pluginConfig, 50 | }), 51 | ], 52 | }, 53 | }; 54 | } 55 | 56 | return collection; 57 | }), 58 | }; 59 | 60 | return processedConfig; 61 | }; 62 | 63 | export default payloadMeilisearch; 64 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from "payload"; 2 | import { Config as PayloadConfig } from "payload/config"; 3 | import MeiliSearch from "meilisearch"; 4 | 5 | type TransformerFieldType = string | number; 6 | 7 | export type FieldSyncConfig = { 8 | name: string; 9 | alias?: string; 10 | transformer?: (field: TransformerFieldType, doc?: any) => any; 11 | }; 12 | 13 | export type SyncConfig = { 14 | collection: string; 15 | fields: FieldSyncConfig[]; 16 | }; 17 | 18 | export type MeilisearchConfig = { 19 | host: string; 20 | apiKey: string; 21 | sync?: SyncConfig[]; 22 | logs?: boolean; 23 | }; 24 | 25 | export type SanitizedMeilisearchConfig = MeilisearchConfig & { 26 | sync: SyncConfig[]; 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "outDir": "./dist", 5 | "allowJs": true, 6 | "module": "commonjs", 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "declarationDir": "./dist", 12 | "skipLibCheck": true, 13 | "strict": true, 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/types'; 2 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/types'); 2 | --------------------------------------------------------------------------------