Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more how-to docs #41

Closed
wants to merge 11 commits into from
38 changes: 38 additions & 0 deletions docs/jsdoc-style-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
id: jsdoc-style-guide
brent-hoover marked this conversation as resolved.
Show resolved Hide resolved
title: JSDoc Style Guide
---

## The basics

Document every JavaScript function by adding [JSDoc](http://usejsdoc.org/) comments above the function definition with the following tags:

### required
- `@summary` can use Markdown here
- `@param` {type} name description, use `[]` square brackets around param for optional params
- `@return` {type} name description, or `@return {undefined}`

### optional
- `@async`
- `@private`
- `@default`
- `@deprecated` - since version number
- `@since` - version number
- `@todo` - any TODO notes here
- `@ignore` - if you don't want the function to output docs
- `@author` - to indicate third-party method authors
- `@see` - link to relevant third-party documentation

## Example

```js
/**
* @summary Import all plugins listed in a JSON file. Relative paths are assumed
* to be relative to the JSON file. This does NOT register the plugins. It builds
* a valid `plugins` object which you can then pass to `api.registerPlugins`.
* @param {String} pluginsFile An absolute or relative file path for a JSON file.
* @param {Function} [transformPlugins] A function that takes the loaded plugins object and
* may return an altered plugins object.
* @returns {Promise<Object>} Plugins object suitable for `api.registerPlugins`
*/
```
182 changes: 182 additions & 0 deletions guides/developers-guide/core/developing-graphql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Developing the GraphQL API

## Extending and Modifying the GraphQL API

The GraphQL schema is written in multiple `.graphql` files, which contain type definitions in the GraphQL schema language. These files live in the plugins to which they relate, in a `schemas` folder. Refer to one of the How To articles:

- [How To: Create a new GraphQL query](../../../how-tos/create-graphql-query.md)
- [How To: Create a new GraphQL mutation](../../../how-tos/create-graphql-mutation.md)

## Documenting a GraphQL Schema

Every type, input type, enum, query, mutation, and field must have documentation. Add a description using a string literal immediately above the thing you are documenting in the `.graphql` file. <strong>This is the official API documentation, so take the time to make it clear, well-formatted, and with no spelling or grammar errors.</strong>
delagroove marked this conversation as resolved.
Show resolved Hide resolved

Tips:
- For `_id`, `clientMutationId` and anything else that appears in multiple places, copy the documentation from elsewhere so that everything matches.
- There is often a `type` and a related `input` type. If the field names match, their documentation should also be identical or very similar.

External references:
- [GraphQL "description" spec](https://facebook.github.io/graphql/draft/#sec-Documentation)
- [Apollo Server: Documenting Your Schema](https://www.apollographql.com/docs/apollo-server/schema/schema/#documentation-strings)

## Where Resolvers are Defined

Every plugin that extends the GraphQL schema should also have a `resolvers` folder, which should have an `index.js` file in which the default export is the full `resolvers` object. This object should be built by importing other files and folders in that directory, such that the folder and file tree looks the same as the `resolvers` object tree.

For example, there are typically folders named `Mutation` and `Query`. In the `accounts` plugin there is also an `Account` folder, where resolvers for that type live. You may choose either folders or single files, depending on how many resolvers there are and how complex they are.

The `resolvers` object for each plugin is deep merged with all the `resolvers` exported by all the other plugins, and the result is the full resolver function tree.

## Resolver Mutations and Queries vs. Plugin Mutations and Queries

The path a GraphQL query or mutation takes is first to a resolver function, which then calls a query or mutation function provided by one of the plugins. It’s important to understand what happens in each.

The resolver function:
- Lives in `resolvers` in a plugin folder
- Returns a Promise (is async)
- Transforms IDs (see [IDs in GraphQL](#ids-in-graphql)) and data structures (where they don’t match internal data structures)
- May pull things from the GraphQL context to pass to the plugin function
- May throw a `ReactionError` if anything goes wrong
- Includes `clientMutationId` in the response (for mutations only)

The plugin function:
- Lives in `queries` or `mutations` in a plugin folder
- Is available on the GraphQL context in `context.queries` or `context.mutations`, and as such can be called by code elsewhere in the app
- Returns a Promise (is async)
- Does all permission checks
- May throw a `ReactionError` if anything goes wrong
- Performs the actual database mutations or queries

TIP: If you’re confused about where to draw the line, generally resolvers are for _transforming_ data (both inbound and outbound)
while plugin functions read or write data and perform business logic.

## The Endpoint

The GraphQL server and `/graphql` endpoint is configured and returned by the `createApolloServer` function, which is called from the `ReactionAPI` class instance.

`createApolloServer` does pretty standard configuration of an Express app using `apollo-server-express`. The main things it does are:
- Checks the identity token using Express middleware
brent-hoover marked this conversation as resolved.
Show resolved Hide resolved
- Builds the `context` object that’s available in all resolver functions. See [The Reaction GraphQL Context](#the-reaction-graphql-context)
- Formats the `errors` array that is returned to clients, to make errors as helpful as possible
- Provides the merged GraphQL schema
- Sets the path as `/graphql` and exposes a GraphQL Playground for GET requests on `/graphql`

## The Reaction GraphQL Context

All GraphQL resolvers receive a [context](https://www.apollographql.com/docs/apollo-server/data/resolvers/#the-context-argument) object as their third argument. The base context is built within the `ReactionAPI` constructor, and additional request-specific properties (like `accountId` and `userHasPermission`) are added to it in `buildContext.js`.

delagroove marked this conversation as resolved.
Show resolved Hide resolved
In Jest tests, you can get a mock context object with mock functions on it:

```js
import mockContext from "/imports/test-utils/helpers/mockContext";
```

Here’s what's on the context object:
- Queries registered by plugins: `context.queries<queryFunctionName>`
- Mutations registered by plugins: `context.mutations<mutationFunctionName>`
- The current user: `context.user`
- The current user’s ID: `context.userId`
- The current account: `context.account`
- The current account ID: `context.accountId`
- The default shop ID (this may go away): `context.shopId`
- To check permissions: `context.userHasPermission(role, shopId)` (returns true or false)
- To check permissions and throw error: `context.checkPermissions(role, shopId)`
- MongoDB collections: `context.collections<CollectionName>`
- The `ReactionAPI` instance: `context.app`
- App events object:
- To emit: `context.appEvents.emit`
- To listen: `context.appEvents.on`
- To retrieve all functions registered as a specific type of function: `context.getFunctionsOfType(type)`
- The app root URL: `context.rootUrl`
- To convert a relative URL to absolute (prefix with the root URL): `context.getAbsoluteUrl(path)`

brent-hoover marked this conversation as resolved.
Show resolved Hide resolved
## How Auth Works

Refer to [Developer Concepts: Authentication](./developer-authentication)

## IDs in GraphQL

All IDs are exposed in GraphQL as globally unique IDs on fields named `_id`. When we finalize the GraphQL API, we may change this field name to `id`, which is more commonly used in the GraphQL world.

The GraphQL server specification has no opinion on what a type's ID field should look like, but it does provide [a built-in ID type](https://graphql.github.io/graphql-spec/draft/#sec-ID).

> The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, it is not intended to be human‐readable. While it is often numeric, it should always serialize as a String.

In particular, note that "it is not intended to be human‐readable". You should never display a field of type `ID` anywhere. They are only for references. If your data comes over from another system and has IDs with some meaning, then you should also store them on a different field where the raw value will not be obfuscated by the GraphQL layer.

Note also that the server specification does not necessarily care whether an ID is globally unique. However, we intend compatibility with both Relay and Apollo for client-side frameworks, and [the Relay specification](https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-Interface) does have a requirement here:

> This `id` should be a globally unique identifier for this object, and given just this `id`, the server should be able to refetch the object.

Additionally, the [Apollo caching docs](https://www.apollographql.com/docs/react/advanced/caching/#normalization) have this to say:
brent-hoover marked this conversation as resolved.
Show resolved Hide resolved

> By default, InMemoryCache will attempt to use the commonly found primary keys of `id` and `_id` for the unique identifier if they exist

This does not specifically require global uniqueness since it also uses `__typename`, but because Relay does, we've opted to ensure IDs are globally unique.

In most cases, actual internal data IDs are in MongoDB collections, so they are guaranteed unique within the collection, but not among all collections. To add that extra layer of uniqueness, we concatenate the namespace with the internal ID, and then to keep it looking like a "not human‐readable" ID, we base64 encode.

To convert internal IDs to opaque UUIDs, we first prefix them with "reaction/\<namespace\>" and then base64 encode them. The primary transformation functions that handle this are in the `api-utils` package.

The GraphQL resolver functions are the place where ID encoding and decoding happens. They then call out to plugin functions that deal exclusively with internal IDs. Any IDs returned by such functions must also be transformed before returning them, although this typically and preferably happens in a type resolver.

### Checking whether an operation was successful
brent-hoover marked this conversation as resolved.
Show resolved Hide resolved

For `insertOne` or `insertMany`:

```js
const { insertedCount } = await SomeCollection.insertOne(/* ... */);
if (insertedCount === 0) {
// throw Error or otherwise handle failure
}
```

For `updateOne` or `updateMany`:

```js
const { modifiedCount } = await SomeCollection.updateOne(/* ... */);
if (modifiedCount === 0) {
// throw Error or otherwise handle failure
}
```

For `deleteOne`:

```js
const { deletedCount } = await SomeCollection.updateOne(/* ... */);
if (deletedCount === 0) {
// throw Error or otherwise handle failure
}
```

Keep in mind that sometimes a zero `modifiedCount` or `deletedCount` might be because nothing matched your query, and depending on the situation, this may not be an error. If you foresee this situation, you can opt to check `matchedCount` instead.

## Optimizing GraphQL resolvers

Because of the way GraphQL queries and relationships work, sometimes a query will include something like this:

```graphql
{
order {
shop {
_id
}
}
}
```

Normally the `shop` relationship would result in a database query, but if `order` already has a `shopId` property, we can actually skip the database lookup because the client has requested only the `_id` property. There is a utility function that helps with this: `optimizeIdOnly`. Check out the `Query.viewer` resolver for an example of how to use it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add link to the Query.viewer resolver here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is that located?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Akarshit second request for clarification here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


## Documenting GraphQL Functions

Reaction GraphQL resolver functions, like all JavaScript functions in all Reaction code, must have JSDoc comments above them. See the [JSDoc Style Guide](../../../docs/jsdoc-style-guide.md)

## Writing Tests

Reaction GraphQL is tested through a combination of unit tests and integration tests, all written in and executed with Jest. Specifically, the coverage requirements are:

- Each query or mutation function in plugins must have unit tests in a `.test.js` file alongside the file being tested.
- Each resolver that is doing anything more than just referencing another function must have a unit test in a `.test.js` file alongside the file being tested.
- The primary expected uses of all queries and mutations must be tested in integration tests in the `/tests` root folder. This helps ensure that all of the related resolvers are working together properly and using correct database calls.

Refer to [Testing Requirements](./testing-requirements.md)
Empty file.
44 changes: 44 additions & 0 deletions how-tos/add-collections-from-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# How do I add a MongoDB collection from a plugin

To create any non-core MongoDB collection that a plugin needs, use the `collections` option in your plugin's `registerPlugin` call:

```js
export default async function register(app) {
await app.registerPlugin({
label: "My Custom Plugin",
name: "my-custom-plugin",
collections: {
MyCustomCollection: {
name: "MyCustomCollection"
}
}
// other props
});
}
```

The `collections` object key is where you will access this collection on `context.collections`, and `name` is the collection name in MongoDB. We recommend you make these the same if you can.

The example above will make `context.collections.MyCustomCollection` available in all query and mutation functions, and all functions that receive `context`, such as startup functions. Note that usually MongoDB will not actually create the collection until the first time you insert into it.

## Ensure MongoDB collection indexes from a plugin

You can add indexes for your MongoDB collection in the same place you define your collection, the `collections` object of your `registerPlugin` call:

```js
export default async function register(app) {
await app.registerPlugin({
label: "My Custom Plugin",
name: "my-custom-plugin",
collections: {
MyCustomCollection: {
name: "MyCustomCollection",
indexes: [
[{ referenceId: 1 }, { unique: true }]
]
}
}
// other props
});
}
```
106 changes: 106 additions & 0 deletions how-tos/add-extra-data-to-a-product.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# How To: Add Data to a Product

## Prerequisite Reading
- [Catalogs](../guides/developers-guide/concepts/catalog-and-publishing.md)
- [Understanding Plugins](../guides/developers-guide/core/build-api-plugin.md)
- [Extending Schemas](./extend-an-existing-schema.md)

## Overview

As a developer customizing Reaction, you may find a need to add some custom property to products. You should avoid this if you can achieve your goals some other way, such as using `metafields`, tags, or a separate data store that references product IDs. But in some cases, extending products is the best way.

Because products have a publishing flow and have variants, extending them requires many steps. In general, they are as follows:
- Extend database schemas
- Extend GraphQL schemas
- Register your custom property as published, if it should be published to the catalog
- Register a function that publishes your custom property, if it should be published to the catalog
- Create a GraphQL mutation for setting your custom property value
- Create a React component that allows an operator to set the custom property value, and wire it up to your mutation, or set your property in some other way

### Extend database schemas

To extend any database schema, you just need a file that is imported into server code. We recommend using a file named `simpleSchemas.js` in your plugin, and then importing that file in your plugin's `index.js`.

Refer to [SimpleSchema docs](https://github.com/aldeed/simple-schema-js#schema-rules) for more information about the object you pass to `extend`.

```js
brent-hoover marked this conversation as resolved.
Show resolved Hide resolved
const { simpleSchemas: { CatalogProduct, CatalogVariantSchema, Product, ProductVariant, VariantBaseSchema }} = context;

const schemaExtension = {
myProperty: {
type: String,
optional: true
}
};

// Extend the Product database schema, if your custom property will be on products
Product.extend(schemaExtension);

// Extend the Variant database schema, if your custom property will be on variants
ProductVariant.extend(schemaExtension);

// Extend the CatalogProduct database schema, if your custom property will be on products
CatalogProduct.extend(schemaExtension);
brent-hoover marked this conversation as resolved.
Show resolved Hide resolved

// Extend the catalog variant database schemas, if your custom property will be on variants. There are two schemas for this one.
VariantBaseSchema.extend(schemaExtension);
CatalogVariantSchema.extend(schemaExtension);
```

### Extend GraphQL schemas

- Extend the `Product` GraphQL type, if your custom property will be on products
- Extend the `ProductVariant` GraphQL type, if your custom property will be on variants
- Extend the `CatalogProduct` GraphQL type, if your custom property will be on products and is published to the catalog
- Extend the `CatalogProductVariant` GraphQL type, if your custom property will be on variants and is published to the catalog
- Create a GraphQL resolver for your property if it needs any transformation

Refer to [How To: Extend GraphQL to add a field](./extend-graphql-to-add-field.md).

### Register custom property as published

*Skip this step if your property is not needed in the published catalog*

A plugin can include a `catalog` object in `registerPlugin`, with `customPublishedProductFields` and `customPublishedProductVariantFields` that are set to arrays of property names. These will be appended to the core list of fields for which published status should be tracked. This is used to build the hashes that are used to display an indicator when changes need to be published.

```js
export default async function register(app) {
await app.registerPlugin({
catalog: {
customPublishedProductFields: ["myProperty"],
customPublishedProductVariantFields: ["myProperty"]
},
// other props
});
}
```

### Register a function that publishes custom property

*Skip this step if your property is not needed in the published catalog*

```js
import publishProductToCatalog from "./publishProductToCatalog";

export default async function register(app) {
await app.registerPlugin({
functionsByType: {
publishProductToCatalog: [publishProductToCatalog]
},
// other props
});
}
```

Where the `publishProductToCatalog` function adds the required fields to `catalogProduct`. For example:

```js
export default function publishProductToCatalog(catalogProduct, { context, product, shop, variants }) {
catalogProduct.myProperty = product.myProperty;
// Also set on each catalogProduct.variants if necessary
}
```

### Create a GraphQL mutation for setting your custom property value

Refer to [How To: Create a new GraphQL mutation](./create-graphql-mutation.md). After mutating the `Product` document in your function, you must also call `context.mutations.hashProduct(<productId>, context.collections, false)`. This will update the current product hash, causing the operator UI to indicate that there are changes needing publishing.
Loading