@warleon/n8n-nodes-payload-cms 1.2.0 → 1.2.2

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/README.md CHANGED
@@ -15,20 +15,54 @@ _Forked from [n8n-payload-dynamic](https://github.com/leadership-institute/n8n-p
15
15
 
16
16
  ## Installation
17
17
 
18
- ### Automatic
18
+ ### Payload
19
19
 
20
- ### Manual
20
+ In your payload server create one endpoint to fetch the permissions for your user
21
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
22
+ #### src/endpoints/reflection.ts
23
+
24
+ ```typescript
25
+ import type { Endpoint, PayloadRequest } from "payload";
26
+
27
+ export const reflectionEndpoint: Endpoint = {
28
+ path: "/reflection",
29
+ method: "get",
30
+ handler: async (req: PayloadRequest) => {
31
+ const { payload } = req;
32
+
33
+ return Response.json({
34
+ collections: payload.config.collections,
35
+ globals: payload.config.globals,
36
+ });
37
+ },
38
+ };
39
+ ```
40
+
41
+ #### src/payload.config.ts
42
+
43
+ ```typescript
44
+ import { buildConfig } from "payload/config";
45
+ import { reflectionEndpoint } from "./endpoints"; // import the endpoint
46
+
47
+ export default buildConfig({
48
+ // your collections, globals, etc
49
+ endpoints: [reflectionEndpoint], // add the endpoint
50
+ });
51
+ ```
52
+
53
+ ### n8n
54
+
55
+ 1. In your n8n instance go to "Settings"
56
+ 1. Then go to "Comunity nodes" and click the install button
57
+ 1. paste the following into the "npm Package Name" field
58
+
59
+ > > ```
60
+ > > @warleon/n8n-nodes-payload-cms
61
+ > > ```
62
+
63
+ 4. Accept the checkpbox and click install
64
+
65
+ ![install](/media/install.png)
32
66
 
33
67
  ## Configuration
34
68
 
@@ -37,8 +71,7 @@ _Forked from [n8n-payload-dynamic](https://github.com/leadership-institute/n8n-p
37
71
  1. In n8n, create new credentials of type "Payload CMS API"
38
72
  2. Configure the following fields:
39
73
  - **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
74
+ - **API key**: the generated api key for your user
42
75
  - **User Collection**: The collection slug for users (default: `users`)
43
76
  - **API Prefix**: The API route prefix (default: `/api`)
44
77
 
@@ -1,7 +1,8 @@
1
- import { ICredentialType, INodeProperties } from "n8n-workflow";
1
+ import { ICredentialTestRequest, ICredentialType, INodeProperties } from "n8n-workflow";
2
2
  export declare class PayloadCmsApi implements ICredentialType {
3
3
  name: string;
4
4
  displayName: string;
5
5
  documentationUrl: string;
6
6
  properties: INodeProperties[];
7
+ test: ICredentialTestRequest;
7
8
  }
@@ -4,34 +4,23 @@ exports.PayloadCmsApi = void 0;
4
4
  class PayloadCmsApi {
5
5
  name = "payloadCmsApi";
6
6
  displayName = "Payload CMS API";
7
- documentationUrl = "https://payloadcms.com/docs/rest-api/overview";
7
+ documentationUrl = "https://github.com/warleon/n8n-payload-dynamic#readme";
8
8
  properties = [
9
9
  {
10
10
  displayName: "Base URL",
11
11
  name: "baseUrl",
12
12
  type: "string",
13
- default: "https://your-payload-instance.com",
13
+ default: "",
14
14
  placeholder: "https://your-payload-instance.com",
15
15
  description: "The base URL of your Payload CMS instance",
16
16
  required: true,
17
17
  },
18
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",
19
+ displayName: "API key",
20
+ name: "apiKey",
29
21
  type: "string",
30
- typeOptions: {
31
- password: true,
32
- },
33
22
  default: "",
34
- description: "Your PayloadCMS user password",
23
+ description: "Your generated api key for the user you want to interface as",
35
24
  required: true,
36
25
  },
37
26
  {
@@ -48,6 +37,23 @@ class PayloadCmsApi {
48
37
  default: "/api",
49
38
  description: "The API route prefix (default: /api)",
50
39
  },
40
+ {
41
+ displayName: "Refection endpoint",
42
+ name: "endpoint",
43
+ type: "string",
44
+ default: "/api/permissions",
45
+ description: "The API route to the reflection endpoint as described in https://github.com/warleon/n8n-payload-dynamic?tab=readme-ov-file#payload",
46
+ },
51
47
  ];
48
+ test = {
49
+ request: {
50
+ baseURL: "={{$credentials.baseUrl}}", // comes from user input
51
+ url: "/{{$credentials.apiPrefix}}/{{$credentials.endpoint}}",
52
+ method: "GET",
53
+ headers: {
54
+ Authorization: "{{$credentials.userCollection}} API-Key {{$credentials.apiKey}}",
55
+ },
56
+ },
57
+ };
52
58
  }
53
59
  exports.PayloadCmsApi = PayloadCmsApi;
@@ -1,19 +1,6 @@
1
1
  import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from "n8n-workflow";
2
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
- }
3
+ import { SanitizedCollectionConfig, SanitizedGlobalConfig } from "./payload.types";
17
4
  export declare class PayloadCms implements INodeType {
18
5
  private static authTokenCache;
19
6
  description: INodeTypeDescription;
@@ -24,10 +11,8 @@ export declare class PayloadCms implements INodeType {
24
11
  getAuthCollections(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
25
12
  };
26
13
  };
27
- discoverCollections(this: ILoadOptionsFunctions): Promise<PayloadCollection[]>;
28
- discoverGlobals(this: ILoadOptionsFunctions): Promise<PayloadGlobal[]>;
29
- authenticate(this: IExecuteFunctions | ILoadOptionsFunctions, credentials: any): Promise<string>;
14
+ discoverCollections(this: ILoadOptionsFunctions): Promise<SanitizedCollectionConfig[]>;
15
+ discoverGlobals(this: ILoadOptionsFunctions): Promise<SanitizedGlobalConfig[]>;
30
16
  makeAuthenticatedRequest(this: IExecuteFunctions | ILoadOptionsFunctions, config: AxiosRequestConfig): Promise<any>;
31
17
  execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
32
18
  }
33
- export {};
@@ -459,144 +459,50 @@ class PayloadCms {
459
459
  async discoverCollections() {
460
460
  const credentials = await this.getCredentials("payloadCmsApi");
461
461
  const baseUrl = credentials.baseUrl;
462
- const apiPrefix = credentials.apiPrefix || "/api";
462
+ const permissionsEndpoint = credentials.permissionsEndpoint || "/api/permissions";
463
463
  try {
464
464
  // First, try to get collections from a potential admin endpoint
465
465
  // This is a common pattern in many CMS systems
466
466
  const response = await PayloadCms.prototype.makeAuthenticatedRequest.call(this, {
467
467
  method: "GET",
468
- url: `${baseUrl}${apiPrefix}/collections`,
468
+ url: `${baseUrl}${permissionsEndpoint}`,
469
469
  });
470
- if (response.data && Array.isArray(response.data)) {
471
- return response.data;
472
- }
470
+ return response.collections;
473
471
  }
474
472
  catch (error) {
475
473
  // If that fails, we'll try some common collection names
476
474
  // This is a fallback approach for discovery
475
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to load collections ensure that ${baseUrl}${permissionsEndpoint} exists. check https://github.com/warleon/n8n-payload-dynamic?tab=readme-ov-file#payload`);
477
476
  }
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
477
  }
512
478
  async discoverGlobals() {
513
479
  const credentials = await this.getCredentials("payloadCmsApi");
514
480
  const baseUrl = credentials.baseUrl;
515
- const apiPrefix = credentials.apiPrefix || "/api";
481
+ const permissionsEndpoint = credentials.permissionsEndpoint || "/api";
516
482
  try {
517
483
  // Try to get globals from a potential admin endpoint
518
484
  const response = await PayloadCms.prototype.makeAuthenticatedRequest.call(this, {
519
485
  method: "GET",
520
- url: `${baseUrl}${apiPrefix}/globals`,
486
+ url: `${baseUrl}${permissionsEndpoint}`,
521
487
  });
522
- if (response.data && Array.isArray(response.data)) {
523
- return response.data;
524
- }
488
+ return response.globals;
525
489
  }
526
490
  catch (error) {
527
491
  // 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"}`);
492
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to load globals ensure that ${baseUrl}${permissionsEndpoint} exists. check https://github.com/warleon/n8n-payload-dynamic?tab=readme-ov-file#payload`);
590
493
  }
591
494
  }
592
495
  // Helper method to make authenticated requests
593
496
  async makeAuthenticatedRequest(config) {
594
497
  const credentials = await this.getCredentials("payloadCmsApi");
595
- const token = await PayloadCms.prototype.authenticate.call(this, credentials);
498
+ const baseUrl = credentials.baseUrl;
499
+ const apiPrefix = credentials.apiPrefix || "/api";
500
+ const apiKey = credentials.apiKey;
501
+ const userCollection = credentials.userCollection;
596
502
  // Add authorization header
597
503
  config.headers = {
598
504
  ...config.headers,
599
- Authorization: `Bearer ${token}`,
505
+ Authorization: `${userCollection} API-Key ${apiKey}`,
600
506
  "Content-Type": "application/json",
601
507
  };
602
508
  return (0, axios_1.default)(config);
@@ -0,0 +1,128 @@
1
+ import type { DeepRequired } from "ts-essentials";
2
+ export interface SanitizedGlobalConfig extends DeepRequired<GlobalConfig> {
3
+ fields: Field[];
4
+ slug: GlobalSlug;
5
+ }
6
+ export interface SanitizedCollectionConfig extends DeepRequired<CollectionConfig> {
7
+ auth: Auth;
8
+ fields: Field[];
9
+ slug: CollectionSlug;
10
+ }
11
+ export type CollectionConfig<TSlug extends object = any> = {
12
+ /**
13
+ * Label configuration
14
+ */
15
+ labels?: {
16
+ plural?: StaticLabel;
17
+ singular?: StaticLabel;
18
+ };
19
+ /**
20
+ * Enables / Disables the ability to lock documents while editing
21
+ * @default true
22
+ */
23
+ lockDocuments?: {
24
+ duration: number;
25
+ } | false;
26
+ /**
27
+ * If true, enables custom ordering for the collection, and documents in the listView can be reordered via drag and drop.
28
+ * New documents are inserted at the end of the list according to this parameter.
29
+ *
30
+ * Under the hood, a field with {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing|fractional indexing} is used to optimize inserts and reorderings.
31
+ *
32
+ * @default false
33
+ *
34
+ * @experimental There may be frequent breaking changes to this API
35
+ */
36
+ orderable?: boolean;
37
+ /**
38
+ * Add `createdAt`, `deletedAt` and `updatedAt` fields
39
+ *
40
+ * @default true
41
+ */
42
+ timestamps?: boolean;
43
+ /**
44
+ * Enables trash support for this collection.
45
+ *
46
+ * When enabled, documents will include a `deletedAt` timestamp field.
47
+ * This allows documents to be marked as deleted without being permanently removed.
48
+ * The `deletedAt` field will be set to the current date and time when a document is trashed.
49
+ *
50
+ * @experimental This is a beta feature and its behavior may be refined in future releases.
51
+ * @default false
52
+ */
53
+ trash?: boolean;
54
+ /**
55
+ * Options used in typescript generation
56
+ */
57
+ typescript?: {
58
+ /**
59
+ * Typescript generation name given to the interface type
60
+ */
61
+ interface?: string;
62
+ };
63
+ };
64
+ export type StaticLabel = string;
65
+ export type Auth = DeepRequired<IncomingAuthType> | false;
66
+ export interface IncomingAuthType {
67
+ /**
68
+ * Set cookie options, including secure, sameSite, and domain. For advanced users.
69
+ */
70
+ cookies?: {
71
+ domain?: string;
72
+ sameSite?: "Lax" | "None" | "Strict" | boolean;
73
+ secure?: boolean;
74
+ };
75
+ /**
76
+ * How many levels deep a user document should be populated when creating the JWT and binding the user to the req. Defaults to 0 and should only be modified if absolutely necessary, as this will affect performance.
77
+ * @default 0
78
+ */
79
+ depth?: number;
80
+ /**
81
+ * Advanced - disable Payload's built-in local auth strategy. Only use this property if you have replaced Payload's auth mechanisms with your own.
82
+ */
83
+ disableLocalStrategy?: {
84
+ /**
85
+ * Include auth fields on the collection even though the local strategy is disabled.
86
+ * Useful when you do not want the database or types to vary depending on the auth configuration.
87
+ */
88
+ enableFields?: true;
89
+ optionalPassword?: true;
90
+ } | true;
91
+ /**
92
+ * Set the time (in milliseconds) that a user should be locked out if they fail authentication more times than maxLoginAttempts allows for.
93
+ */
94
+ lockTime?: number;
95
+ /**
96
+ * Only allow a user to attempt logging in X amount of times. Automatically locks out a user from authenticating if this limit is passed. Set to 0 to disable.
97
+ */
98
+ maxLoginAttempts?: number;
99
+ /***
100
+ * Set to true if you want to remove the token from the returned authentication API responses such as login or refresh.
101
+ */
102
+ removeTokenFromResponses?: true;
103
+ /**
104
+ * Controls how many seconds the token will be valid for. Default is 2 hours.
105
+ * @default 7200
106
+ * @link https://payloadcms.com/docs/authentication/overview#config-options
107
+ */
108
+ tokenExpiration?: number;
109
+ /**
110
+ * Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection.
111
+ * @default false
112
+ * @link https://payloadcms.com/docs/authentication/api-keys
113
+ */
114
+ useAPIKey?: boolean;
115
+ /**
116
+ * Use sessions for authentication. Enabled by default.
117
+ * @default true
118
+ */
119
+ useSessions?: boolean;
120
+ }
121
+ export type Field = any;
122
+ export type CollectionSlug = string;
123
+ export type GlobalSlug = string;
124
+ export type GlobalConfig<TSlug extends GlobalSlug = any> = {
125
+ custom?: Record<string, any>;
126
+ fields: Field[];
127
+ label?: StaticLabel;
128
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warleon/n8n-nodes-payload-cms",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
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
5
  "main": "dist/index.js",
6
6
  "author": "warleon",
@@ -53,6 +53,7 @@
53
53
  "@types/node": "^20.0.0",
54
54
  "n8n-core": "^1.0.0",
55
55
  "n8n-workflow": "^1.0.0",
56
+ "ts-essentials": "^10.1.1",
56
57
  "typescript": "^5.0.0"
57
58
  },
58
59
  "dependencies": {