Compare commits

...

26 Commits

Author SHA1 Message Date
Jacob Fletcher
9788b6b1cf Merge remote-tracking branch 'plugin-zapier/main' into chore/plugin-zapier 2023-10-15 02:01:53 -04:00
Jacob Fletcher
cb36bd1a57 chore: migrates to @payloadcms/eslint-config (#2) 2023-05-18 14:19:05 -04:00
Jarrod Flesch
4868140424 fix: awaits response from shouldSendZap 2022-10-14 17:21:45 -04:00
Jarrod Flesch
8beeec2f44 0.0.8 2022-10-14 14:18:08 -04:00
Jarrod Flesch
55accc57dc fix: optional Enabled type 2022-10-14 14:17:58 -04:00
Jarrod Flesch
4a1999c6c7 0.0.7 2022-10-14 14:16:01 -04:00
Jarrod Flesch
b288133ad6 feat: adds enabled property to plugin 2022-10-14 14:15:56 -04:00
Jarrod Flesch
0ec6254af8 0.0.6 2022-10-14 13:04:34 -04:00
Jarrod Flesch
07ba048789 chore: improves code readability, var naming 2022-10-14 13:04:02 -04:00
Jarrod Flesch
8337fbf853 docs: readme table 2022-10-14 12:57:03 -04:00
Jarrod Flesch
28785a05f4 docs: updates readme, adjusts ts type 2022-10-14 12:55:33 -04:00
Jarrod Flesch
cd6568ec44 0.0.5 2022-10-14 11:57:12 -04:00
Jarrod Flesch
c377ac66c0 chore: simplifies plugin 2022-10-14 11:56:26 -04:00
Jarrod Flesch
4dc74f9790 0.0.4 2022-10-13 12:35:22 -04:00
Jarrod Flesch
87461249c7 0.0.3 2022-10-13 12:34:06 -04:00
Jarrod Flesch
045c70e6cc feat: extends generated collection config options 2022-10-13 12:33:58 -04:00
Jarrod Flesch
53094e197c chore: readme config example semantics 2022-10-13 11:59:20 -04:00
Jarrod Flesch
7947e96288 Merge pull request #1 from payloadcms/feat/zaps-collection
Feat/zaps collection
2022-10-13 11:55:24 -04:00
Jarrod Flesch
bf46b1bc74 0.0.2 2022-10-13 11:54:09 -04:00
Jarrod Flesch
d12e9497c7 doc: capitalizes Zapier in readme 2022-10-13 11:46:47 -04:00
Jarrod Flesch
aad624d0e9 chore: updates docs, removes old properties and regens types 2022-10-13 11:41:06 -04:00
Jarrod Flesch
2677e488c8 feat: simplifies api 2022-10-13 11:33:55 -04:00
Jarrod Flesch
7892226df1 feat: new config shape, allows for zaps to be created in admin panel 2022-10-13 01:37:44 -04:00
Jarrod Flesch
fdabf2d5b8 chore: changes root filename, updates package json 2022-10-11 15:57:06 -04:00
Jarrod Flesch
10660dd301 0.0.1 2022-10-11 15:48:10 -04:00
Dan Ribbens
9abae2b66b feat: zapier webhook integration with collections 2022-09-25 18:59:08 -04:00
22 changed files with 20489 additions and 0 deletions

View 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 = 120

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ['@payloadcms'],
}

104
packages/plugin-zapier/.gitignore vendored Normal file
View File

@@ -0,0 +1,104 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-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/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# 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 variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# 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
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

View File

@@ -0,0 +1 @@
eslint-config

View File

@@ -0,0 +1,8 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -0,0 +1,64 @@
# Payload Zapier Plugin
This plugin can be added to a Payload project to make it simple to send Zaps to a zapier webhook address when collection data changes. It is designed to be flexible and generic to allow for all use-cases to manage zaps.
### How it works
When a collection is created/updated/deleted, the plugin will send a Zap to the webhook address. The Zap will contain the collection name, the operation (create/update/delete), and the data related to the document.
We recommend starting with the Zapier Paths + Webhooks template to get started. [See here](https://zapier.com/apps/paths/integrations/webhook). Using Paths, will allow you to handle many different types of data and operations coming from Payload.
### Requirements
- A Zapier account with 1 webhook configured. [More](https://zapier.com/help/create/code-webhooks/trigger-zaps-from-webhooks)
- A Payload project
## Plugin Config Example
```ts
/* file: payload.config.ts */
import { buildConfig } from 'payload/config'
import path from 'path'
import Users from './collections/Users'
import { zapierPlugin } from '@payloadcms/plugin-zapier'
export default buildConfig({
// ...rest of your config goes here
plugins: [
zapierPlugin({
collections: ['posts'],
webhookURL: 'https://hooks.zapier.com/hooks/catch/123456/abcdef/',
enabled: async req => {
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
// `enabled` can be a boolean OR a function that returns a boolean.
//
// if it is a function, it will be passed the following arguments:
// - all arguments from the hook that triggered the Zap
// - operation (create/update/delete)
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
return req.user.role === 'admin'
},
}),
],
})
```
This plugin is configurable to work across many different Payload collections. A * denotes that the property is required. [Types file](./src/types.ts).
| Option | Description |
| ----------------------------- | ----------- |
| **`collections`*** | Array of collection slugs that will send data to Zapier. `["*"]` can be used to zap every collection. |
| **`webhookURL`*** | Zapier webhook URL to send events to. |
| **`enabled`** | Function or boolean value that is checked before a Zap is sent. |
## Features
**Send Zaps**
Allows for events to be sent to Zapier when a specified collection is updated or deleted.
#### Webhook Data
The data sent to the Zapier webhook URL will include:
1. The `collection` slug that triggered the zap.
1. The `operation` used, one of: 'create', 'update', or 'delete'.
1. The `data` object, which is the Payload document that was created, updated, or deleted.

View File

@@ -0,0 +1,4 @@
MONGODB_URI=mongodb://localhost/plugin-zapier
PAYLOAD_SECRET=
ZAPIER_WEBHOOK_URL=

View File

@@ -0,0 +1,8 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts",
"watch": [
"src/**/*.ts",
"../src/**/*.ts"
]
}

View File

@@ -0,0 +1,26 @@
{
"name": "payload-plugin-zapier-demo",
"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.6.0",
"express": "^4.18.1",
"payload": "^1.1.11"
},
"devDependencies": {
"@types/express": "^4.17.14",
"cross-env": "^7.0.3",
"nodemon": "^2.0.20",
"ts-node": "^9.1.1",
"typescript": "^4.8.3"
}
}

View File

@@ -0,0 +1,51 @@
/* tslint:disable */
/**
* This file was automatically generated by Payload CMS.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
name?: string;
role?: 'admin' | 'publisher' | 'editor';
email?: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
loginAttempts?: number;
lockUntil?: string;
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: string;
title?: string;
description?: string;
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "zaps".
*/
export interface Zap {
id: string;
title: string;
relatedCollection: 'users' | 'posts';
webhookEndpoint: string;
hooks: {
afterChange?: boolean;
afterDelete?: boolean;
};
createdAt: string;
updatedAt: string;
}

View File

@@ -0,0 +1,18 @@
import type { CollectionConfig } from 'payload/types'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
],
}

View File

@@ -0,0 +1,31 @@
import type { CollectionConfig } from 'payload/types'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'role',
type: 'select',
saveToJWT: true,
options: [
{
label: 'Admin',
value: 'admin',
},
{
label: 'Publisher',
value: 'publisher',
},
{
label: 'Editor',
value: 'editor',
},
],
},
],
}

View File

@@ -0,0 +1,19 @@
/* eslint-disable no-process-env, import/no-relative-packages */
import { buildConfig } from 'payload/config'
import { zapierPlugin } from '../../src'
import { Posts } from './collections/Posts'
import { Users } from './collections/Users'
export default buildConfig({
collections: [Users, Posts],
plugins: [
zapierPlugin({
collections: ['*'],
webhookURL: process.env.ZAPIER_WEBHOOK_URL,
enabled: ({ req }) => {
return req.user.role === 'admin'
},
}),
],
})

View File

@@ -0,0 +1,26 @@
import dotenv from 'dotenv'
dotenv.config()
import express from 'express'
import payload from 'payload'
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)

View 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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2022 Payload CMS, INC <info@payloadcms.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,46 @@
{
"name": "@payloadcms/plugin-zapier",
"description": "Zapier plugin for Payload",
"repository": "git@github.com:payloadcms/plugin-zapier.git",
"version": "0.0.8",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"keywords": [
"payload",
"zapier",
"cms",
"plugin"
],
"license": "MIT",
"scripts": {
"build": "tsc",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src dev"
},
"devDependencies": {
"@payloadcms/eslint-config": "^0.0.1",
"@types/express": "^4.17.9",
"@types/node": "18.11.3",
"@types/react": "18.0.21",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"nodemon": "^2.0.6",
"payload": "^1.1.11",
"prettier": "^2.7.1",
"react": "^18.0.0",
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
}
}

View File

@@ -0,0 +1,89 @@
import payload from 'payload'
import type { Config } from 'payload/config'
import type { CollectionConfig } from 'payload/dist/collections/config/types'
import type { PluginConfig, ShouldSendZap, Zap } from './types'
const zap: Zap = ({ collectionSlug, data, operation, webhookURL }) =>
fetch(webhookURL, {
headers: {
'Content-Type': 'application/json',
},
method: {
create: 'POST',
update: 'PUT',
delete: 'DELETE',
}[operation],
body: JSON.stringify({
collectionSlug,
operation,
data,
}),
})
const shouldSendZap: ShouldSendZap = async ({ enabled, hookArgs, operation }) => {
if (typeof enabled === 'function') {
try {
return await enabled({ ...hookArgs, operation })
} catch (error: unknown) {
payload.logger.error(`🚨 [Zapier plugin]: Error checking enabled status on. ${error}`)
return false
}
}
return enabled ?? true
}
export const zapierPlugin =
(options: PluginConfig) =>
(config: Config): Config => {
const { collections: zapCollections, webhookURL, enabled } = options
return {
...config,
collections: (config.collections || []).map((collection: CollectionConfig) => {
const allCollections = zapCollections[0] === '*'
const isZapCollection =
allCollections || zapCollections?.find(slug => slug === collection.slug)
if (!isZapCollection) return collection
return {
...collection,
hooks: {
...collection.hooks,
afterChange: [
...(collection.hooks?.afterChange || []),
async hookArgs => {
const { operation, doc } = hookArgs
if (await shouldSendZap({ enabled, hookArgs, operation })) {
zap({
collectionSlug: collection.slug,
operation,
data: doc,
webhookURL,
})
}
},
],
afterDelete: [
...(collection.hooks?.afterDelete || []),
async hookArgs => {
const operation = 'delete'
if (await shouldSendZap({ enabled, hookArgs, operation })) {
zap({
collectionSlug: collection.slug,
operation,
data: hookArgs.doc,
webhookURL,
})
}
},
],
},
}
}),
}
}

View File

@@ -0,0 +1,26 @@
import type { AfterChangeHook, AfterDeleteHook } from 'payload/dist/collections/config/types'
export interface PluginConfig {
collections: string[]
webhookURL: string
enabled?: Enabled
}
type Operations = 'create' | 'update' | 'delete'
type HookArgs = Parameters<AfterChangeHook>[0] | Parameters<AfterDeleteHook>[0]
type Enabled =
| boolean
| ((args: { operation: Operations } & Partial<HookArgs>) => boolean | Promise<boolean>)
export type Zap = (options: {
collectionSlug: string
operation: Operations
data: unknown
webhookURL: string
}) => void
export type ShouldSendZap = (args: {
enabled?: Enabled
hookArgs: HookArgs
operation: Operations
}) => boolean | Promise<boolean>

View 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/**/*"
],
}

File diff suppressed because it is too large Load Diff