feat: builds plugin
This commit is contained in:
BIN
packages/plugin-search/.DS_Store
vendored
Normal file
BIN
packages/plugin-search/.DS_Store
vendored
Normal file
Binary file not shown.
10
packages/plugin-search/.editorconfig
Normal file
10
packages/plugin-search/.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
max_line_length = null
|
||||
3
packages/plugin-search/.gitignore
vendored
Normal file
3
packages/plugin-search/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.env
|
||||
dist
|
||||
73
packages/plugin-search/README.md
Normal file
73
packages/plugin-search/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Payload Search Plugin
|
||||
|
||||
[](https://www.npmjs.com/package/payload-plugin-search)
|
||||
|
||||
A plugin for [Payload CMS](https://github.com/payloadcms/payload) to create extremely fast search results from your existing documents.
|
||||
|
||||
Core features:
|
||||
- Creates a `search` collection that:
|
||||
- Automatically creates, syncs, and deletes search results related to your documents
|
||||
- Serves search results extremely quickly by saving only search-critical data that you define
|
||||
- Allows you to sort search results by priority
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
yarn add payload-plugin-search
|
||||
# OR
|
||||
npm i payload-plugin-search
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options):
|
||||
|
||||
```js
|
||||
import { buildConfig } from 'payload/config';
|
||||
import search from 'payload-search';
|
||||
|
||||
const config = buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: []
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
search({
|
||||
collections: ['pages'],
|
||||
defaultPriorities: {
|
||||
pages: 10
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `collections`
|
||||
An array of collections slugs to enable sync-to-search. Enabled collections receive a `beforeChange` and `afterDelete` hook that creates, syncs, and deleted the document to its related search document as it changes over time, and also an `afterDelete` hook that deletes.
|
||||
|
||||
- `searchOverrides`
|
||||
|
||||
Override anything on the search collection by sending a [Payload Collection Config](https://payloadcms.com/docs/configuration/collections).
|
||||
|
||||
```
|
||||
searchOverrides: {
|
||||
slug: 'search-results'
|
||||
}
|
||||
```
|
||||
## TypeScript
|
||||
|
||||
All types can be directly imported:
|
||||
```js
|
||||
import {
|
||||
SearchConfig,
|
||||
} from 'payload-plugin-search/dist/types';
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!--  -->
|
||||
4
packages/plugin-search/demo/nodemon.json
Normal file
4
packages/plugin-search/demo/nodemon.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts"
|
||||
}
|
||||
27
packages/plugin-search/demo/package.json
Normal file
27
packages/plugin-search/demo/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "payload-starter-typescript",
|
||||
"description": "Blank template - no collections",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn build:payload && yarn build:server",
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "^0.14.24-beta.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"nodemon": "^2.0.6",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
||||
21
packages/plugin-search/demo/src/collections/Pages/index.ts
Normal file
21
packages/plugin-search/demo/src/collections/Pages/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// const payload = require('payload');
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
labels: {
|
||||
singular: 'Page',
|
||||
plural: 'Pages',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
16
packages/plugin-search/demo/src/collections/Users/index.ts
Normal file
16
packages/plugin-search/demo/src/collections/Users/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
],
|
||||
};
|
||||
43
packages/plugin-search/demo/src/payload.config.ts
Normal file
43
packages/plugin-search/demo/src/payload.config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { buildConfig } from 'payload/config';
|
||||
import path from 'path';
|
||||
import searchPlugin from '../../dist';
|
||||
// import searchPlugin from '../../src';
|
||||
import { Users } from './collections/Users';
|
||||
import { Pages } from './collections/Pages';
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: 'http://localhost:3000',
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
webpack: (config) => {
|
||||
const newConfig = {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
react: path.join(__dirname, "../node_modules/react"),
|
||||
"react-dom": path.join(__dirname, "../node_modules/react-dom"),
|
||||
"payload": path.join(__dirname, "../node_modules/payload"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return newConfig;
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
Users,
|
||||
Pages
|
||||
],
|
||||
plugins: [
|
||||
searchPlugin({
|
||||
collections: [
|
||||
'pages'
|
||||
],
|
||||
}),
|
||||
],
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts')
|
||||
},
|
||||
});
|
||||
24
packages/plugin-search/demo/src/server.ts
Normal file
24
packages/plugin-search/demo/src/server.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import express from 'express';
|
||||
import payload from 'payload';
|
||||
|
||||
require('dotenv').config();
|
||||
const app = express();
|
||||
|
||||
// Redirect root to Admin panel
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin');
|
||||
});
|
||||
|
||||
// Initialize Payload
|
||||
payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
|
||||
},
|
||||
});
|
||||
|
||||
// Add your own express routes here
|
||||
|
||||
app.listen(3000);
|
||||
19
packages/plugin-search/demo/tsconfig.json
Normal file
19
packages/plugin-search/demo/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "../",
|
||||
"jsx": "react",
|
||||
},
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
10615
packages/plugin-search/demo/yarn.lock
Normal file
10615
packages/plugin-search/demo/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
BIN
packages/plugin-search/images/.DS_Store
vendored
Normal file
BIN
packages/plugin-search/images/.DS_Store
vendored
Normal file
Binary file not shown.
37
packages/plugin-search/package.json
Normal file
37
packages/plugin-search/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "payload-plugin-search",
|
||||
"version": "1.0.0",
|
||||
"description": "Search plugin for Payload CMS",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"payload",
|
||||
"cms",
|
||||
"plugin",
|
||||
"typescript",
|
||||
"react",
|
||||
"search",
|
||||
"algolia"
|
||||
],
|
||||
"author": "dev@trbl.design",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"payload": "^0.14.24-beta.0",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"payload": "^0.14.24-beta.0",
|
||||
"react": "^17.0.2",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3"
|
||||
}
|
||||
}
|
||||
32
packages/plugin-search/src/Search/hooks/deleteFromSearch.ts
Normal file
32
packages/plugin-search/src/Search/hooks/deleteFromSearch.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { CollectionAfterDeleteHook } from 'payload/types';
|
||||
|
||||
const deleteFromSearch: CollectionAfterDeleteHook = ({ req: { payload }, doc }) => {
|
||||
try {
|
||||
const deleteSearchDoc = async () => {
|
||||
const searchDocQuery = await payload.find({
|
||||
collection: 'search',
|
||||
where: {
|
||||
'doc.value': {
|
||||
equals: doc.id,
|
||||
},
|
||||
},
|
||||
depth: 0,
|
||||
}) as any;
|
||||
|
||||
if (searchDocQuery?.docs?.[0]) {
|
||||
payload.delete({
|
||||
collection: 'search',
|
||||
id: searchDocQuery?.docs?.[0]?.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
deleteSearchDoc();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
export default deleteFromSearch;
|
||||
129
packages/plugin-search/src/Search/hooks/syncWithSearch.ts
Normal file
129
packages/plugin-search/src/Search/hooks/syncWithSearch.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { CollectionAfterChangeHook } from 'payload/types';
|
||||
|
||||
const syncWithSearch: CollectionAfterChangeHook = async ({
|
||||
req: { payload },
|
||||
doc,
|
||||
operation,
|
||||
// @ts-ignore
|
||||
collection,
|
||||
}) => {
|
||||
const {
|
||||
id,
|
||||
status,
|
||||
} = doc || {};
|
||||
|
||||
// TODO: inject default priorities here
|
||||
let defaultPriority = 0;
|
||||
|
||||
// TODO: call a function from the config that returns transformed search data
|
||||
const dataToSave = {
|
||||
doc: {
|
||||
relationTo: collection,
|
||||
value: id,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
if (operation === 'create') {
|
||||
if (status === 'published') {
|
||||
payload.create({
|
||||
collection: 'search',
|
||||
data: {
|
||||
...dataToSave,
|
||||
priority: defaultPriority,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'update') {
|
||||
try {
|
||||
// find the correct doc to sync with
|
||||
const searchDocQuery = await payload.find({
|
||||
collection: 'search',
|
||||
where: {
|
||||
'doc.value': {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
depth: 0,
|
||||
}) as any;
|
||||
|
||||
const docs: {
|
||||
id: string
|
||||
priority?: number
|
||||
}[] = searchDocQuery?.docs || [];
|
||||
|
||||
const [foundDoc, ...duplicativeDocs] = docs;
|
||||
|
||||
// delete all duplicative search docs (docs that reference the same page)
|
||||
// to ensure the same, out-of-date result does not appear twice (where only syncing the first found doc)
|
||||
if (duplicativeDocs.length > 0) {
|
||||
try {
|
||||
Promise.all(duplicativeDocs.map(({ id: duplicativeDocID }) => payload.delete({
|
||||
collection: 'search',
|
||||
id: duplicativeDocID,
|
||||
})));
|
||||
} catch (err) {
|
||||
payload.logger.error(`Error deleting duplicative search documents.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundDoc) {
|
||||
const {
|
||||
id: searchDocID,
|
||||
} = foundDoc;
|
||||
|
||||
if (status === 'published') {
|
||||
// update the doc normally
|
||||
try {
|
||||
payload.update({
|
||||
collection: 'search',
|
||||
id: searchDocID,
|
||||
data: {
|
||||
...dataToSave,
|
||||
priority: foundDoc.priority || defaultPriority,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
payload.logger.error(`Error updating search document.`);
|
||||
}
|
||||
}
|
||||
if (status === 'draft') {
|
||||
// do not include draft docs in search results, so delete the record
|
||||
try {
|
||||
payload.delete({
|
||||
collection: 'search',
|
||||
id: searchDocID,
|
||||
});
|
||||
} catch (err) {
|
||||
payload.logger.error(`Error deleting search document.`);
|
||||
}
|
||||
}
|
||||
} else if (status === 'published') {
|
||||
try {
|
||||
payload.create({
|
||||
collection: 'search',
|
||||
data: {
|
||||
...dataToSave,
|
||||
priority: defaultPriority,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
payload.logger.error(err);
|
||||
payload.logger.error(`Error creating search document.`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
payload.logger.error(`Error finding search document.`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
payload.logger.error(err);
|
||||
payload.logger.error(`Error syncing search document related to ${collection} with id: '${id}'`);
|
||||
}
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
export default syncWithSearch;
|
||||
55
packages/plugin-search/src/Search/index.ts
Normal file
55
packages/plugin-search/src/Search/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
import { SearchConfig } from '../types';
|
||||
import deepMerge from '../utilities/deepMerge';
|
||||
|
||||
// all settings can be overridden by the config
|
||||
export const generateSearchCollection = (searchConfig: SearchConfig): CollectionConfig => deepMerge({
|
||||
slug: searchConfig?.searchOverrides?.slug || 'search',
|
||||
labels: {
|
||||
singular: 'Search Result',
|
||||
plural: 'Search Results',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: [
|
||||
'title',
|
||||
],
|
||||
description: 'This is a collection of automatically created search results. These results are used by the global site search and will be updated automatically as documents in the CMS are created or updated.',
|
||||
enableRichTextRelationship: false,
|
||||
},
|
||||
access: {
|
||||
read: (): boolean => true,
|
||||
create: (): boolean => false,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
admin: {
|
||||
position: 'sidebar'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'doc',
|
||||
type: 'relationship',
|
||||
relationTo: searchConfig.collections || [],
|
||||
required: true,
|
||||
index: true,
|
||||
maxDepth: 0,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
admin: {
|
||||
position: 'sidebar'
|
||||
}
|
||||
},
|
||||
],
|
||||
}, searchConfig.searchOverrides || {});
|
||||
55
packages/plugin-search/src/index.ts
Normal file
55
packages/plugin-search/src/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Config } from 'payload/config';
|
||||
import { generateSearchCollection } from './Search';
|
||||
import syncWithSearch from './Search/hooks/syncWithSearch';
|
||||
import deleteFromSearch from './Search/hooks/deleteFromSearch';
|
||||
import { SearchConfig } from './types';
|
||||
// import path from 'path';
|
||||
|
||||
const Search = (incomingSearchConfig: SearchConfig) => (config: Config): Config => {
|
||||
const searchConfig: SearchConfig = {
|
||||
...incomingSearchConfig,
|
||||
// modify as necessary
|
||||
};
|
||||
|
||||
// add a beforeChange hook to every search-enabled collection
|
||||
const collectionsWithSearchHooks = config?.collections?.map((collection) => {
|
||||
const {
|
||||
hooks: existingHooks
|
||||
} = collection;
|
||||
|
||||
const enabledCollections = searchConfig.collections || [];
|
||||
const isEnabled = enabledCollections.indexOf(collection.slug) > -1;
|
||||
|
||||
if (isEnabled) {
|
||||
return {
|
||||
...collection,
|
||||
hooks: {
|
||||
...collection.hooks,
|
||||
beforeChange: [
|
||||
...(existingHooks?.beforeChange || []),
|
||||
async (args: any) => syncWithSearch({
|
||||
...args,
|
||||
collection: 'pages'
|
||||
}),
|
||||
],
|
||||
afterDelete: [
|
||||
...(existingHooks?.afterDelete || []),
|
||||
deleteFromSearch,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return collection;
|
||||
}).filter(Boolean);
|
||||
|
||||
return {
|
||||
...config,
|
||||
collections: [
|
||||
...collectionsWithSearchHooks || [],
|
||||
generateSearchCollection(searchConfig),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default Search;
|
||||
9
packages/plugin-search/src/types.ts
Normal file
9
packages/plugin-search/src/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
export type SearchConfig = {
|
||||
searchOverrides?: Partial<CollectionConfig>
|
||||
collections?: string[]
|
||||
defaultPriorities?: {
|
||||
[collection: string]: number
|
||||
}
|
||||
}
|
||||
26
packages/plugin-search/src/utilities/deepMerge.ts
Normal file
26
packages/plugin-search/src/utilities/deepMerge.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function isObject(item: unknown): boolean {
|
||||
return Boolean(item && typeof item === 'object' && !Array.isArray(item));
|
||||
}
|
||||
|
||||
export default function deepMerge<T, R>(target: T, source: R): T {
|
||||
const output = { ...target };
|
||||
if (isObject(target) && isObject(source)) {
|
||||
Object.keys(source).forEach((key) => {
|
||||
// @ts-ignore
|
||||
if (isObject(source[key])) {
|
||||
if (!(key in target)) {
|
||||
// @ts-ignore
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
} else {
|
||||
// @ts-ignore
|
||||
output[key] = deepMerge(target[key], source[key]);
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
18
packages/plugin-search/tsconfig.json
Normal file
18
packages/plugin-search/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"outDir": "./dist",
|
||||
"allowJs": true,
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
}
|
||||
10196
packages/plugin-search/yarn.lock
Normal file
10196
packages/plugin-search/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user