From a9f9c2b0941ce45267fc02c4ae076334742d9a88 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Fri, 28 Jan 2022 18:25:31 +0800 Subject: [PATCH 1/9] feat: add create graphql query Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 232 +++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 guides/create-graphql-query.md diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md new file mode 100644 index 0000000..8427be3 --- /dev/null +++ b/guides/create-graphql-query.md @@ -0,0 +1,232 @@ +# How To: Create a new GraphQL query + +## Step 1: Identify which plugin owns the query + +The complete Reaction Commerce GraphQL API is created by stitching together domain-specific APIs from all of the API plugins. So when adding a new query, the first step is to decide which plugin should own it. This is usually obvious, but not always. You should think about whether any other plugins or services will need to call your query. If the query is fundamental to the system, then it may need to go in the "core" plugin, if no better alternative exists. + +## Step 2: Understand the difference between a plugin query function and a GraphQL query resolver + +See [Resolver Mutations and Queries vs. Plugin Mutations and Queries](../guides/developers-guide/core/developing-graphql.md#resolver-mutations-and-queries-vs-plugin-mutations-and-queries) + +## Step 3: Name the query + +When choosing a name for the query, there are a few rules to follow: +- In keeping with general GraphQL best practices, do not use verbs such as "list", "get", or "find" at the beginning of your query name. For example, use "cart" instead of "getCart" and "carts" instead of "listCarts". +- Prefix with adjectives as necessary to fully describe what the query returns. For example, "anonymousCart" and "accountCart" queries. +- If there are similar queries that take slightly different parameters, add a suffix to clarify. In most cases, we begin the suffix with "By". For example, "accountCartById" and "accountCartByAccountId". + +## Step 4: Define the query in the schema + +1. If it doesn't already exist, create `schemas` folder in the plugin, and add an `index.js` file there. +1. If it doesn't already exist, create `schema.graphql` in `schemas` in the plugin. +1. Import the GraphQL file into `index.js` and default export it in an array: + + ```js + import importAsString from "@reactioncommerce/api-utils/importAsString.js"; + + const schema = importAsString("./schema.graphql"); + + export default [schema]; + ``` + + > NOTE: For large plugins, you can split to multiple `.graphql` files and export a multi-item array. + +1. In the `.graphql` file, add your query within `extend type Query { }`. Add an `extend type Query` section near the top if the file doesn't have it yet. +1. If your query returns multiple documents, it should return a Relay-compatible Connection and accept standard connection arguments. This is true of any `fields` on any types you create as well. + + Example: `groups(after: ConnectionCursor, before: ConnectionCursor, first: ConnectionLimitInt, last: ConnectionLimitInt, sortOrder: SortOrder = asc, sortBy: GroupSortByField = createdAt): GroupConnection` + +1. Document your query, the new types, and all fields in those types using string literals. See [Documenting a GraphQL Schema](../guides/developers-guide/core/developing-graphql.md#documenting-a-graphql-schema). +1. If not already done, register your schemas in the plugin's `index.js` file: + + ```js + import schemas from "./schemas"; + + export default async function register(app) { + await app.registerPlugin({ + graphQL: { + schemas + }, + // other props + }); + } + ``` + +## Step 5: Create the plugin query file + +1. If it doesn't already exist, create `queries` folder in the plugin, and add an `index.js` file there. +2. In `queries`, create a file for the query, e.g. `widgets.js` for the `widgets` query. The file should look something like this: + +```js +import Logger from "@reactioncommerce/logger"; + +/** + * @method widgets + * @summary TODO + * @param {Object} context - an object containing the per-request state + * @return {Promise} TODO + */ +export default async function widgets(context) { + Logger.info("widgets query is not yet implemented"); + return null; +} +``` + +## Step 6: Add the plugin query to the queries context + +In `queries/index.js` in the plugin, import your query and add it to the default export object. Example: + +```js +import widgets from "./widgets" + +export default { + widgets +}; +``` + +If this is the first query for the plugin, you'll also need to pass the full `queries` object to `registerPlugin` in the plugin's `index.js` file: + +```js +import queries from "./queries"; + +export default async function register(app) { + await app.registerPlugin({ + queries, + // other props + }); +} +``` + +Your plugin query function is now available in the GraphQL context as `context.queries.widgets`. + +> NOTE: The queries objects from all plugins are merged, so be sure that another plugin does not have a query with the same name. The last one registered with that name will win, and plugins are generally registered in alphabetical order by plugin name. Tip: You can use this to your advantage if you want to override the query function of a core plugin without modifying core code. + +## Step 7: Add a test file for your query + +If your query is in a file named `widgets.js`, your Jest tests should be in a file named `widgets.test.js` in the same folder. Initially you can copy and paste the following test: + +```js +import mockContext from "/imports/test-utils/helpers/mockContext"; +import widgets from "./widgets"; + +test("expect to return a Promise that resolves to null", async () => { + const result = await widgets(mockContext); + expect(result).toEqual(null); +}); +``` + +This of course should be updated with tests that are appropriate for whatever your query does. + +## Step 8: Create the GraphQL query resolver file + +1. If it doesn't already exist, create `resolvers` folder in the plugin, and add an `index.js` file there. +2. If it doesn't already exist, create `resolvers/Query` folder in the plugin, and add an `index.js` file there. "Query" must be capitalized. +3. In `resolvers/Query`, create a file for the query resolver, e.g. `widgets.js` for the `widgets` query. The file should look something like this initially: + +```js +/** + * @name "Query.widgets" + * @method + * @memberof MyPlugin/GraphQL + * @summary resolver for the widgets GraphQL query + * @param {Object} parentResult - unused + * @param {Object} args - an object of all arguments that were sent by the client + * @param {Object} context - an object containing the per-request state + * @return {Promise} TODO + */ +export default async function widgets(parentResult, args, context) { + // TODO: decode incoming IDs here + return context.queries.widgets(context); +} +``` + +Make adjustments to the resolver function so that it reads and passes along the parameters correctly. The general pattern is: +- Decode any opaque IDs that are in the arguments +- Call `context.queries.` (your new plugin query) with the necessary arguments, and `await` a response. +- Return a single document or an array of them using either `getPaginatedResponse` or `xformArrayToConnection` util function. + +## Step 9: Register the resolver + +In `resolvers/Query/index.js` in the plugin, import your query resolver and add it to the default export object. Example: + +```js +import widgets from "./widgets" + +export default { + widgets +}; +``` + +If this is the first query for the plugin, you'll also need to import the `Query` object into the `resolvers` object. In `resolvers/index.js` in the plugin, import `Query` and add it to the default export object. + +```js +import Query from "./Query" + +export default { + Query +}; +``` + +If you are returning multiple documents (see step #3) you'll need to add an additional export here, `getConnectionTypeResolvers`, in order to be able to query `edges->node`: + +```js +import { getConnectionTypeResolvers } from "@reactioncommerce/reaction-graphql-utils"; +import Query from "./Query" + +export default { + Query, + ...getConnectionTypeResolvers("QueryName") +}; +``` + +Then pass the full `resolvers` object to `registerPlugin` in the plugin's `index.js` file: + +```js +import resolvers from "./resolvers"; +import schemas from "./schemas"; + +export default async function register(app) { + await app.registerPlugin({ + graphQL: { + resolvers, + schemas + }, + // other props + }); +} +``` + +Calling your query with GraphQL should now work. + +## Step 10: Add a test file for your query resolver + +If your query resolver is in a file named `widgets.js`, your Jest tests should be in a file named `widgets.test.js` in the same folder. Initially you can copy and paste the following test: + +```js +import widgets from "./widgets"; + +test("calls queries.widgets and returns the result", async () => { + const mockResponse = "MOCK_RESPONSE"; + const mockQuery = jest.fn().mockName("queries.widgets").mockReturnValueOnce(Promise.resolve(mockResponse)); + + const result = await widgets(null, { /* TODO */ }, { + queries: { widgets: mockQuery }, + userId: "123" + }); + + expect(result).toEqual(mockResponse); + expect(mockQuery).toHaveBeenCalled(); +}); +``` + +This of course should be updated with tests that are appropriate for whatever your query resolver does. For example, verify that all ID and schema transformations happen. + +## Step 11: Finish implementing your query + +Adjust the query function and the query resolver function until they work as expected, with tests that prove it. This will likely involve adding additional arguments, ID transformations, permission checks, and MongoDB queries. + +Refer to [Developing the GraphQL API](./graphql-developing) for answers to any questions you might have while implementing your mutation. + +## Step 12: Update the JSDoc comments + +Write/update jsdoc comments for the plugin query function, the query resolver, and any util functions. The resolver function must have `@memberof /GraphQL` in the jsdoc, and the `@name` must be the full GraphQL schema path in quotation marks, e.g., "Query.widgets". (The quotation marks are necessary for the output API documentation to be correct due to the periods.) From f6b437f44ee5ef8940195209da8587a39802347e Mon Sep 17 00:00:00 2001 From: Alvaro Bueno Date: Fri, 4 Feb 2022 13:46:44 -0500 Subject: [PATCH 2/9] fix: giving proper format to guide on GraphQL queries Signed-off-by: Alvaro Bueno Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 60 ++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 8427be3..3980255 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -1,25 +1,27 @@ -# How To: Create a new GraphQL query +## At a glance -## Step 1: Identify which plugin owns the query +In this guide, we’ll guide you through the steps you need to create a successful query in GraphQL + +## Identify which plugin owns the query The complete Reaction Commerce GraphQL API is created by stitching together domain-specific APIs from all of the API plugins. So when adding a new query, the first step is to decide which plugin should own it. This is usually obvious, but not always. You should think about whether any other plugins or services will need to call your query. If the query is fundamental to the system, then it may need to go in the "core" plugin, if no better alternative exists. -## Step 2: Understand the difference between a plugin query function and a GraphQL query resolver +## Understand the difference between a plugin query function and a GraphQL query resolver See [Resolver Mutations and Queries vs. Plugin Mutations and Queries](../guides/developers-guide/core/developing-graphql.md#resolver-mutations-and-queries-vs-plugin-mutations-and-queries) -## Step 3: Name the query +## Name the query When choosing a name for the query, there are a few rules to follow: - In keeping with general GraphQL best practices, do not use verbs such as "list", "get", or "find" at the beginning of your query name. For example, use "cart" instead of "getCart" and "carts" instead of "listCarts". - Prefix with adjectives as necessary to fully describe what the query returns. For example, "anonymousCart" and "accountCart" queries. - If there are similar queries that take slightly different parameters, add a suffix to clarify. In most cases, we begin the suffix with "By". For example, "accountCartById" and "accountCartByAccountId". -## Step 4: Define the query in the schema +## Define the query in the schema -1. If it doesn't already exist, create `schemas` folder in the plugin, and add an `index.js` file there. -1. If it doesn't already exist, create `schema.graphql` in `schemas` in the plugin. -1. Import the GraphQL file into `index.js` and default export it in an array: +- If it doesn't already exist, create `schemas` folder in the plugin, and add an `index.js` file there. +- If it doesn't already exist, create `schema.graphql` in `schemas` in the plugin. +- Import the GraphQL file into `index.js` and default export it in an array: ```js import importAsString from "@reactioncommerce/api-utils/importAsString.js"; @@ -31,13 +33,13 @@ When choosing a name for the query, there are a few rules to follow: > NOTE: For large plugins, you can split to multiple `.graphql` files and export a multi-item array. -1. In the `.graphql` file, add your query within `extend type Query { }`. Add an `extend type Query` section near the top if the file doesn't have it yet. -1. If your query returns multiple documents, it should return a Relay-compatible Connection and accept standard connection arguments. This is true of any `fields` on any types you create as well. +- In the `.graphql` file, add your query within `extend type Query { }`. Add an `extend type Query` section near the top if the file doesn't have it yet. +- If your query returns multiple documents, it should return a Relay-compatible Connection and accept standard connection arguments. This is true of any `fields` on any types you create as well. Example: `groups(after: ConnectionCursor, before: ConnectionCursor, first: ConnectionLimitInt, last: ConnectionLimitInt, sortOrder: SortOrder = asc, sortBy: GroupSortByField = createdAt): GroupConnection` -1. Document your query, the new types, and all fields in those types using string literals. See [Documenting a GraphQL Schema](../guides/developers-guide/core/developing-graphql.md#documenting-a-graphql-schema). -1. If not already done, register your schemas in the plugin's `index.js` file: +- Document your query, the new types, and all fields in those types using string literals. See [Documenting a GraphQL Schema](../guides/developers-guide/core/developing-graphql.md#documenting-a-graphql-schema). +- If not already done, register your schemas in the plugin's `index.js` file: ```js import schemas from "./schemas"; @@ -52,10 +54,10 @@ When choosing a name for the query, there are a few rules to follow: } ``` -## Step 5: Create the plugin query file +## Create the plugin query file -1. If it doesn't already exist, create `queries` folder in the plugin, and add an `index.js` file there. -2. In `queries`, create a file for the query, e.g. `widgets.js` for the `widgets` query. The file should look something like this: +- If it doesn't already exist, create `queries` folder in the plugin, and add an `index.js` file there. +- In `queries`, create a file for the query, e.g. `widgets.js` for the `widgets` query. The file should look something like this: ```js import Logger from "@reactioncommerce/logger"; @@ -72,7 +74,7 @@ export default async function widgets(context) { } ``` -## Step 6: Add the plugin query to the queries context +## Add the plugin query to the queries context In `queries/index.js` in the plugin, import your query and add it to the default export object. Example: @@ -101,7 +103,7 @@ Your plugin query function is now available in the GraphQL context as `context.q > NOTE: The queries objects from all plugins are merged, so be sure that another plugin does not have a query with the same name. The last one registered with that name will win, and plugins are generally registered in alphabetical order by plugin name. Tip: You can use this to your advantage if you want to override the query function of a core plugin without modifying core code. -## Step 7: Add a test file for your query +## Add a test file for your query If your query is in a file named `widgets.js`, your Jest tests should be in a file named `widgets.test.js` in the same folder. Initially you can copy and paste the following test: @@ -117,11 +119,11 @@ test("expect to return a Promise that resolves to null", async () => { This of course should be updated with tests that are appropriate for whatever your query does. -## Step 8: Create the GraphQL query resolver file +## Create the GraphQL query resolver file -1. If it doesn't already exist, create `resolvers` folder in the plugin, and add an `index.js` file there. -2. If it doesn't already exist, create `resolvers/Query` folder in the plugin, and add an `index.js` file there. "Query" must be capitalized. -3. In `resolvers/Query`, create a file for the query resolver, e.g. `widgets.js` for the `widgets` query. The file should look something like this initially: +- If it doesn't already exist, create `resolvers` folder in the plugin, and add an `index.js` file there. +- If it doesn't already exist, create `resolvers/Query` folder in the plugin, and add an `index.js` file there. "Query" must be capitalized. +- In `resolvers/Query`, create a file for the query resolver, e.g. `widgets.js` for the `widgets` query. The file should look something like this initially: ```js /** @@ -145,7 +147,7 @@ Make adjustments to the resolver function so that it reads and passes along the - Call `context.queries.` (your new plugin query) with the necessary arguments, and `await` a response. - Return a single document or an array of them using either `getPaginatedResponse` or `xformArrayToConnection` util function. -## Step 9: Register the resolver +## Register the resolver In `resolvers/Query/index.js` in the plugin, import your query resolver and add it to the default export object. Example: @@ -167,7 +169,7 @@ export default { }; ``` -If you are returning multiple documents (see step #3) you'll need to add an additional export here, `getConnectionTypeResolvers`, in order to be able to query `edges->node`: +If you are returning multiple documents, you'll need to add an additional export here, `getConnectionTypeResolvers`, in order to be able to query `edges->node`: ```js import { getConnectionTypeResolvers } from "@reactioncommerce/reaction-graphql-utils"; @@ -198,7 +200,7 @@ export default async function register(app) { Calling your query with GraphQL should now work. -## Step 10: Add a test file for your query resolver +## Add a test file for your query resolver If your query resolver is in a file named `widgets.js`, your Jest tests should be in a file named `widgets.test.js` in the same folder. Initially you can copy and paste the following test: @@ -221,12 +223,14 @@ test("calls queries.widgets and returns the result", async () => { This of course should be updated with tests that are appropriate for whatever your query resolver does. For example, verify that all ID and schema transformations happen. -## Step 11: Finish implementing your query +## Finish implementing your query Adjust the query function and the query resolver function until they work as expected, with tests that prove it. This will likely involve adding additional arguments, ID transformations, permission checks, and MongoDB queries. -Refer to [Developing the GraphQL API](./graphql-developing) for answers to any questions you might have while implementing your mutation. - -## Step 12: Update the JSDoc comments +## Update the JSDoc comments Write/update jsdoc comments for the plugin query function, the query resolver, and any util functions. The resolver function must have `@memberof /GraphQL` in the jsdoc, and the `@name` must be the full GraphQL schema path in quotation marks, e.g., "Query.widgets". (The quotation marks are necessary for the output API documentation to be correct due to the periods.) + +## More resources + +[Build an API plugin guide](https://mailchimp.com/developer/open-commerce/guides/build-api-plugin/) \ No newline at end of file From ea53e2afe5851d9ca95d41f6ee57c9ecb958bbcc Mon Sep 17 00:00:00 2001 From: Alvaro Bueno Date: Fri, 4 Feb 2022 14:09:50 -0500 Subject: [PATCH 3/9] fix: giving proper format to guide on GraphQL queries Signed-off-by: Alvaro Bueno Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 3980255..8e34b6a 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -6,9 +6,9 @@ In this guide, we’ll guide you through the steps you need to create a successf The complete Reaction Commerce GraphQL API is created by stitching together domain-specific APIs from all of the API plugins. So when adding a new query, the first step is to decide which plugin should own it. This is usually obvious, but not always. You should think about whether any other plugins or services will need to call your query. If the query is fundamental to the system, then it may need to go in the "core" plugin, if no better alternative exists. -## Understand the difference between a plugin query function and a GraphQL query resolver +## Understand the difference between a plugin mutation function and a GraphQL mutation resolver -See [Resolver Mutations and Queries vs. Plugin Mutations and Queries](../guides/developers-guide/core/developing-graphql.md#resolver-mutations-and-queries-vs-plugin-mutations-and-queries) +See [Resolver mutations and queries vs. plugin mutations and queries in the GraphQL concepts docs](.docs/graphql-concepts.md) ## Name the query From 12700004b70e1ca4b8aa6d0993de89d758807b56 Mon Sep 17 00:00:00 2001 From: Alvaro Bueno Date: Mon, 7 Feb 2022 18:27:10 -0500 Subject: [PATCH 4/9] Update guides/create-graphql-query.md Co-authored-by: Mohan Narayana <75854210+MohanNarayana@users.noreply.github.com> Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 8e34b6a..2dc3ba6 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -6,7 +6,7 @@ In this guide, we’ll guide you through the steps you need to create a successf The complete Reaction Commerce GraphQL API is created by stitching together domain-specific APIs from all of the API plugins. So when adding a new query, the first step is to decide which plugin should own it. This is usually obvious, but not always. You should think about whether any other plugins or services will need to call your query. If the query is fundamental to the system, then it may need to go in the "core" plugin, if no better alternative exists. -## Understand the difference between a plugin mutation function and a GraphQL mutation resolver +## Understand the difference between a plugin query function and a GraphQL query resolver See [Resolver mutations and queries vs. plugin mutations and queries in the GraphQL concepts docs](.docs/graphql-concepts.md) From 1d0e2b298359b01e99d61f76dc885942019900d8 Mon Sep 17 00:00:00 2001 From: Alvaro Bueno Date: Mon, 7 Feb 2022 18:28:57 -0500 Subject: [PATCH 5/9] fix: graphql guide updates Signed-off-by: Alvaro Bueno Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 2dc3ba6..0f1a258 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -8,7 +8,7 @@ The complete Reaction Commerce GraphQL API is created by stitching together doma ## Understand the difference between a plugin query function and a GraphQL query resolver -See [Resolver mutations and queries vs. plugin mutations and queries in the GraphQL concepts docs](.docs/graphql-concepts.md) +See [Resolver mutations and queries vs. plugin mutations and queries in the GraphQL concepts docs](../docs/graphql-concepts.md) ## Name the query From 4701f7798d45f19020678426d2dbd24ec215b75d Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Mon, 21 Feb 2022 10:28:56 +0800 Subject: [PATCH 6/9] feat: update to more fit the format of a guide Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 100 +++++++++++++++++---------------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 0f1a258..5987ab0 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -1,14 +1,16 @@ ## At a glance -In this guide, we’ll guide you through the steps you need to create a successful query in GraphQL +You're working on a new implementation for Leader of the Pack, an outdoor equipment retailer. Let's say that you don't ship Kayaks to every location because of the logistical challenges of shipping something that large. So you need to add +a query that will show you if a particular product can be shipped to a particular zip code. Once you have that data in the system you need to have a way to make it available to the client. This is usually done by creating a GraphQL query. In this guide, we’ll walk you through the steps you need to create a query in GraphQL and have it recognized by the system. -## Identify which plugin owns the query +## What you need -The complete Reaction Commerce GraphQL API is created by stitching together domain-specific APIs from all of the API plugins. So when adding a new query, the first step is to decide which plugin should own it. This is usually obvious, but not always. You should think about whether any other plugins or services will need to call your query. If the query is fundamental to the system, then it may need to go in the "core" plugin, if no better alternative exists. +You should already have an understanding of what fields/data you wish to share with the client and what GraphQL type each field is. Consult the [GraphQL Reference](https://graphql.org/learn/schema/) for more info on GraphQL types. -## Understand the difference between a plugin query function and a GraphQL query resolver +## Identify which plugin owns the query -See [Resolver mutations and queries vs. plugin mutations and queries in the GraphQL concepts docs](../docs/graphql-concepts.md) +The complete Open Commerce GraphQL API is created by stitching together domain-specific APIs from all the API plugins. So when adding a new query, the first step is to decide which plugin should own it. This is usually obvious, but not always. You should think about whether any other plugins or services will need to call your query. +Typically, if you are adding data your new query will exist in your custom plugin. ## Name the query @@ -23,53 +25,53 @@ When choosing a name for the query, there are a few rules to follow: - If it doesn't already exist, create `schema.graphql` in `schemas` in the plugin. - Import the GraphQL file into `index.js` and default export it in an array: - ```js - import importAsString from "@reactioncommerce/api-utils/importAsString.js"; +```js +import importAsString from "@reactioncommerce/api-utils/importAsString.js"; - const schema = importAsString("./schema.graphql"); +const schema = importAsString("./schema.graphql"); - export default [schema]; - ``` +export default [schema]; +``` - > NOTE: For large plugins, you can split to multiple `.graphql` files and export a multi-item array. +> NOTE: For large plugins, you can split to multiple `.graphql` files and export a multi-item array. - In the `.graphql` file, add your query within `extend type Query { }`. Add an `extend type Query` section near the top if the file doesn't have it yet. - If your query returns multiple documents, it should return a Relay-compatible Connection and accept standard connection arguments. This is true of any `fields` on any types you create as well. Example: `groups(after: ConnectionCursor, before: ConnectionCursor, first: ConnectionLimitInt, last: ConnectionLimitInt, sortOrder: SortOrder = asc, sortBy: GroupSortByField = createdAt): GroupConnection` -- Document your query, the new types, and all fields in those types using string literals. See [Documenting a GraphQL Schema](../guides/developers-guide/core/developing-graphql.md#documenting-a-graphql-schema). +- Document your query, the new types, and all fields in those types using string literals. . - If not already done, register your schemas in the plugin's `index.js` file: - ```js - import schemas from "./schemas"; +```js +import schemas from "./schemas"; - export default async function register(app) { - await app.registerPlugin({ - graphQL: { - schemas - }, - // other props - }); - } - ``` +export default async function register(app) { + await app.registerPlugin({ + graphQL: { + schemas + }, + // other props + }); +} +``` ## Create the plugin query file - If it doesn't already exist, create `queries` folder in the plugin, and add an `index.js` file there. -- In `queries`, create a file for the query, e.g. `widgets.js` for the `widgets` query. The file should look something like this: +- In `queries`, create a file for the query, e.g. `isAvailableToShip.js` for the `isAvailableToShip` query. The file should look something like this: ```js import Logger from "@reactioncommerce/logger"; /** - * @method widgets + * @method isAvailableToShip * @summary TODO * @param {Object} context - an object containing the per-request state * @return {Promise} TODO */ -export default async function widgets(context) { - Logger.info("widgets query is not yet implemented"); +export default async function isAvailableToShip(context) { + Logger.info("isAvailableToShip query is not yet implemented"); return null; } ``` @@ -79,10 +81,10 @@ export default async function widgets(context) { In `queries/index.js` in the plugin, import your query and add it to the default export object. Example: ```js -import widgets from "./widgets" +import isAvailableToShip from "./isAvailableToShip.js" export default { - widgets + isAvailableToShip }; ``` @@ -99,20 +101,20 @@ export default async function register(app) { } ``` -Your plugin query function is now available in the GraphQL context as `context.queries.widgets`. +Your plugin query function is now available in the GraphQL context as `context.queries.isAvailableToShip`. > NOTE: The queries objects from all plugins are merged, so be sure that another plugin does not have a query with the same name. The last one registered with that name will win, and plugins are generally registered in alphabetical order by plugin name. Tip: You can use this to your advantage if you want to override the query function of a core plugin without modifying core code. ## Add a test file for your query -If your query is in a file named `widgets.js`, your Jest tests should be in a file named `widgets.test.js` in the same folder. Initially you can copy and paste the following test: +If your query is in a file named `isAvailableToShip.js`, your Jest tests should be in a file named `isAvailableToShip.test.js` in the same folder. Initially you can copy and paste the following test: ```js import mockContext from "/imports/test-utils/helpers/mockContext"; -import widgets from "./widgets"; +import isAvailableToShip from "./isAvailableToShip.js"; test("expect to return a Promise that resolves to null", async () => { - const result = await widgets(mockContext); + const result = await isAvailableToShip(mockContext); expect(result).toEqual(null); }); ``` @@ -121,24 +123,26 @@ This of course should be updated with tests that are appropriate for whatever yo ## Create the GraphQL query resolver file +> Note: All of these elements are required for your query to work and if missing often will not throw an error. If your query is not showing or not returning any data up you probably need to recheck that all of these pieces are here and have the correct name and format. + - If it doesn't already exist, create `resolvers` folder in the plugin, and add an `index.js` file there. - If it doesn't already exist, create `resolvers/Query` folder in the plugin, and add an `index.js` file there. "Query" must be capitalized. -- In `resolvers/Query`, create a file for the query resolver, e.g. `widgets.js` for the `widgets` query. The file should look something like this initially: +- In `resolvers/Query`, create a file for the query resolver, e.g. `isAvailableToShip.js` for the `isAvailableToShip` query. The file should look something like this initially: ```js /** - * @name "Query.widgets" + * @name "Query.isAvailableToShip" * @method * @memberof MyPlugin/GraphQL - * @summary resolver for the widgets GraphQL query + * @summary resolver for the isAvailableToShip GraphQL query * @param {Object} parentResult - unused * @param {Object} args - an object of all arguments that were sent by the client * @param {Object} context - an object containing the per-request state * @return {Promise} TODO */ -export default async function widgets(parentResult, args, context) { +export default async function isAvailableToShip(parentResult, args, context) { // TODO: decode incoming IDs here - return context.queries.widgets(context); + return context.queries.isAvailableToShip(context); } ``` @@ -152,10 +156,10 @@ Make adjustments to the resolver function so that it reads and passes along the In `resolvers/Query/index.js` in the plugin, import your query resolver and add it to the default export object. Example: ```js -import widgets from "./widgets" +import isAvailableToShip from "./isAvailableToShip.js" export default { - widgets + isAvailableToShip }; ``` @@ -202,17 +206,17 @@ Calling your query with GraphQL should now work. ## Add a test file for your query resolver -If your query resolver is in a file named `widgets.js`, your Jest tests should be in a file named `widgets.test.js` in the same folder. Initially you can copy and paste the following test: +If your query resolver is in a file named `isAvailableToShip.js`, your Jest tests should be in a file named `isAvailableToShip.test.js` in the same folder. Initially you can copy and paste the following test: ```js -import widgets from "./widgets"; +import isAvailableToShip from "./isAvailableToShip.js"; -test("calls queries.widgets and returns the result", async () => { +test("calls queries.isAvailableToShip and returns the result", async () => { const mockResponse = "MOCK_RESPONSE"; - const mockQuery = jest.fn().mockName("queries.widgets").mockReturnValueOnce(Promise.resolve(mockResponse)); + const mockQuery = jest.fn().mockName("queries.isAvailableToShip").mockReturnValueOnce(Promise.resolve(mockResponse)); - const result = await widgets(null, { /* TODO */ }, { - queries: { widgets: mockQuery }, + const result = await isAvailableToShip(null, { /* TODO */ }, { + queries: { isAvailableToShip: mockQuery }, userId: "123" }); @@ -229,8 +233,8 @@ Adjust the query function and the query resolver function until they work as exp ## Update the JSDoc comments -Write/update jsdoc comments for the plugin query function, the query resolver, and any util functions. The resolver function must have `@memberof /GraphQL` in the jsdoc, and the `@name` must be the full GraphQL schema path in quotation marks, e.g., "Query.widgets". (The quotation marks are necessary for the output API documentation to be correct due to the periods.) +Write/update jsdoc comments for the plugin query function, the query resolver, and any util functions. The resolver function must have `@memberof /GraphQL` in the jsdoc, and the `@name` must be the full GraphQL schema path in quotation marks, e.g., "Query.isAvailableToShip". (The quotation marks are necessary for the output API documentation to be correct due to the periods.) ## More resources -[Build an API plugin guide](https://mailchimp.com/developer/open-commerce/guides/build-api-plugin/) \ No newline at end of file +[Build an API plugin guide](https://mailchimp.com/developer/open-commerce/guides/build-api-plugin/) From 7912e00703d8c014e2b93c9916276d0e4b316638 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Mon, 21 Feb 2022 10:35:45 +0800 Subject: [PATCH 7/9] fix: remove line break Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 5987ab0..3c239b1 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -1,7 +1,6 @@ ## At a glance -You're working on a new implementation for Leader of the Pack, an outdoor equipment retailer. Let's say that you don't ship Kayaks to every location because of the logistical challenges of shipping something that large. So you need to add -a query that will show you if a particular product can be shipped to a particular zip code. Once you have that data in the system you need to have a way to make it available to the client. This is usually done by creating a GraphQL query. In this guide, we’ll walk you through the steps you need to create a query in GraphQL and have it recognized by the system. +You're working on a new implementation for Leader of the Pack, an outdoor equipment retailer. Let's say that you don't ship Kayaks to every location because of the logistical challenges of shipping something that large. So you need to add a query that will show you if a particular product can be shipped to a particular zip code. Once you have that data in the system you need to have a way to make it available to the client. This is usually done by creating a GraphQL query. In this guide, we’ll walk you through the steps you need to create a query in GraphQL and have it recognized by the system. ## What you need From 9ae83a666626ed32b782189f83561c6de5186414 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Wed, 23 Feb 2022 20:25:21 +0800 Subject: [PATCH 8/9] fix: changes from c/r Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 3c239b1..00a9adf 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -1,6 +1,7 @@ ## At a glance -You're working on a new implementation for Leader of the Pack, an outdoor equipment retailer. Let's say that you don't ship Kayaks to every location because of the logistical challenges of shipping something that large. So you need to add a query that will show you if a particular product can be shipped to a particular zip code. Once you have that data in the system you need to have a way to make it available to the client. This is usually done by creating a GraphQL query. In this guide, we’ll walk you through the steps you need to create a query in GraphQL and have it recognized by the system. +You're working on a new implementation for Leader of the Pack, an outdoor equipment retailer that we just took +through [building an API plugin](https://mailchimp.com/developer/open-commerce/guides/build-api-plugin/). Let's say that you don't ship Kayaks to every location because of the logistical challenges of shipping something that large. So you need to add a query that will show you if a particular product can be shipped to a particular zip code. Once you have that data in the system you need to have a way to make it available to the client. This is usually done by creating a GraphQL query. In this guide, we’ll walk you through the steps you need to create a query in GraphQL and have it recognized by the system. ## What you need @@ -20,9 +21,8 @@ When choosing a name for the query, there are a few rules to follow: ## Define the query in the schema -- If it doesn't already exist, create `schemas` folder in the plugin, and add an `index.js` file there. -- If it doesn't already exist, create `schema.graphql` in `schemas` in the plugin. -- Import the GraphQL file into `index.js` and default export it in an array: +If it doesn't already exist, create schemas folder in the plugin and add an index.js file there. Then check to see if there is a schema.graphql file in your schemas directory within the plugin. If there isn't, create that file now. +Next import the GraphQL file into `index.js` and default export it in an array: ```js import importAsString from "@reactioncommerce/api-utils/importAsString.js"; @@ -34,13 +34,15 @@ export default [schema]; > NOTE: For large plugins, you can split to multiple `.graphql` files and export a multi-item array. -- In the `.graphql` file, add your query within `extend type Query { }`. Add an `extend type Query` section near the top if the file doesn't have it yet. -- If your query returns multiple documents, it should return a Relay-compatible Connection and accept standard connection arguments. This is true of any `fields` on any types you create as well. +In the `.graphql` file, add your query within `extend type Query { }`. Add an `extend type Query` section near the top if the file doesn't have it yet. +If your query returns multiple documents, it should return a Relay-compatible Connection and accept standard connection arguments. This is true of any `fields` on any types you create as well. Example: `groups(after: ConnectionCursor, before: ConnectionCursor, first: ConnectionLimitInt, last: ConnectionLimitInt, sortOrder: SortOrder = asc, sortBy: GroupSortByField = createdAt): GroupConnection` -- Document your query, the new types, and all fields in those types using string literals. . -- If not already done, register your schemas in the plugin's `index.js` file: +Next, document your query, the new types, and all fields in those types using string literals. . + +If not already done, register your schemas in the plugin's `index.js` file: ```js import schemas from "./schemas"; From 99cbb93844fb2cd9fa1c75349b1e437ad27144a8 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Wed, 16 Mar 2022 10:41:56 +0800 Subject: [PATCH 9/9] fix: update text and code samples to reflect the narrative Signed-off-by: Brent Hoover --- guides/create-graphql-query.md | 137 +++++++++++++++++++++------------ 1 file changed, 88 insertions(+), 49 deletions(-) diff --git a/guides/create-graphql-query.md b/guides/create-graphql-query.md index 00a9adf..4cedac7 100644 --- a/guides/create-graphql-query.md +++ b/guides/create-graphql-query.md @@ -1,27 +1,42 @@ ## At a glance -You're working on a new implementation for Leader of the Pack, an outdoor equipment retailer that we just took -through [building an API plugin](https://mailchimp.com/developer/open-commerce/guides/build-api-plugin/). Let's say that you don't ship Kayaks to every location because of the logistical challenges of shipping something that large. So you need to add a query that will show you if a particular product can be shipped to a particular zip code. Once you have that data in the system you need to have a way to make it available to the client. This is usually done by creating a GraphQL query. In this guide, we’ll walk you through the steps you need to create a query in GraphQL and have it recognized by the system. +Queries are a way to allow clients to ask a question of the system. Queries can be complex similar to a database +query, or they can be simple like asking the system a simple yes or no question. This is opposed to mutations which +are ways via GraphQL to change data. Queries never change data, just return it. + +We're working on a new implementation for Leader of the Pack, the outdoor equipment retailer that we just built an API plugin for. The store stocks kayaks, but can't ship them to every location because of the logistical challenges of shipping something that large. This leads to annoyed customers who get excited about buying the kayaks of their dreams but then find that we can't get it to them. We need a way to show if a particular product can be shipped to a particular ZIP code. This is usually accomplished by creating a GraphQL query. + +In this guide, we'll walk you through the process of creating a GraphQL query `canShipStandardGround` that will +query by `productId` and `postalCode` whether a product can be shipped to a particular address. ## What you need -You should already have an understanding of what fields/data you wish to share with the client and what GraphQL type each field is. Consult the [GraphQL Reference](https://graphql.org/learn/schema/) for more info on GraphQL types. +You should have already created a plugin from the [example plugin template](https://github.com/reactioncommerce/api-plugin-example) and that plugin should be installed into your local development environment. +You can also look at the [Leader of the Pack example plugin](https://github.com/reactioncommerce/leader-of-the-pack-example) which contains the finished files for this and other guides. -## Identify which plugin owns the query +You should already have an understanding of what fields/data you wish to share with the client and what GraphQL type +each field is. Consult the [GraphQL Reference](https://graphql.org/learn/schema/) for more info on GraphQL types. +For our example we will query using `productId` and `postalCode` which are both strings, and return a simple true or +false, indicating if we can ship this product via a standard ground method. -The complete Open Commerce GraphQL API is created by stitching together domain-specific APIs from all the API plugins. So when adding a new query, the first step is to decide which plugin should own it. This is usually obvious, but not always. You should think about whether any other plugins or services will need to call your query. -Typically, if you are adding data your new query will exist in your custom plugin. ## Name the query When choosing a name for the query, there are a few rules to follow: - In keeping with general GraphQL best practices, do not use verbs such as "list", "get", or "find" at the beginning of your query name. For example, use "cart" instead of "getCart" and "carts" instead of "listCarts". -- Prefix with adjectives as necessary to fully describe what the query returns. For example, "anonymousCart" and "accountCart" queries. -- If there are similar queries that take slightly different parameters, add a suffix to clarify. In most cases, we begin the suffix with "By". For example, "accountCartById" and "accountCartByAccountId". +- Add prefixes with adjectives as necessary to fully describe what the query returns. For example, use "anonymousCart" or "accountCart" to name your queries. +- If there are similar queries that take slightly different parameters, add a suffix to clarify. In most cases, we + begin the suffix with "By". For example, "accountCartById" and "accountCartByAccountId". + +For our example we will use `canShipStandardGround`. ## Define the query in the schema -If it doesn't already exist, create schemas folder in the plugin and add an index.js file there. Then check to see if there is a schema.graphql file in your schemas directory within the plugin. If there isn't, create that file now. +If it doesn't already exist, create a `schemas` folder in the plugin and add an index.js file there. Then check to see +if there is a schema.graphql file in your schemas directory within the plugin. If there isn't, create that file now. + +> Note: You must have `@reactioncommerce/api-utils` installed as a dependency + Next import the GraphQL file into `index.js` and default export it in an array: ```js @@ -42,6 +57,37 @@ If your query returns multiple documents, it should return a Relay-compatible Co Next, document your query, the new types, and all fields in those types using string literals. . +Here is what our example query for LOTP looks like: + +```graphql + +input CanShipStandardGroundInput { + "The product we want to ship" + productId: String! + + "The postal code of the address we want to ship to" + postalCode: String! +} + +type CanShipStandardGroupPayload { + "Can ship standard ground" + canShipStandardGround: Boolean! +} + +extend type Query { + "Can ship standard ground" + canShipStandardGround( + input: CanShipStandardGroundInput + ): Boolean! +} +``` + +Notice that we created an `input` type since our query contains multiple fields, but we just return a simple scalar +value so there is no need to declare another type. All the fields are required so they are suffixed by the ! +character. Also notice that we document each field with a short description of what is in the field, but we don't need to repeat it's type. + +Next we need to register the schema within the plugin. + If not already done, register your schemas in the plugin's `index.js` file: ```js @@ -57,23 +103,23 @@ export default async function register(app) { } ``` +> NOTE: Be careful to make it `graphQL` and not `graphQl` as this will be a difficult bug to find + ## Create the plugin query file - If it doesn't already exist, create `queries` folder in the plugin, and add an `index.js` file there. -- In `queries`, create a file for the query, e.g. `isAvailableToShip.js` for the `isAvailableToShip` query. The file should look something like this: +- In `queries`, create a file for the query, e.g. `canShipStandardGround.js` for the `canShipStandardGround` query. The file should look something like this: ```js -import Logger from "@reactioncommerce/logger"; - /** - * @method isAvailableToShip - * @summary TODO + * @method canShipStandardGround + * @summary Query the shipping service and return if we can ship a product via standard ground to the zipcode * @param {Object} context - an object containing the per-request state - * @return {Promise} TODO + * @return {Promise} - If we can ship this product standard ground to this zipcode */ -export default async function isAvailableToShip(context) { - Logger.info("isAvailableToShip query is not yet implemented"); - return null; +export default async function canShipStandardGround(context) { + // Put your logic here. For this example we just always return false + return false; } ``` @@ -82,10 +128,10 @@ export default async function isAvailableToShip(context) { In `queries/index.js` in the plugin, import your query and add it to the default export object. Example: ```js -import isAvailableToShip from "./isAvailableToShip.js" +import canShipStandardGround from "./canShipStandardGround.js"; export default { - isAvailableToShip + canShipStandardGround }; ``` @@ -102,48 +148,45 @@ export default async function register(app) { } ``` -Your plugin query function is now available in the GraphQL context as `context.queries.isAvailableToShip`. +Your plugin query function is now available in the GraphQL context as `context.queries.canShipStandardGround`. > NOTE: The queries objects from all plugins are merged, so be sure that another plugin does not have a query with the same name. The last one registered with that name will win, and plugins are generally registered in alphabetical order by plugin name. Tip: You can use this to your advantage if you want to override the query function of a core plugin without modifying core code. ## Add a test file for your query -If your query is in a file named `isAvailableToShip.js`, your Jest tests should be in a file named `isAvailableToShip.test.js` in the same folder. Initially you can copy and paste the following test: +If your query is in a file named `canShipStandardGround.js`, your Jest tests should be in a file named +`canShipStandardGround.test.js` in +the same folder. Initially you can copy and paste the following test: ```js import mockContext from "/imports/test-utils/helpers/mockContext"; -import isAvailableToShip from "./isAvailableToShip.js"; +import canShipStandardGround from "./canShipStandardGround.js"; test("expect to return a Promise that resolves to null", async () => { - const result = await isAvailableToShip(mockContext); - expect(result).toEqual(null); + const result = await canShipStandardGround(mockContext); + expect(result).toEqual(false); }); ``` -This of course should be updated with tests that are appropriate for whatever your query does. - ## Create the GraphQL query resolver file > Note: All of these elements are required for your query to work and if missing often will not throw an error. If your query is not showing or not returning any data up you probably need to recheck that all of these pieces are here and have the correct name and format. - If it doesn't already exist, create `resolvers` folder in the plugin, and add an `index.js` file there. - If it doesn't already exist, create `resolvers/Query` folder in the plugin, and add an `index.js` file there. "Query" must be capitalized. -- In `resolvers/Query`, create a file for the query resolver, e.g. `isAvailableToShip.js` for the `isAvailableToShip` query. The file should look something like this initially: +- In `resolvers/Query`, create a file for the query resolver, e.g. `canShipStandardGround.js` for the `canShipStandardGround` query. The file should look something like this initially: ```js /** - * @name "Query.isAvailableToShip" - * @method - * @memberof MyPlugin/GraphQL - * @summary resolver for the isAvailableToShip GraphQL query + * @summary resolver for the canShipStandardGround GraphQL query * @param {Object} parentResult - unused * @param {Object} args - an object of all arguments that were sent by the client * @param {Object} context - an object containing the per-request state - * @return {Promise} TODO + * @return {Promise} Results of the canShipStandardGround query */ -export default async function isAvailableToShip(parentResult, args, context) { - // TODO: decode incoming IDs here - return context.queries.isAvailableToShip(context); +export default async function canShipStandardGround(parentResult, args, context) { + // TODO: decode incoming IDs here + return context.queries.canShipStandardGround(context); } ``` @@ -157,10 +200,10 @@ Make adjustments to the resolver function so that it reads and passes along the In `resolvers/Query/index.js` in the plugin, import your query resolver and add it to the default export object. Example: ```js -import isAvailableToShip from "./isAvailableToShip.js" +import canShipStandardGround from "./canShipStandardGround.js"; export default { - isAvailableToShip + canShipStandardGround }; ``` @@ -207,17 +250,17 @@ Calling your query with GraphQL should now work. ## Add a test file for your query resolver -If your query resolver is in a file named `isAvailableToShip.js`, your Jest tests should be in a file named `isAvailableToShip.test.js` in the same folder. Initially you can copy and paste the following test: +If your query resolver is in a file named `canShipStandardGround.js`, your Jest tests should be in a file named `canShipStandardGround.test.js` in the same folder. Initially you can copy and paste the following test: ```js -import isAvailableToShip from "./isAvailableToShip.js"; +import canShipStandardGround from "./canShipStandardGround.js"; -test("calls queries.isAvailableToShip and returns the result", async () => { - const mockResponse = "MOCK_RESPONSE"; - const mockQuery = jest.fn().mockName("queries.isAvailableToShip").mockReturnValueOnce(Promise.resolve(mockResponse)); +test("calls queries.canShipStandardGround and returns the result", async () => { + const mockResponse = false; + const mockQuery = jest.fn().mockName("queries.canShipStandardGround").mockReturnValueOnce(Promise.resolve(mockResponse)); - const result = await isAvailableToShip(null, { /* TODO */ }, { - queries: { isAvailableToShip: mockQuery }, + const result = await canShipStandardGround(null, { /* TODO */ }, { + queries: { canShipStandardGround: mockQuery }, userId: "123" }); @@ -232,10 +275,6 @@ This of course should be updated with tests that are appropriate for whatever yo Adjust the query function and the query resolver function until they work as expected, with tests that prove it. This will likely involve adding additional arguments, ID transformations, permission checks, and MongoDB queries. -## Update the JSDoc comments - -Write/update jsdoc comments for the plugin query function, the query resolver, and any util functions. The resolver function must have `@memberof /GraphQL` in the jsdoc, and the `@name` must be the full GraphQL schema path in quotation marks, e.g., "Query.isAvailableToShip". (The quotation marks are necessary for the output API documentation to be correct due to the periods.) - ## More resources [Build an API plugin guide](https://mailchimp.com/developer/open-commerce/guides/build-api-plugin/)