@warleon/n8n-nodes-payload-cms 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 n8n-nodes-payload-dynamic
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.
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # n8n Payload CMS Node
2
+
3
+ A dynamic n8n node for Payload CMS that automatically discovers collections and operations, allowing you to interact with any Payload CMS instance via the REST API.
4
+
5
+ _Forked from [n8n-payload-dynamic](https://github.com/leadership-institute/n8n-payload-dynamic)_
6
+
7
+ ## Features
8
+
9
+ - **Dynamic Collection Discovery**: Automatically discovers available collections from your Payload CMS instance
10
+ - **Dynamic Global Discovery**: Automatically discovers available globals from your Payload CMS instance
11
+ - **Full CRUD Operations**: Support for all standard operations (Create, Read, Update, Delete, Count)
12
+ - **Advanced Query Support**: Includes support for filtering, sorting, pagination, depth control, and localization
13
+ - **Flexible Authentication**: Uses API key authentication with configurable API prefix
14
+ - **Error Handling**: Robust error handling with detailed error messages
15
+
16
+ ## Installation
17
+
18
+ ### Automatic
19
+
20
+ ### Manual
21
+
22
+ 1. Clone or download this repository
23
+ 2. Install dependencies:
24
+ ```bash
25
+ npm install
26
+ ```
27
+ 3. Build the node:
28
+ ```bash
29
+ npm run build
30
+ ```
31
+ 4. Install the node in your n8n instance by copying the built files to your n8n custom nodes directory
32
+
33
+ ## Configuration
34
+
35
+ ### Credentials Setup
36
+
37
+ 1. In n8n, create new credentials of type "Payload CMS API"
38
+ 2. Configure the following fields:
39
+ - **Base URL**: The base URL of your Payload CMS instance (e.g., `https://your-payload-instance.com`)
40
+ - **Email**: Your PayloadCMS user email
41
+ - **Password**: Your PayloadCMS user password
42
+ - **User Collection**: The collection slug for users (default: `users`)
43
+ - **API Prefix**: The API route prefix (default: `/api`)
44
+
45
+ ### Getting Credentials
46
+
47
+ Use any valid user account from your PayloadCMS instance. The node will automatically:
48
+
49
+ - Login when first used
50
+ - Cache the authentication token for 1 hour
51
+ - Refresh tokens as needed
52
+ - Handle session management transparently
53
+
54
+ No API keys are required - the node uses PayloadCMS's standard login authentication flow.
55
+
56
+ ## Usage
57
+
58
+ ### Collection Operations
59
+
60
+ The node supports the following operations on collections:
61
+
62
+ - **Find**: Retrieve multiple documents from a collection
63
+ - **Find by ID**: Retrieve a specific document by its ID
64
+ - **Create**: Create a new document in a collection
65
+ - **Update**: Update multiple documents in a collection (requires where clause)
66
+ - **Update by ID**: Update a specific document by its ID
67
+ - **Delete**: Delete multiple documents from a collection (requires where clause)
68
+ - **Delete by ID**: Delete a specific document by its ID
69
+ - **Count**: Count documents in a collection
70
+
71
+ ### Global Operations
72
+
73
+ The node supports the following operations on globals:
74
+
75
+ - **Get**: Retrieve global data
76
+ - **Update**: Update global data
77
+
78
+ ### Auth Operations
79
+
80
+ The node supports the following authentication operations:
81
+
82
+ - **Login**: Authenticate a user with email and password
83
+ - **Logout**: Log out the current user
84
+ - **Me**: Get current authenticated user information
85
+ - **Refresh Token**: Refresh the authentication token
86
+ - **Forgot Password**: Send a forgot password email
87
+ - **Reset Password**: Reset user password with a token
88
+ - **Verify**: Verify user account with a verification token
89
+ - **Unlock**: Unlock a user account
90
+
91
+ ### Query Parameters
92
+
93
+ The node supports all standard Payload CMS query parameters:
94
+
95
+ - **Depth**: Control how deep to populate relationships (default: 1)
96
+ - **Limit**: Maximum number of documents to return (default: 10)
97
+ - **Page**: Page number for pagination (default: 1)
98
+ - **Sort**: Sort field (use `-` prefix for descending, e.g., `-createdAt`)
99
+ - **Where**: JSON object for filtering documents
100
+ - **Select**: Comma-separated list of fields to include in the response
101
+ - **Locale**: Locale for localized content
102
+
103
+ ### Example Usage
104
+
105
+ #### Finding Posts with Filtering
106
+
107
+ ```json
108
+ {
109
+ "resource": "collection",
110
+ "collection": "posts",
111
+ "operation": "find",
112
+ "additionalOptions": {
113
+ "where": {
114
+ "status": {
115
+ "equals": "published"
116
+ }
117
+ },
118
+ "limit": 20,
119
+ "sort": "-createdAt"
120
+ }
121
+ }
122
+ ```
123
+
124
+ #### Creating a New Document
125
+
126
+ ```json
127
+ {
128
+ "resource": "collection",
129
+ "collection": "posts",
130
+ "operation": "create",
131
+ "data": {
132
+ "title": "My New Post",
133
+ "content": "This is the content of my new post",
134
+ "status": "draft"
135
+ }
136
+ }
137
+ ```
138
+
139
+ #### Updating Global Settings
140
+
141
+ ```json
142
+ {
143
+ "resource": "global",
144
+ "global": "settings",
145
+ "operation": "update",
146
+ "data": {
147
+ "siteName": "My Updated Site Name",
148
+ "siteDescription": "Updated description"
149
+ }
150
+ }
151
+ ```
152
+
153
+ ## Dynamic Discovery
154
+
155
+ The node automatically discovers available collections and globals from your Payload CMS instance using the following methods:
156
+
157
+ 1. **Primary Method**: Attempts to fetch collections/globals from admin endpoints (`/api/collections`, `/api/globals`)
158
+ 2. **Fallback Method**: Tests common collection/global names to discover what's available
159
+
160
+ ### Common Collections Tested
161
+
162
+ - users
163
+ - posts
164
+ - pages
165
+ - media
166
+ - categories
167
+ - tags
168
+
169
+ ### Common Globals Tested
170
+
171
+ - settings
172
+ - config
173
+ - navigation
174
+ - footer
175
+ - header
176
+
177
+ ## Error Handling
178
+
179
+ The node includes comprehensive error handling:
180
+
181
+ - **Connection Errors**: Clear messages when unable to connect to Payload CMS
182
+ - **Authentication Errors**: Specific messages for API key issues
183
+ - **Discovery Errors**: Helpful messages when collections/globals cannot be discovered
184
+ - **Operation Errors**: Detailed error information for failed operations
185
+
186
+ ## Development
187
+
188
+ ### Building
189
+
190
+ ```bash
191
+ npm run build
192
+ ```
193
+
194
+ ### Development Mode
195
+
196
+ ```bash
197
+ npm run dev
198
+ ```
199
+
200
+ ### Linting
201
+
202
+ ```bash
203
+ npm run lint
204
+ ```
205
+
206
+ ### Formatting
207
+
208
+ ```bash
209
+ npm run format
210
+ ```
211
+
212
+ ## Payload CMS REST API Reference
213
+
214
+ This node is built according to the official Payload CMS REST API documentation:
215
+ https://payloadcms.com/docs/rest-api/overview
216
+
217
+ ## Supported Payload CMS Versions
218
+
219
+ This node is designed to work with Payload CMS v2.x and v3.x. It uses the standard REST API endpoints that are consistent across versions.
220
+
221
+ ## Contributing
222
+
223
+ 1. Fork the repository
224
+ 2. Create a feature branch
225
+ 3. Make your changes
226
+ 4. Add tests if applicable
227
+ 5. Submit a pull request
228
+
229
+ ## License
230
+
231
+ This project is licensed under the MIT License.
232
+
233
+ ## Support
234
+
235
+ For issues and questions:
236
+
237
+ 1. Check the Payload CMS documentation: https://payloadcms.com/docs
238
+ 2. Review the n8n node development documentation
239
+ 3. Open an issue in this repository
240
+
241
+ ## Changelog
242
+
243
+ ### v1.2.0
244
+
245
+ - **Simplified Authentication**: Removed API key authentication, now uses username/password only
246
+ - **Cleaner Interface**: Simplified credentials configuration with fewer options
247
+ - **Focused on PayloadCMS Standards**: Aligned with PayloadCMS's primary authentication method
248
+ - **Reduced Complexity**: Streamlined codebase by removing unused authentication paths
249
+
250
+ ### v1.1.0
251
+
252
+ - **Automatic Authentication**: Added support for username/password authentication with automatic login
253
+ - **Token Caching**: Implemented 1-hour token caching for improved performance
254
+ - **Session Management**: Automatic token refresh and session handling
255
+ - **Backward Compatibility**: Maintained support for API key authentication
256
+ - **Enhanced Credentials**: New authentication method selection in credentials configuration
257
+
258
+ ### v1.0.0
259
+
260
+ - Initial release
261
+ - Dynamic collection and global discovery
262
+ - Full CRUD operations support
263
+ - Advanced query parameter support
264
+ - Robust error handling
@@ -0,0 +1,7 @@
1
+ import { ICredentialType, INodeProperties } from "n8n-workflow";
2
+ export declare class PayloadCmsApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PayloadCmsApi = void 0;
4
+ class PayloadCmsApi {
5
+ name = "payloadCmsApi";
6
+ displayName = "Payload CMS API";
7
+ documentationUrl = "https://payloadcms.com/docs/rest-api/overview";
8
+ properties = [
9
+ {
10
+ displayName: "Base URL",
11
+ name: "baseUrl",
12
+ type: "string",
13
+ default: "https://your-payload-instance.com",
14
+ placeholder: "https://your-payload-instance.com",
15
+ description: "The base URL of your Payload CMS instance",
16
+ required: true,
17
+ },
18
+ {
19
+ displayName: "Email",
20
+ name: "email",
21
+ type: "string",
22
+ default: "",
23
+ description: "Your PayloadCMS user email",
24
+ required: true,
25
+ },
26
+ {
27
+ displayName: "Password",
28
+ name: "password",
29
+ type: "string",
30
+ typeOptions: {
31
+ password: true,
32
+ },
33
+ default: "",
34
+ description: "Your PayloadCMS user password",
35
+ required: true,
36
+ },
37
+ {
38
+ displayName: "User Collection",
39
+ name: "userCollection",
40
+ type: "string",
41
+ default: "users",
42
+ description: "The collection slug for users (default: users)",
43
+ },
44
+ {
45
+ displayName: "API Prefix",
46
+ name: "apiPrefix",
47
+ type: "string",
48
+ default: "/api",
49
+ description: "The API route prefix (default: /api)",
50
+ },
51
+ ];
52
+ }
53
+ exports.PayloadCmsApi = PayloadCmsApi;
@@ -0,0 +1,2 @@
1
+ export * from "./nodes/PayloadCms/PayloadCms.node";
2
+ export * from "./credentials/PayloadCmsApi.credentials";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./nodes/PayloadCms/PayloadCms.node"), exports);
18
+ __exportStar(require("./credentials/PayloadCmsApi.credentials"), exports);
@@ -0,0 +1,33 @@
1
+ import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from "n8n-workflow";
2
+ import { AxiosRequestConfig } from "axios";
3
+ interface PayloadCollection {
4
+ slug: string;
5
+ labels?: {
6
+ singular: string;
7
+ plural: string;
8
+ };
9
+ fields?: any[];
10
+ auth?: boolean;
11
+ }
12
+ interface PayloadGlobal {
13
+ slug: string;
14
+ label?: string;
15
+ fields?: any[];
16
+ }
17
+ export declare class PayloadCms implements INodeType {
18
+ private static authTokenCache;
19
+ description: INodeTypeDescription;
20
+ methods: {
21
+ loadOptions: {
22
+ getCollections(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
23
+ getGlobals(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
24
+ getAuthCollections(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
25
+ };
26
+ };
27
+ discoverCollections(this: ILoadOptionsFunctions): Promise<PayloadCollection[]>;
28
+ discoverGlobals(this: ILoadOptionsFunctions): Promise<PayloadGlobal[]>;
29
+ authenticate(this: IExecuteFunctions | ILoadOptionsFunctions, credentials: any): Promise<string>;
30
+ makeAuthenticatedRequest(this: IExecuteFunctions | ILoadOptionsFunctions, config: AxiosRequestConfig): Promise<any>;
31
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
32
+ }
33
+ export {};
@@ -0,0 +1,796 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PayloadCms = void 0;
7
+ const n8n_workflow_1 = require("n8n-workflow");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ class PayloadCms {
10
+ // Cache for authentication tokens
11
+ static authTokenCache = new Map();
12
+ description = {
13
+ displayName: "Payload CMS",
14
+ name: "payloadCms",
15
+ icon: "file:payloadcms.svg",
16
+ group: ["transform"],
17
+ version: 1,
18
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
19
+ description: "Interact with Payload CMS collections and globals",
20
+ defaults: {
21
+ name: "Payload CMS",
22
+ },
23
+ inputs: ["main" /* NodeConnectionType.Main */],
24
+ outputs: ["main" /* NodeConnectionType.Main */],
25
+ credentials: [
26
+ {
27
+ name: "payloadCmsApi",
28
+ required: true,
29
+ },
30
+ ],
31
+ properties: [
32
+ {
33
+ displayName: "Resource",
34
+ name: "resource",
35
+ type: "options",
36
+ noDataExpression: true,
37
+ required: true,
38
+ default: "collection",
39
+ options: [
40
+ {
41
+ name: "Collection",
42
+ value: "collection",
43
+ },
44
+ {
45
+ name: "Global",
46
+ value: "global",
47
+ },
48
+ {
49
+ name: "Auth",
50
+ value: "auth",
51
+ },
52
+ ],
53
+ },
54
+ // Collection operations
55
+ {
56
+ displayName: "Collection",
57
+ name: "collection",
58
+ type: "options",
59
+ typeOptions: {
60
+ loadOptionsMethod: "getCollections",
61
+ },
62
+ required: true,
63
+ default: "",
64
+ displayOptions: {
65
+ show: {
66
+ resource: ["collection"],
67
+ },
68
+ },
69
+ description: "Choose the collection to operate on",
70
+ },
71
+ {
72
+ displayName: "Operation",
73
+ name: "operation",
74
+ type: "options",
75
+ noDataExpression: true,
76
+ required: true,
77
+ default: "find",
78
+ displayOptions: {
79
+ show: {
80
+ resource: ["collection"],
81
+ },
82
+ },
83
+ options: [
84
+ {
85
+ name: "Find",
86
+ value: "find",
87
+ description: "Find documents in collection",
88
+ },
89
+ {
90
+ name: "Find by ID",
91
+ value: "findById",
92
+ description: "Find document by ID",
93
+ },
94
+ {
95
+ name: "Create",
96
+ value: "create",
97
+ description: "Create new document",
98
+ },
99
+ {
100
+ name: "Update",
101
+ value: "update",
102
+ description: "Update documents",
103
+ },
104
+ {
105
+ name: "Update by ID",
106
+ value: "updateById",
107
+ description: "Update document by ID",
108
+ },
109
+ {
110
+ name: "Delete",
111
+ value: "delete",
112
+ description: "Delete documents",
113
+ },
114
+ {
115
+ name: "Delete by ID",
116
+ value: "deleteById",
117
+ description: "Delete document by ID",
118
+ },
119
+ {
120
+ name: "Count",
121
+ value: "count",
122
+ description: "Count documents",
123
+ },
124
+ ],
125
+ },
126
+ // Global operations
127
+ {
128
+ displayName: "Global",
129
+ name: "global",
130
+ type: "options",
131
+ typeOptions: {
132
+ loadOptionsMethod: "getGlobals",
133
+ },
134
+ required: true,
135
+ default: "",
136
+ displayOptions: {
137
+ show: {
138
+ resource: ["global"],
139
+ },
140
+ },
141
+ description: "Choose the global to operate on",
142
+ },
143
+ {
144
+ displayName: "Operation",
145
+ name: "operation",
146
+ type: "options",
147
+ noDataExpression: true,
148
+ required: true,
149
+ default: "get",
150
+ displayOptions: {
151
+ show: {
152
+ resource: ["global"],
153
+ },
154
+ },
155
+ options: [
156
+ {
157
+ name: "Get",
158
+ value: "get",
159
+ description: "Get global data",
160
+ },
161
+ {
162
+ name: "Update",
163
+ value: "update",
164
+ description: "Update global data",
165
+ },
166
+ ],
167
+ },
168
+ // ID field for operations that need it
169
+ {
170
+ displayName: "Document ID",
171
+ name: "documentId",
172
+ type: "string",
173
+ required: true,
174
+ default: "",
175
+ displayOptions: {
176
+ show: {
177
+ resource: ["collection"],
178
+ operation: ["findById", "updateById", "deleteById"],
179
+ },
180
+ },
181
+ description: "The ID of the document to operate on",
182
+ },
183
+ // Data field for create/update operations
184
+ {
185
+ displayName: "Data",
186
+ name: "data",
187
+ type: "json",
188
+ required: true,
189
+ default: "{}",
190
+ displayOptions: {
191
+ show: {
192
+ resource: ["collection"],
193
+ operation: ["create", "update", "updateById"],
194
+ },
195
+ },
196
+ description: "The data to send (JSON format)",
197
+ },
198
+ {
199
+ displayName: "Data",
200
+ name: "data",
201
+ type: "json",
202
+ required: true,
203
+ default: "{}",
204
+ displayOptions: {
205
+ show: {
206
+ resource: ["global"],
207
+ operation: ["update"],
208
+ },
209
+ },
210
+ description: "The data to send (JSON format)",
211
+ },
212
+ // Auth operations
213
+ {
214
+ displayName: "Auth Collection",
215
+ name: "authCollection",
216
+ type: "options",
217
+ typeOptions: {
218
+ loadOptionsMethod: "getAuthCollections",
219
+ },
220
+ required: true,
221
+ default: "users",
222
+ displayOptions: {
223
+ show: {
224
+ resource: ["auth"],
225
+ },
226
+ },
227
+ description: "Choose the auth-enabled collection",
228
+ },
229
+ {
230
+ displayName: "Operation",
231
+ name: "operation",
232
+ type: "options",
233
+ noDataExpression: true,
234
+ required: true,
235
+ default: "login",
236
+ displayOptions: {
237
+ show: {
238
+ resource: ["auth"],
239
+ },
240
+ },
241
+ options: [
242
+ {
243
+ name: "Login",
244
+ value: "login",
245
+ description: "Login user",
246
+ },
247
+ {
248
+ name: "Logout",
249
+ value: "logout",
250
+ description: "Logout user",
251
+ },
252
+ {
253
+ name: "Me",
254
+ value: "me",
255
+ description: "Get current user",
256
+ },
257
+ {
258
+ name: "Refresh Token",
259
+ value: "refresh",
260
+ description: "Refresh authentication token",
261
+ },
262
+ {
263
+ name: "Forgot Password",
264
+ value: "forgotPassword",
265
+ description: "Send forgot password email",
266
+ },
267
+ {
268
+ name: "Reset Password",
269
+ value: "resetPassword",
270
+ description: "Reset user password",
271
+ },
272
+ {
273
+ name: "Verify",
274
+ value: "verify",
275
+ description: "Verify user account",
276
+ },
277
+ {
278
+ name: "Unlock",
279
+ value: "unlock",
280
+ description: "Unlock user account",
281
+ },
282
+ ],
283
+ },
284
+ // Auth data fields
285
+ {
286
+ displayName: "Email",
287
+ name: "email",
288
+ type: "string",
289
+ required: true,
290
+ default: "",
291
+ displayOptions: {
292
+ show: {
293
+ resource: ["auth"],
294
+ operation: ["login", "forgotPassword"],
295
+ },
296
+ },
297
+ description: "User email address",
298
+ },
299
+ {
300
+ displayName: "Password",
301
+ name: "password",
302
+ type: "string",
303
+ typeOptions: {
304
+ password: true,
305
+ },
306
+ required: true,
307
+ default: "",
308
+ displayOptions: {
309
+ show: {
310
+ resource: ["auth"],
311
+ operation: ["login"],
312
+ },
313
+ },
314
+ description: "User password",
315
+ },
316
+ {
317
+ displayName: "Token",
318
+ name: "token",
319
+ type: "string",
320
+ required: true,
321
+ default: "",
322
+ displayOptions: {
323
+ show: {
324
+ resource: ["auth"],
325
+ operation: ["verify", "resetPassword"],
326
+ },
327
+ },
328
+ description: "Verification or reset token",
329
+ },
330
+ {
331
+ displayName: "New Password",
332
+ name: "newPassword",
333
+ type: "string",
334
+ typeOptions: {
335
+ password: true,
336
+ },
337
+ required: true,
338
+ default: "",
339
+ displayOptions: {
340
+ show: {
341
+ resource: ["auth"],
342
+ operation: ["resetPassword"],
343
+ },
344
+ },
345
+ description: "New password for reset",
346
+ },
347
+ // Query parameters
348
+ {
349
+ displayName: "Additional Options",
350
+ name: "additionalOptions",
351
+ type: "collection",
352
+ placeholder: "Add Option",
353
+ default: {},
354
+ options: [
355
+ {
356
+ displayName: "Depth",
357
+ name: "depth",
358
+ type: "number",
359
+ default: 1,
360
+ description: "How deep to populate relationships",
361
+ },
362
+ {
363
+ displayName: "Limit",
364
+ name: "limit",
365
+ type: "number",
366
+ default: 10,
367
+ description: "Maximum number of documents to return",
368
+ },
369
+ {
370
+ displayName: "Page",
371
+ name: "page",
372
+ type: "number",
373
+ default: 1,
374
+ description: "Page number for pagination",
375
+ },
376
+ {
377
+ displayName: "Sort",
378
+ name: "sort",
379
+ type: "string",
380
+ default: "",
381
+ description: "Sort field (use - for descending, e.g., -createdAt)",
382
+ },
383
+ {
384
+ displayName: "Where",
385
+ name: "where",
386
+ type: "json",
387
+ default: "{}",
388
+ description: "Where clause for filtering (JSON format)",
389
+ },
390
+ {
391
+ displayName: "Select",
392
+ name: "select",
393
+ type: "string",
394
+ default: "",
395
+ description: "Fields to select (comma-separated)",
396
+ },
397
+ {
398
+ displayName: "Locale",
399
+ name: "locale",
400
+ type: "string",
401
+ default: "",
402
+ description: "Locale for localized content",
403
+ },
404
+ ],
405
+ },
406
+ ],
407
+ };
408
+ methods = {
409
+ loadOptions: {
410
+ async getCollections() {
411
+ try {
412
+ const collections = await PayloadCms.prototype.discoverCollections.call(this);
413
+ return collections.map((collection) => ({
414
+ name: collection.labels?.plural || collection.slug,
415
+ value: collection.slug,
416
+ }));
417
+ }
418
+ catch (error) {
419
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to load collections: ${error instanceof Error ? error.message : "Unknown error"}`);
420
+ }
421
+ },
422
+ async getGlobals() {
423
+ try {
424
+ const globals = await PayloadCms.prototype.discoverGlobals.call(this);
425
+ return globals.map((global) => ({
426
+ name: global.label || global.slug,
427
+ value: global.slug,
428
+ }));
429
+ }
430
+ catch (error) {
431
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to load globals: ${error instanceof Error ? error.message : "Unknown error"}`);
432
+ }
433
+ },
434
+ async getAuthCollections() {
435
+ try {
436
+ const collections = await PayloadCms.prototype.discoverCollections.call(this);
437
+ // Filter for auth-enabled collections or return common auth collections
438
+ const authCollections = collections.filter((collection) => collection.auth || collection.slug === "users");
439
+ if (authCollections.length > 0) {
440
+ return authCollections.map((collection) => ({
441
+ name: collection.labels?.plural || collection.slug,
442
+ value: collection.slug,
443
+ }));
444
+ }
445
+ // Fallback to common auth collection names
446
+ return [
447
+ { name: "Users", value: "users" },
448
+ { name: "Admins", value: "admins" },
449
+ { name: "Members", value: "members" },
450
+ ];
451
+ }
452
+ catch (error) {
453
+ // Return default auth collections if discovery fails
454
+ return [{ name: "Users", value: "users" }];
455
+ }
456
+ },
457
+ },
458
+ };
459
+ async discoverCollections() {
460
+ const credentials = await this.getCredentials("payloadCmsApi");
461
+ const baseUrl = credentials.baseUrl;
462
+ const apiPrefix = credentials.apiPrefix || "/api";
463
+ try {
464
+ // First, try to get collections from a potential admin endpoint
465
+ // This is a common pattern in many CMS systems
466
+ const response = await PayloadCms.prototype.makeAuthenticatedRequest.call(this, {
467
+ method: "GET",
468
+ url: `${baseUrl}${apiPrefix}/collections`,
469
+ });
470
+ if (response.data && Array.isArray(response.data)) {
471
+ return response.data;
472
+ }
473
+ }
474
+ catch (error) {
475
+ // If that fails, we'll try some common collection names
476
+ // This is a fallback approach for discovery
477
+ }
478
+ // Fallback: Try some common collection names
479
+ const commonCollections = [
480
+ "users",
481
+ "posts",
482
+ "pages",
483
+ "media",
484
+ "categories",
485
+ "tags",
486
+ ];
487
+ const discoveredCollections = [];
488
+ for (const slug of commonCollections) {
489
+ try {
490
+ await PayloadCms.prototype.makeAuthenticatedRequest.call(this, {
491
+ method: "GET",
492
+ url: `${baseUrl}${apiPrefix}/${slug}`,
493
+ params: { limit: 1 },
494
+ });
495
+ discoveredCollections.push({
496
+ slug,
497
+ labels: {
498
+ singular: slug.slice(0, -1),
499
+ plural: slug,
500
+ },
501
+ });
502
+ }
503
+ catch (error) {
504
+ // Collection doesn't exist, continue
505
+ }
506
+ }
507
+ if (discoveredCollections.length === 0) {
508
+ throw new Error("Could not discover any collections. Please ensure your Payload CMS instance is accessible and you have valid credentials.");
509
+ }
510
+ return discoveredCollections;
511
+ }
512
+ async discoverGlobals() {
513
+ const credentials = await this.getCredentials("payloadCmsApi");
514
+ const baseUrl = credentials.baseUrl;
515
+ const apiPrefix = credentials.apiPrefix || "/api";
516
+ try {
517
+ // Try to get globals from a potential admin endpoint
518
+ const response = await PayloadCms.prototype.makeAuthenticatedRequest.call(this, {
519
+ method: "GET",
520
+ url: `${baseUrl}${apiPrefix}/globals`,
521
+ });
522
+ if (response.data && Array.isArray(response.data)) {
523
+ return response.data;
524
+ }
525
+ }
526
+ catch (error) {
527
+ // If that fails, we'll try some common global names
528
+ }
529
+ // Fallback: Try some common global names
530
+ const commonGlobals = [
531
+ "settings",
532
+ "config",
533
+ "navigation",
534
+ "footer",
535
+ "header",
536
+ ];
537
+ const discoveredGlobals = [];
538
+ for (const slug of commonGlobals) {
539
+ try {
540
+ await PayloadCms.prototype.makeAuthenticatedRequest.call(this, {
541
+ method: "GET",
542
+ url: `${baseUrl}${apiPrefix}/globals/${slug}`,
543
+ });
544
+ discoveredGlobals.push({
545
+ slug,
546
+ label: slug.charAt(0).toUpperCase() + slug.slice(1),
547
+ });
548
+ }
549
+ catch (error) {
550
+ // Global doesn't exist, continue
551
+ }
552
+ }
553
+ return discoveredGlobals;
554
+ }
555
+ // Helper method to authenticate and get token
556
+ async authenticate(credentials) {
557
+ const baseUrl = credentials.baseUrl;
558
+ const apiPrefix = credentials.apiPrefix || "/api";
559
+ const email = credentials.email;
560
+ const password = credentials.password;
561
+ const userCollection = credentials.userCollection || "users";
562
+ // Create cache key
563
+ const cacheKey = `${baseUrl}:${email}:${userCollection}`;
564
+ // Check if we have a valid cached token
565
+ const cached = PayloadCms.authTokenCache.get(cacheKey);
566
+ if (cached && cached.expires > Date.now()) {
567
+ return cached.token;
568
+ }
569
+ try {
570
+ // Login to get token
571
+ const loginResponse = await axios_1.default.post(`${baseUrl}${apiPrefix}/${userCollection}/login`, {
572
+ email,
573
+ password,
574
+ }, {
575
+ headers: {
576
+ "Content-Type": "application/json",
577
+ },
578
+ });
579
+ if (loginResponse.data && loginResponse.data.token) {
580
+ const token = loginResponse.data.token;
581
+ // Cache token for 1 hour (adjust as needed)
582
+ const expires = Date.now() + 60 * 60 * 1000;
583
+ PayloadCms.authTokenCache.set(cacheKey, { token, expires });
584
+ return token;
585
+ }
586
+ throw new Error("No token received from login response");
587
+ }
588
+ catch (error) {
589
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`);
590
+ }
591
+ }
592
+ // Helper method to make authenticated requests
593
+ async makeAuthenticatedRequest(config) {
594
+ const credentials = await this.getCredentials("payloadCmsApi");
595
+ const token = await PayloadCms.prototype.authenticate.call(this, credentials);
596
+ // Add authorization header
597
+ config.headers = {
598
+ ...config.headers,
599
+ Authorization: `Bearer ${token}`,
600
+ "Content-Type": "application/json",
601
+ };
602
+ return (0, axios_1.default)(config);
603
+ }
604
+ async execute() {
605
+ const items = this.getInputData();
606
+ const returnData = [];
607
+ for (let i = 0; i < items.length; i++) {
608
+ try {
609
+ const resource = this.getNodeParameter("resource", i);
610
+ const operation = this.getNodeParameter("operation", i);
611
+ const additionalOptions = this.getNodeParameter("additionalOptions", i, {});
612
+ const credentials = await this.getCredentials("payloadCmsApi");
613
+ const baseUrl = credentials.baseUrl;
614
+ const apiPrefix = credentials.apiPrefix || "/api";
615
+ let url = "";
616
+ let method = "GET";
617
+ let data = undefined;
618
+ const params = {};
619
+ // Add query parameters
620
+ if (additionalOptions.depth !== undefined)
621
+ params.depth = additionalOptions.depth;
622
+ if (additionalOptions.limit !== undefined)
623
+ params.limit = additionalOptions.limit;
624
+ if (additionalOptions.page !== undefined)
625
+ params.page = additionalOptions.page;
626
+ if (additionalOptions.sort)
627
+ params.sort = additionalOptions.sort;
628
+ if (additionalOptions.where) {
629
+ // Handle where clause - it should be a JSON object
630
+ const whereClause = typeof additionalOptions.where === "string"
631
+ ? JSON.parse(additionalOptions.where)
632
+ : additionalOptions.where;
633
+ params.where = JSON.stringify(whereClause);
634
+ }
635
+ if (additionalOptions.select)
636
+ params.select = additionalOptions.select;
637
+ if (additionalOptions.locale)
638
+ params.locale = additionalOptions.locale;
639
+ if (resource === "collection") {
640
+ const collection = this.getNodeParameter("collection", i);
641
+ switch (operation) {
642
+ case "find":
643
+ url = `${baseUrl}${apiPrefix}/${collection}`;
644
+ method = "GET";
645
+ break;
646
+ case "findById":
647
+ const docId = this.getNodeParameter("documentId", i);
648
+ url = `${baseUrl}${apiPrefix}/${collection}/${docId}`;
649
+ method = "GET";
650
+ break;
651
+ case "create":
652
+ url = `${baseUrl}${apiPrefix}/${collection}`;
653
+ method = "POST";
654
+ data = this.getNodeParameter("data", i);
655
+ break;
656
+ case "update":
657
+ url = `${baseUrl}${apiPrefix}/${collection}`;
658
+ method = "PATCH";
659
+ data = this.getNodeParameter("data", i);
660
+ break;
661
+ case "updateById":
662
+ const updateId = this.getNodeParameter("documentId", i);
663
+ url = `${baseUrl}${apiPrefix}/${collection}/${updateId}`;
664
+ method = "PATCH";
665
+ data = this.getNodeParameter("data", i);
666
+ break;
667
+ case "delete":
668
+ url = `${baseUrl}${apiPrefix}/${collection}`;
669
+ method = "DELETE";
670
+ // For bulk delete, we need to pass where clause in the body
671
+ if (additionalOptions.where) {
672
+ data = {
673
+ where: typeof additionalOptions.where === "string"
674
+ ? JSON.parse(additionalOptions.where)
675
+ : additionalOptions.where,
676
+ };
677
+ }
678
+ break;
679
+ case "deleteById":
680
+ const deleteId = this.getNodeParameter("documentId", i);
681
+ url = `${baseUrl}${apiPrefix}/${collection}/${deleteId}`;
682
+ method = "DELETE";
683
+ break;
684
+ case "count":
685
+ url = `${baseUrl}${apiPrefix}/${collection}/count`;
686
+ method = "GET";
687
+ break;
688
+ }
689
+ }
690
+ else if (resource === "global") {
691
+ const global = this.getNodeParameter("global", i);
692
+ switch (operation) {
693
+ case "get":
694
+ url = `${baseUrl}${apiPrefix}/globals/${global}`;
695
+ method = "GET";
696
+ break;
697
+ case "update":
698
+ url = `${baseUrl}${apiPrefix}/globals/${global}`;
699
+ method = "POST";
700
+ data = this.getNodeParameter("data", i);
701
+ break;
702
+ }
703
+ }
704
+ else if (resource === "auth") {
705
+ const authCollection = this.getNodeParameter("authCollection", i);
706
+ switch (operation) {
707
+ case "login":
708
+ url = `${baseUrl}${apiPrefix}/${authCollection}/login`;
709
+ method = "POST";
710
+ data = {
711
+ email: this.getNodeParameter("email", i),
712
+ password: this.getNodeParameter("password", i),
713
+ };
714
+ break;
715
+ case "logout":
716
+ url = `${baseUrl}${apiPrefix}/${authCollection}/logout`;
717
+ method = "POST";
718
+ break;
719
+ case "me":
720
+ url = `${baseUrl}${apiPrefix}/${authCollection}/me`;
721
+ method = "GET";
722
+ break;
723
+ case "refresh":
724
+ url = `${baseUrl}${apiPrefix}/${authCollection}/refresh-token`;
725
+ method = "POST";
726
+ break;
727
+ case "forgotPassword":
728
+ url = `${baseUrl}${apiPrefix}/${authCollection}/forgot-password`;
729
+ method = "POST";
730
+ data = {
731
+ email: this.getNodeParameter("email", i),
732
+ };
733
+ break;
734
+ case "resetPassword":
735
+ url = `${baseUrl}${apiPrefix}/${authCollection}/reset-password`;
736
+ method = "POST";
737
+ data = {
738
+ token: this.getNodeParameter("token", i),
739
+ password: this.getNodeParameter("newPassword", i),
740
+ };
741
+ break;
742
+ case "verify":
743
+ const token = this.getNodeParameter("token", i);
744
+ url = `${baseUrl}${apiPrefix}/${authCollection}/verify/${token}`;
745
+ method = "POST";
746
+ break;
747
+ case "unlock":
748
+ url = `${baseUrl}${apiPrefix}/${authCollection}/unlock`;
749
+ method = "POST";
750
+ data = {
751
+ email: this.getNodeParameter("email", i),
752
+ };
753
+ break;
754
+ }
755
+ }
756
+ const requestConfig = {
757
+ method: method,
758
+ url,
759
+ params,
760
+ };
761
+ if (data) {
762
+ requestConfig.data =
763
+ typeof data === "string" ? JSON.parse(data) : data;
764
+ }
765
+ const response = await PayloadCms.prototype.makeAuthenticatedRequest.call(this, requestConfig);
766
+ returnData.push({
767
+ json: response.data,
768
+ pairedItem: {
769
+ item: i,
770
+ },
771
+ });
772
+ }
773
+ catch (error) {
774
+ if (this.continueOnFail()) {
775
+ returnData.push({
776
+ json: {
777
+ error: error instanceof Error
778
+ ? error.message
779
+ : "Unknown error occurred",
780
+ },
781
+ pairedItem: {
782
+ item: i,
783
+ },
784
+ });
785
+ }
786
+ else {
787
+ throw error instanceof Error
788
+ ? error
789
+ : new Error("Unknown error occurred");
790
+ }
791
+ }
792
+ }
793
+ return [returnData];
794
+ }
795
+ }
796
+ exports.PayloadCms = PayloadCms;
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@warleon/n8n-nodes-payload-cms",
3
+ "version": "1.2.0",
4
+ "description": "Dynamic n8n node for Payload CMS that auto-discovers collections and operations forked and extended from https://github.com/leadership-institute/n8n-payload-dynamic",
5
+ "main": "dist/index.js",
6
+ "author": "warleon",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/warleon/n8n-payload-dynamic#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/warleon/n8n-payload-dynamic.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/warleon/n8n-payload-dynamic/issues"
15
+ },
16
+ "files": [
17
+ "dist/**/*",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "dev": "tsc --watch",
27
+ "format": "prettier --write .",
28
+ "lint": "eslint .",
29
+ "test": "jest",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "keywords": [
33
+ "n8n",
34
+ "n8n-community-node",
35
+ "n8n-node",
36
+ "payload",
37
+ "payloadcms",
38
+ "cms",
39
+ "headless-cms",
40
+ "rest-api",
41
+ "automation",
42
+ "workflow"
43
+ ],
44
+ "n8n": {
45
+ "nodes": [
46
+ "dist/nodes/PayloadCms/PayloadCms.node.js"
47
+ ],
48
+ "credentials": [
49
+ "dist/credentials/PayloadCmsApi.credentials.js"
50
+ ]
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.0.0",
54
+ "n8n-core": "^1.0.0",
55
+ "n8n-workflow": "^1.0.0",
56
+ "typescript": "^5.0.0"
57
+ },
58
+ "dependencies": {
59
+ "axios": "^1.10.0"
60
+ }
61
+ }