Repository: BuilderIO/builder
Pull request #3945: feat[pluginTools][builderRequest]: ENG-8621 ensure BuilderRequest can define POST requests as well with request-body
Author: login: clyde-builderio
Created: 2025-03-04T11:09:00Z | Merged: 2025-03-07T10:21:17Z

## Description

Ensure BuilderRequest can define POST requests as well with request-body

**Dependent PRs for same feature**

1. Content API https://github.com/BuilderIO/builder-internal/pull/8046
2. Web-App https://github.com/BuilderIO/builder-internal/pull/8056

**Jira Ticket**
https://builder-io.atlassian.net/browse/ENG-8621

**Loom**

**Tasks**
- [ ] Add Loom for testing
- [x]  Add dependent Content API and Web-App PRs to this description
- [x] Replace `data` with `body` in web-app and API (Reference to code-review comment: [here](https://github.com/BuilderIO/builder/pull/3945#discussion_r1981273680))


Diff:
diff --git a/packages/plugin-tools/README.md b/packages/plugin-tools/README.md
index be749d9a2bc..b2380ced8d7 100644
--- a/packages/plugin-tools/README.md
+++ b/packages/plugin-tools/README.md
@@ -96,7 +96,12 @@ registerCommercePlugin(
           return {
             '@type': '@builder.io/core:Request',
             request: {
-              url: baseUrl(`products/${id}`),
+                url: 'https://fakestoreapi.com/graphql',
+                method: 'POST',
+                data: {
+                  query: '{ query($id: String!) { products(id: $id) { id name } }',
+                  variables: { id: '1' },
+                },
             },
             options: {
               product: id,
@@ -110,7 +115,7 @@ registerCommercePlugin(
 ```
 
 ### Data Plugin
-
+#### With `toURL()` returning a string
 ```ts
 import { registerDataPlugin } from '@builder.io/plugin-tools';
 
@@ -193,6 +198,108 @@ registerDataPlugin(
 );
 ```
 
+#### With `toUrl()` returning a `BuilderRequest`
+
+```ts
+import {
+  APIOperations,
+  registerDataPlugin,
+  ResourceEntryType,
+} from "@builder.io/plugin-tools";
+
+registerDataPlugin(
+  {
+    name: 'Foobar',
+    id: '@builder.io/plugin-foobar',
+    settings: [
+      {
+        name: 'foo',
+        friendlyName: 'Lorem Ipsum',
+        type: 'string',
+        required: true,
+        helperText: 'Lorem Ipsum',
+      },
+    ],
+    ctaText: `Connect FooBar`,
+  },
+  async (settings): Promise<APIOperations> => {
+    const spaceId = settings.get("spaceId")?.trim();
+    return {
+      async getResourceTypes() {
+        return [
+          {
+            name: 'Resource 1',
+            id: `resource-1`,
+            canPickEntries: true,
+            // inputs are the query parameter definitions for your public API
+            inputs: () => {
+              return [
+                {
+                  name: 'include',
+                  friendlyName: 'Retrieve linked assets level',
+                  advanced: true,
+                  defaultValue: 2,
+                  // contentful api restricts include to be between 0 and 10
+                  min: 0,
+                  max: 10,
+                  type: 'number',
+                },
+                {
+                  name: 'limit',
+                  defaultValue: 10,
+                  // contentful api restricts limit to be between 0 and 100
+                  min: 0,
+                  max: 100,
+                  type: 'number',
+                },
+
+                {
+                  name: 'order',
+                  friendlyName: 'Order By',
+                  type: 'string',
+                  enum: ['a', 'b'],
+                },
+              ];
+            },
+            toUrl: (options: any) => {
+              const params = qs.stringify(options, {
+                allowDots: true,
+                skipNulls: true,
+              });
+              
+              const builderRequest: BuilderRequest = {
+                "@type": "@builder.io/core:Request",
+                request: {
+                  url: "https://fakestoreapi.com/graphql",
+                  method: "POST",
+                  query: params,
+                  data: {
+                    query: "{ query($id: String!) { spaces(id: $id) { id name } }",
+                    variables: { id: options.spaceId },
+                  },
+                },
+              };
+              return builderRequest;
+            },
+          },
+        ];
+      },
+      async getEntriesByResourceType(
+        typeId: string,
+        options
+      ): Promise<ResourceEntryType[]> {
+        const results = await fetch(
+          `https://public.example.com/spaces/${spaceId}?${qs.stringify(
+            options
+          )}`
+        );
+        return results.body as any as ResourceEntryType[];
+      },
+    };
+  }
+);
+```
+
 ## API
 
 ### `registerCommercePlugin(config, settingsCallback)`
diff --git a/packages/plugin-tools/src/data.ts b/packages/plugin-tools/src/data.ts
index e328e681270..aff4f3df2e5 100644
--- a/packages/plugin-tools/src/data.ts
+++ b/packages/plugin-tools/src/data.ts
@@ -2,6 +2,7 @@ import { Builder } from '@builder.io/sdk';
 import { Input } from '@builder.io/sdk';
 import appState from '@builder.io/app-context';
 import { ReactNode } from 'react';
+import { BuilderRequest } from './interfaces/builder-request';
 
 interface OnSaveActions {
   updateSettings(partal: Record<string, any>): Promise<void>;
@@ -24,7 +25,7 @@ export type ResourceType = {
   icon?: string;
   description?: string;
   inputs?: () => Partial<Input>[];
-  toUrl: (options: Record<string, any>) => string | Promise<string>;
+  toUrl: (options: Record<string, any>) => string | BuilderRequest | Promise<string> | Promise<BuilderRequest>;
   canPickEntries?: boolean;
 };
 
diff --git a/packages/plugin-tools/src/interfaces/builder-request.ts b/packages/plugin-tools/src/interfaces/builder-request.ts
index 6e695082037..53be968fd18 100644
--- a/packages/plugin-tools/src/interfaces/builder-request.ts
+++ b/packages/plugin-tools/src/interfaces/builder-request.ts
@@ -4,7 +4,8 @@ export interface BuilderRequest {
     url: string;
     query?: { [key: string]: string };
     headers?: { [key: string]: string };
-    method?: string;
+    method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
+    body?: any;
   };
   options?: { [key: string]: any };
   bindings?: { [key: string]: string };


Reviews:
user: login: shyam-builder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:47:59Z
user: login: clyde-builderio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:54:47Z
user: login: clyde-builderio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:02:09Z
user: login: clyde-builderio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:04:51Z
user: login: clyde-builderio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:08:08Z
user: login: clyde-builderio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:21:45Z
user: login: shyam-builder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:05:34Z
user: login: clyde-builderio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:11:20Z
user: login: shyam-builder
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T08:49:03Z

Review comments:
diff_hunk: @@ -4,7 +4,8 @@ export interface BuilderRequest {
     url: string;
     query?: { [key: string]: string };
     headers?: { [key: string]: string };
-    method?: string;
+    method?: 'GET' | 'POST';
+    data?: any;
path: packages/plugin-tools/src/interfaces/builder-request.ts
user: login: shyam-builder
body: should this be `data` or `body`?
created_at: 2025-03-05T11:44:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -124,69 +133,92 @@ registerDataPlugin(
         friendlyName: 'Lorem Ipsum',
         type: 'string',
         required: true,
-        helperText:
-          'Lorem Ipsum',
-      }
+        helperText: 'Lorem Ipsum',
+      },
     ],
     ctaText: `Connect FooBar`,
   },
-  async settings => {
-    const spaceId = settings.get('spaceId')?.trim();
+  async (settings): Promise<APIOperations> => {
+    const spaceId = settings.get("spaceId")?.trim();
     return {
       async getResourceTypes() {
         return [
           {
-          name: 'Resource 1',
-          id: `resource-1`,
-          canPickEntries: true,
-          // inputs are the query parameter definitions for your public API
-          inputs: () => {
-            return [
-              {
-                name: 'include',
-                friendlyName: 'Retrieve linked assets level',
-                advanced: true,
-                defaultValue: 2,
-                // contentful api restricts include to be between 0 and 10
-                min: 0,
-                max: 10,
-                type: 'number',
-              },
-              {
-                name: 'limit',
-                defaultValue: 10,
-                // contentful api restricts limit to be between 0 and 100
-                min: 0,
-                max: 100,
-                type: 'number',
-              },
-
-              {
-                name: 'order',
-                friendlyName: 'Order By'
-                type: 'string',
-                enum: ['a', 'b']
-              },
-            ];
-          },
-          toUrl: (options: any) => {
-            // by entry
-            // https://cdn.contentful.com/spaces/{space_id}/environments/{environment_id}/entries/{entry_id}?access_token={access_token}
-            if (options.entry) {
-              return `https://public.example.com/spacs/${spaceId}/resource/${options.entry}`
-            }
-            // by query
-            const params = qs.stringify(options,
-              { allowDots: true, skipNulls: true }
-            );
-            return  `https://public.example.com/spaces/${spaceId}/search?${params}`
-            );
+            name: 'Resource 1',
+            id: `resource-1`,
+            canPickEntries: true,
+            // inputs are the query parameter definitions for your public API
+            inputs: () => {
+              return [
+                {
+                  name: 'include',
+                  friendlyName: 'Retrieve linked assets level',
+                  advanced: true,
+                  defaultValue: 2,
+                  // contentful api restricts include to be between 0 and 10
+                  min: 0,
+                  max: 10,
+                  type: 'number',
+                },
+                {
+                  name: 'limit',
+                  defaultValue: 10,
+                  // contentful api restricts limit to be between 0 and 100
+                  min: 0,
+                  max: 100,
+                  type: 'number',
+                },
+
+                {
+                  name: 'order',
+                  friendlyName: 'Order By',
+                  type: 'string',
+                  enum: ['a', 'b'],
+                },
+              ];
+            },
+            toUrl: (options: any) => {
+              // by graphql
+              if (options.graphQLRequestBody) {
path: packages/plugin-tools/README.md
user: login: shyam-builder
body: what is this new option?
created_at: 2025-03-05T11:45:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,7 +25,7 @@ export type ResourceType = {
   icon?: string;
   description?: string;
   inputs?: () => Partial<Input>[];
-  toUrl: (options: Record<string, any>) => string | Promise<string>;
+  toUrl: (options: Record<string, any>) => string | BuilderRequest | Promise<string> | Promise<BuilderRequest>;
path: packages/plugin-tools/src/data.ts
user: login: shyam-builder
body: Changing this might not be backward compatible? What is the impact and where all is this used?
created_at: 2025-03-05T11:47:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,7 +25,7 @@ export type ResourceType = {
   icon?: string;
   description?: string;
   inputs?: () => Partial<Input>[];
-  toUrl: (options: Record<string, any>) => string | Promise<string>;
+  toUrl: (options: Record<string, any>) => string | BuilderRequest | Promise<string> | Promise<BuilderRequest>;
path: packages/plugin-tools/src/data.ts
user: login: clyde-builderio
body: `toUrl()` is not used anywhere else in the `plugin-tools` package. If [@builder.io/plugin-tools](https://www.npmjs.com/package/@builder.io/plugin-tools) is added as a Dependency in one's data-plugin, then this would matter. Here's an example
https://github.com/BuilderIO/builder/blob/4e6acb49a56d3b7be762b1e8bcaa1024d5b815f6/plugins/example-data-plugin/src/plugin.ts#L37
created_at: 2025-03-05T11:54:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,7 +4,8 @@ export interface BuilderRequest {
     url: string;
     query?: { [key: string]: string };
     headers?: { [key: string]: string };
-    method?: string;
+    method?: 'GET' | 'POST';
+    data?: any;
path: packages/plugin-tools/src/interfaces/builder-request.ts
user: login: clyde-builderio
body: Will make it `body`. And make similar changes to web-app and api
created_at: 2025-03-05T12:02:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,7 +25,7 @@ export type ResourceType = {
   icon?: string;
   description?: string;
   inputs?: () => Partial<Input>[];
-  toUrl: (options: Record<string, any>) => string | Promise<string>;
+  toUrl: (options: Record<string, any>) => string | BuilderRequest | Promise<string> | Promise<BuilderRequest>;
path: packages/plugin-tools/src/data.ts
user: login: clyde-builderio
body: toUrl() is used in https://github.com/BuilderIO/builder/blob/4e6acb49a56d3b7be762b1e8bcaa1024d5b815f6/plugins/contentful/src/plugin.ts#L159
created_at: 2025-03-05T12:04:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -124,69 +133,92 @@ registerDataPlugin(
         friendlyName: 'Lorem Ipsum',
         type: 'string',
         required: true,
-        helperText:
-          'Lorem Ipsum',
-      }
+        helperText: 'Lorem Ipsum',
+      },
     ],
     ctaText: `Connect FooBar`,
   },
-  async settings => {
-    const spaceId = settings.get('spaceId')?.trim();
+  async (settings): Promise<APIOperations> => {
+    const spaceId = settings.get("spaceId")?.trim();
     return {
       async getResourceTypes() {
         return [
           {
-          name: 'Resource 1',
-          id: `resource-1`,
-          canPickEntries: true,
-          // inputs are the query parameter definitions for your public API
-          inputs: () => {
-            return [
-              {
-                name: 'include',
-                friendlyName: 'Retrieve linked assets level',
-                advanced: true,
-                defaultValue: 2,
-                // contentful api restricts include to be between 0 and 10
-                min: 0,
-                max: 10,
-                type: 'number',
-              },
-              {
-                name: 'limit',
-                defaultValue: 10,
-                // contentful api restricts limit to be between 0 and 100
-                min: 0,
-                max: 100,
-                type: 'number',
-              },
-
-              {
-                name: 'order',
-                friendlyName: 'Order By'
-                type: 'string',
-                enum: ['a', 'b']
-              },
-            ];
-          },
-          toUrl: (options: any) => {
-            // by entry
-            // https://cdn.contentful.com/spaces/{space_id}/environments/{environment_id}/entries/{entry_id}?access_token={access_token}
-            if (options.entry) {
-              return `https://public.example.com/spacs/${spaceId}/resource/${options.entry}`
-            }
-            // by query
-            const params = qs.stringify(options,
-              { allowDots: true, skipNulls: true }
-            );
-            return  `https://public.example.com/spaces/${spaceId}/search?${params}`
-            );
+            name: 'Resource 1',
+            id: `resource-1`,
+            canPickEntries: true,
+            // inputs are the query parameter definitions for your public API
+            inputs: () => {
+              return [
+                {
+                  name: 'include',
+                  friendlyName: 'Retrieve linked assets level',
+                  advanced: true,
+                  defaultValue: 2,
+                  // contentful api restricts include to be between 0 and 10
+                  min: 0,
+                  max: 10,
+                  type: 'number',
+                },
+                {
+                  name: 'limit',
+                  defaultValue: 10,
+                  // contentful api restricts limit to be between 0 and 100
+                  min: 0,
+                  max: 100,
+                  type: 'number',
+                },
+
+                {
+                  name: 'order',
+                  friendlyName: 'Order By',
+                  type: 'string',
+                  enum: ['a', 'b'],
+                },
+              ];
+            },
+            toUrl: (options: any) => {
+              // by graphql
+              if (options.graphQLRequestBody) {
path: packages/plugin-tools/README.md
user: login: clyde-builderio
body: Just wanted a way for the README to show an example of a POST request so added it to the same function. Alternatively can add another title 
### Data Plugin (with `toUrl` returning a `BuilderRequest`)
created_at: 2025-03-05T12:08:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,7 +25,7 @@ export type ResourceType = {
   icon?: string;
   description?: string;
   inputs?: () => Partial<Input>[];
-  toUrl: (options: Record<string, any>) => string | Promise<string>;
+  toUrl: (options: Record<string, any>) => string | BuilderRequest | Promise<string> | Promise<BuilderRequest>;
path: packages/plugin-tools/src/data.ts
user: login: clyde-builderio
body: Its the web-app that will ultimately invoke the `.toUrl()` method. Here's a list of the occurrences https://www.loom.com/share/0825660bc27f4a5e87cb6a4a306fa754?t=190. As long as the web-app can receive a `BuilderRequest` from `toUrl()` it should work okay.
created_at: 2025-03-05T12:21:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,7 +4,8 @@ export interface BuilderRequest {
     url: string;
     query?: { [key: string]: string };
     headers?: { [key: string]: string };
-    method?: string;
+    method?: 'GET' | 'POST';
path: packages/plugin-tools/src/interfaces/builder-request.ts
user: login: shyam-builder
body: why only GET or POST? Why not PUT / PATCH?
created_at: 2025-03-06T11:05:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,7 +4,8 @@ export interface BuilderRequest {
     url: string;
     query?: { [key: string]: string };
     headers?: { [key: string]: string };
-    method?: string;
+    method?: 'GET' | 'POST';
path: packages/plugin-tools/src/interfaces/builder-request.ts
user: login: clyde-builderio
body: `BuilderRequest` will be used by Data Plugin's who's main purpose is to fetch some data from an external source.
Even the Content API and Web-App use BuilderRequest to fetch data.

GET and POST (for GraplQL requests) would be sufficient for this purpose.
created_at: 2025-03-06T11:11:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-04T11:09:04Z
body: ###  ⚠️  No Changeset found

Latest commit: 9fbd8c15a0508dd9b210b3ff52c560ce15a1c933

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/clyde-builderio/builder/new/ENG-8621?filename=.changeset/khaki-cooks-give.md&value=---%0A%22%40builder.io%2Fpackages%22%3A%20patch%0A---%0A%0Afeat%5BpluginTools%5D%5BbuilderRequest%5D%3A%20ENG-8621%20ensure%20BuilderRequest%20can%20define%20POST%20requests%20as%20well%20with%20request-body%0A)


author_association: NONE
user: login: nx-cloud[bot]
created_at: 2025-03-04T11:12:24Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/67caa12eda022951556dd4c3?utm_source=pull-request&utm_medium=comment) for commit 9fbd8c15a0508dd9b210b3ff52c560ce15a1c933.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx test @e2e/qwik-city` | ✅ Succeeded | 7m 22s | [View ↗](https://cloud.nx.app/runs/cL3GnE1q14?utm_source=pull-request&utm_medium=comment) |
| `nx test @e2e/nextjs-sdk-next-app` | ✅ Succeeded | 7m 9s | [View ↗](https://cloud.nx.app/runs/NQolwynKEz?utm_source=pull-request&utm_medium=comment) |
| `nx test @e2e/nuxt` | ✅ Succeeded | 6m 45s | [View ↗](https://cloud.nx.app/runs/z6HslDwkYX?utm_source=pull-request&utm_medium=comment) |
| `nx test @e2e/angular-16-ssr` | ✅ Succeeded | 6m 21s | [View ↗](https://cloud.nx.app/runs/UCL1y0Mznv?utm_source=pull-request&utm_medium=comment) |
| `nx test @e2e/angular-16` | ✅ Succeeded | 5m 47s | [View ↗](https://cloud.nx.app/runs/rmNncPOfG9?utm_source=pull-request&utm_medium=comment) |
| `nx test @e2e/react-sdk-next-pages` | ✅ Succeeded | 5m 44s | [View ↗](https://cloud.nx.app/runs/Oy78A9F2x4?utm_source=pull-request&utm_medium=comment) |
| `nx test @e2e/angular-19-ssr` | ✅ Succeeded | 5m 21s | [View ↗](https://cloud.nx.app/runs/dUQGAf1SXP?utm_source=pull-request&utm_medium=comment) |
| `nx test @e2e/hydrogen` | ✅ Succeeded | 5m 9s | [View ↗](https://cloud.nx.app/runs/mNyLcd7xzn?utm_source=pull-request&utm_medium=comment) |
| `Additional runs (36)` | ✅ Succeeded | ... | [View ↗](https://cloud.nx.app/cipes/67caa12eda022951556dd4c3?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-03-07 07:40:29` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE