diff --git a/.eslintignore b/.eslintignore index 9468f0f87e4d8..def618dbae5a7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -20,6 +20,7 @@ integration-tests **/*.d.ts packages/*/*.js +packages/gatsby-source-shopify/**/*.js packages/gatsby-plugin-preload-fonts/prepare/*.js packages/gatsby-image/withIEPolyfill/index.js packages/gatsby/cache-dir/commonjs/**/* diff --git a/packages/gatsby-source-shopify/.babelrc b/packages/gatsby-source-shopify/.babelrc deleted file mode 100644 index ac0ad292bb087..0000000000000 --- a/packages/gatsby-source-shopify/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": [["babel-preset-gatsby-package"]] -} diff --git a/packages/gatsby-source-shopify/.gitignore b/packages/gatsby-source-shopify/.gitignore index c59797558bd39..4df7757a8c07d 100644 --- a/packages/gatsby-source-shopify/.gitignore +++ b/packages/gatsby-source-shopify/.gitignore @@ -1,3 +1,7 @@ -/__tests__ -/yarn.lock -/*.js +*.js +*.js.map +*.d.ts +!/types/*.d.ts +README.md + +!jest.config.js \ No newline at end of file diff --git a/packages/gatsby-source-shopify/.npmignore b/packages/gatsby-source-shopify/.npmignore index 2eab442003abb..aa8e45f12bce4 100644 --- a/packages/gatsby-source-shopify/.npmignore +++ b/packages/gatsby-source-shopify/.npmignore @@ -1,34 +1 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git -node_modules -*.un~ -yarn.lock -src/__tests__ -flow-typed -coverage -decls -examples +src/ \ No newline at end of file diff --git a/packages/gatsby-source-shopify/.prettierignore b/packages/gatsby-source-shopify/.prettierignore new file mode 100644 index 0000000000000..e64ecf4fc46bd --- /dev/null +++ b/packages/gatsby-source-shopify/.prettierignore @@ -0,0 +1,5 @@ +node_modules +.env +*.graphql +.cache +public/ diff --git a/packages/gatsby-source-shopify/README.md b/packages/gatsby-source-shopify/README.md index 32f4a267d7347..5bfd05737fcb5 100644 --- a/packages/gatsby-source-shopify/README.md +++ b/packages/gatsby-source-shopify/README.md @@ -1,813 +1,327 @@ -# 📣 Looking for the latest version of `gatsby-source-shopify`? +

+ +

+ +

+ + Gatsby and gatsby-source-shopify are released under the MIT license. + + + Current npm package version. + + + Downloads per month on npm. + + + Total downloads on npm. + + + PRs welcome! + + + Follow @gatsbyjs + +

+ +
+ +- 🏃‍ [Getting started](#getting-started) + - 🔩 [Install](#install) + - 🔑 [Configure](#configure) + - 🙌 [Retrieving API Information from Shopify](#retrieving-api-information-from-shopify) + - 🛒 [Enabling Cart and Checkout features](#enabling-cart-and-checkout-features) + - 🔥 [Fire it up](#fire-it-up) +- 🔌 [Plugin options](#plugin-options) +- 🎨 [Images](#images) + - 🚀 [Use Shopify CDN](#use-shopify-cdn) + - 🚥 [Use runtime images](#use-runtime-images) + - 🖼️ [Download images up front](#download-images-up-front) +- 🚨 [Limitations](#limitations) +- 🛠 [Development](#development) +- 💾 [Migrating from v4 to v5](#migrating-from-v4-to-v5) + +# gatsby-source-shopify + +A scalable solution for sourcing data from Shopify. + +This plugin works by leveraging [Shopify's bulk operations API][bulk-operations], which allows it to process large amounts of data at once. This gives it a more resilient and reliable build process. It also enables incremental builds so that your site can build quickly when you change your data in Shopify. + +
+ +## Getting started + +This takes you through the minimal steps to see your Shopify data in your Gatsby site's GraphiQL explorer. + +
+ +### Install + +Install this plugin to your Gatsby site. -👉 Get started at [`gatsbyjs/gatsby-source-shopify`](https://github.com/gatsbyjs/gatsby-source-shopify) - -- Support for incremental builds -- Scale up to 10k products -- Up to 16x faster builds - -`gatsbyjs/gatsby-source-shopify` will replace the below package with the next minor release of `gatsbyjs/gatsby`. +```shell +npm install gatsby-source-shopify +``` ---- +
-## Legacy source plugin instructions: +### Configure -Source plugin for pulling data into [Gatsby][gatsby] from [Shopify][shopify] -stores via the [Shopify Storefront API][shopify-storefront-api]. +Add the plugin to your `gatsby-config.js`: -## Features +```js:title=gatsby-config.js +require("dotenv").config() -- Provides public shop data available via the [Shopify Storefront API][shopify-storefront-api] -- Supports `gatsby-transformer-sharp` and `gatsby-image` for product and - article images +module.exports = { + plugins: [ + { + resolve: "gatsby-source-shopify", + options: { + apiKey: process.env.SHOPIFY_ADMIN_API_KEY, + password: process.env.SHOPIFY_ADMIN_PASSWORD, + storeUrl: process.env.SHOPIFY_STORE_URL, + }, + }, + "gatsby-plugin-image", + ], +} +``` -## Install +
-```shell -npm install gatsby-source-shopify -``` +### Retrieving API Information from Shopify -## How to use +In Shopify admin, `SHOPIFY_STORE_URL` is the Store address you enter when logging into your Shopify account. This typically is in the format of `myshop.myshopify.com`. -[See the Shopify tutorial on gatsbyjs.com for a full getting started guide](https://www.gatsbyjs.com/docs/building-an-ecommerce-site-with-shopify/) +Once logged into Shopify admin, navigate to the `Apps` page and click the link at the bottom to `Manage private apps`. This will allow you to turn on private apps and create an app that Gatsby will use to access Shopify's Admin API. -Ensure you have an access token for the [Shopify Storefront API][shopify-storefront-api]. The token should have the following permissions: +For the Private app name enter `Gatsby` (the name does not really matter). Add the following under the `Active Permissions for this App` section: -- Read products, variants, and collections -- Read product tags -- Read content like articles, blogs, and comments +- `Read access` for `Products` +- `Read access` for `Product listings` if you want to use Shopify's Product Collections in your Gatsby site +- `Read access` for `Orders` if you want to use order information in your Gatsby site -Then in your `gatsby-config.js` add the following config to enable this plugin: +
-```js -plugins: [ - /* - * Gatsby's data processing layer begins with “source” - * plugins. Here the site sources its data from Shopify. - */ - { - resolve: "gatsby-source-shopify", - options: { - // The domain name of your Shopify shop. This is required. - // Example: 'gatsby-source-shopify-test-shop' if your Shopify address is - // 'gatsby-source-shopify-test-shop.myshopify.com'. - // If you are running your shop on a custom domain, you need to use that - // as the shop name, without a trailing slash, for example: - // shopName: "gatsby-shop.com", - shopName: "gatsby-source-shopify-test-shop", - - // An API access token to your Shopify shop. This is required. - // You can generate an access token in the "Manage private apps" section - // of your shop's Apps settings. In the Storefront API section, be sure - // to select "Allow this app to access your storefront data using the - // Storefront API". - // See: https://help.shopify.com/api/custom-storefronts/storefront-api/getting-started#authentication - accessToken: "example-wou7evoh0eexuf6chooz2jai2qui9pae4tieph1sei4deiboj", - - // Set the API version you want to use. For a list of available API versions, - // see: https://help.shopify.com/en/api/storefront-api/reference/queryroot - // Defaults to 2019-07 - apiVersion: "2020-01", - - // Set verbose to true to display a verbose output on `npm run develop` - // or `npm run build`. This prints which nodes are being fetched and how - // much time was required to fetch and process the data. - // Defaults to true. - verbose: true, - - // Number of records to fetch on each request when building the cache - // at startup. If your application encounters timeout errors during - // startup, try decreasing this number. - paginationSize: 250, - - // List of collections you want to fetch. - // Possible values are: 'shop' and 'content'. - // Defaults to ['shop', 'content']. - includeCollections: ["shop", "content"], - // Download Images Locally - // set to false if you plan on using shopify's CDN - downloadImages: true, - - // Allow overriding the default queries - // This allows you to include/exclude extra fields when sourcing nodes - // Available keys are: articles, blogs, collections, products, shopPolicies, and pages - // Queries need to accept arguments for first and after - // You will need to include all the fields you want available for a - // specific key. View the `shopifyQueries Defaults` section below for a - // full list of keys and fields. - shopifyQueries: { - products: ` - query GetProducts($first: Int!, $after: String) { - products(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - availableForSale - } - } - } - } - `, - }, - }, - }, -] -``` +#### Enabling Cart and Checkout features -NOTE: By default, all metafields are private. In order to pull metafields, -you must first [expose the metafield to the Storefront API](https://help.shopify.com/en/api/guides/metafields/storefront-api-metafields#expose-metafields-to-the-storefront-api). +If you are planning on managing your cart within Gatsby you will also need to check the box next to `Allow this app to access your storefront data using the Storefront API` and make sure to check `Read and modify checkouts`. This source plugin does not require Shopify Storefront API access to work, however, this is needed to add items to a Shopify checkout before passing the user to Shopify's managed checkout workflow. See [Gatsby Starter Shopify](https://github.com/gatsbyjs/gatsby-starter-shopify) for an example. -## How to query +
-You can query nodes created from Shopify using GraphQL like the following: +### Fire it up -**Note**: Learn to use the GraphQL tool and Ctrl+Spacebar at -`http://localhost:8000/___graphql` to discover the types and properties of your -GraphQL model. +Run your site with `gatsby develop`. When the site builds successfully, you should see output like this: -```graphql -{ - allShopifyProduct { - edges { - node { - id - title - handle - productType - vendor - variants { - id - title - price - } - } - } - } -} +``` +You can now view test-site in the browser. +⠀ + http://localhost:8000/ +⠀ +View GraphiQL, an in-browser IDE, to explore your site's data and schema +⠀ + http://localhost:8000/___graphql +⠀ +Note that the development build is not optimized. +To create a production build, use gatsby build ``` -All Shopify data is pulled using the [Shopify Storefront -API][shopify-storefront-api]. Data is made available in the same structure as -provided by the API, with a few exceptions noted below. +Now follow the second link to explore your Shopify data! -The following data types are available: +
-| Name | Description | -| ------------------ | --------------------------------------------------------------------------------------------------------------------- | -| **Article** | A blog entry. | -| **Blog** | Collection of articles. | -| **Comment** | A comment on a blog entry. | -| **Collection** | Represents a grouping of products that a shop owner can create to organize them or make their shops easier to browse. | -| **Product** | Represents an individual item for sale in a Shopify store. | -| **ProductOption** | Custom product property names. | -| **ProductVariant** | Represents a different version of a product, such as differing sizes or differing colors. | -| **ShopPolicy** | Policy that a merchant has configured for their store, such as their refund or privacy policy. | -| **ShopDetails** | Name, description and money format that a merchant has configured for their store. | +## Plugin options -For each data type listed above, `shopify${typeName}` and -`allShopify${typeName}` is made available. Nodes that are closely related, such -as `Article` and `Comment`, are provided as node fields as described below. +`apiKey: string` -**Note**: The following examples are not a complete reference to the available -fields for each node. Utilize Gatsby's built-in GraphQL tool to discover the -types and properties available. +The admin API key for the Shopify store + app you're using -### Query articles +`password: string` -The associated blog data is provided on the `blog` field. Article comments are -provided on the `comments` field. +The admin password for the Shopify store + app you're using -```graphql -{ - allShopifyArticle { - edges { - node { - id - author { - email - name - } - blog { - title - } - comments { - id - author { - email - name - } - contentHtml - } - contentHtml - publishedAt(formatString: "ddd, MMMM Do, YYYY") - } - } - } -} -``` +`storeUrl: string` -### Query blogs +Your Shopify store URL, e.g. some-shop.myshopify.com -Blog data is provided on the `blog` field on `Article`, but it can be queried -directly like the following: +`shopifyConnections: string[]` -```graphql -{ - allShopifyBlog { - edges { - node { - id - title - url - } - } - } -} -``` +An optional array of additional data types to source. -### Query article comments +Accepted values: `'orders'`, `'collections'` -Comments are provided on the `comments` field on `Article`, but they can be -queried directly like the following: +`downloadImages: bool` -```graphql -{ - allShopifyComment { - edges { - node { - id - author { - email - name - } - contentHtml - } - } - } -} -``` +Not set by default. If set to `true`, this plugin will download and process images during the build. -### Query product collections +The plugin's default behavior is to fall back to Shopify's CDN. -Products in the collection are provided on the `products` field. +`typePrefix: string` -```graphql -{ - allShopifyCollection { - edges { - node { - id - descriptionHtml - handle - image { - src - alt - } - products { - id - handle - title - } - title - } - } - } -} -``` +Not set by default. If set to a string (example `MyStore`) node names will be `allMyStoreShopifyProducts` instead of `allShopifyProducts`. -### Query products +`salesChannel: string` -Product variants and options are provided on the `variants` and `options` -fields. +Not set by default. If set to a string (example `My Sales Channel`), only products and collections that are active in that channel will be sourced. If no sales channel is provided, the default behavior is to source products that are available in the online store. -```graphql -{ - allShopifyProduct { - edges { - node { - id - descriptionHtml - handle - images { - originalSrc - } - variants { - id - availableForSale - image { - originalSrc - } - price - selectedOptions { - name - value - } - sku - title - } - title - } - } - } -} -``` +
-### Query product options +## Images -Product options are provided on the `options` field on `Product`, but they can -be queried directly like the following: +We offer two options for displaying Shopify images in your Gatsby site. The default option is to use the Shopify CDN along with [gatsby-plugin-image][gatsby-plugin-image], but you can also opt-in to downloading the images as part of the build process. Your choice will result in differences to the schema. Both options are explained below. -```graphql -{ - allShopifyProductOption { - edges { - node { - id - name - values - } - } - } -} -``` +
-### Query product variants +### Use Shopify CDN -Product variants are provided on the `variants` field on `Product`, but they -can be queried directly like the following: +This is the default behavior and is intended to be used in conjunction with [gatsby-plugin-image][gatsby-plugin-image]. In this case, querying for image data from your Gatsby site might look like this: ```graphql -{ - allShopifyProductVariant { - edges { - node { +products: allShopifyProduct( + sort: { fields: [publishedAt], order: ASC } +) { + edges { + node { + id + storefrontId + featuredImage { id - availableForSale - image { - originalSrc - } - price - selectedOptions { - name - value - } - sku - title + altText + gatsbyImageData(width: 910, height: 910) } } } } ``` -### Query shop policies - -Shop policies include the following types: +You could then display the image in your component like this: -- Privacy Policy (`privacyPolicy`) -- Refund Policy (`refundPolicy`) -- Terms of Service (`termsOfService`) +```jsx +import { GatsbyImage } from "gatsby-plugin-image" -The type of policy is provided on the `type` field. Policies can be queried -like the following: - -```graphql -{ - allShopifyShopPolicy { - edges { - node { - body - title - type - } - } - } +function ProductListing(product) { + return ( + + ) } ``` -### Query pages +
-Shopify merchants can create pages to hold static HTML content. +### Use runtime images -```graphql -{ - allShopifyPage { - edges { - node { - id - handle - title - body - bodySummary - } - } - } -} -``` +If you get Shopify images at runtime that don't have the `gatsbyImageData` resolver, for example from the cart or Storefront API, you can use the `getShopifyImage` function to create an imagedata object to use with ``. -### Query shop details +It expects an `image` object that contains the properties `width`, `height` and `originalSrc`, such as [a Storefront API `Image` object](https://shopify.dev/docs/storefront-api/reference/common-objects/image). -Shopify merchants can give their shop a name, description and a money format. +```jsx +import { GatsbyImage } from "gatsby-plugin-image" +import { getShopifyImage } from "gatsby-source-shopify" -```graphql -{ - shopifyShop { - name - description - moneyFormat - } +function CartImage(storefrontProduct) { + // This is data from Storefront, not from Gatsby + const image = storefrontProduct.images.edges[0].node + const imageData = getShopifyImage({ + image, + layout: "fixed", + width: 200, + height: 200, + }) + + return } ``` -### Image processing +
-To use image processing you need `gatsby-transformer-sharp`, -`gatsby-plugin-sharp`, and their dependencies `gatsby-image` and -`gatsby-source-filesystem` in your `gatsby-config.js`. +### Download images up front -You can apply image processing to any image field on a node. Image processing -of inline images added to description fields is currently not supported. +If you wish to download your images during the build, you can specify `downloadImages: true` as a plugin option: -To access image processing in your queries, you need to use this pattern, where -`...ImageFragment` is one of the [`gatsby-transformer-sharp` -fragments][gatsby-image-fragments]: +```js:title=gatsby-config.js +require("dotenv").config() -```graphql -{ - allShopifyProduct { - edges { - node { - id - images { - localFile { - childImageSharp { - ...ImageFragment - } - } - } - } - } - } +module.exports = { + plugins: [ + { + resolve: "gatsby-source-shopify", + options: { + apiKey: process.env.SHOPIFY_ADMIN_API_KEY, + password: process.env.SHOPIFY_ADMIN_PASSWORD, + storeUrl: process.env.SHOPIFY_STORE_URL, + downloadImages: true, + }, + }, + "gatsby-plugin-image", + ], } ``` -Full example: +This will make the build take longer but will make images appear on your page faster at runtime. If you use this option, you can query for your image data like this. ```graphql -{ - allShopifyProduct { - edges { - node { +products: allShopifyProduct( + sort: { fields: [publishedAt], order: ASC } +) { + edges { + node { + id + storefrontId + featuredImage { id - images { - localFile { - childImageSharp { - resolutions(width: 500, height: 300) { - ...GatsbyImageSharpResolutions_withWebp - } - } + localFile { + childImageSharp { + gatsbyImageData(width: 910, height: 910, placeholder: BLURRED) } } + altText } } } } ``` -To learn more about image processing, check the documentation of -[gatsby-plugin-sharp][gatsby-plugin-sharp]. - -## Site's `gatsby-node.js` example +Then you would use `gatsby-plugin-image` to render the image: ```js -const path = require("path") +import { GatsbyImage, getImage } from "gatsby-plugin-image" -exports.createPages = async ({ graphql, actions }) => { - const { createPage } = actions +function ProductListing(product) { + const image = getImage(product.featuredImage.localFile) - const pages = await graphql(` - { - allShopifyProduct { - edges { - node { - id - handle - } - } - } - } - `) - - pages.data.allShopifyProduct.edges.forEach(edge => { - createPage({ - path: `/${edge.node.handle}`, - component: path.resolve("./src/templates/product.js"), - context: { - id: edge.node.id, - }, - }) - }) + return } ``` -## shopifyQueries Defaults +
-The following can be used in gatsby-config.js to override the default queries. +## Limitations -```js -shopifyQueries: { - articles: ` - query GetArticles($first: Int!, $after: String) { - articles(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - author { - bio - email - firstName - lastName - name - } - blog { - id - } - comments(first: 250) { - edges { - node { - author { - email - name - } - content - contentHtml - id - } - } - } - content - contentHtml - excerpt - excerptHtml - id - handle - image { - altText - id - src - } - publishedAt - tags - title - url - seo { - title - description - } - } - } - } - } - `, - blogs: ` - query GetBlogs($first: Int!, $after: String) { - blogs(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - id - handle - title - url - } - } - } - } - `, - collections: ` - query GetCollections($first: Int!, $after: String) { - collections(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - description - descriptionHtml - handle - id - image { - altText - id - src - } - products(first: 250) { - edges { - node { - id - } - } - } - title - updatedAt - } - } - } - } - `, - products: ` - query GetProducts($first: Int!, $after: String) { - products(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - availableForSale - createdAt - description - descriptionHtml - handle - id - images(first: 250) { - edges { - node { - id - altText - originalSrc - } - } - } - metafields(first: 250) { - edges { - node { - description - id - key - namespace - value - valueType - } - } - } - onlineStoreUrl - options { - id - name - values - } - priceRange { - minVariantPrice { - amount - currencyCode - } - maxVariantPrice { - amount - currencyCode - } - } - productType - publishedAt - tags - title - updatedAt - variants(first: 250) { - edges { - node { - availableForSale - compareAtPrice - compareAtPriceV2 { - amount - currencyCode - } - id - image { - altText - id - originalSrc - } - metafields(first: 250) { - edges { - node { - description - id - key - namespace - value - valueType - } - } - } - price - priceV2 { - amount - currencyCode - } - requiresShipping - selectedOptions { - name - value - } - sku - title - weight - weightUnit - presentmentPrices(first: 250) { - edges { - node { - price { - amount - currencyCode - } - compareAtPrice { - amount - currencyCode - } - } - } - } - } - } - } - vendor - } - } - } - } - `, - shopPolicies: ` - query GetPolicies { - shop { - privacyPolicy { - body - id - title - url - } - refundPolicy { - body - id - title - url - } - termsOfService { - body - id - title - url - } - } - } - `, - pages: ` - query GetPages($first: Int!, $after: String) { - pages(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - id - handle - title - body - bodySummary - updatedAt - url - } - } - } - } - `, -}, -``` +The bulk API was chosen for resiliency, but it comes with some limitations. For a given store + app combination, only one bulk operation can be run at a time, so this plugin will wait for in-progress operations to complete. If your store contains a lot of data and there are multiple developers doing a clean build at the same time, they could be waiting on each other for a significant period of time. + +
+ +## Development + +This is a yarn workspace with the plugin code in a `plugin/` folder and a test Gatsby site in the `test-site/` folder. After cloning the repo, you can run `yarn` from the project root and all dependencies for both the plugin and the test site will be installed. Then you compile the plugin in watch mode and run the test site. In other words, + +1. From the project root, run `yarn` +1. `cd plugin` +1. `yarn watch` +1. Open a new terminal window to the `test-site/` folder +1. `yarn start` + +Subsequent builds will be incremental unless you run `yarn clean` from the `test-site/` folder to clear Gatsby's cache. + +You can also test an incremental build without restarting the test site by running `yarn refresh` from the `test-site/` folder. -## A note on customer information +[bulk-operations]: https://shopify.dev/tutorials/perform-bulk-operations-with-admin-api +[gatsby-plugin-image]: https://www.npmjs.com/package/gatsby-plugin-image -Not all Shopify nodes have been implemented as they are not necessary for the -static portion of a Gatsby-generated website. This includes any node that -contains sensitive customer-specific information, such as `Order` and -`Payment`. +
-If you are in need of this data (e.g. building a private, internal website), -please open an issue. Until then, the nodes will not be implemented to lessen -the chances of someone accidentally making private information publicly -available. +## Migrating from v4 to v5 -[gatsby]: https://www.gatsbyjs.org/ -[shopify]: https://www.shopify.com/ -[shopify-storefront-api]: https://help.shopify.com/api/custom-storefronts/storefront-api -[graphql-inline-fragments]: https://graphql.org/learn/queries/#inline-fragments -[gatsby-plugin-sharp]: https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-plugin-sharp -[gatsby-image-fragments]: https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-image#gatsby-transformer-sharp +We don't currently have a migration guide but you can find some tips in [this issue](https://github.com/gatsbyjs/gatsby-source-shopify/issues/151). Please read through it and add a comment with any additional information you might have. diff --git a/packages/gatsby-source-shopify/__tests__/fixtures.ts b/packages/gatsby-source-shopify/__tests__/fixtures.ts new file mode 100644 index 0000000000000..2c4d97533b795 --- /dev/null +++ b/packages/gatsby-source-shopify/__tests__/fixtures.ts @@ -0,0 +1,59 @@ +import { + GraphQLContext, + GraphQLRequest, + ResponseResolver, + graphql, + ResponseComposition, + MockedResponse, +} from "msw" + +type Resolver = ResponseResolver< + GraphQLRequest>, + GraphQLContext, + any +> + +export function resolveOnce(data: T): Resolver { + return (_req, res, ctx): MockedResponse | Promise => + res.once(ctx.data(data)) +} + +export function resolve(data: T): Resolver { + return (_req, res, ctx): MockedResponse | Promise => + res(ctx.data(data)) +} + +export function currentBulkOperation( + status: BulkOperationStatus +): Record { + return { + currentBulkOperation: { + id: ``, + status, + }, + } +} + +type BulkNodeOverrides = { + [key in keyof BulkOperationNode]?: BulkOperationNode[key] +} + +export const startOperation = (overrides: BulkNodeOverrides = {}): any => { + const { id = `12345` } = overrides + + return graphql.mutation( + `INITIATE_BULK_OPERATION`, + resolve({ + bulkOperationRunQuery: { + bulkOperation: { + id, + objectCount: `0`, + query: ``, + status: `CREATED`, + url: ``, + }, + userErrors: [], + }, + }) + ) +} diff --git a/packages/gatsby-source-shopify/__tests__/get-shopify-image.ts b/packages/gatsby-source-shopify/__tests__/get-shopify-image.ts new file mode 100644 index 0000000000000..545f0a383b0f7 --- /dev/null +++ b/packages/gatsby-source-shopify/__tests__/get-shopify-image.ts @@ -0,0 +1,23 @@ +import { getShopifyImage } from "../src/get-shopify-image" + +const image = { + originalSrc: `https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero.jpg?v=1460125603`, + width: 2048, + height: 1535, +} + +describe(`the getShopifyImage helper`, () => { + it(`generates an imagedata object`, () => { + const data = getShopifyImage({ image, layout: `fullWidth` }) + expect(data?.images?.fallback?.srcSet).toMatchInlineSnapshot(` + "https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_320x240_crop_center.jpg?v=1460125603 320w, + https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_654x490_crop_center.jpg?v=1460125603 654w, + https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_768x576_crop_center.jpg?v=1460125603 768w, + https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_1024x768_crop_center.jpg?v=1460125603 1024w, + https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_1366x1024_crop_center.jpg?v=1460125603 1366w, + https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_1600x1199_crop_center.jpg?v=1460125603 1600w, + https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_1920x1439_crop_center.jpg?v=1460125603 1920w, + https://cdn.shopify.com/s/files/1/0854/5382/products/front-hero_2048x1535_crop_center.jpg?v=1460125603 2048w" + `) + }) +}) diff --git a/packages/gatsby-source-shopify/__tests__/make-source-from-operation.ts b/packages/gatsby-source-shopify/__tests__/make-source-from-operation.ts new file mode 100644 index 0000000000000..6078d34fbead8 --- /dev/null +++ b/packages/gatsby-source-shopify/__tests__/make-source-from-operation.ts @@ -0,0 +1,919 @@ +import { graphql, rest } from "msw" +import { setupServer } from "msw/node" +import { SourceNodesArgs } from "gatsby" +import { shiftLeft } from "shift-left" + +import { makeSourceFromOperation } from "../src/make-source-from-operation" +import { createOperations } from "../src/operations" +import { pluginErrorCodes } from "../src/errors" + +import { + resolve, + resolveOnce, + currentBulkOperation, + startOperation, +} from "./fixtures" + +const server = setupServer() + +// @ts-ignore +global.setTimeout = (fn: Promise): Promise => fn() + +jest.mock(`gatsby-source-filesystem`, () => { + return { + createRemoteFileNode: jest.fn().mockResolvedValue({ id: `12345` }), + } +}) + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +describe(`The collections operation`, () => { + const firstId = `gid://shopify/Collection/12345` + const secondId = `gid://shopify/Collection/54321` + const firstProductId = `gid://shopify/Product/22345` + const secondProductId = `gid://shopify/Product/32345` + const thirdProductId = `gid://shopify/Product/64321` + + const bulkResults = [ + { + id: firstId, + }, + { + id: firstProductId, + __parentId: firstId, + }, + { + id: secondId, + }, + { + id: secondProductId, + __parentId: firstId, + }, + { + id: thirdProductId, + __parentId: secondId, + }, + ] + + beforeEach(() => { + server.use( + graphql.query( + `OPERATION_STATUS`, + resolveOnce(currentBulkOperation(`COMPLETED`)) + ), + startOperation(), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolveOnce({ + node: { + status: `CREATED`, + id: ``, + objectCount: `0`, + query: ``, + url: ``, + }, + }) + ), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `COMPLETED`, + id: `12345`, + objectCount: `1`, + query: ``, + url: `http://results.url`, + }, + }) + ), + rest.get(`http://results.url`, (_req, res, ctx) => + res(ctx.text(bulkResults.map(r => JSON.stringify(r)).join(`\n`))) + ) + ) + }) + + it(`attaches product IDs to collection nodes`, async () => { + const createNode = jest.fn() + const createNodeId = jest.fn().mockImplementation(id => id) + + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode, + }, + createContentDigest: jest.fn(), + createNodeId, + reporter: { + info: jest.fn(), + error: jest.fn(), + panic: jest.fn(), + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + downloadImages: true, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.createCollectionsOperation) + + expect(createNode).toHaveBeenCalledTimes(2) + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstId, + productIds: expect.arrayContaining([firstProductId, secondProductId]), + }) + ) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: secondId, + productIds: expect.arrayContaining([thirdProductId]), + }) + ) + }) +}) + +describe(`When polling an operation`, () => { + const id = `54321` + + beforeEach(() => { + server.use( + graphql.query( + `OPERATION_STATUS`, + resolveOnce(currentBulkOperation(`COMPLETED`)) + ), + startOperation({ id }), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolveOnce({ + node: { + status: `CREATED`, + id, + objectCount: `0`, + query: ``, + url: ``, + }, + }) + ), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolveOnce({ + node: { + status: `RUNNING`, + id, + objectCount: `1`, + query: ``, + url: `http://results.url`, + }, + }) + ), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `COMPLETED`, + id, + objectCount: `1`, + query: ``, + url: `http://results.url`, + }, + }) + ), + rest.get(`http://results.url`, (_req, res, ctx) => + res(ctx.text(JSON.stringify({ id: `gid://shopify/Product/12345` }))) + ) + ) + }) + + it(`reports status changes`, async () => { + const setStatus = jest.fn() + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode: jest.fn(), + }, + createContentDigest: jest.fn(), + createNodeId: jest.fn(), + reporter: { + info: jest.fn(), + error: jest.fn(), + panic: jest.fn(), + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus, + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + downloadImages: true, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.createProductsOperation) + + expect(setStatus).toHaveBeenCalledWith(shiftLeft` + Polling bulk operation: ${id} + Status: RUNNING + Object count: 1 + `) + }) +}) + +describe(`When downloading images`, () => { + const bulkResult = { + id: `gid://shopify/Product/12345`, + featuredMedia: { + preview: { + image: { + originalSrc: `http://www.example.com/some-image.jpg`, + }, + }, + }, + } + + beforeEach(() => { + server.use( + graphql.query( + `OPERATION_STATUS`, + resolveOnce(currentBulkOperation(`COMPLETED`)) + ), + startOperation(), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `COMPLETED`, + id: ``, + objectCount: `1`, + query: ``, + url: `http://results.url`, + }, + }) + ), + rest.get(`http://results.url`, (_req, res, ctx) => + res(ctx.text(JSON.stringify(bulkResult))) + ) + ) + }) + + it(`links a local file to the featured media`, async () => { + const createNode = jest.fn() + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode, + }, + createContentDigest: jest.fn(), + createNodeId: jest.fn(), + reporter: { + info: jest.fn(), + error: jest.fn(), + panic: jest.fn(), + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + downloadImages: true, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.createProductsOperation, true) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ + shopifyId: bulkResult.id, + featuredMedia: { + preview: { + image: expect.objectContaining({ + localFile: `12345`, + }), + }, + }, + }) + ) + }) +}) + +describe(`A production build`, () => { + const bulkResult = { id: `gid://shopify/Product/12345` } + + beforeEach(() => { + server.use( + graphql.query( + `OPERATION_STATUS`, + resolveOnce(currentBulkOperation(`RUNNING`)) + ), + graphql.mutation( + `CANCEL_OPERATION`, + resolve({ + bulkOperationCancel: { + bulkOperation: { + id: ``, + status: `CANCELING`, + objectCount: `0`, + url: ``, + query: ``, + }, + userErrors: [], + }, + }) + ), + graphql.query( + `OPERATION_STATUS`, + resolve(currentBulkOperation(`CANCELED`)) + ), + startOperation() + ) + }) + + it(`panics if it finds itself canceled`, async () => { + server.use( + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `CANCELED`, + id: ``, + objectCount: `0`, + query: ``, + url: ``, + }, + }) + ) + ) + const panic = jest.fn() + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode: jest.fn(), + }, + createContentDigest: jest.fn(), + createNodeId: jest.fn(), + reporter: { + info: jest.fn(), + error: jest.fn(), + panic, + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.createProductsOperation, true) + + expect(panic).toHaveBeenCalledWith( + expect.objectContaining({ id: pluginErrorCodes.apiConflict }) + ) + }) + + it(`cancels other operations in progress`, async () => { + server.use( + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `COMPLETED`, + id: ``, + objectCount: `1`, + query: ``, + url: `http://results.url`, + }, + }) + ), + rest.get(`http://results.url`, (_req, res, ctx) => + res(ctx.text(JSON.stringify(bulkResult))) + ) + ) + const createNode = jest.fn() + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode, + }, + createContentDigest: jest.fn(), + createNodeId: jest.fn(), + reporter: { + info: jest.fn(), + error: jest.fn(), + panic: jest.fn(), + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.createProductsOperation, true) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ shopifyId: bulkResult.id }) + ) + }) +}) + +describe(`When an operation gets canceled`, () => { + const bulkResult = { id: `gid://shopify/Product/12345` } + + beforeEach(() => { + server.use( + graphql.query( + `OPERATION_STATUS`, + resolve(currentBulkOperation(`COMPLETED`)) + ), + startOperation(), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolveOnce({ + node: { + status: `CANCELED`, + id: ``, + objectCount: `0`, + query: ``, + url: ``, + }, + }) + ), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `COMPLETED`, + id: ``, + objectCount: `1`, + query: ``, + url: `http://results.url`, + }, + }) + ), + rest.get(`http://results.url`, (_req, res, ctx) => + res(ctx.text(JSON.stringify(bulkResult))) + ) + ) + }) + + it(`tries again`, async () => { + const createNode = jest.fn() + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode, + }, + createContentDigest: jest.fn(), + createNodeId: jest.fn(), + reporter: { + info: jest.fn(), + error: jest.fn(), + panic: jest.fn(), + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.createProductsOperation) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ shopifyId: bulkResult.id }) + ) + }) +}) + +describe(`When an operation fails with bad credentials`, () => { + beforeEach(() => { + server.use( + graphql.query( + `OPERATION_STATUS`, + resolve(currentBulkOperation(`COMPLETED`)) + ), + startOperation(), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `FAILED`, + id: ``, + objectCount: `0`, + query: ``, + url: ``, + errorCode: `ACCESS_DENIED`, + }, + }) + ) + ) + }) + + it(`panics and reports the error code`, async () => { + const panic = jest.fn() + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode: jest.fn(), + }, + reporter: { + info: jest.fn(), + error: jest.fn(), + panic, + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.createProductsOperation) + expect(panic).toHaveBeenCalledWith( + expect.objectContaining({ + id: pluginErrorCodes.unknownSourcingFailure, + context: { + sourceMessage: expect.stringContaining(`ACCESS_DENIED`), + }, + }) + ) + }) +}) + +describe(`The incremental products processor`, () => { + const firstProductId = `gid://shopify/Product/22345` + const firstImageId = `gid://shopify/ProductImage/33333` + const secondImageId = `gid://shopify/ProductImage/44444` + const firstVariantId = `gid://shopify/ProductVariant/11111` + const secondVariantId = `gid://shopify/ProductVariant/22222` + const firstMetadataId = `gid://shopify/Metafield/12345` + const secondMetadataId = `gid://shopify/Metafield/12346` + + const bulkResults = [ + { + id: firstProductId, + }, + { + id: firstVariantId, + __parentId: firstProductId, + }, + { + id: firstMetadataId, + __parentId: firstVariantId, + }, + { + id: firstImageId, + __parentId: firstProductId, + }, + ] + + beforeEach(() => { + server.use( + graphql.query( + `OPERATION_STATUS`, + resolveOnce(currentBulkOperation(`COMPLETED`)) + ), + startOperation(), + graphql.query<{ node: BulkOperationNode }>( + `OPERATION_BY_ID`, + resolve({ + node: { + status: `COMPLETED`, + id: `12345`, + objectCount: `2`, + query: ``, + url: `http://results.url`, + }, + }) + ), + rest.get(`http://results.url`, (_req, res, ctx) => + res(ctx.text(bulkResults.map(r => JSON.stringify(r)).join(`\n`))) + ) + ) + }) + + it(`deletes variants belonging to the products`, async () => { + const createNode = jest.fn() + const deleteNode = jest.fn() + const createNodeId = jest.fn().mockImplementation(id => id) + + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + }, + actions: { + createNode, + deleteNode, + }, + createContentDigest: jest.fn(), + createNodeId, + reporter: { + info: jest.fn(), + error: jest.fn(), + panic: jest.fn(), + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + getNodesByType: jest.fn().mockImplementation((nodeType: string) => { + switch (nodeType) { + case `ShopifyProductVariant`: + return [ + { + id: firstVariantId, + productId: firstProductId, + }, + { + id: secondVariantId, + productId: firstProductId, + }, + ] + case `ShopifyProductVariantMetafield`: + return [ + { + id: firstMetadataId, + productVariantId: firstVariantId, + }, + { + id: secondMetadataId, + productVariantId: secondVariantId, + }, + ] + case `ShopifyProductImage`: + return [ + { + id: firstImageId, + productId: firstProductId, + }, + { + id: secondImageId, + productId: firstProductId, + }, + ] + default: + return [] + } + }), + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + downloadImages: true, + } + const operations = createOperations(options, gatsbyApi()) + + const sourceFromOperation = makeSourceFromOperation( + operations.finishLastOperation, + operations.completedOperation, + operations.cancelOperationInProgress, + gatsbyApi(), + options + ) + + await sourceFromOperation(operations.incrementalProducts(new Date())) + + expect(createNode).toHaveBeenCalledTimes(4) + expect(deleteNode).toHaveBeenCalledTimes(6) + + expect(deleteNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstVariantId, + productId: firstProductId, + }) + ) + + expect(deleteNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: secondVariantId, + productId: firstProductId, + }) + ) + + expect(deleteNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstMetadataId, + productVariantId: firstVariantId, + }) + ) + + expect(deleteNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: secondMetadataId, + productVariantId: secondVariantId, + }) + ) + + expect(deleteNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstImageId, + productId: firstProductId, + }) + ) + + expect(deleteNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: secondImageId, + productId: firstProductId, + }) + ) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstImageId, + productId: firstProductId, + }) + ) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstMetadataId, + productVariantId: firstVariantId, + }) + ) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstVariantId, + productId: firstProductId, + }) + ) + + expect(createNode).toHaveBeenCalledWith( + expect.objectContaining({ + id: firstProductId, + }) + ) + }) +}) diff --git a/packages/gatsby-source-shopify/__tests__/node-builder.ts b/packages/gatsby-source-shopify/__tests__/node-builder.ts new file mode 100644 index 0000000000000..8f29b12013f82 --- /dev/null +++ b/packages/gatsby-source-shopify/__tests__/node-builder.ts @@ -0,0 +1,86 @@ +import { SourceNodesArgs, NodeInput } from "gatsby" +import { createRemoteFileNode } from "gatsby-source-filesystem" + +jest.mock(`gatsby-source-filesystem`, () => { + return { + createRemoteFileNode: jest.fn().mockResolvedValue({ + id: `12345`, + }), + } +}) + +import { processorMap } from "../src/node-builder" + +describe(`When a variant has an image set`, () => { + const node = { + image: { + id: `foo1`, + originalSrc: `https://via.placeholder.com/100x100`, + }, + id: `foo2`, + internal: { + type: `foo3`, + contentDigest: `foo4`, + }, + } as NodeInput + + const createNode = jest.fn() + const gatsbyApiMock = jest.fn().mockImplementation(() => { + return { + cache: { + set: jest.fn(), + get: jest.fn(), + }, + actions: { + createNode, + }, + createContentDigest: jest.fn(), + createNodeId: jest.fn(), + store: jest.fn(), + reporter: { + info: jest.fn(), + error: jest.fn(), + panic: jest.fn(), + activityTimer: (): Record => { + return { + start: jest.fn(), + end: jest.fn(), + setStatus: jest.fn(), + } + }, + }, + } + }) + + const gatsbyApi = gatsbyApiMock as jest.Mock + + describe(`and options are set, not to download images`, () => { + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + } + + it(`doesn't create localFile on the node.`, async () => { + await processorMap.ProductVariant(node, gatsbyApi(), options) + const mock = createRemoteFileNode as jest.Mock + expect(mock).not.toHaveBeenCalled() + }) + }) + + describe(`and options are set to download images`, () => { + const options = { + apiKey: ``, + password: ``, + storeUrl: `my-shop.shopify.com`, + downloadImages: true, + } + + it(`creates localFile on the node.`, async () => { + await processorMap.ProductVariant(node, gatsbyApi(), options) + const mock = createRemoteFileNode as jest.Mock + expect(mock).toHaveBeenCalled() + expect(node.image).toHaveProperty(`localFile`) + }) + }) +}) diff --git a/packages/gatsby-source-shopify/__tests__/tsconfig.json b/packages/gatsby-source-shopify/__tests__/tsconfig.json new file mode 100644 index 0000000000000..1f1ebf22c7883 --- /dev/null +++ b/packages/gatsby-source-shopify/__tests__/tsconfig.json @@ -0,0 +1,6 @@ +{ + "include": ["../**/*.ts"], + "compilerOptions": { + "outDir": "../temp" + } +} diff --git a/packages/gatsby-source-shopify/package.json b/packages/gatsby-source-shopify/package.json index b18903a8a1360..2cffd339bdd19 100644 --- a/packages/gatsby-source-shopify/package.json +++ b/packages/gatsby-source-shopify/package.json @@ -1,49 +1,43 @@ { "name": "gatsby-source-shopify", - "version": "4.8.0-next.1", + "version": "5.0.0-next.0", "description": "Gatsby source plugin for building websites using Shopify as a data source.", "scripts": { - "build": "babel src --out-dir . --ignore \"**/__tests__\"", - "prepare": "cross-env NODE_ENV=production npm run build", - "watch": "npm run build -- --watch" + "watch": "tsc-watch --outDir .", + "build": "tsc --outDir .", + "prepublish": "yarn build && cp ../README.md ." }, - "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-source-shopify#readme", "repository": { "type": "git", "url": "https://github.com/gatsbyjs/gatsby.git", "directory": "packages/gatsby-source-shopify" }, + "author": "Sam Slotsky , Daniel Lew ", + "license": "MIT", "bugs": { "url": "https://github.com/gatsbyjs/gatsby/issues" }, - "keywords": [ - "gatsby", - "gatsby-plugin", - "gatsby-source-plugin", - "shopify" - ], - "author": "Angelo Ashmore", - "license": "MIT", - "peerDependencies": { - "gatsby": "^3.0.0-next.0" - }, - "devDependencies": { - "@babel/cli": "^7.14.0", - "@babel/core": "^7.14.0", - "cross-env": "^7.0.3" - }, + "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-source-shopify#readme", "dependencies": { - "babel-preset-gatsby-package": "^1.8.0-next.1", - "chalk": "^4.1.0", - "gatsby-node-helpers": "^0.3.0", - "gatsby-source-filesystem": "^3.8.0-next.1", - "graphql-request": "^1.8.2", - "lodash": "^4.17.21", - "p-iteration": "^1.1.8", - "prettyjson": "^1.2.1" + "gatsby-core-utils": "^2.8.0-next.0", + "gatsby-source-filesystem": "^3.8.0-next.0", + "node-fetch": "^2.6.1", + "sharp": "^0.28.0", + "shift-left": "^0.1.5" }, - "engines": { - "node": ">=12.13.0" + "devDependencies": { + "@types/jest": "^26.0.20", + "@types/node": "^14.14.34", + "@types/node-fetch": "^2.5.8", + "@types/sharp": "^0.28.0", + "gatsby-plugin-image": "^1.8.0-next.0", + "msw": "^0.27.1", + "prettier": "^2.2.1", + "prettier-check": "^2.0.0", + "tsc-watch": "^4.2.9", + "typescript": "^4.2.3" }, - "main": "index.js" + "peerDependencies": { + "gatsby-plugin-image": "^1.1.0" + } } diff --git a/packages/gatsby-source-shopify/src/__tests__/__snapshots__/index.js.snap b/packages/gatsby-source-shopify/src/__tests__/__snapshots__/index.js.snap deleted file mode 100644 index fa5517a35f3ad..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/__snapshots__/index.js.snap +++ /dev/null @@ -1,3083 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`gatsby-source-shopify Generates nodes 1`] = ` -Object { - "Shopify__Blog__Z2lkOi8vc2hvcGlmeS9CbG9nLzEyNzIzNDg2OTc=": Object { - "children": Array [], - "id": "Shopify__Blog__Z2lkOi8vc2hvcGlmeS9CbG9nLzEyNzIzNDg2OTc=", - "internal": Object { - "contentDigest": "b796c451cadc7db2c68166806ef0a774", - "type": "ShopifyBlog", - }, - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9CbG9nLzEyNzIzNDg2OTc=", - "title": "News", - "url": "https://gatsby-swag.myshopify.com/blogs/news", - }, - "Shopify__Collection__Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzMzNzc1NzQ3MTYw": Object { - "children": Array [], - "description": "The items in this collection are offered to contributors for free as a token of our appreciate for contributing to Gatsby.", - "descriptionHtml": "The items in this collection are offered to contributors for free as a token of our appreciate for contributing to Gatsby.", - "handle": "contributor-swag", - "id": "Shopify__Collection__Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzMzNzc1NzQ3MTYw", - "image": null, - "internal": Object { - "contentDigest": "26722d264ec8614eee54f73815a1a509", - "type": "ShopifyCollection", - }, - "parent": "__SOURCE__", - "products___NODE": Array [ - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA==", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTU2NTI0NDA=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTUyNTkyMjQ=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzM4MTkwNDIxNzcxMTI=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwNTg0MDQxNjc3Njg=", - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzMzNzc1NzQ3MTYw", - "title": "Contributor Swag", - "updatedAt": "2019-08-09T07:35:01Z", - }, - "Shopify__Collection__Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzcwNTQwMDAxNTM=": Object { - "children": Array [], - "description": "", - "descriptionHtml": "", - "handle": "frontpage", - "id": "Shopify__Collection__Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzcwNTQwMDAxNTM=", - "image": null, - "internal": Object { - "contentDigest": "edd8cca27a08ef6a2e844638aa6e8db5", - "type": "ShopifyCollection", - }, - "parent": "__SOURCE__", - "products___NODE": Array [ - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzEzMTIxMjA1Njk5NDQ=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA==", - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzcwNTQwMDAxNTM=", - "title": "Home page", - "updatedAt": "2019-08-09T07:35:01Z", - }, - "Shopify__Collection__Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzg4MjE2NzY0NTA0": Object { - "children": Array [], - "description": "", - "descriptionHtml": "", - "handle": "level-2", - "id": "Shopify__Collection__Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzg4MjE2NzY0NTA0", - "image": null, - "internal": Object { - "contentDigest": "2f9de13f7b583e455579c10d1a26c2e9", - "type": "ShopifyCollection", - }, - "parent": "__SOURCE__", - "products___NODE": Array [ - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MjM0MTkzNDA4ODg=", - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzg4MjE2NzY0NTA0", - "title": "Level 2", - "updatedAt": "2019-08-08T16:05:04Z", - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE3NTY2ODU1MDA1MDQ=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE3NTY2ODU1MDA1MDQ=", - "internal": Object { - "contentDigest": "4d8c66df4e7da219ce29b45d2051fad1", - "type": "ShopifyProductOption", - }, - "name": "Title", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE3NTY2ODU1MDA1MDQ=", - "values": Array [ - "Default Title", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODI3MDcyODg=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODI3MDcyODg=", - "internal": Object { - "contentDigest": "2e9069356a7e121c17168852f34a24e0", - "type": "ShopifyProductOption", - }, - "name": "Title", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODI3MDcyODg=", - "values": Array [ - "Default Title", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODMyNjQzNDQ=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODMyNjQzNDQ=", - "internal": Object { - "contentDigest": "0818971c1f5532b744e30bc94cd79fa9", - "type": "ShopifyProductOption", - }, - "name": "Title", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODMyNjQzNDQ=", - "values": Array [ - "Default Title", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNDY4ODA2Njc3MzY=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNDY4ODA2Njc3MzY=", - "internal": Object { - "contentDigest": "3ff8a67ed3695804a8d2577d2a6db2f7", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNDY4ODA2Njc3MzY=", - "values": Array [ - "SM — Women", - "MD - Women", - "LG — Women", - "XL - Women", - "SM — Unisex", - "MD - Unisex", - "LG — Unisex", - "XL — Unisex", - "2XL — Unisex", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNTAwNjU2ODQ1Njg=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNTAwNjU2ODQ1Njg=", - "internal": Object { - "contentDigest": "90568d8f206a4614bd438239597a3489", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNTAwNjU2ODQ1Njg=", - "values": Array [ - "One size fits most", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI2OTk3ODgzODYzOTI=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI2OTk3ODgzODYzOTI=", - "internal": Object { - "contentDigest": "c2e66d7f53c9456a2f5625ad29afeae1", - "type": "ShopifyProductOption", - }, - "name": "Title", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI2OTk3ODgzODYzOTI=", - "values": Array [ - "Default Title", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjg2ODMwOTY=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjg2ODMwOTY=", - "internal": Object { - "contentDigest": "3a4835fa4ae571f43550cdd98a7f62ab", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjg2ODMwOTY=", - "values": Array [ - "XS - Unisex", - "SM - Unisex", - "MD - Unisex", - "L - Unisex", - "XL - Unisex", - "2XL - Unisex", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjk2MzMzNjg=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjk2MzMzNjg=", - "internal": Object { - "contentDigest": "b8195058dbbf0e598fb855afefc887ae", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjk2MzMzNjg=", - "values": Array [ - "XS - Unisex", - "SM - Unisex", - "MD - Unisex", - "L - Unisex", - "XL - Unisex", - "2XL - Unisex", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3NzExMDcwODY0MjQ=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3NzExMDcwODY0MjQ=", - "internal": Object { - "contentDigest": "da26fd3cb460052d35cb8321a34d4df2", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3NzExMDcwODY0MjQ=", - "values": Array [ - "XS - Unisex", - "S - Unisex", - "M - Unisex", - "L - Unisex", - "XL - Unisex", - "2XL - Unisex", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3OTYxNTk3NjI1MjA=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3OTYxNTk3NjI1MjA=", - "internal": Object { - "contentDigest": "75c0d5cbea3abf149d39cbbaadc9497c", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3OTYxNTk3NjI1MjA=", - "values": Array [ - "S - Men", - "MD - Men", - "LG - Men", - "XL - Men", - "2XL - Men", - "3XL - Men", - "S - Womens", - "MD - Womens", - "LG - Womens", - "XL - Womens", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4NTY2MDg5ODkyNzI=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4NTY2MDg5ODkyNzI=", - "internal": Object { - "contentDigest": "38bb3f25c9d97a273cf437717cdd725b", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4NTY2MDg5ODkyNzI=", - "values": Array [ - "X-Small", - "Small", - "Medium", - "Large", - "X-Large", - "XX-Large", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4ODQ2NjU2NzE3Njg=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4ODQ2NjU2NzE3Njg=", - "internal": Object { - "contentDigest": "985a9913d4cda3f1e32cb352cd2efe55", - "type": "ShopifyProductOption", - }, - "name": "Title", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4ODQ2NjU2NzE3Njg=", - "values": Array [ - "Default Title", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzIxMzUwODQ1NjQ1Njg=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzIxMzUwODQ1NjQ1Njg=", - "internal": Object { - "contentDigest": "aeb018e8694217e289c7591351f67fba", - "type": "ShopifyProductOption", - }, - "name": "Title", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzIxMzUwODQ1NjQ1Njg=", - "values": Array [ - "Default Title", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzMwNTEwMTk5NjA1Nw==": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzMwNTEwMTk5NjA1Nw==", - "internal": Object { - "contentDigest": "b50652dfd63bcb98c4ab73149d72246f", - "type": "ShopifyProductOption", - }, - "name": "Size", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzMwNTEwMTk5NjA1Nw==", - "values": Array [ - "SM — Women", - "MD — Women", - "LG — Women", - "XL - Women", - "SM - Unisex", - "MD - Unisex", - "LG — Unisex", - "XL — Unisex", - "2XL — Unisex", - ], - }, - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzQ5OTgzMjAzMjQ2OTY=": Object { - "children": Array [], - "id": "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzQ5OTgzMjAzMjQ2OTY=", - "internal": Object { - "contentDigest": "f987805fb7889d847d4b96f366b93e9a", - "type": "ShopifyProductOption", - }, - "name": "Title", - "parent": "__SOURCE__", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzQ5OTgzMjAzMjQ2OTY=", - "values": Array [ - "Default Title", - ], - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjA5MTc2": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjA5MTc2", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "a3ee401ff2a8db5db5031f54f5e8820d", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "SM — Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjA5MTc2", - "sku": "PT-S-W", - "title": "SM — Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjQxOTQ0": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjQxOTQ0", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "35c6299896a00cfff7d7c18dac0f4eb4", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "LG — Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjQxOTQ0", - "sku": "PT-LG-W", - "title": "LG — Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzA3NDgw": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzA3NDgw", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "b95cc0d09fc67973b61bf6d87bf875e0", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "SM — Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzA3NDgw", - "sku": "PT-SM-M", - "title": "SM — Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzczMDE2": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzczMDE2", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "3d0967c9e5fd64fc9baf31a0273b70fd", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "LG — Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzczMDE2", - "sku": "PT-LG-M", - "title": "LG — Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzNDA1Nzg0": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzNDA1Nzg0", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "02dbf1b3c90354eca2dd4295192a4c36", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL — Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzNDA1Nzg0", - "sku": "PT-XL-M", - "title": "XL — Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjAwMjk0NzYwNTM2": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjAwMjk0NzYwNTM2", - "image": Object { - "altText": "Mockup of the Gatsby socks.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjYyNTI0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-mockup_1.jpg?v=1531343345", - }, - "internal": Object { - "contentDigest": "b70e12f3f05b5b8b14e26652cb0c5c51", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "One size fits most", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjAwMjk0NzYwNTM2", - "sku": "S-ONE", - "title": "One size fits most", - "weight": 3, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzI0OTA0MDI0": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzI0OTA0MDI0", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "0016aad4c9786bf6fd6ffcd6702bcc0a", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "2XL — Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzI0OTA0MDI0", - "sku": "PT-2XL-M", - "title": "2XL — Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzIwODQwNzky": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzIwODQwNzky", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "b7ac2da5e2775c93253624c67a9ce9be", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "2XL — Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzIwODQwNzky", - "sku": "DDT-2XL-M", - "title": "2XL — Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjIzOTc3Mjg3Njg4OA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjIzOTc3Mjg3Njg4OA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDYzMjM1Mjg1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6208.jpg?v=1555548544", - }, - "internal": Object { - "contentDigest": "d3e975028946f23934ff851a76921704", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "2.00", - "priceNumber": 2, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzEzMTIxMjA1Njk5NDQ=", - "selectedOptions": Array [ - Object { - "name": "Title", - "value": "Default Title", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjIzOTc3Mjg3Njg4OA==", - "sku": "STICKERS-01", - "title": "Default Title", - "weight": 1, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjM3NjMyMTUyMzgwMA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjM3NjMyMTUyMzgwMA==", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "edd0bd9b9f486cc781aa1081a392377f", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "LG — Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjM3NjMyMTUyMzgwMA==", - "sku": "DDT-LG-W", - "title": "LG — Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjQ0NTY4NzcwOTc4NA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjQ0NTY4NzcwOTc4NA==", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "dd04923fdfccea530b7720c411168318", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD - Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjQ0NTY4NzcwOTc4NA==", - "sku": "PT-M-W", - "title": "MD - Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0MjA4ODc5Mg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0MjA4ODc5Mg==", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "da5ec3103887a13262573985eaee2010", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0MjA4ODc5Mg==", - "sku": "DDT-MD-M", - "title": "MD - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0NTg1NzExMg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0NTg1NzExMg==", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "b49fd6f96b04c56a0ec8576d128c7590", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "SM - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0NTg1NzExMg==", - "sku": "DDT-SM-M", - "title": "SM - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDMyNjUyMzk5Mg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDMyNjUyMzk5Mg==", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "a48eb1e29980030eba1796f178735e13", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDMyNjUyMzk5Mg==", - "sku": "PT-MD-M", - "title": "MD - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2MTYzMzExMg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2MTYzMzExMg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzEzNTQ0OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4839_002.jpg?v=1546015349", - }, - "internal": Object { - "contentDigest": "031a0c7d6c7a4cd61b80c29149f95c1c", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTUyNTkyMjQ=", - "selectedOptions": Array [ - Object { - "name": "Title", - "value": "Default Title", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2MTYzMzExMg==", - "sku": "1756S", - "title": "Default Title", - "weight": 4, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2NTIwNDgyNA==": Object { - "availableForSale": false, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2NTIwNDgyNA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODA4NTcyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4862.jpg?v=1540860991", - }, - "internal": Object { - "contentDigest": "1e3fbc328de15d4703534a3692e33e08", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTU2NTI0NDA=", - "selectedOptions": Array [ - Object { - "name": "Title", - "value": "Default Title", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2NTIwNDgyNA==", - "sku": "BA540", - "title": "Default Title", - "weight": 4, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xNTM5ODYzOTc5NjMxMg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xNTM5ODYzOTc5NjMxMg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODk5NTI4MjAwOA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4947.png?v=1542402545", - }, - "internal": Object { - "contentDigest": "ff82d5ccc2c81914ae081db2e2e8e096", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "2.00", - "priceNumber": 2, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE1NjYyNjk5OTcxNDQ=", - "selectedOptions": Array [ - Object { - "name": "Title", - "value": "Default Title", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xNTM5ODYzOTc5NjMxMg==", - "sku": "lapelpin", - "title": "Default Title", - "weight": 0.5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTA2ODQx": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTA2ODQx", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "5d026c59b31e1afb5aa1502e41f4a97f", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "SM — Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTA2ODQx", - "sku": "DDT-SM-W", - "title": "SM — Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTM5NjA5": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTM5NjA5", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "c28be7fcd6d7fbdc6eb4eae7a5112fc7", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD — Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTM5NjA5", - "sku": "DDT-MD-W", - "title": "MD — Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MjcwNjgx": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MjcwNjgx", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "676cfb826c6503f9fd7f88b49f5e2384", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "LG — Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MjcwNjgx", - "sku": "DDT-LG-M", - "title": "LG — Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MzAzNDQ5": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MzAzNDQ5", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "90774ef31230bc817339b937734211a4", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL — Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MzAzNDQ5", - "sku": "DDT-XL-M", - "title": "XL — Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTQ1MjAwMDczMTIyNA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTQ1MjAwMDczMTIyNA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODM5MTUxMg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5659.png?v=1543972353", - }, - "internal": Object { - "contentDigest": "6e40fde47dd34c9b42a7d5cebd02abae", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MjM0MTkzNDA4ODg=", - "selectedOptions": Array [ - Object { - "name": "Title", - "value": "Default Title", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTQ1MjAwMDczMTIyNA==", - "sku": "V-12-00002", - "title": "Default Title", - "weight": 6, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTg5Mjk1Mg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTg5Mjk1Mg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613", - }, - "internal": Object { - "contentDigest": "54cc5d9a012455ac0c8b5d12cb8711ac", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XS - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTg5Mjk1Mg==", - "sku": "FLP-1", - "title": "XS - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk1ODQ4OA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk1ODQ4OA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613", - }, - "internal": Object { - "contentDigest": "9603ce48113f4428eaa0fdc4770b9f7f", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk1ODQ4OA==", - "sku": "FLP-3", - "title": "MD - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk5MTI1Ng==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk5MTI1Ng==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613", - }, - "internal": Object { - "contentDigest": "32a38d46cea7aedb829954bffabc9f1b", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "L - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk5MTI1Ng==", - "sku": "FLP-4", - "title": "L - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTkyNTcyMA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTkyNTcyMA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613", - }, - "internal": Object { - "contentDigest": "bdd32030543110e4e2e2235f202e6901", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "SM - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTkyNTcyMA==", - "sku": "FLP-2", - "title": "SM - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjA1Njc5Mg==": Object { - "availableForSale": false, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjA1Njc5Mg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613", - }, - "internal": Object { - "contentDigest": "ced8908979cee42693fdeeae897041c3", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "2XL - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjA1Njc5Mg==", - "sku": "FLP-6", - "title": "2XL - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjAyNDAyNA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjAyNDAyNA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613", - }, - "internal": Object { - "contentDigest": "e12cd1858f654a895284634f99aaafeb", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjAyNDAyNA==", - "sku": "FLP-5", - "title": "XL - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzA3MjYwMA==": Object { - "availableForSale": false, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzA3MjYwMA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022", - }, - "internal": Object { - "contentDigest": "e477def9619ab33fb88008895e1eeb61", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XS - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzA3MjYwMA==", - "sku": "JSJP-1", - "title": "XS - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzE3MDkwNA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzE3MDkwNA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022", - }, - "internal": Object { - "contentDigest": "0b72649db00228f90c39877713c0905f", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "L - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzE3MDkwNA==", - "sku": "JSJP-4", - "title": "L - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEwNTM2OA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEwNTM2OA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022", - }, - "internal": Object { - "contentDigest": "d6a31e8d02ef91df652d8c03c3d2217d", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "SM - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEwNTM2OA==", - "sku": "JSJP-2", - "title": "SM - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEzODEzNg==": Object { - "availableForSale": false, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEzODEzNg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022", - }, - "internal": Object { - "contentDigest": "3101871722c3f260a3811b0739ee1496", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEzODEzNg==", - "sku": "JSJP-3", - "title": "MD - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIwMzY3Mg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIwMzY3Mg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022", - }, - "internal": Object { - "contentDigest": "14d3577481332fee33171db68c549878", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIwMzY3Mg==", - "sku": "JSJP-5", - "title": "XL - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIzNjQ0MA==": Object { - "availableForSale": false, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIzNjQ0MA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022", - }, - "internal": Object { - "contentDigest": "d8422f8b268010cda4c10374cebb1842", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "2XL - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIzNjQ0MA==", - "sku": "JSJP-6", - "title": "2XL - Unisex", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjQ3MzAwMTA0OA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjQ3MzAwMTA0OA==", - "image": Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - "internal": Object { - "contentDigest": "4fc1e1bdd9db19e5c9f60a31588e765e", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL - Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjQ3MzAwMTA0OA==", - "sku": "PT-XL-W", - "title": "XL - Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjUyMjgwODQwOA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjUyMjgwODQwOA==", - "image": Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - "internal": Object { - "contentDigest": "f6a63b58c1ec66377cedfaed21a2e0a9", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL - Women", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjUyMjgwODQwOA==", - "sku": "DDT-XL-W", - "title": "XL - Women", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MTc0MzcwNA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MTc0MzcwNA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115", - }, - "internal": Object { - "contentDigest": "4cae0b23be3894fa0a67cf66bbd3d857", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XS - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MTc0MzcwNA==", - "sku": "AA9590XS", - "title": "XS - Unisex", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MjU5NTY3Mg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MjU5NTY3Mg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115", - }, - "internal": Object { - "contentDigest": "185c427847c680988c7e4d9cbb3e809d", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "S - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MjU5NTY3Mg==", - "sku": "AA9590S", - "title": "S - Unisex", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NjM5Njc2MA==": Object { - "availableForSale": false, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NjM5Njc2MA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115", - }, - "internal": Object { - "contentDigest": "9880bab23e7adc1552ebc2ac00f583fc", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "M - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NjM5Njc2MA==", - "sku": "AA9590M", - "title": "M - Unisex", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0Njc4OTk3Ng==": Object { - "availableForSale": false, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0Njc4OTk3Ng==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115", - }, - "internal": Object { - "contentDigest": "2bdbefa4c461684d753d222c8bdc1976", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "L - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0Njc4OTk3Ng==", - "sku": "AA9590L", - "title": "L - Unisex", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NzI4MTQ5Ng==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NzI4MTQ5Ng==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115", - }, - "internal": Object { - "contentDigest": "d4bb4397a485a4cb5b2286e3d2a7d74e", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NzI4MTQ5Ng==", - "sku": "AA9590XL", - "title": "XL - Unisex", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0ODEzMzQ2NA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0ODEzMzQ2NA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115", - }, - "internal": Object { - "contentDigest": "64b0878287761c063d0a0101faf5a81e", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "2XL - Unisex", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0ODEzMzQ2NA==", - "sku": "AA95902XL", - "title": "2XL - Unisex", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTU5OTk2MA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTU5OTk2MA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "dfe073cf911dba2c1712dc4357093b0f", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "LG - Men", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTU5OTk2MA==", - "sku": "TM98140", - "title": "LG - Men", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTUzNDQyNA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTUzNDQyNA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "91e971a4a8a9a78a70142e0e76575028", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD - Men", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTUzNDQyNA==", - "sku": "TM98139", - "title": "MD - Men", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY2NTQ5Ng==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY2NTQ5Ng==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "1e1bd054b2976f40848b5fa7735a8e17", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL - Men", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY2NTQ5Ng==", - "sku": "TM98141", - "title": "XL - Men", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY5ODI2NA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY5ODI2NA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "cf20f4368a61f4085aebb4951a86f759", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "2XL - Men", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY5ODI2NA==", - "sku": "TM98142", - "title": "2XL - Men", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTc2MzgwMA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTc2MzgwMA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "b46a8f0597e8da6f3c20ad08e7bf8089", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "3XL - Men", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTc2MzgwMA==", - "sku": "TM98143", - "title": "3XL - Men", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY0NTk4MzMyMA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY0NTk4MzMyMA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "48ea6af557034607b6536735c5bc985f", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "S - Womens", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY0NTk4MzMyMA==", - "sku": "TM18135S", - "title": "S - Womens", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY1ODUzMzQ2NA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY1ODUzMzQ2NA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "10cf7cbb14bd1531f1a6d1f2b124a524", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "MD - Womens", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY1ODUzMzQ2NA==", - "sku": "TM18135M", - "title": "MD - Womens", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY3MjQ1OTg2NA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY3MjQ1OTg2NA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "c0ffb0a2893859d36a70bb321e8db710", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "LG - Womens", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY3MjQ1OTg2NA==", - "sku": "TM18135", - "title": "LG - Womens", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY4MDU1MzU2MA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY4MDU1MzU2MA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "31a879ae7af658f6c0c4ce02d2c4506f", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XL - Womens", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY4MDU1MzU2MA==", - "sku": "TM18135", - "title": "XL - Womens", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0Mjc0NTA3Mzc1Mg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0Mjc0NTA3Mzc1Mg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - "internal": Object { - "contentDigest": "b320d5f24acbd889c803704d9e4d9f8b", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "26.00", - "priceNumber": 26, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "S - Men", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0Mjc0NTA3Mzc1Mg==", - "sku": "TM18135S", - "title": "S - Men", - "weight": 7, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY0ODUzNg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY0ODUzNg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165", - }, - "internal": Object { - "contentDigest": "565d432180af8778827f0e68ecd7b342", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "Small", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY0ODUzNg==", - "sku": "6735", - "title": "Small", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY4MTMwNA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY4MTMwNA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165", - }, - "internal": Object { - "contentDigest": "ef2951de88dc0f2af2f703be493c8340", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "Medium", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY4MTMwNA==", - "sku": "6736", - "title": "Medium", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjYxNTc2OA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjYxNTc2OA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165", - }, - "internal": Object { - "contentDigest": "00c6060131140ad9a4d8be5bb8d97c78", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "X-Small", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjYxNTc2OA==", - "sku": "6734", - "title": "X-Small", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc0Njg0MA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc0Njg0MA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165", - }, - "internal": Object { - "contentDigest": "5d98703fae6648dd154e4a26ff7fc9ef", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "Large", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc0Njg0MA==", - "sku": "6737", - "title": "Large", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc3OTYwOA==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc3OTYwOA==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165", - }, - "internal": Object { - "contentDigest": "7572c259afaf6358fff3072d740eda92", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "X-Large", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc3OTYwOA==", - "sku": "6738", - "title": "X-Large", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjgxMjM3Ng==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjgxMjM3Ng==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165", - }, - "internal": Object { - "contentDigest": "942ff4f56f86aaed34eb0907c5a305cc", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "selectedOptions": Array [ - Object { - "name": "Size", - "value": "XX-Large", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjgxMjM3Ng==", - "sku": "6739", - "title": "XX-Large", - "weight": 5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDMxNTMyMDA5MDcxMg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDMxNTMyMDA5MDcxMg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0Mzc1NjcyNjM2MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6192.jpg?v=1555695177", - }, - "internal": Object { - "contentDigest": "7a9842c6c7040f7fbd44f3f78e7881e1", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwNTg0MDQxNjc3Njg=", - "selectedOptions": Array [ - Object { - "name": "Title", - "value": "Default Title", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDMxNTMyMDA5MDcxMg==", - "sku": "SCNCH", - "title": "Default Title", - "weight": 0.5, - "weightUnit": "OUNCES", - }, - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yOTAyOTE1MzQzOTgzMg==": Object { - "availableForSale": true, - "children": Array [], - "compareAtPrice": null, - "id": "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yOTAyOTE1MzQzOTgzMg==", - "image": Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTE2NzQ3MjcyMTkyODg=", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6689.jpg?v=1560443279", - }, - "internal": Object { - "contentDigest": "a97fbe8c6c26de50b281ab9738e6b627", - "type": "ShopifyProductVariant", - }, - "parent": "__SOURCE__", - "price": "10.00", - "priceNumber": 10, - "product___NODE": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzM4MTkwNDIxNzcxMTI=", - "selectedOptions": Array [ - Object { - "name": "Title", - "value": "Default Title", - }, - ], - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yOTAyOTE1MzQzOTgzMg==", - "sku": "jamstackhat", - "title": "Default Title", - "weight": 4, - "weightUnit": "OUNCES", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTU2NTI0NDA=": Object { - "availableForSale": false, - "children": Array [], - "createdAt": "2018-10-30T00:55:50Z", - "description": "Dark grey and black, mesh, 6-panel trucker hat with snapback closure. Made of 100% polyester linen front panels and bill. Care Instructions: Spot clean with a damp cloth.", - "descriptionHtml": "

Dark grey and black, mesh, 6-panel trucker hat with snapback closure. Made of 100% polyester linen front panels and bill.

-

Care Instructions: Spot clean with a damp cloth.

", - "handle": "monogram-trucker-hat", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTU2NTI0NDA=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODA4NTcyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4862.jpg?v=1540860991", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODE1MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4855.jpg?v=1540861007", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODE4NDAyNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4859_002.jpg?v=1540861027", - }, - ], - "internal": Object { - "contentDigest": "819881f63ae1dcd3c5810ef75268fcb5", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODMyNjQzNDQ=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "Hat", - "publishedAt": "2018-10-30T00:55:50Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTU2NTI0NDA=", - "tags": Array [], - "title": "Monogram Trucker Hat", - "updatedAt": "2019-08-05T22:55:26Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2NTIwNDgyNA==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTUyNTkyMjQ=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-10-30T00:52:49Z", - "description": "Add more blazingly blazing speed to your wardrobe with this solid purple laundered chino twill hat. (Fine print: this is just a hat. It will not affect your speed.) Care Instructions: Spot clean with a damp cloth.", - "descriptionHtml": "

Add more blazingly blazing speed to your wardrobe with this solid purple laundered chino twill hat. (Fine print: this is just a hat. It will not affect your speed.)

-

Care Instructions: Spot clean with a damp cloth.

", - "handle": "blazing-purple-hat", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTUyNTkyMjQ=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzEzNTQ0OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4839_002.jpg?v=1546015349", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzMzMjA1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5147.jpg?v=1540860789", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzM5NzU5Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4848.jpg?v=1540860804", - }, - ], - "internal": Object { - "contentDigest": "e3147e4ed64dd3d27402beb7d22da986", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODI3MDcyODg=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "Hat", - "publishedAt": "2018-10-30T00:52:49Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTUyNTkyMjQ=", - "tags": Array [], - "title": "This Purple Hat Is Blazing Fast", - "updatedAt": "2019-08-07T17:39:01Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2MTYzMzExMg==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE1NjYyNjk5OTcxNDQ=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-11-16T19:36:04Z", - "description": "Show us how you rock your custom Gatsby lapel pin! Silver plating, clois-tech lapel pin with a clutch attachment. Size – 0.835” Care Instructions: Please do not machine wash.", - "descriptionHtml": "

Show us how you rock your custom Gatsby lapel pin! Silver plating, clois-tech lapel pin with a clutch attachment. Size – 0.835”

-

Care Instructions: Please do not machine wash.

", - "handle": "gatsby-lapel-pin", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE1NjYyNjk5OTcxNDQ=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODk5NTI4MjAwOA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4947.png?v=1542402545", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODYzNTQ1NjYwMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4972.jpg?v=1542402545", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODYzNjgwMDA4OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4956.jpg?v=1542402545", - }, - ], - "internal": Object { - "contentDigest": "2d0002a3d40b64e5d81dc86f9e9aee47", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzIxMzUwODQ1NjQ1Njg=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "2.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "2.0", - "currencyCode": "USD", - }, - }, - "productType": "Pin", - "publishedAt": "2018-11-16T19:36:04Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE1NjYyNjk5OTcxNDQ=", - "tags": Array [], - "title": "Gatsby Lapel Pin", - "updatedAt": "2019-08-07T13:10:10Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xNTM5ODYzOTc5NjMxMg==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MjM0MTkzNDA4ODg=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-12-05T01:12:21Z", - "description": "Keep your hot beverages BLAZING hot for hours with this stainless vacuum-insulated Fifty/Fifty 12 oz/345mL bottle. Also works to keep cold beverages, er, blazing cold? Care Instructions: Do not put in microwave, freezer, or dishwasher. Hand wash with hot soapy water. Leave cap off and allow to air dry. Do not use cleaners containing bleach or chlorine.", - "descriptionHtml": "

Keep your hot beverages BLAZING hot for hours with this stainless vacuum-insulated Fifty/Fifty 12 oz/345mL bottle. Also works to keep cold beverages, er, blazing cold?

-

Care Instructions: Do not put in microwave, freezer, or dishwasher. Hand wash with hot soapy water. Leave cap off and allow to air dry. Do not use cleaners containing bleach or chlorine.

", - "handle": "12oz-travel-mug", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MjM0MTkzNDA4ODg=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODM5MTUxMg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5659.png?v=1543972353", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODQ1NzA0OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5667.JPG?v=1543972362", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODU1NTM1Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5664.JPG?v=1543972368", - }, - ], - "internal": Object { - "contentDigest": "332cd4baa31218f8ec5e9c7088fc5138", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI2OTk3ODgzODYzOTI=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - }, - "productType": "Water Bottle", - "publishedAt": "2018-12-05T01:12:21Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MjM0MTkzNDA4ODg=", - "tags": Array [], - "title": "Mug for (Blazing) Hot Beverages", - "updatedAt": "2019-08-06T11:22:51Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTQ1MjAwMDczMTIyNA==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-12-28T22:36:24Z", - "description": "For the work-from-home professional, Gatsby's Freelance Pants take “business casual” to a whole new level. These slick black pants will help you stay calm and reach #maximumcomf even when you're under pressure.", - "descriptionHtml": "For the work-from-home professional, Gatsby's Freelance Pants take “business casual” to a whole new level. These slick black pants will help you stay calm and reach #maximumcomf even when you're under pressure.", - "handle": "freelance-pants", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613", - }, - ], - "internal": Object { - "contentDigest": "e0714d0bd6c30f33792f3682cd9be41d", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjg2ODMwOTY=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - }, - "productType": "Pajama Pants", - "publishedAt": "2018-12-28T22:36:24Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=", - "tags": Array [], - "title": "Freelance Pants", - "updatedAt": "2019-08-07T07:53:05Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTg5Mjk1Mg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTkyNTcyMA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk1ODQ4OA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk5MTI1Ng==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjAyNDAyNA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjA1Njc5Mg==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-12-28T22:43:00Z", - "description": "Be the life of your own comfy party with these purple and playful JAMstack Jammies. Did we mention they have pockets?", - "descriptionHtml": "Be the life of your own comfy party with these purple and playful JAMstack Jammies. Did we mention they have pockets?", - "handle": "jamstack-jammies", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022", - }, - ], - "internal": Object { - "contentDigest": "c7a8763c58abee73f3a7615d9e5e0dec", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjk2MzMzNjg=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - }, - "productType": "Pajama Pants", - "publishedAt": "2018-12-28T22:43:00Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=", - "tags": Array [], - "title": "JAMstack Jammies", - "updatedAt": "2019-08-08T16:03:30Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzA3MjYwMA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEwNTM2OA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEzODEzNg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzE3MDkwNA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIwMzY3Mg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIzNjQ0MA==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2019-03-01T16:53:32Z", - "description": "This fleece zip hoodie is perfect for keeping cozy! Featured with drawcords and split kangaroo pockets. Material: 52/48 Airlume combed and ringspun cotton/poly fleece. Care Instructions: Machine wash cold, tumble dry low.", - "descriptionHtml": "

This fleece zip hoodie is perfect for keeping cozy! Featured with drawcords and split kangaroo pockets. Material: 52/48 Airlume combed and ringspun cotton/poly fleece.

-

Care Instructions: Machine wash cold, tumble dry low.

", - "handle": "dark-monogram-hoodie", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115", - }, - ], - "internal": Object { - "contentDigest": "1d17cea33a8247b0c6e1136bcae34396", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3NzExMDcwODY0MjQ=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - }, - "productType": "Sweatshirt", - "publishedAt": "2019-03-01T16:53:32Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=", - "tags": Array [], - "title": "Dark Monogram Hoodie", - "updatedAt": "2019-08-07T18:18:24Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MTc0MzcwNA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MjU5NTY3Mg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NjM5Njc2MA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0Njc4OTk3Ng==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NzI4MTQ5Ng==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0ODEzMzQ2NA==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2019-03-13T23:30:56Z", - "description": "Say “no” to boring colors and go all purple everything with this comfy cotton-poly blend hoodie. Rib knit cuff with thumb exits, a rib knit hem, an interior phone pocket, and headphone cord port. Care Instructions: Super hot water and high dryer heat may shrink the sweatshirt due to the cotton content. Suggested to turn inside out while washing to protect the wear of the printed logo.", - "descriptionHtml": "

Say “no” to boring colors and go all purple everything with this comfy cotton-poly blend hoodie. Rib knit cuff with thumb exits, a rib knit hem, an interior phone pocket, and headphone cord port. 

-

Care Instructions: Super hot water and high dryer heat may shrink the sweatshirt due to the cotton content. Suggested to turn inside out while washing to protect the wear of the printed logo.

", - "handle": "all-purple-everything-full-zip-hoodie", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884", - }, - ], - "internal": Object { - "contentDigest": "7662506f16af28d2c6e20919de3cc7c2", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3OTYxNTk3NjI1MjA=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "26.0", - "currencyCode": "USD", - }, - }, - "productType": "Sweatshirt", - "publishedAt": "2019-03-13T23:30:56Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=", - "tags": Array [], - "title": "All Purple Everything Full-Zip Hoodie", - "updatedAt": "2019-08-07T17:42:00Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0Mjc0NTA3Mzc1Mg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTUzNDQyNA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTU5OTk2MA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY2NTQ5Ng==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY5ODI2NA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTc2MzgwMA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY0NTk4MzMyMA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY1ODUzMzQ2NA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY3MjQ1OTg2NA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY4MDU1MzU2MA==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzEzMTIxMjA1Njk5NDQ=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-07-13T22:13:27Z", - "description": "This 6-pack of die-cut Gatsby stickers is a great way to show off how fast your websites are. Stickers range in size from 1\\" (2.54 cm) to 3\\" (7.62 cm).", - "descriptionHtml": "This 6-pack of die-cut Gatsby stickers is a great way to show off how fast your websites are. Stickers range in size from 1\\" (2.54 cm) to 3\\" (7.62 cm).", - "handle": "gatsby-sticker-6-pack", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzEzMTIxMjA1Njk5NDQ=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDYzMjM1Mjg1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6208.jpg?v=1555548544", - }, - ], - "internal": Object { - "contentDigest": "52c444feb5908793a4a63eeca79ff8b4", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE3NTY2ODU1MDA1MDQ=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "2.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "2.0", - "currencyCode": "USD", - }, - }, - "productType": "Stickers", - "publishedAt": "2018-07-13T22:13:27Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzEzMTIxMjA1Njk5NDQ=", - "tags": Array [ - "stickers", - ], - "title": "Gatsby Sticker Pack", - "updatedAt": "2019-08-09T07:52:40Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjIzOTc3Mjg3Njg4OA==", - ], - "vendor": "Gatsby Swag", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2019-04-05T18:30:25Z", - "description": "A perfect triblend tank dress that is incredibly soft. Featured with our Gatsby monogram logo on the back of the dress! Care: Machine wash cold, tumble dry low.", - "descriptionHtml": "

A perfect triblend tank dress that is incredibly soft. Featured with our Gatsby monogram logo on the back of the dress!

-

Care: Machine wash cold, tumble dry low.

", - "handle": "gatsby-racerback-tank-dress", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzA1MzE2NTQ5NDM2MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dress.JPG?v=1555695165", - }, - ], - "internal": Object { - "contentDigest": "352ea0e791a31273e2c181b987f5a812", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4NTY2MDg5ODkyNzI=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "Tank Dress", - "publishedAt": "2019-04-05T18:30:25Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=", - "tags": Array [], - "title": "Gatsby Racerback Tank Dress", - "updatedAt": "2019-08-05T23:01:56Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjYxNTc2OA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY0ODUzNg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY4MTMwNA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc0Njg0MA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc3OTYwOA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjgxMjM3Ng==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwNTg0MDQxNjc3Njg=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2019-04-17T22:38:22Z", - "description": "Put your hair up in style with our satin JAMstack scrunchies! Care: Hand wash with cold water, lay flat to dry.", - "descriptionHtml": "

Put your hair up in style with our satin JAMstack scrunchies!

-

Care: Hand wash with cold water, lay flat to dry.

", - "handle": "jamstack-scrunchie", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwNTg0MDQxNjc3Njg=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0Mzc1NjcyNjM2MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6192.jpg?v=1555695177", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0Mzc1NjYyODA1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6190.jpg?v=1555695177", - }, - ], - "internal": Object { - "contentDigest": "58fd339adc87b0aa51c30f9a4697dbb1", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4ODQ2NjU2NzE3Njg=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "", - "publishedAt": "2019-04-17T22:38:22Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwNTg0MDQxNjc3Njg=", - "tags": Array [], - "title": "JAMstack Scrunchie", - "updatedAt": "2019-08-05T23:17:40Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDMxNTMyMDA5MDcxMg==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-05-19T11:38:50Z", - "description": "Natural process soft ink print with a black logo on a dark grey shirt. Care Instructions Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!", - "descriptionHtml": "

Natural process soft ink print with a black logo on a dark grey shirt.

-

Care Instructions

-

Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. 

- -

Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!

", - "handle": "dark-deploy-tee", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "images": Array [ - Object { - "altText": "Mockup of the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706", - }, - Object { - "altText": "Marisa wearing the Dark Deploy tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDkxNTU0NA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/marisa-dark-deploy.jpg?v=1531343717", - }, - ], - "internal": Object { - "contentDigest": "8c5ecba23dee023edbe135bff4eba651", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzMwNTEwMTk5NjA1Nw==", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "Shirt", - "publishedAt": "2018-05-19T12:12:33Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==", - "tags": Array [], - "title": "Dark Deploy Tee", - "updatedAt": "2019-08-08T23:41:15Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTA2ODQx", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTM5NjA5", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjM3NjMyMTUyMzgwMA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjUyMjgwODQwOA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0NTg1NzExMg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0MjA4ODc5Mg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MjcwNjgx", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MzAzNDQ5", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzIwODQwNzky", - ], - "vendor": "Gatsby Swag", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzM4MTkwNDIxNzcxMTI=": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2019-06-13T16:27:53Z", - "description": "Subtle in the front, JAMstack party under the cap! Get your JAM on in this trucker hat.", - "descriptionHtml": "Subtle in the front, JAMstack party under the cap! Get your JAM on in this trucker hat.", - "handle": "jamstack-hat", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzM4MTkwNDIxNzcxMTI=", - "images": Array [ - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTE2NzQ3MjcyMTkyODg=", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6689.jpg?v=1560443279", - }, - Object { - "altText": null, - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTE2NzQ3MjcyODQ4MjQ=", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/JAmstack_hat_2.JPG?v=1560443280", - }, - ], - "internal": Object { - "contentDigest": "e31bda0218e5fc546ce40a35235cef24", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzQ5OTgzMjAzMjQ2OTY=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "Hat", - "publishedAt": "2019-06-13T16:27:53Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzM4MTkwNDIxNzcxMTI=", - "tags": Array [], - "title": "JAMstack Hat", - "updatedAt": "2019-08-06T15:49:35Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yOTAyOTE1MzQzOTgzMg==", - ], - "vendor": "Gatsby Store", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-06-04T00:51:11Z", - "description": "Keep it simple with this vintage purple tee. This t-shirt is super soft and printed with a natural process that makes the print weightless. Care Instructions Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!", - "descriptionHtml": "

Keep it simple with this vintage purple tee. This t-shirt is super soft and printed with a natural process that makes the print weightless.

-

Care Instructions

-

Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. 

- -

Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!

", - "handle": "vintage-purple-tee", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "images": Array [ - Object { - "altText": "Mockup of the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564", - }, - Object { - "altText": "Action shot of Kyle wearing the Purple Logo tee.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI1MjgxMDMyOA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-kyle.jpg?v=1531344575", - }, - ], - "internal": Object { - "contentDigest": "fe0346d30001661fcb81713d203ac952", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNDY4ODA2Njc3MzY=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "Shirt", - "publishedAt": "2018-06-04T00:51:11Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==", - "tags": Array [], - "title": "Vintage Purple Tee", - "updatedAt": "2019-08-09T07:52:40Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjA5MTc2", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjQ0NTY4NzcwOTc4NA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjQxOTQ0", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjQ3MzAwMTA0OA==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzA3NDgw", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDMyNjUyMzk5Mg==", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzczMDE2", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzNDA1Nzg0", - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzI0OTA0MDI0", - ], - "vendor": "Gatsby Swag", - }, - "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA==": Object { - "availableForSale": true, - "children": Array [], - "createdAt": "2018-06-12T07:00:09Z", - "description": "Get your feet into these spaced-out black socks with a Gatsby purple border and heel. The bright pops of color keep it fun while keeping you warm. Care Instructions Keep those socks comfy on your feet and looking bright by washing them in cold water with darker colors. Tumble dry on low so they don't shrink! Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!", - "descriptionHtml": "

Get your feet into these spaced-out black socks with a Gatsby purple border and heel. The bright pops of color keep it fun while keeping you warm.

-

Care Instructions

-

Keep those socks comfy on your feet and looking bright by washing them in cold water with darker colors. Tumble dry on low so they don't shrink!

-

Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!

", - "handle": "space-socks", - "id": "Shopify__Product__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA==", - "images": Array [ - Object { - "altText": "Mockup of the Gatsby socks.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjYyNTI0MA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-mockup_1.jpg?v=1531343345", - }, - Object { - "altText": "Feet propped up on a desk wearing Gatsby socks. Photo by Jason Lengstorf", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIyOTU0NTA0OA==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/socks-on-desk_1.jpg?v=1546015563", - }, - Object { - "altText": "Close-up detail photo of the Gatsby socks.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjY5MDc3Ng==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-detail-1_1.jpg?v=1531343390", - }, - Object { - "altText": "Close-up detail photo of the Gatsby socks.", - "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjkyMDE1Mg==", - "localFile___NODE": undefined, - "originalSrc": "https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-detail-2_1.jpg?v=1531343400", - }, - ], - "internal": Object { - "contentDigest": "d38f44a19a212480a9111e291f125c4c", - "type": "ShopifyProduct", - }, - "metafields___NODE": Array [], - "onlineStoreUrl": null, - "options___NODE": Array [ - "Shopify__ProductOption__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNTAwNjU2ODQ1Njg=", - ], - "parent": "__SOURCE__", - "priceRange": Object { - "maxVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - "minVariantPrice": Object { - "amount": "10.0", - "currencyCode": "USD", - }, - }, - "productType": "Socks", - "publishedAt": "2018-06-12T07:00:09Z", - "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA==", - "tags": Array [], - "title": "Space Socks", - "updatedAt": "2019-08-09T07:32:15Z", - "variants___NODE": Array [ - "Shopify__ProductVariant__Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjAwMjk0NzYwNTM2", - ], - "vendor": "Gatsby Swag", - }, - "Shopify__Shop__undefined": Object { - "children": Array [], - "description": "", - "id": "Shopify__Shop__undefined", - "internal": Object { - "contentDigest": "7e8bd622d5376313766d087914010149", - "type": "ShopifyShop", - }, - "moneyFormat": "", - "name": "", - "parent": "__SOURCE__", - }, -} -`; diff --git a/packages/gatsby-source-shopify/src/__tests__/create-client.js b/packages/gatsby-source-shopify/src/__tests__/create-client.js deleted file mode 100644 index 66c6ea088f8ea..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/create-client.js +++ /dev/null @@ -1,15 +0,0 @@ -const { createClient } = require(`../create-client`) - -describe(`create-client`, () => { - it(`Allows a domain as shop name`, () => { - expect(createClient(`my-shop.com`, `token`).url).toEqual( - expect.not.stringContaining(`myshopify.com`) - ) - }) - - it(`Allows a non-domain shop name`, () => { - expect(createClient(`my-shop`, `token`).url).toEqual( - expect.stringContaining(`myshopify.com`) - ) - }) -}) diff --git a/packages/gatsby-source-shopify/src/__tests__/fixtures/articles.json b/packages/gatsby-source-shopify/src/__tests__/fixtures/articles.json deleted file mode 100644 index 86d16db125bf5..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/fixtures/articles.json +++ /dev/null @@ -1 +0,0 @@ -{"articles":{"pageInfo":{"hasNextPage":false},"edges":[]}} diff --git a/packages/gatsby-source-shopify/src/__tests__/fixtures/blogs.json b/packages/gatsby-source-shopify/src/__tests__/fixtures/blogs.json deleted file mode 100644 index 25838e5a78c31..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/fixtures/blogs.json +++ /dev/null @@ -1 +0,0 @@ -{"blogs":{"pageInfo":{"hasNextPage":false},"edges":[{"cursor":"eyJsYXN0X2lkIjoxMjcyMzQ4Njk3LCJsYXN0X3ZhbHVlIjoiMTI3MjM0ODY5NyJ9","node":{"id":"Z2lkOi8vc2hvcGlmeS9CbG9nLzEyNzIzNDg2OTc=","title":"News","url":"https://gatsby-swag.myshopify.com/blogs/news"}}]}} diff --git a/packages/gatsby-source-shopify/src/__tests__/fixtures/collections.json b/packages/gatsby-source-shopify/src/__tests__/fixtures/collections.json deleted file mode 100644 index bf3d0d1f64d7b..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/fixtures/collections.json +++ /dev/null @@ -1 +0,0 @@ -{"collections":{"pageInfo":{"hasNextPage":false},"edges":[{"cursor":"eyJsYXN0X2lkIjo3MDU0MDAwMTUzLCJsYXN0X3ZhbHVlIjoiNzA1NDAwMDE1MyJ9","node":{"description":"","descriptionHtml":"","handle":"frontpage","id":"Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzcwNTQwMDAxNTM=","image":null,"products":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw=="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg=="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzEzMTIxMjA1Njk5NDQ="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA=="}}]},"title":"Home page","updatedAt":"2019-08-09T07:35:01Z"}},{"cursor":"eyJsYXN0X2lkIjozMzc3NTc0NzE2MCwibGFzdF92YWx1ZSI6IjMzNzc1NzQ3MTYwIn0=","node":{"description":"The items in this collection are offered to contributors for free as a token of our appreciate for contributing to Gatsby.","descriptionHtml":"The items in this collection are offered to contributors for free as a token of our appreciate for contributing to Gatsby.","handle":"contributor-swag","id":"Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzMzNzc1NzQ3MTYw","image":null,"products":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw=="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg=="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA=="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTU2NTI0NDA="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTUyNTkyMjQ="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzM4MTkwNDIxNzcxMTI="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwNTg0MDQxNjc3Njg="}}]},"title":"Contributor Swag","updatedAt":"2019-08-09T07:35:01Z"}},{"cursor":"eyJsYXN0X2lkIjo4ODIxNjc2NDUwNCwibGFzdF92YWx1ZSI6Ijg4MjE2NzY0NTA0In0=","node":{"description":"","descriptionHtml":"","handle":"level-2","id":"Z2lkOi8vc2hvcGlmeS9Db2xsZWN0aW9uLzg4MjE2NzY0NTA0","image":null,"products":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI="}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MjM0MTkzNDA4ODg="}}]},"title":"Level 2","updatedAt":"2019-08-08T16:05:04Z"}}]}} diff --git a/packages/gatsby-source-shopify/src/__tests__/fixtures/pages.json b/packages/gatsby-source-shopify/src/__tests__/fixtures/pages.json deleted file mode 100644 index 60b32e284852f..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/fixtures/pages.json +++ /dev/null @@ -1 +0,0 @@ -{"pages":{"pageInfo":{"hasNextPage":false},"edges":[]}} diff --git a/packages/gatsby-source-shopify/src/__tests__/fixtures/products.json b/packages/gatsby-source-shopify/src/__tests__/fixtures/products.json deleted file mode 100644 index 50e15ad62b899..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/fixtures/products.json +++ /dev/null @@ -1 +0,0 @@ -{"products":{"pageInfo":{"hasNextPage":false},"edges":[{"cursor":"eyJsYXN0X2lkIjoyMzk3Mzk5OTQxMzcsImxhc3RfdmFsdWUiOiIyMzk3Mzk5OTQxMzcifQ==","node":{"availableForSale":true,"createdAt":"2018-05-19T11:38:50Z","description":"Natural process soft ink print with a black logo on a dark grey shirt. Care Instructions Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!","descriptionHtml":"

Natural process soft ink print with a black logo on a dark grey shirt.

\n

Care Instructions

\n

Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. 

\n\n

Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!

","handle":"dark-deploy-tee","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIzOTczOTk5NDEzNw==","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","altText":"Mockup of the Dark Deploy tee.","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDkxNTU0NA==","altText":"Marisa wearing the Dark Deploy tee.","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/marisa-dark-deploy.jpg?v=1531343717"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzMwNTEwMTk5NjA1Nw==","name":"Size","values":["SM — Women","MD — Women","LG — Women","XL - Women","SM - Unisex","MD - Unisex","LG — Unisex","XL — Unisex","2XL — Unisex"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"Shirt","publishedAt":"2018-05-19T12:12:33Z","tags":[],"title":"Dark Deploy Tee","updatedAt":"2019-08-08T23:41:15Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTA2ODQx","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"SM — Women"}],"sku":"DDT-SM-W","title":"SM — Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MTM5NjA5","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"MD — Women"}],"sku":"DDT-MD-W","title":"MD — Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjM3NjMyMTUyMzgwMA==","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"LG — Women"}],"sku":"DDT-LG-W","title":"LG — Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjUyMjgwODQwOA==","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"XL - Women"}],"sku":"DDT-XL-W","title":"XL - Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0NTg1NzExMg==","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"SM - Unisex"}],"sku":"DDT-SM-M","title":"SM - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDM0MjA4ODc5Mg==","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"MD - Unisex"}],"sku":"DDT-MD-M","title":"MD - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MjcwNjgx","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"LG — Unisex"}],"sku":"DDT-LG-M","title":"LG — Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xODAwNTQ3MzAzNDQ5","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"XL — Unisex"}],"sku":"DDT-XL-M","title":"XL — Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzIwODQwNzky","image":{"altText":"Mockup of the Dark Deploy tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0MDM5MTI1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/dark-deploy-mockup.jpg?v=1531343706"},"price":"10.00","selectedOptions":[{"name":"Size","value":"2XL — Unisex"}],"sku":"DDT-2XL-M","title":"2XL — Unisex","weight":5,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Swag"}},{"cursor":"eyJsYXN0X2lkIjo3NjQ5MTQ2NjM1MTIsImxhc3RfdmFsdWUiOiI3NjQ5MTQ2NjM1MTIifQ==","node":{"availableForSale":true,"createdAt":"2018-06-04T00:51:11Z","description":"Keep it simple with this vintage purple tee. This t-shirt is super soft and printed with a natural process that makes the print weightless. Care Instructions Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!","descriptionHtml":"

Keep it simple with this vintage purple tee. This t-shirt is super soft and printed with a natural process that makes the print weightless.

\n

Care Instructions

\n

Machine wash cold and tumble dry only. These shirts can’t take the heat (literally)! We want to make sure you’re happy with our shirts, but they require a little TLC. 

\n\n

Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!

","handle":"vintage-purple-tee","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NDkxNDY2MzUxMg==","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","altText":"Mockup of the Purple Logo tee.","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI1MjgxMDMyOA==","altText":"Action shot of Kyle wearing the Purple Logo tee.","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-kyle.jpg?v=1531344575"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNDY4ODA2Njc3MzY=","name":"Size","values":["SM — Women","MD - Women","LG — Women","XL - Women","SM — Unisex","MD - Unisex","LG — Unisex","XL — Unisex","2XL — Unisex"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"Shirt","publishedAt":"2018-06-04T00:51:11Z","tags":[],"title":"Vintage Purple Tee","updatedAt":"2019-08-09T07:52:40Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjA5MTc2","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"SM — Women"}],"sku":"PT-S-W","title":"SM — Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjQ0NTY4NzcwOTc4NA==","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"MD - Women"}],"sku":"PT-M-W","title":"MD - Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMjQxOTQ0","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"LG — Women"}],"sku":"PT-LG-W","title":"LG — Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUyMjQ3MzAwMTA0OA==","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"XL - Women"}],"sku":"PT-XL-W","title":"XL - Women","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzA3NDgw","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"SM — Unisex"}],"sku":"PT-SM-M","title":"SM — Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjcxNDMyNjUyMzk5Mg==","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"MD - Unisex"}],"sku":"PT-MD-M","title":"MD - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzMzczMDE2","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"LG — Unisex"}],"sku":"PT-LG-M","title":"LG — Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MTkxODkzNDA1Nzg0","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"XL — Unisex"}],"sku":"PT-XL-M","title":"XL — Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjE0MzI0OTA0MDI0","image":{"altText":"Mockup of the Purple Logo tee.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjI0NjA2MDEyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/purple-logo-mockup.jpg?v=1531344564"},"price":"10.00","selectedOptions":[{"name":"Size","value":"2XL — Unisex"}],"sku":"PT-2XL-M","title":"2XL — Unisex","weight":5,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Swag"}},{"cursor":"eyJsYXN0X2lkIjo3NjcwMjI0OTc4ODAsImxhc3RfdmFsdWUiOiI3NjcwMjI0OTc4ODAifQ==","node":{"availableForSale":true,"createdAt":"2018-06-12T07:00:09Z","description":"Get your feet into these spaced-out black socks with a Gatsby purple border and heel. The bright pops of color keep it fun while keeping you warm. Care Instructions Keep those socks comfy on your feet and looking bright by washing them in cold water with darker colors. Tumble dry on low so they don't shrink! Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!","descriptionHtml":"

Get your feet into these spaced-out black socks with a Gatsby purple border and heel. The bright pops of color keep it fun while keeping you warm.

\n

Care Instructions

\n

Keep those socks comfy on your feet and looking bright by washing them in cold water with darker colors. Tumble dry on low so they don't shrink!

\n

Don't see your size? Send us an email team@gatsbyjs.com and we'll see if we can help!

","handle":"space-socks","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0Lzc2NzAyMjQ5Nzg4MA==","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjYyNTI0MA==","altText":"Mockup of the Gatsby socks.","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-mockup_1.jpg?v=1531343345"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIyOTU0NTA0OA==","altText":"Feet propped up on a desk wearing Gatsby socks. Photo by Jason Lengstorf","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/socks-on-desk_1.jpg?v=1546015563"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjY5MDc3Ng==","altText":"Close-up detail photo of the Gatsby socks.","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-detail-1_1.jpg?v=1531343390"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjkyMDE1Mg==","altText":"Close-up detail photo of the Gatsby socks.","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-detail-2_1.jpg?v=1531343400"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzEzNTAwNjU2ODQ1Njg=","name":"Size","values":["One size fits most"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"Socks","publishedAt":"2018-06-12T07:00:09Z","tags":[],"title":"Space Socks","updatedAt":"2019-08-09T07:32:15Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC84MjAwMjk0NzYwNTM2","image":{"altText":"Mockup of the Gatsby socks.","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzY0NjIzMjYyNTI0MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/sock-mockup_1.jpg?v=1531343345"},"price":"10.00","selectedOptions":[{"name":"Size","value":"One size fits most"}],"sku":"S-ONE","title":"One size fits most","weight":3,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Swag"}},{"cursor":"eyJsYXN0X2lkIjoxMzEyMTIwNTY5OTQ0LCJsYXN0X3ZhbHVlIjoiMTMxMjEyMDU2OTk0NCJ9","node":{"availableForSale":true,"createdAt":"2018-07-13T22:13:27Z","description":"This 6-pack of die-cut Gatsby stickers is a great way to show off how fast your websites are. Stickers range in size from 1\" (2.54 cm) to 3\" (7.62 cm).","descriptionHtml":"This 6-pack of die-cut Gatsby stickers is a great way to show off how fast your websites are. Stickers range in size from 1\" (2.54 cm) to 3\" (7.62 cm).","handle":"gatsby-sticker-6-pack","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzEzMTIxMjA1Njk5NDQ=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDYzMjM1Mjg1Ng==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6208.jpg?v=1555548544"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE3NTY2ODU1MDA1MDQ=","name":"Title","values":["Default Title"]}],"priceRange":{"minVariantPrice":{"amount":"2.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"2.0","currencyCode":"USD"}},"productType":"Stickers","publishedAt":"2018-07-13T22:13:27Z","tags":["stickers"],"title":"Gatsby Sticker Pack","updatedAt":"2019-08-09T07:52:40Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjIzOTc3Mjg3Njg4OA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDYzMjM1Mjg1Ng==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6208.jpg?v=1555548544"},"price":"2.00","selectedOptions":[{"name":"Title","value":"Default Title"}],"sku":"STICKERS-01","title":"Default Title","weight":1,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Swag"}},{"cursor":"eyJsYXN0X2lkIjoxNDAzMjU1MjU5MjI0LCJsYXN0X3ZhbHVlIjoiMTQwMzI1NTI1OTIyNCJ9","node":{"availableForSale":true,"createdAt":"2018-10-30T00:52:49Z","description":"Add more blazingly blazing speed to your wardrobe with this solid purple laundered chino twill hat. (Fine print: this is just a hat. It will not affect your speed.) Care Instructions: Spot clean with a damp cloth.","descriptionHtml":"

Add more blazingly blazing speed to your wardrobe with this solid purple laundered chino twill hat. (Fine print: this is just a hat. It will not affect your speed.)

\n

Care Instructions: Spot clean with a damp cloth.

","handle":"blazing-purple-hat","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTUyNTkyMjQ=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzEzNTQ0OA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4839_002.jpg?v=1546015349"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzMzMjA1Ng==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5147.jpg?v=1540860789"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzM5NzU5Mg==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4848.jpg?v=1540860804"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODI3MDcyODg=","name":"Title","values":["Default Title"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"Hat","publishedAt":"2018-10-30T00:52:49Z","tags":[],"title":"This Purple Hat Is Blazing Fast","updatedAt":"2019-08-07T17:39:01Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2MTYzMzExMg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5NzEzNTQ0OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4839_002.jpg?v=1546015349"},"price":"10.00","selectedOptions":[{"name":"Title","value":"Default Title"}],"sku":"1756S","title":"Default Title","weight":4,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoxNDAzMjU1NjUyNDQwLCJsYXN0X3ZhbHVlIjoiMTQwMzI1NTY1MjQ0MCJ9","node":{"availableForSale":false,"createdAt":"2018-10-30T00:55:50Z","description":"Dark grey and black, mesh, 6-panel trucker hat with snapback closure. Made of 100% polyester linen front panels and bill. Care Instructions: Spot clean with a damp cloth.","descriptionHtml":"

Dark grey and black, mesh, 6-panel trucker hat with snapback closure. Made of 100% polyester linen front panels and bill.

\n

Care Instructions: Spot clean with a damp cloth.

","handle":"monogram-trucker-hat","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE0MDMyNTU2NTI0NDA=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODA4NTcyMA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4862.jpg?v=1540860991"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODE1MTI1Ng==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4855.jpg?v=1540861007"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODE4NDAyNA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4859_002.jpg?v=1540861027"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzE4ODQ1ODMyNjQzNDQ=","name":"Title","values":["Default Title"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"Hat","publishedAt":"2018-10-30T00:55:50Z","tags":[],"title":"Monogram Trucker Hat","updatedAt":"2019-08-05T22:55:26Z","variants":{"edges":[{"node":{"availableForSale":false,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjk4NzA2NTIwNDgyNA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMzkxNTc5ODA4NTcyMA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4862.jpg?v=1540860991"},"price":"10.00","selectedOptions":[{"name":"Title","value":"Default Title"}],"sku":"BA540","title":"Default Title","weight":4,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoxNTY2MjY5OTk3MTQ0LCJsYXN0X3ZhbHVlIjoiMTU2NjI2OTk5NzE0NCJ9","node":{"availableForSale":true,"createdAt":"2018-11-16T19:36:04Z","description":"Show us how you rock your custom Gatsby lapel pin! Silver plating, clois-tech lapel pin with a clutch attachment. Size – 0.835” Care Instructions: Please do not machine wash.","descriptionHtml":"

Show us how you rock your custom Gatsby lapel pin! Silver plating, clois-tech lapel pin with a clutch attachment. Size – 0.835”

\n

Care Instructions: Please do not machine wash.

","handle":"gatsby-lapel-pin","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE1NjYyNjk5OTcxNDQ=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODk5NTI4MjAwOA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4947.png?v=1542402545"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODYzNTQ1NjYwMA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4972.jpg?v=1542402545"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODYzNjgwMDA4OA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4956.jpg?v=1542402545"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzIxMzUwODQ1NjQ1Njg=","name":"Title","values":["Default Title"]}],"priceRange":{"minVariantPrice":{"amount":"2.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"2.0","currencyCode":"USD"}},"productType":"Pin","publishedAt":"2018-11-16T19:36:04Z","tags":[],"title":"Gatsby Lapel Pin","updatedAt":"2019-08-07T13:10:10Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xNTM5ODYzOTc5NjMxMg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNDQ5ODk5NTI4MjAwOA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_4947.png?v=1542402545"},"price":"2.00","selectedOptions":[{"name":"Title","value":"Default Title"}],"sku":"lapelpin","title":"Default Title","weight":0.5,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoxOTIzNDE5MzQwODg4LCJsYXN0X3ZhbHVlIjoiMTkyMzQxOTM0MDg4OCJ9","node":{"availableForSale":true,"createdAt":"2018-12-05T01:12:21Z","description":"Keep your hot beverages BLAZING hot for hours with this stainless vacuum-insulated Fifty/Fifty 12 oz/345mL bottle. Also works to keep cold beverages, er, blazing cold? Care Instructions: Do not put in microwave, freezer, or dishwasher. Hand wash with hot soapy water. Leave cap off and allow to air dry. Do not use cleaners containing bleach or chlorine.","descriptionHtml":"

Keep your hot beverages BLAZING hot for hours with this stainless vacuum-insulated Fifty/Fifty 12 oz/345mL bottle. Also works to keep cold beverages, er, blazing cold?

\n

Care Instructions: Do not put in microwave, freezer, or dishwasher. Hand wash with hot soapy water. Leave cap off and allow to air dry. Do not use cleaners containing bleach or chlorine.

","handle":"12oz-travel-mug","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MjM0MTkzNDA4ODg=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODM5MTUxMg==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5659.png?v=1543972353"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODQ1NzA0OA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5667.JPG?v=1543972362"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODU1NTM1Mg==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5664.JPG?v=1543972368"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI2OTk3ODgzODYzOTI=","name":"Title","values":["Default Title"]}],"priceRange":{"minVariantPrice":{"amount":"26.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"26.0","currencyCode":"USD"}},"productType":"Water Bottle","publishedAt":"2018-12-05T01:12:21Z","tags":[],"title":"Mug for (Blazing) Hot Beverages","updatedAt":"2019-08-06T11:22:51Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTQ1MjAwMDczMTIyNA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjYwMTc4ODM5MTUxMg==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG-5659.png?v=1543972353"},"price":"26.00","selectedOptions":[{"name":"Title","value":"Default Title"}],"sku":"V-12-00002","title":"Default Title","weight":6,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoxOTMyNjk0NTg1NDMyLCJsYXN0X3ZhbHVlIjoiMTkzMjY5NDU4NTQzMiJ9","node":{"availableForSale":true,"createdAt":"2018-12-28T22:36:24Z","description":"For the work-from-home professional, Gatsby's Freelance Pants take “business casual” to a whole new level. These slick black pants will help you stay calm and reach #maximumcomf even when you're under pressure.","descriptionHtml":"For the work-from-home professional, Gatsby's Freelance Pants take “business casual” to a whole new level. These slick black pants will help you stay calm and reach #maximumcomf even when you're under pressure.","handle":"freelance-pants","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTQ1ODU0MzI=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjg2ODMwOTY=","name":"Size","values":["XS - Unisex","SM - Unisex","MD - Unisex","L - Unisex","XL - Unisex","2XL - Unisex"]}],"priceRange":{"minVariantPrice":{"amount":"26.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"26.0","currencyCode":"USD"}},"productType":"Pajama Pants","publishedAt":"2018-12-28T22:36:24Z","tags":[],"title":"Freelance Pants","updatedAt":"2019-08-07T07:53:05Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTg5Mjk1Mg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XS - Unisex"}],"sku":"FLP-1","title":"XS - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTkyNTcyMA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613"},"price":"26.00","selectedOptions":[{"name":"Size","value":"SM - Unisex"}],"sku":"FLP-2","title":"SM - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk1ODQ4OA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613"},"price":"26.00","selectedOptions":[{"name":"Size","value":"MD - Unisex"}],"sku":"FLP-3","title":"MD - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NTk5MTI1Ng==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613"},"price":"26.00","selectedOptions":[{"name":"Size","value":"L - Unisex"}],"sku":"FLP-4","title":"L - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjAyNDAyNA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XL - Unisex"}],"sku":"FLP-5","title":"XL - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":false,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NjA1Njc5Mg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTA4NDUwNA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5137.png?v=1546036613"},"price":"26.00","selectedOptions":[{"name":"Size","value":"2XL - Unisex"}],"sku":"FLP-6","title":"2XL - Unisex","weight":5,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoxOTMyNjk1MDc2OTUyLCJsYXN0X3ZhbHVlIjoiMTkzMjY5NTA3Njk1MiJ9","node":{"availableForSale":true,"createdAt":"2018-12-28T22:43:00Z","description":"Be the life of your own comfy party with these purple and playful JAMstack Jammies. Did we mention they have pockets?","descriptionHtml":"Be the life of your own comfy party with these purple and playful JAMstack Jammies. Did we mention they have pockets?","handle":"jamstack-jammies","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5MzI2OTUwNzY5NTI=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3MTI1Mjk2MzMzNjg=","name":"Size","values":["XS - Unisex","SM - Unisex","MD - Unisex","L - Unisex","XL - Unisex","2XL - Unisex"]}],"priceRange":{"minVariantPrice":{"amount":"26.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"26.0","currencyCode":"USD"}},"productType":"Pajama Pants","publishedAt":"2018-12-28T22:43:00Z","tags":[],"title":"JAMstack Jammies","updatedAt":"2019-08-08T16:03:30Z","variants":{"edges":[{"node":{"availableForSale":false,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzA3MjYwMA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XS - Unisex"}],"sku":"JSJP-1","title":"XS - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEwNTM2OA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022"},"price":"26.00","selectedOptions":[{"name":"Size","value":"SM - Unisex"}],"sku":"JSJP-2","title":"SM - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":false,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzEzODEzNg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022"},"price":"26.00","selectedOptions":[{"name":"Size","value":"MD - Unisex"}],"sku":"JSJP-3","title":"MD - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzE3MDkwNA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022"},"price":"26.00","selectedOptions":[{"name":"Size","value":"L - Unisex"}],"sku":"JSJP-4","title":"L - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIwMzY3Mg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XL - Unisex"}],"sku":"JSJP-5","title":"XL - Unisex","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":false,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTUwOTk0NzIzNjQ0MA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjY0NjYwOTkzNjQ3Mg==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_5136.png?v=1546037022"},"price":"26.00","selectedOptions":[{"name":"Size","value":"2XL - Unisex"}],"sku":"JSJP-6","title":"2XL - Unisex","weight":5,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoxOTc0NDMyMjAyODQwLCJsYXN0X3ZhbHVlIjoiMTk3NDQzMjIwMjg0MCJ9","node":{"availableForSale":true,"createdAt":"2019-03-01T16:53:32Z","description":"This fleece zip hoodie is perfect for keeping cozy! Featured with drawcords and split kangaroo pockets. Material: 52/48 Airlume combed and ringspun cotton/poly fleece. Care Instructions: Machine wash cold, tumble dry low.","descriptionHtml":"

This fleece zip hoodie is perfect for keeping cozy! Featured with drawcords and split kangaroo pockets. Material: 52/48 Airlume combed and ringspun cotton/poly fleece.

\n

Care Instructions: Machine wash cold, tumble dry low.

","handle":"dark-monogram-hoodie","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5NzQ0MzIyMDI4NDA=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3NzExMDcwODY0MjQ=","name":"Size","values":["XS - Unisex","S - Unisex","M - Unisex","L - Unisex","XL - Unisex","2XL - Unisex"]}],"priceRange":{"minVariantPrice":{"amount":"26.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"26.0","currencyCode":"USD"}},"productType":"Sweatshirt","publishedAt":"2019-03-01T16:53:32Z","tags":[],"title":"Dark Monogram Hoodie","updatedAt":"2019-08-07T18:18:24Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MTc0MzcwNA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XS - Unisex"}],"sku":"AA9590XS","title":"XS - Unisex","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0MjU5NTY3Mg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115"},"price":"26.00","selectedOptions":[{"name":"Size","value":"S - Unisex"}],"sku":"AA9590S","title":"S - Unisex","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":false,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NjM5Njc2MA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115"},"price":"26.00","selectedOptions":[{"name":"Size","value":"M - Unisex"}],"sku":"AA9590M","title":"M - Unisex","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":false,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0Njc4OTk3Ng==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115"},"price":"26.00","selectedOptions":[{"name":"Size","value":"L - Unisex"}],"sku":"AA9590L","title":"L - Unisex","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0NzI4MTQ5Ng==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XL - Unisex"}],"sku":"AA9590XL","title":"XL - Unisex","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTcwOTk0ODEzMzQ2NA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE5NTYzODA3MTM4NA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dark_Hoodie.jpg?v=1556318115"},"price":"26.00","selectedOptions":[{"name":"Size","value":"2XL - Unisex"}],"sku":"AA95902XL","title":"2XL - Unisex","weight":7,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoxOTkyNjMyMDQxNTYwLCJsYXN0X3ZhbHVlIjoiMTk5MjYzMjA0MTU2MCJ9","node":{"availableForSale":true,"createdAt":"2019-03-13T23:30:56Z","description":"Say “no” to boring colors and go all purple everything with this comfy cotton-poly blend hoodie. Rib knit cuff with thumb exits, a rib knit hem, an interior phone pocket, and headphone cord port. Care Instructions: Super hot water and high dryer heat may shrink the sweatshirt due to the cotton content. Suggested to turn inside out while washing to protect the wear of the printed logo.","descriptionHtml":"

Say “no” to boring colors and go all purple everything with this comfy cotton-poly blend hoodie. Rib knit cuff with thumb exits, a rib knit hem, an interior phone pocket, and headphone cord port. 

\n

Care Instructions: Super hot water and high dryer heat may shrink the sweatshirt due to the cotton content. Suggested to turn inside out while washing to protect the wear of the printed logo.

","handle":"all-purple-everything-full-zip-hoodie","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzE5OTI2MzIwNDE1NjA=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI3OTYxNTk3NjI1MjA=","name":"Size","values":["S - Men","MD - Men","LG - Men","XL - Men","2XL - Men","3XL - Men","S - Womens","MD - Womens","LG - Womens","XL - Womens"]}],"priceRange":{"minVariantPrice":{"amount":"26.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"26.0","currencyCode":"USD"}},"productType":"Sweatshirt","publishedAt":"2019-03-13T23:30:56Z","tags":[],"title":"All Purple Everything Full-Zip Hoodie","updatedAt":"2019-08-07T17:42:00Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0Mjc0NTA3Mzc1Mg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"S - Men"}],"sku":"TM18135S","title":"S - Men","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTUzNDQyNA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"MD - Men"}],"sku":"TM98139","title":"MD - Men","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTU5OTk2MA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"LG - Men"}],"sku":"TM98140","title":"LG - Men","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY2NTQ5Ng==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XL - Men"}],"sku":"TM98141","title":"XL - Men","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTY5ODI2NA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"2XL - Men"}],"sku":"TM98142","title":"2XL - Men","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjU3MTc2MzgwMA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"3XL - Men"}],"sku":"TM98143","title":"3XL - Men","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY0NTk4MzMyMA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"S - Womens"}],"sku":"TM18135S","title":"S - Womens","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY1ODUzMzQ2NA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"MD - Womens"}],"sku":"TM18135M","title":"MD - Womens","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY3MjQ1OTg2NA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"LG - Womens"}],"sku":"TM18135","title":"LG - Womens","weight":7,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xOTg0MjY4MDU1MzU2MA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNjg5NzUxMTk1NjU2OA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6058.jpg?v=1552519884"},"price":"26.00","selectedOptions":[{"name":"Size","value":"XL - Womens"}],"sku":"TM18135","title":"XL - Womens","weight":7,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoyMDM2ODIxMjI5NjU2LCJsYXN0X3ZhbHVlIjoiMjAzNjgyMTIyOTY1NiJ9","node":{"availableForSale":true,"createdAt":"2019-04-05T18:30:25Z","description":"A perfect triblend tank dress that is incredibly soft. Featured with our Gatsby monogram logo on the back of the dress! Care: Machine wash cold, tumble dry low.","descriptionHtml":"

A perfect triblend tank dress that is incredibly soft. Featured with our Gatsby monogram logo on the back of the dress!

\n

Care: Machine wash cold, tumble dry low.

","handle":"gatsby-racerback-tank-dress","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwMzY4MjEyMjk2NTY=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzA1MzE2NTQ5NDM2MA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/Gatsby_Dress.JPG?v=1555695165"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4NTY2MDg5ODkyNzI=","name":"Size","values":["X-Small","Small","Medium","Large","X-Large","XX-Large"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"Tank Dress","publishedAt":"2019-04-05T18:30:25Z","tags":[],"title":"Gatsby Racerback Tank Dress","updatedAt":"2019-08-05T23:01:56Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjYxNTc2OA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165"},"price":"10.00","selectedOptions":[{"name":"Size","value":"X-Small"}],"sku":"6734","title":"X-Small","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY0ODUzNg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165"},"price":"10.00","selectedOptions":[{"name":"Size","value":"Small"}],"sku":"6735","title":"Small","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjY4MTMwNA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165"},"price":"10.00","selectedOptions":[{"name":"Size","value":"Medium"}],"sku":"6736","title":"Medium","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc0Njg0MA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165"},"price":"10.00","selectedOptions":[{"name":"Size","value":"Large"}],"sku":"6737","title":"Large","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5Mjc3OTYwOA==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165"},"price":"10.00","selectedOptions":[{"name":"Size","value":"X-Large"}],"sku":"6738","title":"X-Large","weight":5,"weightUnit":"OUNCES"}},{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDIwNjA5MjgxMjM3Ng==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0NDM0MDMyNDQ0MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6198.jpg?v=1555695165"},"price":"10.00","selectedOptions":[{"name":"Size","value":"XX-Large"}],"sku":"6739","title":"XX-Large","weight":5,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjoyMDU4NDA0MTY3NzY4LCJsYXN0X3ZhbHVlIjoiMjA1ODQwNDE2Nzc2OCJ9","node":{"availableForSale":true,"createdAt":"2019-04-17T22:38:22Z","description":"Put your hair up in style with our satin JAMstack scrunchies! Care: Hand wash with cold water, lay flat to dry.","descriptionHtml":"

Put your hair up in style with our satin JAMstack scrunchies!

\n

Care: Hand wash with cold water, lay flat to dry.

","handle":"jamstack-scrunchie","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzIwNTg0MDQxNjc3Njg=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0Mzc1NjcyNjM2MA==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6192.jpg?v=1555695177"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0Mzc1NjYyODA1Ng==","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6190.jpg?v=1555695177"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzI4ODQ2NjU2NzE3Njg=","name":"Title","values":["Default Title"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"","publishedAt":"2019-04-17T22:38:22Z","tags":[],"title":"JAMstack Scrunchie","updatedAt":"2019-08-05T23:17:40Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yMDMxNTMyMDA5MDcxMg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvNzE0Mzc1NjcyNjM2MA==","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6192.jpg?v=1555695177"},"price":"10.00","selectedOptions":[{"name":"Title","value":"Default Title"}],"sku":"SCNCH","title":"Default Title","weight":0.5,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}},{"cursor":"eyJsYXN0X2lkIjozODE5MDQyMTc3MTEyLCJsYXN0X3ZhbHVlIjoiMzgxOTA0MjE3NzExMiJ9","node":{"availableForSale":true,"createdAt":"2019-06-13T16:27:53Z","description":"Subtle in the front, JAMstack party under the cap! Get your JAM on in this trucker hat.","descriptionHtml":"Subtle in the front, JAMstack party under the cap! Get your JAM on in this trucker hat.","handle":"jamstack-hat","id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzM4MTkwNDIxNzcxMTI=","images":{"edges":[{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTE2NzQ3MjcyMTkyODg=","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6689.jpg?v=1560443279"}},{"node":{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTE2NzQ3MjcyODQ4MjQ=","altText":null,"originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/JAmstack_hat_2.JPG?v=1560443280"}}]},"metafields":{"edges":[]},"onlineStoreUrl":null,"options":[{"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0T3B0aW9uLzQ5OTgzMjAzMjQ2OTY=","name":"Title","values":["Default Title"]}],"priceRange":{"minVariantPrice":{"amount":"10.0","currencyCode":"USD"},"maxVariantPrice":{"amount":"10.0","currencyCode":"USD"}},"productType":"Hat","publishedAt":"2019-06-13T16:27:53Z","tags":[],"title":"JAMstack Hat","updatedAt":"2019-08-06T15:49:35Z","variants":{"edges":[{"node":{"availableForSale":true,"compareAtPrice":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8yOTAyOTE1MzQzOTgzMg==","image":{"altText":null,"id":"Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTE2NzQ3MjcyMTkyODg=","originalSrc":"https://cdn.shopify.com/s/files/1/0011/5936/4633/products/IMG_6689.jpg?v=1560443279"},"price":"10.00","selectedOptions":[{"name":"Title","value":"Default Title"}],"sku":"jamstackhat","title":"Default Title","weight":4,"weightUnit":"OUNCES"}}]},"vendor":"Gatsby Store"}}]}} diff --git a/packages/gatsby-source-shopify/src/__tests__/fixtures/shop-details.json b/packages/gatsby-source-shopify/src/__tests__/fixtures/shop-details.json deleted file mode 100644 index 089bc8406334d..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/fixtures/shop-details.json +++ /dev/null @@ -1 +0,0 @@ -{ "shop": { "description": "", "name": "", "moneyFormat": "" } } \ No newline at end of file diff --git a/packages/gatsby-source-shopify/src/__tests__/fixtures/shop-policies.json b/packages/gatsby-source-shopify/src/__tests__/fixtures/shop-policies.json deleted file mode 100644 index 01df293312e37..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/fixtures/shop-policies.json +++ /dev/null @@ -1 +0,0 @@ -{ "shop": { "privacyPolicy": null, "refundPolicy": null, "termsOfService": null } } diff --git a/packages/gatsby-source-shopify/src/__tests__/index.js b/packages/gatsby-source-shopify/src/__tests__/index.js deleted file mode 100644 index 9b9bbe73a6db1..0000000000000 --- a/packages/gatsby-source-shopify/src/__tests__/index.js +++ /dev/null @@ -1,84 +0,0 @@ -jest.mock(`gatsby-source-filesystem`, () => { - return { - createRemoteFileNode: jest.fn(), - } -}) - -import * as mockQueries from "../queries" -jest.mock(`../create-client`, () => { - return { - createClient: jest.fn(() => { - return { - request: async query => { - // Hack alert. match query text, from that get query name (like SHOP_POLICIES_QUERY) and convert to filename like policies.json - const fixturePathFromQuery = (query, mockQueries) => { - const [queryName] = Object.entries(mockQueries).find( - ([queryName, queryString]) => queryString === query - ) - const jsonFile = queryName - .split(`_`) - .map(el => el.toLowerCase()) - .filter(el => el !== `query`) - .join(`-`) - return jsonFile + `.json` - } - const jsonFile = fixturePathFromQuery(query, mockQueries) - return require(`./fixtures/${jsonFile}`) - }, - } - }), - } -}) - -const { sourceNodes } = require(`../gatsby-node`) - -describe(`gatsby-source-shopify`, () => { - /** - * This test is pretty bare-bones. Among other things: - * - * - Some of the fixtures are empty responses - * - There's no pagination testing - * - There's no validation that nodes are created correctly, other than a snapshot - * - There's no way to test different responses for the same query - * - * TODO: more and better tests - * - * Mocking setup is borrowed from gatsby-source-drupal - */ - const nodes = {} - const actions = { - createNode: jest.fn(node => (nodes[node.id] = node)), - } - - const activity = { - start: jest.fn(), - end: jest.fn(), - } - - const reporter = { - info: jest.fn(), - activityTimer: jest.fn(() => activity), - log: jest.fn(), - } - - const cache = { - get: jest.fn(), - } - - const args = { - actions, - reporter, - cache, - // getNode: id => nodes[id], - } - - beforeAll(async () => { - await sourceNodes(args, { shopName: `test-shop` }) - }) - - it(`Generates nodes`, () => { - expect(Object.keys(nodes).length).not.toEqual(0) - // better than no tests? ¯\_(ツ)_/¯ - expect(nodes).toMatchSnapshot() - }) -}) diff --git a/packages/gatsby-source-shopify/src/client.ts b/packages/gatsby-source-shopify/src/client.ts new file mode 100644 index 0000000000000..00b4007554113 --- /dev/null +++ b/packages/gatsby-source-shopify/src/client.ts @@ -0,0 +1,48 @@ +import fetch from "node-fetch" +import { HttpError } from "./errors" + +const adminUrl = (options: ShopifyPluginOptions): string => + `https://@${options.storeUrl}/admin/api/2021-01/graphql.json` + +const MAX_BACKOFF_MILLISECONDS = 60000 + +interface IGraphQLClient { + request: (query: string, variables?: Record) => Promise +} + +export function createClient(options: ShopifyPluginOptions): IGraphQLClient { + const url = adminUrl(options) + + async function graphqlFetch( + query: string, + variables?: Record, + retries = 0 + ): Promise { + const response = await fetch(url, { + method: `POST`, + headers: { + "Content-Type": `application/json`, + "X-Shopify-Access-Token": options.password, + }, + body: JSON.stringify({ + query, + variables, + }), + }) + + if (!response.ok) { + const waitTime = 2 ** (retries + 1) + 500 + if (response.status >= 500 && waitTime < MAX_BACKOFF_MILLISECONDS) { + await new Promise(resolve => setTimeout(resolve, waitTime)) + return graphqlFetch(query, variables, retries + 1) + } + + throw new HttpError(response) + } + + const json = await response.json() + return json.data as T + } + + return { request: graphqlFetch } +} diff --git a/packages/gatsby-source-shopify/src/constants.js b/packages/gatsby-source-shopify/src/constants.js deleted file mode 100644 index 7265e6a20b62a..0000000000000 --- a/packages/gatsby-source-shopify/src/constants.js +++ /dev/null @@ -1,26 +0,0 @@ -// Node prefix -export const TYPE_PREFIX = `Shopify` - -// Node types -export const ARTICLE = `Article` -export const BLOG = `Blog` -export const COLLECTION = `Collection` -export const COMMENT = `Comment` -export const PRODUCT = `Product` -export const PRODUCT_OPTION = `ProductOption` -export const PRODUCT_VARIANT = `ProductVariant` -export const PRODUCT_METAFIELD = `ProductMetafield` -export const PRODUCT_VARIANT_METAFIELD = `ProductVariantMetafield` -export const SHOP_POLICY = `ShopPolicy` -export const SHOP_DETAILS = `Shop` -export const PAGE = `Page` -export const SHOP = `shop` -export const CONTENT = `content` - -export const NODE_TO_ENDPOINT_MAPPING = { - [ARTICLE]: `articles`, - [BLOG]: `blogs`, - [COLLECTION]: `collections`, - [PRODUCT]: `products`, - [PAGE]: `pages`, -} diff --git a/packages/gatsby-source-shopify/src/create-client.js b/packages/gatsby-source-shopify/src/create-client.js deleted file mode 100644 index aa9a7c0e61bec..0000000000000 --- a/packages/gatsby-source-shopify/src/create-client.js +++ /dev/null @@ -1,17 +0,0 @@ -import { GraphQLClient } from "graphql-request" -/** - * Create a Shopify Storefront GraphQL client for the provided name and token. - */ -export const createClient = (shopName, accessToken, apiVersion) => { - let url - if (shopName.includes(`.`)) { - url = `https://${shopName}/api/${apiVersion}/graphql.json` - } else { - url = `https://${shopName}.myshopify.com/api/${apiVersion}/graphql.json` - } - return new GraphQLClient(url, { - headers: { - "X-Shopify-Storefront-Access-Token": accessToken, - }, - }) -} diff --git a/packages/gatsby-source-shopify/src/create-schema-customization.ts b/packages/gatsby-source-shopify/src/create-schema-customization.ts new file mode 100644 index 0000000000000..e430549fa26d9 --- /dev/null +++ b/packages/gatsby-source-shopify/src/create-schema-customization.ts @@ -0,0 +1,295 @@ +import { + CreateSchemaCustomizationArgs, + NodePluginSchema, + GatsbyGraphQLObjectType, +} from "../../gatsby" + +function addFields( + def: GatsbyGraphQLObjectType, + fields: GatsbyGraphQLObjectType["config"]["fields"] +): void { + def.config.fields = { + ...(def.config.fields || {}), + ...fields, + } +} + +function defineImageNode( + name: string, + schema: NodePluginSchema, + pluginOptions: ShopifyPluginOptions, + fields: GatsbyGraphQLObjectType["config"]["fields"] = {} +): GatsbyGraphQLObjectType { + const imageDef = schema.buildObjectType({ + name, + }) + + if (pluginOptions.downloadImages) { + imageDef.config.fields = { + localFile: { + type: `File`, + extensions: { + link: {}, + }, + }, + } + } + + addFields(imageDef, { + ...fields, + altText: `String`, + height: `Int`, + id: `String`, + originalSrc: `String!`, + transformedSrc: `String!`, + width: `Int`, + }) + + return imageDef +} + +export function createSchemaCustomization( + { actions, schema }: CreateSchemaCustomizationArgs, + pluginOptions: ShopifyPluginOptions +): void { + const includeCollections = pluginOptions.shopifyConnections?.includes( + `collections` + ) + + const includeOrders = pluginOptions.shopifyConnections?.includes(`orders`) + + const name = (name: string): string => + `${pluginOptions.typePrefix || ``}${name}` + + const sharedMetafieldFields: GatsbyGraphQLObjectType["config"]["fields"] = { + createdAt: `Date!`, + description: `String`, + id: `ID!`, + key: `String!`, + namespace: `String!`, + ownerType: `String!`, + updatedAt: `Date!`, + value: `String!`, + valueType: `String!`, + } + + const metafieldInterface = schema.buildInterfaceType({ + name: name(`ShopifyMetafieldInterface`), + fields: sharedMetafieldFields, + // @ts-ignore TODO: Once Gatsby Core updates its graphql-compose package to a version that has the right + // types to support interfaces implementing other interfaces, remove the ts-ignore + interfaces: [`Node`], + }) + + const metafieldOwnerTypes = [`Product`, `ProductVariant`] + if (includeCollections) { + metafieldOwnerTypes.push(`Collection`) + } + + const metafieldTypes = metafieldOwnerTypes.map((ownerType: string) => { + const parentKey = ownerType.charAt(0).toLowerCase() + ownerType.slice(1) + return schema.buildObjectType({ + name: name(`Shopify${ownerType}Metafield`), + fields: { + ...sharedMetafieldFields, + [parentKey]: { + type: name(`Shopify${ownerType}`), + extensions: { + link: { + from: `${parentKey}Id`, + by: `id`, + }, + }, + }, + }, + interfaces: [`Node`, name(`ShopifyMetafieldInterface`)], + }) + }) + + const productDef = schema.buildObjectType({ + name: name(`ShopifyProduct`), + fields: { + variants: { + type: `[${name(`ShopifyProductVariant`)}]`, + extensions: { + link: { + from: `id`, + by: `productId`, + }, + }, + }, + metafields: { + type: `[${name(`ShopifyProductMetafield`)}]`, + extensions: { + link: { + from: `id`, + by: `productId`, + }, + }, + }, + images: { + type: `[${name(`ShopifyProductImage`)}]`, + extensions: { + link: { + from: `id`, + by: `productId`, + }, + }, + }, + }, + interfaces: [`Node`], + }) + + const productImageDef = defineImageNode( + name(`ShopifyProductImage`), + schema, + pluginOptions, + { + product: { + type: name(`ShopifyProduct!`), + extensions: { + link: { + from: `productId`, + by: `id`, + }, + }, + }, + } + ) + + productImageDef.config.interfaces = [`Node`] + + if (includeCollections) { + addFields(productDef, { + collections: { + type: `[${name(`ShopifyCollection`)}]`, + extensions: { + link: { + from: `id`, + by: `productIds`, + }, + }, + }, + }) + } + + const typeDefs = [ + productDef, + productImageDef, + schema.buildObjectType({ + name: name(`ShopifyProductVariant`), + fields: { + product: { + type: name(`ShopifyProduct!`), + extensions: { + link: { + from: `productId`, + by: `id`, + }, + }, + }, + metafields: { + type: `[${name(`ShopifyProductVariantMetafield`)}]`, + extensions: { + link: { + from: `id`, + by: `productVariantId`, + }, + }, + }, + }, + interfaces: [`Node`], + }), + metafieldInterface, + ...metafieldTypes, + ] + + if (includeCollections) { + typeDefs.push( + schema.buildObjectType({ + name: name(`ShopifyCollection`), + fields: { + products: { + type: `[${name(`ShopifyProduct`)}]`, + extensions: { + link: { + from: `productIds`, + by: `id`, + }, + }, + }, + metafields: { + type: `[${name(`ShopifyCollectionMetafield`)}]`, + extensions: { + link: { + from: `id`, + by: `collectionId`, + }, + }, + }, + }, + interfaces: [`Node`], + }) + ) + } + + if (includeOrders) { + typeDefs.push( + schema.buildObjectType({ + name: name(`ShopifyOrder`), + fields: { + lineItems: { + type: `[${name(`ShopifyLineItem`)}]`, + extensions: { + link: { + from: `id`, + by: `orderId`, + }, + }, + }, + }, + interfaces: [`Node`], + }), + schema.buildObjectType({ + name: name(`ShopifyLineItem`), + fields: { + product: { + type: name(`ShopifyProduct`), + extensions: { + link: { + from: `productId`, + by: `id`, + }, + }, + }, + order: { + type: name(`ShopifyOrder!`), + extensions: { + link: { + from: `orderId`, + by: `id`, + }, + }, + }, + }, + interfaces: [`Node`], + }) + ) + } + + typeDefs.push( + ...[ + `ShopifyProductFeaturedImage`, + `ShopifyProductFeaturedMediaPreviewImage`, + `ShopifyProductVariantImage`, + ].map(typeName => defineImageNode(name(typeName), schema, pluginOptions)) + ) + + if (includeCollections) { + typeDefs.push( + defineImageNode(name(`ShopifyCollectionImage`), schema, pluginOptions) + ) + } + + actions.createTypes(typeDefs) +} diff --git a/packages/gatsby-source-shopify/src/errors.ts b/packages/gatsby-source-shopify/src/errors.ts new file mode 100644 index 0000000000000..585f87684cbb5 --- /dev/null +++ b/packages/gatsby-source-shopify/src/errors.ts @@ -0,0 +1,34 @@ +import { Response } from "node-fetch" + +export const pluginErrorCodes = { + bulkOperationFailed: `111000`, + unknownSourcingFailure: `111001`, + unknownApiError: `111002`, + + apiConflict: `111003`, +} + +export class OperationError extends Error { + public node: BulkOperationNode + + constructor(node: BulkOperationNode) { + const { errorCode, id } = node + super(`Operation ${id} failed with ${errorCode}`) + + this.node = node + + Error.captureStackTrace(this, OperationError) + } +} + +export class HttpError extends Error { + public response: Response + + constructor(response: Response) { + super(response.statusText) + + this.response = response + + Error.captureStackTrace(this, HttpError) + } +} diff --git a/packages/gatsby-source-shopify/src/events.ts b/packages/gatsby-source-shopify/src/events.ts new file mode 100644 index 0000000000000..4e86e8392e244 --- /dev/null +++ b/packages/gatsby-source-shopify/src/events.ts @@ -0,0 +1,58 @@ +import { makeShopifyFetch } from "./rest" + +interface IEvent { + subject_id: number + subject_type: string +} + +export function eventsApi( + options: ShopifyPluginOptions +): { + fetchDestroyEventsSince: (date: Date) => Promise> +} { + const shopifyFetch = makeShopifyFetch(options) + + return { + async fetchDestroyEventsSince(date): Promise> { + let resp = await shopifyFetch( + `/events.json?limit=250&verb=destroy&created_at_min=${date.toISOString()}` + ) + + const { events } = await resp.json() + + let gatherPaginatedEvents = true + + while (gatherPaginatedEvents) { + const paginationInfo = resp.headers.get(`link`) + if (!paginationInfo) { + gatherPaginatedEvents = false + break + } + + const pageLinks: Array<{ + url: string + rel: string + }> = paginationInfo.split(`,`).map((pageData: string) => { + const [, url, rel] = pageData.match(/<(.*)>; rel="(.*)"/) || [] + return { + url, + rel, + } + }) + + const nextPage = pageLinks.find(l => l.rel === `next`) + + if (nextPage) { + resp = await shopifyFetch(nextPage.url) + const { events: nextEvents } = await resp.json() + events.push(...nextEvents) + } else { + gatherPaginatedEvents = false + break + } + } + + return events + }, + } +} diff --git a/packages/gatsby-source-shopify/src/gatsby-node.js b/packages/gatsby-source-shopify/src/gatsby-node.js deleted file mode 100644 index 127143d4777cd..0000000000000 --- a/packages/gatsby-source-shopify/src/gatsby-node.js +++ /dev/null @@ -1,275 +0,0 @@ -import { pipe } from "lodash/fp" -import chalk from "chalk" -import { forEach } from "p-iteration" -import { printGraphQLError, queryAll, queryOnce } from "./lib" -import { createClient } from "./create-client" - -import { - ArticleNode, - BlogNode, - CollectionNode, - CommentNode, - ProductNode, - ProductOptionNode, - ProductVariantNode, - ProductMetafieldNode, - ProductVariantMetafieldNode, - ShopPolicyNode, - ShopDetailsNode, - PageNode, -} from "./nodes" -import { - SHOP, - CONTENT, - NODE_TO_ENDPOINT_MAPPING, - ARTICLE, - BLOG, - COLLECTION, - PRODUCT, - SHOP_POLICY, - SHOP_DETAILS, - PAGE, -} from "./constants" -import { - ARTICLES_QUERY, - BLOGS_QUERY, - COLLECTIONS_QUERY, - PRODUCTS_QUERY, - SHOP_POLICIES_QUERY, - SHOP_DETAILS_QUERY, - PAGES_QUERY, -} from "./queries" - -export const sourceNodes = async ( - { - actions: { createNode, touchNode }, - createNodeId, - store, - cache, - getCache, - reporter, - getNode, - }, - { - shopName, - accessToken, - apiVersion = `2020-07`, - verbose = true, - paginationSize = 250, - includeCollections = [SHOP, CONTENT], - downloadImages = true, - shopifyQueries = {}, - } -) => { - const client = createClient(shopName, accessToken, apiVersion) - - const defaultQueries = { - articles: ARTICLES_QUERY, - blogs: BLOGS_QUERY, - collections: COLLECTIONS_QUERY, - products: PRODUCTS_QUERY, - shopPolicies: SHOP_POLICIES_QUERY, - shopDetails: SHOP_DETAILS_QUERY, - pages: PAGES_QUERY, - } - - const queries = { ...defaultQueries, ...shopifyQueries } - - // Convenience function to namespace console messages. - const formatMsg = msg => - chalk`\n{blue gatsby-source-shopify/${shopName}} ${msg}` - - try { - console.log(formatMsg(`starting to fetch data from Shopify`)) - - // Arguments used for file node creation. - const imageArgs = { - createNode, - createNodeId, - touchNode, - store, - cache, - getCache, - getNode, - reporter, - downloadImages, - } - - // Arguments used for node creation. - const args = { - client, - createNode, - createNodeId, - formatMsg, - verbose, - imageArgs, - paginationSize, - queries, - } - - // Message printed when fetching is complete. - const msg = formatMsg(`finished fetching data from Shopify`) - - let promises = [] - if (includeCollections.includes(SHOP)) { - promises = promises.concat([ - createNodes(COLLECTION, queries.collections, CollectionNode, args), - createNodes( - PRODUCT, - queries.products, - ProductNode, - args, - async (product, productNode) => { - if (product.variants) - await forEach(product.variants.edges, async edge => { - const v = edge.node - if (v.metafields) - await forEach(v.metafields.edges, async edge => - createNode( - await ProductVariantMetafieldNode(imageArgs)(edge.node) - ) - ) - return createNode( - await ProductVariantNode(imageArgs, productNode)(edge.node) - ) - }) - - if (product.metafields) - await forEach(product.metafields.edges, async edge => - createNode(await ProductMetafieldNode(imageArgs)(edge.node)) - ) - - if (product.options) - await forEach(product.options, async option => - createNode(await ProductOptionNode(imageArgs)(option)) - ) - } - ), - createShopPolicies(args), - createShopDetails(args), - ]) - } - if (includeCollections.includes(CONTENT)) { - promises = promises.concat([ - createNodes(BLOG, queries.blogs, BlogNode, args), - createNodes(ARTICLE, queries.articles, ArticleNode, args, async x => { - if (x.comments) - await forEach(x.comments.edges, async edge => - createNode(await CommentNode(imageArgs)(edge.node)) - ) - }), - createPageNodes(PAGE, queries.pages, PageNode, args), - ]) - } - - console.time(msg) - await Promise.all(promises) - console.timeEnd(msg) - } catch (e) { - console.error(chalk`\n{red error} an error occurred while sourcing data`) - - // If not a GraphQL request error, let Gatsby print the error. - if (!e.hasOwnProperty(`request`)) throw e - - printGraphQLError(e) - } -} - -/** - * Fetch and create nodes for the provided endpoint, query, and node factory. - */ -const createNodes = async ( - endpoint, - query, - nodeFactory, - { client, createNode, formatMsg, verbose, imageArgs, paginationSize }, - f = async () => {} -) => { - // Message printed when fetching is complete. - const msg = formatMsg(`fetched and processed ${endpoint} nodes`) - - if (verbose) console.time(msg) - await forEach( - await queryAll( - client, - [NODE_TO_ENDPOINT_MAPPING[endpoint]], - query, - paginationSize - ), - async entity => { - const node = await nodeFactory(imageArgs)(entity) - createNode(node) - await f(entity, node) - } - ) - if (verbose) console.timeEnd(msg) -} - -/** - * Fetch and create nodes for shop policies. - */ -const createShopDetails = async ({ - client, - createNode, - formatMsg, - verbose, - queries, -}) => { - // // Message printed when fetching is complete. - const msg = formatMsg(`fetched and processed ${SHOP_DETAILS} nodes`) - - if (verbose) console.time(msg) - const { shop } = await queryOnce(client, queries.shopDetails) - createNode(ShopDetailsNode(shop)) - if (verbose) console.timeEnd(msg) -} - -/** - * Fetch and create nodes for shop policies. - */ -const createShopPolicies = async ({ - client, - createNode, - formatMsg, - verbose, - queries, -}) => { - // Message printed when fetching is complete. - const msg = formatMsg(`fetched and processed ${SHOP_POLICY} nodes`) - - if (verbose) console.time(msg) - const { shop: policies } = await queryOnce(client, queries.shopPolicies) - Object.entries(policies) - .filter(([_, policy]) => Boolean(policy)) - .forEach( - pipe(([type, policy]) => ShopPolicyNode(policy, { type }), createNode) - ) - if (verbose) console.timeEnd(msg) -} - -const createPageNodes = async ( - endpoint, - query, - nodeFactory, - { client, createNode, formatMsg, verbose, paginationSize }, - f = async () => {} -) => { - // Message printed when fetching is complete. - const msg = formatMsg(`fetched and processed ${endpoint} nodes`) - - if (verbose) console.time(msg) - await forEach( - await queryAll( - client, - [NODE_TO_ENDPOINT_MAPPING[endpoint]], - query, - paginationSize - ), - async entity => { - const node = await nodeFactory(entity) - createNode(node) - await f(entity) - } - ) - if (verbose) console.timeEnd(msg) -} diff --git a/packages/gatsby-source-shopify/src/gatsby-node.ts b/packages/gatsby-source-shopify/src/gatsby-node.ts new file mode 100644 index 0000000000000..cd71b6233364f --- /dev/null +++ b/packages/gatsby-source-shopify/src/gatsby-node.ts @@ -0,0 +1,292 @@ +import { createOperations } from "./operations" +import { eventsApi } from "./events" +import { + CreateResolversArgs, + NodePluginArgs, + PluginOptionsSchemaArgs, + SourceNodesArgs, +} from "gatsby" +import { makeResolveGatsbyImageData } from "./resolve-gatsby-image-data" +import { + getGatsbyImageResolver, + IGatsbyGraphQLResolverArgumentConfig, +} from "gatsby-plugin-image/graphql-utils" +import { shiftLeft } from "shift-left" +import { pluginErrorCodes as errorCodes } from "./errors" +import { makeSourceFromOperation } from "./make-source-from-operation" +export { createSchemaCustomization } from "./create-schema-customization" +import { createNodeId } from "./node-builder" +import { JoiObject } from "joi" + +export function pluginOptionsSchema({ + Joi, +}: PluginOptionsSchemaArgs): JoiObject { + // @ts-ignore TODO: When Gatsby updates Joi version, update type + // Vague type error that we're not able to figure out related to isJoi missing + // Probably related to Joi being outdated + return Joi.object({ + apiKey: Joi.string().required(), + password: Joi.string().required(), + storeUrl: Joi.string() + .pattern(/^[a-z-]+\.myshopify\.com$/) + .message( + `The storeUrl value should be your store's myshopify.com URL in the form "my-site.myshopify.com", without https or slashes` + ) + .required(), + downloadImages: Joi.boolean(), + typePrefix: Joi.string() + .pattern(new RegExp(`(^[A-Z]w*)`)) + .message( + `"typePrefix" can only be alphanumeric characters, starting with an uppercase letter` + ) + .default(``), + shopifyConnections: Joi.array() + .default([]) + .items(Joi.string().valid(`orders`, `collections`)), + salesChannel: Joi.string(), + }) +} + +async function sourceAllNodes( + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): Promise { + const { + createProductsOperation, + createOrdersOperation, + createCollectionsOperation, + finishLastOperation, + completedOperation, + cancelOperationInProgress, + } = createOperations(pluginOptions, gatsbyApi) + + const operations = [createProductsOperation] + if (pluginOptions.shopifyConnections?.includes(`orders`)) { + operations.push(createOrdersOperation) + } + + if (pluginOptions.shopifyConnections?.includes(`collections`)) { + operations.push(createCollectionsOperation) + } + + const sourceFromOperation = makeSourceFromOperation( + finishLastOperation, + completedOperation, + cancelOperationInProgress, + gatsbyApi, + pluginOptions + ) + + for (const op of operations) { + await sourceFromOperation(op) + } +} + +const shopifyNodeTypes = [ + `ShopifyLineItem`, + `ShopifyProductMetafield`, + `ShopifyProductVariantMetafield`, + `ShopifyCollectionMetafield`, + `ShopifyOrder`, + `ShopifyProduct`, + `ShopifyCollection`, + `ShopifyProductImage`, + `ShopifyCollectionImage`, + `ShopifyProductFeaturedImage`, + `ShopifyProductVariant`, + `ShopifyProductVariantImage`, + `ShopifyProductVariantPricePair`, + `ShopifyProductFeaturedMediaPreviewImage`, +] + +async function sourceChangedNodes( + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): Promise { + const { + incrementalProducts, + incrementalOrders, + incrementalCollections, + finishLastOperation, + completedOperation, + cancelOperationInProgress, + } = createOperations(pluginOptions, gatsbyApi) + const { typePrefix = `` } = pluginOptions + const lastBuildTime = new Date( + gatsbyApi.store.getState().status.plugins?.[`gatsby-source-shopify`]?.[ + `lastBuildTime${typePrefix}` + ] + ) + + for (const nodeType of shopifyNodeTypes) { + gatsbyApi + .getNodesByType(`${typePrefix}${nodeType}`) + .forEach(node => gatsbyApi.actions.touchNode(node)) + } + + const operations = [incrementalProducts(lastBuildTime)] + if (pluginOptions.shopifyConnections?.includes(`orders`)) { + operations.push(incrementalOrders(lastBuildTime)) + } + + if (pluginOptions.shopifyConnections?.includes(`collections`)) { + operations.push(incrementalCollections(lastBuildTime)) + } + + const sourceFromOperation = makeSourceFromOperation( + finishLastOperation, + completedOperation, + cancelOperationInProgress, + gatsbyApi, + pluginOptions + ) + + for (const op of operations) { + await sourceFromOperation(op) + } + + const { fetchDestroyEventsSince } = eventsApi(pluginOptions) + const destroyEvents = await fetchDestroyEventsSince(lastBuildTime) + + gatsbyApi.reporter.info( + `${destroyEvents.length} items have been deleted since ${lastBuildTime}` + ) + + if (destroyEvents.length) { + gatsbyApi.reporter.info(`Removing matching nodes from Gatsby`) + destroyEvents.forEach(e => { + const id = `${typePrefix}gid://shopify/${e.subject_type}/${e.subject_id}` + gatsbyApi.reporter.info(`Looking up node with ID: ${id}`) + const nodeId = createNodeId(id, gatsbyApi, pluginOptions) + const node = gatsbyApi.getNode(nodeId) + + if (node) { + gatsbyApi.reporter.info( + `Removing ${node.internal.type}: ${node.id} with shopifyId ${e.subject_id}` + ) + gatsbyApi.actions.deleteNode(node) + } else { + gatsbyApi.reporter.info(`Couldn't find node with ID: ${id}`) + } + }) + } +} + +export async function sourceNodes( + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): Promise { + const pluginStatus = gatsbyApi.store.getState().status.plugins?.[ + `gatsby-source-shopify` + ] + + const lastBuildTime = + pluginStatus?.[`lastBuildTime${pluginOptions.typePrefix || ``}`] + + if (lastBuildTime !== undefined) { + gatsbyApi.reporter.info(`Cache is warm, running an incremental build`) + await sourceChangedNodes(gatsbyApi, pluginOptions) + } else { + gatsbyApi.reporter.info(`Cache is cold, running a clean build`) + await sourceAllNodes(gatsbyApi, pluginOptions) + } + + gatsbyApi.reporter.info(`Finished sourcing nodes, caching last build time`) + gatsbyApi.actions.setPluginStatus( + pluginStatus !== undefined + ? { + ...pluginStatus, + [`lastBuildTime${pluginOptions.typePrefix || ``}`]: Date.now(), + } + : { + [`lastBuildTime${pluginOptions.typePrefix || ``}`]: Date.now(), + } + ) +} + +export function createResolvers( + { createResolvers, cache }: CreateResolversArgs, + { + downloadImages, + typePrefix = ``, + shopifyConnections = [], + }: ShopifyPluginOptions +): void { + if (!downloadImages) { + const args = { + placeholder: { + description: `Low resolution version of the image`, + type: `String`, + defaultValue: null, + } as IGatsbyGraphQLResolverArgumentConfig, + } + const imageNodeTypes = [ + `ShopifyProductImage`, + `ShopifyProductVariantImage`, + `ShopifyProductFeaturedImage`, + `ShopifyProductFeaturedMediaPreviewImage`, + ] + + if (shopifyConnections.includes(`collections`)) { + imageNodeTypes.push(`ShopifyCollectionImage`) + } + + const resolvers = imageNodeTypes.reduce((r, nodeType) => { + return { + ...r, + [`${typePrefix}${nodeType}`]: { + gatsbyImageData: getGatsbyImageResolver( + makeResolveGatsbyImageData(cache), + args + ), + }, + } + }, {}) + + createResolvers(resolvers) + } +} + +interface IErrorContext { + sourceMessage: string +} + +const getErrorText = (context: IErrorContext): string => context.sourceMessage + +export function onPreInit({ reporter }: NodePluginArgs): void { + reporter.setErrorMap({ + [errorCodes.bulkOperationFailed]: { + text: getErrorText, + level: `ERROR`, + category: `USER`, + }, + [errorCodes.apiConflict]: { + text: (): string => shiftLeft` + Your operation was canceled. You might have another production site for this Shopify store. + + Shopify only allows one bulk operation at a time for a given shop, so we recommend that you + avoid having two production sites that point to the same Shopify store. + + If the duplication is intentional, please wait for the other operation to finish before trying + again. Otherwise, consider deleting the other site or pointing it to a test store instead. + `, + level: `ERROR`, + category: `USER`, + }, + /** + * If we don't know what it is, we haven't done our due + * diligence to handle it explicitly. That means it's our + * fault, so THIRD_PARTY indicates us, the plugin authors. + */ + [errorCodes.unknownSourcingFailure]: { + text: getErrorText, + level: `ERROR`, + category: `THIRD_PARTY`, + }, + [errorCodes.unknownApiError]: { + text: getErrorText, + level: `ERROR`, + category: `THIRD_PARTY`, + }, + }) +} diff --git a/packages/gatsby-source-shopify/src/get-shopify-image.ts b/packages/gatsby-source-shopify/src/get-shopify-image.ts new file mode 100644 index 0000000000000..de78b865fdcb4 --- /dev/null +++ b/packages/gatsby-source-shopify/src/get-shopify-image.ts @@ -0,0 +1,72 @@ +import { + IUrlBuilderArgs, + IGetImageDataArgs, + getImageData, + IGatsbyImageData, +} from "gatsby-plugin-image" +const validFormats = new Set([`jpg`, `png`, `webp`, `auto`]) + +export interface IShopifyImage { + width: number + height: number + originalSrc: string +} +export interface IGetShopifyImageArgs + extends Omit< + IGetImageDataArgs, + "urlBuilder" | "baseUrl" | "formats" | "sourceWidth" | "sourceHeight" + > { + image: IShopifyImage +} +export function urlBuilder({ + width, + height, + baseUrl, + format, +}: IUrlBuilderArgs): string { + if (!validFormats.has(format)) { + console.warn( + `${format} is not a valid format. Valid formats are: ${[ + ...validFormats, + ].join(`, `)}` + ) + format = `auto` + } + + let [basename, version] = baseUrl.split(`?`) + + const dot = basename.lastIndexOf(`.`) + let ext = `` + if (dot !== -1) { + ext = basename.slice(dot + 1) + basename = basename.slice(0, dot) + } + let suffix = `` + if (format === ext || format === `auto`) { + suffix = `.${ext}` + } else { + suffix = `.${ext}.${format}` + } + + return `${basename}_${width}x${height}_crop_center${suffix}?${version}` +} + +export function getShopifyImage({ + image, + ...args +}: IGetShopifyImageArgs): IGatsbyImageData { + const { + originalSrc: baseUrl, + width: sourceWidth, + height: sourceHeight, + } = image + + return getImageData({ + ...args, + baseUrl, + sourceWidth, + sourceHeight, + urlBuilder, + formats: [`auto`], + }) +} diff --git a/packages/gatsby-source-shopify/src/index.js b/packages/gatsby-source-shopify/src/index.js deleted file mode 100644 index 23fdb69814b6d..0000000000000 --- a/packages/gatsby-source-shopify/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from "./constants" diff --git a/packages/gatsby-source-shopify/src/index.ts b/packages/gatsby-source-shopify/src/index.ts new file mode 100644 index 0000000000000..e6f10954e72f6 --- /dev/null +++ b/packages/gatsby-source-shopify/src/index.ts @@ -0,0 +1 @@ +export { getShopifyImage } from "./get-shopify-image" diff --git a/packages/gatsby-source-shopify/src/lib.js b/packages/gatsby-source-shopify/src/lib.js deleted file mode 100644 index 98520225bb5c3..0000000000000 --- a/packages/gatsby-source-shopify/src/lib.js +++ /dev/null @@ -1,60 +0,0 @@ -import prettyjson from "prettyjson" -import chalk from "chalk" -import { get, getOr, last } from "lodash/fp" - -/** - * Print an error from a GraphQL client - */ -export const printGraphQLError = e => { - const prettyjsonOptions = { keysColor: `red`, dashColor: `red` } - - if (e.response && e.response.errors) { - if (e.message.startsWith(`access denied`)) { - console.error(chalk`\n{yellow Check your token has this read authorization, - or omit fetching this object using the "includeCollections" options in gatsby-source-shopify plugin options}`) - } - console.error(prettyjson.render(e.response.errors, prettyjsonOptions)) - } - - if (e.request) console.error(prettyjson.render(e.request, prettyjsonOptions)) -} - -/** - * Request a query from a client. - */ -export const queryOnce = async (client, query, first = 250, after) => - await client.request(query, { first, after }) - -/** - * Get all paginated data from a query. Will execute multiple requests as - * needed. - */ -export const queryAll = async ( - client, - path, - query, - first = 250, - after = null, - aggregatedResponse = null -) => { - const data = await queryOnce(client, query, first, after) - const edges = getOr([], [...path, `edges`], data) - const nodes = edges.map(edge => edge.node) - - aggregatedResponse = aggregatedResponse - ? aggregatedResponse.concat(nodes) - : nodes - - if (get([...path, `pageInfo`, `hasNextPage`], data)) { - return queryAll( - client, - path, - query, - first, - last(edges).cursor, - aggregatedResponse - ) - } - - return aggregatedResponse -} diff --git a/packages/gatsby-source-shopify/src/make-source-from-operation.ts b/packages/gatsby-source-shopify/src/make-source-from-operation.ts new file mode 100644 index 0000000000000..35623de3c60d3 --- /dev/null +++ b/packages/gatsby-source-shopify/src/make-source-from-operation.ts @@ -0,0 +1,178 @@ +import fetch from "node-fetch" +import { SourceNodesArgs } from "gatsby" +import { createInterface } from "readline" +import { shiftLeft } from "shift-left" + +import { nodeBuilder } from "./node-builder" +import { IShopifyBulkOperation } from "./operations" +import { + OperationError, + HttpError, + pluginErrorCodes as errorCodes, +} from "./errors" + +export function makeSourceFromOperation( + finishLastOperation: () => Promise, + completedOperation: (id: string) => Promise<{ node: BulkOperationNode }>, + cancelOperationInProgress: () => Promise, + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +) { + return async function sourceFromOperation( + op: IShopifyBulkOperation, + isPriorityBuild = process.env.IS_PRODUCTION_BRANCH === `true` + ): Promise { + const { reporter, actions } = gatsbyApi + + const operationTimer = reporter.activityTimer( + `Source from bulk operation ${op.name}` + ) + + operationTimer.start() + + try { + if (isPriorityBuild) { + await cancelOperationInProgress() + } else { + await finishLastOperation() + } + + reporter.info(`Initiating bulk operation query ${op.name}`) + const { + bulkOperationRunQuery: { userErrors, bulkOperation }, + } = await op.execute() + + if (userErrors.length) { + reporter.panic( + userErrors.map(e => { + const msg = e.field + ? `${e.field.join(`.`)}: ${e.message}` + : e.message + + return { + id: errorCodes.bulkOperationFailed, + context: { + sourceMessage: `Couldn't initiate bulk operation query`, + }, + error: new Error(msg), + } + }) + ) + } + + const resp = await completedOperation(bulkOperation.id) + reporter.info(`Completed bulk operation ${op.name}: ${bulkOperation.id}`) + + if (parseInt(resp.node.objectCount, 10) === 0) { + reporter.info(`No data was returned for this operation`) + operationTimer.end() + return + } + + operationTimer.setStatus( + `Fetching ${resp.node.objectCount} results for ${op.name}: ${bulkOperation.id}` + ) + + const results = await fetch(resp.node.url) + + operationTimer.setStatus( + `Processing ${resp.node.objectCount} results for ${op.name}: ${bulkOperation.id}` + ) + const rl = createInterface({ + input: results.body, + crlfDelay: Infinity, + }) + + reporter.info(`Creating nodes from bulk operation ${op.name}`) + + const objects: BulkResults = [] + + for await (const line of rl) { + objects.push(JSON.parse(line)) + } + + await Promise.all( + op + .process( + objects, + nodeBuilder(gatsbyApi, pluginOptions), + gatsbyApi, + pluginOptions + ) + .map(async promise => { + const node = await promise + actions.createNode(node) + }) + ) + + operationTimer.end() + } catch (e) { + if (e instanceof OperationError) { + const code = errorCodes.bulkOperationFailed + + if (e.node.status === `CANCELED`) { + if (isPriorityBuild) { + /** + * There are at least two production sites for this Shopify + * store trying to run an operation at the same time. + */ + reporter.panic({ + id: errorCodes.apiConflict, + error: e, + context: {}, + }) + } else { + // A prod build canceled me, wait and try again + operationTimer.setStatus( + `This operation has been canceled by a higher priority build. It will retry shortly.` + ) + operationTimer.end() + await new Promise(resolve => setTimeout(resolve, 5000)) + await sourceFromOperation(op) + } + } + + if (e.node.errorCode === `ACCESS_DENIED`) { + reporter.panic({ + id: code, + context: { + sourceMessage: `Your credentials don't have access to a resource you requested`, + }, + error: e, + }) + } + + reporter.panic({ + id: errorCodes.unknownSourcingFailure, + context: { + sourceMessage: shiftLeft` + Operation ${op.name} failed after ${e.node.objectCount} objects + - With status: ${e.node.status} - error code: ${e.node.errorCode} + `, + }, + error: e, + }) + } + + if (e instanceof HttpError) { + reporter.panic({ + id: errorCodes.unknownApiError, + context: { + sourceMessage: `Received error ${ + e.response.status + } from Shopify: ${await e.response.text()}`, + }, + error: e, + }) + } + + reporter.panic({ + id: errorCodes.unknownSourcingFailure, + context: { + sourceMessage: `Could not source from bulk operation`, + }, + error: e, + }) + } + } +} diff --git a/packages/gatsby-source-shopify/src/node-builder.ts b/packages/gatsby-source-shopify/src/node-builder.ts new file mode 100644 index 0000000000000..fb809b40fd015 --- /dev/null +++ b/packages/gatsby-source-shopify/src/node-builder.ts @@ -0,0 +1,194 @@ +import { NodeInput, SourceNodesArgs } from "gatsby" +import { createRemoteFileNode } from "gatsby-source-filesystem" + +// 'gid://shopify/Metafield/6936247730264' +export const pattern = /^gid:\/\/shopify\/(\w+)\/(.+)$/ + +export function createNodeId( + shopifyId: string, + gatsbyApi: SourceNodesArgs, + { typePrefix = `` }: ShopifyPluginOptions +): string { + return gatsbyApi.createNodeId(`${typePrefix}${shopifyId}`) +} + +function attachParentId( + result: BulkResult, + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): void { + if (result.__parentId) { + const [fullId, remoteType] = result.__parentId.match(pattern) || [] + const field = remoteType.charAt(0).toLowerCase() + remoteType.slice(1) + const idField = `${field}Id` + result[idField] = createNodeId(fullId, gatsbyApi, pluginOptions) + } +} + +const downloadImageAndCreateFileNode = async ( + { url, nodeId }: { url: string; nodeId: string }, + { + actions: { createNode }, + createNodeId, + cache, + store, + reporter, + }: SourceNodesArgs +): Promise => { + const fileNode = await createRemoteFileNode({ + url, + cache, + createNode, + createNodeId, + parentNodeId: nodeId, + store, + reporter, + }) + + return fileNode.id +} + +interface IProcessorMap { + [remoteType: string]: ( + node: NodeInput, + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions + ) => Promise +} + +interface IImageData { + id: string + originalSrc: string + localFile: string | undefined +} + +async function processChildImage( + node: NodeInput, + getImageData: (node: NodeInput) => IImageData | undefined, + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): Promise { + if (pluginOptions.downloadImages) { + const image = getImageData(node) + + if (image) { + const url = image.originalSrc + const fileNodeId = await downloadImageAndCreateFileNode( + { + url, + nodeId: node.id, + }, + gatsbyApi + ) + + image.localFile = fileNodeId + } + } +} + +export const processorMap: IProcessorMap = { + LineItem: async (node, gatsbyApi, pluginOptions) => { + const lineItem = node + if (lineItem.product) { + lineItem.productId = createNodeId( + (lineItem.product as BulkResult).id, + gatsbyApi, + pluginOptions + ) + delete lineItem.product + } + }, + ProductImage: async (node, gatsbyApi, options) => { + if (options.downloadImages) { + const url = node.originalSrc as string + const fileNodeId = await downloadImageAndCreateFileNode( + { + url, + nodeId: node.id, + }, + gatsbyApi + ) + + node.localFile = fileNodeId + } + }, + Collection: async (node, gatsbyApi, options) => + processChildImage( + node, + node => node.image as IImageData, + gatsbyApi, + options + ), + Product: async (node, gatsbyApi, options) => { + await processChildImage( + node, + node => node.featuredImage as IImageData, + gatsbyApi, + options + ) + await processChildImage( + node, + node => { + const media = node.featuredMedia as + | { + preview?: { + image?: IImageData + } + } + | undefined + + return media?.preview?.image + }, + gatsbyApi, + options + ) + }, + ProductVariant: async (node, gatsbyApi, options) => + processChildImage( + node, + node => node.image as IImageData, + gatsbyApi, + options + ), + Metafield: async (node, _gatsbyApi, { typePrefix = `` }) => { + const [, parentType] = (node.__parentId as string).match(pattern) || [] + const internalType = `${typePrefix}Shopify${parentType}Metafield` + node.internal.type = internalType + }, +} + +export function nodeBuilder( + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): NodeBuilder { + return { + async buildNode(result: BulkResult): Promise { + if (!pattern.test(result.id)) { + throw new Error( + `Expected an ID in the format gid://shopify//` + ) + } + + const [, remoteType] = result.id.match(pattern) || [] + + const processor = + processorMap[remoteType] || ((): Promise => Promise.resolve()) + + attachParentId(result, gatsbyApi, pluginOptions) + + const node = { + ...result, + shopifyId: result.id, + id: createNodeId(result.id, gatsbyApi, pluginOptions), + internal: { + type: `${pluginOptions.typePrefix || ``}Shopify${remoteType}`, + contentDigest: gatsbyApi.createContentDigest(result), + }, + } + + await processor(node, gatsbyApi, pluginOptions) + + return node + }, + } +} diff --git a/packages/gatsby-source-shopify/src/nodes.js b/packages/gatsby-source-shopify/src/nodes.js deleted file mode 100644 index e57f998dfded3..0000000000000 --- a/packages/gatsby-source-shopify/src/nodes.js +++ /dev/null @@ -1,195 +0,0 @@ -import createNodeHelpers from "gatsby-node-helpers" -import { map } from "p-iteration" -import { createRemoteFileNode } from "gatsby-source-filesystem" - -import { - TYPE_PREFIX, - ARTICLE, - BLOG, - COLLECTION, - COMMENT, - PRODUCT, - PRODUCT_OPTION, - PRODUCT_VARIANT, - PRODUCT_METAFIELD, - PRODUCT_VARIANT_METAFIELD, - SHOP_POLICY, - SHOP_DETAILS, - PAGE, -} from "./constants" - -const { createNodeFactory, generateNodeId } = createNodeHelpers({ - typePrefix: TYPE_PREFIX, -}) - -const downloadImageAndCreateFileNode = async ( - { url, nodeId }, - { - createNode, - createNodeId, - touchNode, - store, - cache, - getCache, - getNode, - reporter, - downloadImages, - } -) => { - if (!downloadImages) return undefined - - const mediaDataCacheKey = `${TYPE_PREFIX}__Media__${url}` - const cacheMediaData = await cache.get(mediaDataCacheKey) - - if (cacheMediaData) { - const fileNodeID = cacheMediaData.fileNodeID - touchNode(getNode(fileNodeID)) - return fileNodeID - } - - const fileNode = await createRemoteFileNode({ - url, - store, - cache, - createNode, - createNodeId, - getCache, - parentNodeId: nodeId, - reporter, - }) - - if (fileNode) { - const fileNodeID = fileNode.id - await cache.set(mediaDataCacheKey, { fileNodeID }) - return fileNodeID - } - - return undefined -} - -export const ArticleNode = imageArgs => - createNodeFactory(ARTICLE, async node => { - if (node.blog) node.blog___NODE = generateNodeId(BLOG, node.blog.id) - - if (node.comments) - node.comments___NODE = node.comments.edges.map(edge => - generateNodeId(COMMENT, edge.node.id) - ) - - if (node.image) - node.image.localFile___NODE = await downloadImageAndCreateFileNode( - { id: node.image.id, url: node.image.src, nodeId: node.id }, - imageArgs - ) - - return node - }) - -export const BlogNode = _imageArgs => createNodeFactory(BLOG) - -export const CollectionNode = imageArgs => - createNodeFactory(COLLECTION, async node => { - if (node.products) { - node.products___NODE = node.products.edges.map(edge => - generateNodeId(PRODUCT, edge.node.id) - ) - delete node.products - } - if (node.image) - node.image.localFile___NODE = await downloadImageAndCreateFileNode( - { - id: node.image.id, - url: node.image.src, - nodeId: node.id, - }, - imageArgs - ) - return node - }) - -export const CommentNode = _imageArgs => createNodeFactory(COMMENT) - -export const ProductNode = imageArgs => - createNodeFactory(PRODUCT, async node => { - if (node.variants) { - const variants = node.variants.edges.map(edge => edge.node) - - node.variants___NODE = variants.map(variant => - generateNodeId(PRODUCT_VARIANT, variant.id) - ) - - delete node.variants - } - - if (node.metafields) { - const metafields = node.metafields.edges.map(edge => edge.node) - - node.metafields___NODE = metafields.map(metafield => - generateNodeId(PRODUCT_METAFIELD, metafield.id) - ) - delete node.metafields - } - - if (node.options) { - node.options___NODE = node.options.map(option => - generateNodeId(PRODUCT_OPTION, option.id) - ) - delete node.options - } - - if (node.images && node.images.edges) - node.images = await map(node.images.edges, async edge => { - edge.node.localFile___NODE = await downloadImageAndCreateFileNode( - { - id: edge.node.id, - url: edge.node.originalSrc, - }, - imageArgs - ) - return edge.node - }) - - return node - }) - -export const ProductMetafieldNode = _imageArgs => - createNodeFactory(PRODUCT_METAFIELD) - -export const ProductOptionNode = _imageArgs => createNodeFactory(PRODUCT_OPTION) - -export const ProductVariantNode = (imageArgs, productNode) => - createNodeFactory(PRODUCT_VARIANT, async node => { - if (node.metafields) { - const metafields = node.metafields.edges.map(edge => edge.node) - - node.metafields___NODE = metafields.map(metafield => - generateNodeId(PRODUCT_VARIANT_METAFIELD, metafield.id) - ) - delete node.metafields - } - - if (node.image) - node.image.localFile___NODE = await downloadImageAndCreateFileNode( - { - id: node.image.id, - url: node.image.originalSrc, - }, - imageArgs - ) - - if (!isNaN(node.price)) { - node.priceNumber = parseFloat(node.price) - } - - node.product___NODE = productNode.id - return node - }) - -export const ProductVariantMetafieldNode = _imageArgs => - createNodeFactory(PRODUCT_VARIANT_METAFIELD) - -export const ShopPolicyNode = createNodeFactory(SHOP_POLICY) - -export const ShopDetailsNode = createNodeFactory(SHOP_DETAILS) - -export const PageNode = createNodeFactory(PAGE) diff --git a/packages/gatsby-source-shopify/src/operations.ts b/packages/gatsby-source-shopify/src/operations.ts new file mode 100644 index 0000000000000..e782fe433c2de --- /dev/null +++ b/packages/gatsby-source-shopify/src/operations.ts @@ -0,0 +1,255 @@ +import { NodeInput, SourceNodesArgs } from "gatsby" +import { shiftLeft } from "shift-left" +import { createClient } from "./client" +import { ProductsQuery } from "./query-builders/products-query" +import { CollectionsQuery } from "./query-builders/collections-query" +import { OrdersQuery } from "./query-builders/orders-query" +import { + collectionsProcessor, + incrementalProductsProcessor, +} from "./processors" +import { OperationError } from "./errors" + +import { + OPERATION_STATUS_QUERY, + OPERATION_BY_ID, + CANCEL_OPERATION, +} from "./static-queries" + +export interface IShopifyBulkOperation { + execute: () => Promise + name: string + process: ( + objects: BulkResults, + nodeBuilder: NodeBuilder, + _gatsbyApi: SourceNodesArgs, + _pluginOptions: ShopifyPluginOptions + ) => Array> +} + +interface IOperations { + incrementalProducts: (date: Date) => IShopifyBulkOperation + incrementalOrders: (date: Date) => IShopifyBulkOperation + incrementalCollections: (date: Date) => IShopifyBulkOperation + createProductsOperation: IShopifyBulkOperation + createOrdersOperation: IShopifyBulkOperation + createCollectionsOperation: IShopifyBulkOperation + cancelOperationInProgress: () => Promise + cancelOperation: (id: string) => Promise + finishLastOperation: () => Promise + completedOperation: ( + operationId: string, + interval?: number + ) => Promise<{ node: BulkOperationNode }> +} + +const finishedStatuses = [`COMPLETED`, `FAILED`, `CANCELED`, `EXPIRED`] +const failedStatuses = [`FAILED`, `CANCELED`] + +function defaultProcessor( + objects: BulkResults, + builder: NodeBuilder +): Array> { + return objects.map(builder.buildNode) +} + +export function createOperations( + options: ShopifyPluginOptions, + { reporter }: SourceNodesArgs +): IOperations { + const client = createClient(options) + + function currentOperation(): Promise { + return client.request(OPERATION_STATUS_QUERY) + } + + function createOperation( + operationQuery: string, + name: string, + process?: IShopifyBulkOperation["process"] + ): IShopifyBulkOperation { + return { + execute: (): Promise => + client.request(operationQuery), + name, + process: process || defaultProcessor, + } + } + + async function finishLastOperation(): Promise { + let { currentBulkOperation } = await currentOperation() + if (currentBulkOperation && currentBulkOperation.id) { + const timer = reporter.activityTimer( + `Waiting for operation ${currentBulkOperation.id} : ${currentBulkOperation.status}` + ) + timer.start() + + while (!finishedStatuses.includes(currentBulkOperation.status)) { + await new Promise(resolve => setTimeout(resolve, 1000)) + currentBulkOperation = (await currentOperation()).currentBulkOperation + timer.setStatus( + `Polling operation ${currentBulkOperation.id} : ${currentBulkOperation.status}` + ) + } + + timer.end() + } + } + + async function cancelOperation( + id: string + ): Promise { + return client.request(CANCEL_OPERATION, { + id, + }) + } + + async function cancelOperationInProgress(): Promise { + let { currentBulkOperation: bulkOperation } = await currentOperation() + if (!bulkOperation) { + return + } + + const cancelTimer = reporter.activityTimer( + `Canceling previous operation: ${bulkOperation.id}` + ) + + cancelTimer.start() + + if (bulkOperation.status === `RUNNING`) { + cancelTimer.setStatus( + `Canceling a currently running operation: ${bulkOperation.id}, this could take a few moments` + ) + + const { bulkOperationCancel } = await cancelOperation(bulkOperation.id) + + bulkOperation = bulkOperationCancel.bulkOperation + + while (bulkOperation.status !== `CANCELED`) { + await new Promise(resolve => setTimeout(resolve, 100)) + const currentOp = await currentOperation() + bulkOperation = currentOp.currentBulkOperation + cancelTimer.setStatus( + `Waiting for operation to cancel: ${bulkOperation.id}, ${bulkOperation.status}` + ) + } + } else { + /** + * Just because it's not running doesn't mean it's done. For + * example, it could be CANCELING. We still have to wait for it + * to be officially finished before we start a new one. + */ + while (!finishedStatuses.includes(bulkOperation.status)) { + await new Promise(resolve => setTimeout(resolve, 100)) + bulkOperation = (await currentOperation()).currentBulkOperation + cancelTimer.setStatus( + `Waiting for operation to cancel: ${bulkOperation.id}, ${bulkOperation.status}` + ) + } + } + + cancelTimer.end() + } + + /* Maybe the interval should be adjustable, because users + * with larger data sets could easily wait longer. We could + * perhaps detect that the interval being used is too small + * based on returned object counts and iteration counts, and + * surface feedback to the user suggesting that they increase + * the interval. + */ + async function completedOperation( + operationId: string, + interval = 1000 + ): Promise<{ node: BulkOperationNode }> { + let operation = await client.request<{ + node: BulkOperationNode + }>(OPERATION_BY_ID, { + id: operationId, + }) + + const completedTimer = reporter.activityTimer( + `Waiting for bulk operation to complete` + ) + + completedTimer.start() + + let waitForOperation = true + + while (waitForOperation) { + if (failedStatuses.includes(operation.node.status)) { + completedTimer.end() + waitForOperation = false + throw new OperationError(operation.node) + } + + if (operation.node.status === `COMPLETED`) { + completedTimer.end() + waitForOperation = false + return operation + } + + await new Promise(resolve => setTimeout(resolve, interval)) + + operation = await client.request<{ + node: BulkOperationNode + }>(OPERATION_BY_ID, { + id: operationId, + }) + + completedTimer.setStatus(shiftLeft` + Polling bulk operation: ${operation.node.id} + Status: ${operation.node.status} + Object count: ${operation.node.objectCount} + `) + } + + throw new Error(`It should never reach this error`) + } + + return { + incrementalProducts(date: Date): IShopifyBulkOperation { + return createOperation( + new ProductsQuery(options).query(date), + `INCREMENTAL_PRODUCTS`, + incrementalProductsProcessor + ) + }, + + incrementalOrders(date: Date): IShopifyBulkOperation { + return createOperation( + new OrdersQuery(options).query(date), + `INCREMENTAL_ORDERS` + ) + }, + + incrementalCollections(date: Date): IShopifyBulkOperation { + return createOperation( + new CollectionsQuery(options).query(date), + `INCREMENTAL_COLLECTIONS`, + collectionsProcessor + ) + }, + + createProductsOperation: createOperation( + new ProductsQuery(options).query(), + `PRODUCTS` + ), + + createOrdersOperation: createOperation( + new OrdersQuery(options).query(), + `ORDERS` + ), + + createCollectionsOperation: createOperation( + new CollectionsQuery(options).query(), + `COLLECTIONS`, + collectionsProcessor + ), + + cancelOperationInProgress, + cancelOperation, + finishLastOperation, + completedOperation, + } +} diff --git a/packages/gatsby-source-shopify/src/processors/collections.ts b/packages/gatsby-source-shopify/src/processors/collections.ts new file mode 100644 index 0000000000000..9747485c482a6 --- /dev/null +++ b/packages/gatsby-source-shopify/src/processors/collections.ts @@ -0,0 +1,49 @@ +import { NodeInput, SourceNodesArgs } from "gatsby" +import { pattern as idPattern, createNodeId } from "../node-builder" + +export function collectionsProcessor( + objects: BulkResults, + builder: NodeBuilder, + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): Array> { + const promises = [] + const collectionProductIndex: { [collectionId: string]: Array } = {} + + /** + * Read results in reverse so we can collect child node IDs. + * See Shopify Bulk Operation guide for more info. + * + * https://shopify.dev/tutorials/perform-bulk-operations-with-admin-api#download-result-data + */ + for (let i = objects.length - 1; i >= 0; i--) { + const result = objects[i] + const [id, remoteType] = result.id.match(idPattern) || [] + if (remoteType === `Product`) { + /** + * We source products in a separate operation. Here we are + * just collecting product IDs so we can tell Gatsby about + * the many-to-many relationship between collections and + * products. + */ + if (!collectionProductIndex[result.__parentId]) { + collectionProductIndex[result.__parentId] = [] + } + + collectionProductIndex[result.__parentId].unshift( + createNodeId(id, gatsbyApi, pluginOptions) + ) + } + + if (remoteType === `Metafield`) { + promises.push(builder.buildNode(result)) + } + + if (remoteType == `Collection`) { + result.productIds = collectionProductIndex[result.id] || [] + promises.push(builder.buildNode(result)) + } + } + + return promises +} diff --git a/packages/gatsby-source-shopify/src/processors/incremental-products.ts b/packages/gatsby-source-shopify/src/processors/incremental-products.ts new file mode 100644 index 0000000000000..c7719de918375 --- /dev/null +++ b/packages/gatsby-source-shopify/src/processors/incremental-products.ts @@ -0,0 +1,58 @@ +import { NodeInput, SourceNodesArgs } from "gatsby" +import { pattern as idPattern, createNodeId } from "../node-builder" + +export function incrementalProductsProcessor( + objects: BulkResults, + builder: NodeBuilder, + gatsbyApi: SourceNodesArgs, + pluginOptions: ShopifyPluginOptions +): Array> { + const { typePrefix = `` } = pluginOptions + const products = objects.filter(obj => { + const [, remoteType] = obj.id.match(idPattern) || [] + + return remoteType === `Product` + }) + + const nodeIds = products.map(product => + createNodeId(product.id, gatsbyApi, pluginOptions) + ) + + /** + * The events API doesn't tell us about deleted variants or images, so when we + * get the list of changed products, we have to compare those product + * variants/images with what we have in the cache, and delete those that are + * not present in the newer API results. + */ + const variantsToDelete = gatsbyApi + .getNodesByType(`${typePrefix}ShopifyProductVariant`) + .filter(node => nodeIds.includes(node.productId as string)) + + variantsToDelete.forEach(variant => { + gatsbyApi.actions.deleteNode(variant) + }) + + const imagesToDelete = gatsbyApi + .getNodesByType(`${typePrefix}ShopifyProductImage`) + .filter(node => nodeIds.includes(node.productId as string)) + + imagesToDelete.forEach(image => { + gatsbyApi.actions.deleteNode(image) + }) + + /** + * Additionally, product variants have metafields attached to them, so + * we must delete those as well to avoid oprhaned nodes building up in + * the cache. + */ + const variantIds = variantsToDelete.map(v => v.id) + gatsbyApi + .getNodesByType(`${typePrefix}ShopifyProductVariantMetafield`) + .forEach(metafield => { + if (variantIds.includes(metafield.productVariantId as string)) { + gatsbyApi.actions.deleteNode(metafield) + } + }) + + return objects.map(builder.buildNode) +} diff --git a/packages/gatsby-source-shopify/src/processors/index.ts b/packages/gatsby-source-shopify/src/processors/index.ts new file mode 100644 index 0000000000000..d6d75574ae031 --- /dev/null +++ b/packages/gatsby-source-shopify/src/processors/index.ts @@ -0,0 +1,2 @@ +export * from "./collections" +export * from "./incremental-products" diff --git a/packages/gatsby-source-shopify/src/queries.js b/packages/gatsby-source-shopify/src/queries.js deleted file mode 100644 index 60592bbb34086..0000000000000 --- a/packages/gatsby-source-shopify/src/queries.js +++ /dev/null @@ -1,292 +0,0 @@ -export const ARTICLES_QUERY = ` - query GetArticles($first: Int!, $after: String) { - articles(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - author { - bio - email - firstName - lastName - name - } - blog { - id - } - comments(first: 250) { - edges { - node { - author { - email - name - } - content - contentHtml - id - } - } - } - content - contentHtml - excerpt - excerptHtml - handle - id - handle - image { - altText - id - src - } - publishedAt - tags - title - url - seo { - title - description - } - } - } - } - } -` - -export const BLOGS_QUERY = ` - query GetBlogs($first: Int!, $after: String) { - blogs(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - handle - id - handle - title - url - } - } - } - } -` - -export const COLLECTIONS_QUERY = ` - query GetCollections($first: Int!, $after: String) { - collections(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - description - descriptionHtml - handle - id - image { - altText - id - src - } - products(first: 250) { - edges { - node { - id - } - } - } - title - updatedAt - } - } - } - } -` - -export const PRODUCTS_QUERY = ` - query GetProducts($first: Int!, $after: String) { - products(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - availableForSale - createdAt - description - descriptionHtml - handle - id - images(first: 250) { - edges { - node { - id - altText - originalSrc - } - } - } - metafields(first: 250) { - edges { - node { - description - id - key - namespace - value - valueType - } - } - } - onlineStoreUrl - options { - id - name - values - } - priceRange { - minVariantPrice { - amount - currencyCode - } - maxVariantPrice { - amount - currencyCode - } - } - productType - publishedAt - tags - title - updatedAt - variants(first: 250) { - edges { - node { - availableForSale - compareAtPrice - compareAtPriceV2 { - amount - currencyCode - } - id - image { - altText - id - originalSrc - } - metafields(first: 250) { - edges { - node { - description - id - key - namespace - value - valueType - } - } - } - price - priceV2 { - amount - currencyCode - } - requiresShipping - selectedOptions { - name - value - } - sku - title - weight - weightUnit - presentmentPrices(first: 250) { - edges { - node { - price { - amount - currencyCode - } - compareAtPrice { - amount - currencyCode - } - } - } - } - } - } - } - vendor - } - } - } - } -` - -export const SHOP_DETAILS_QUERY = ` -query GetShop { - shop { - description - moneyFormat - name - } -} -` - -export const SHOP_POLICIES_QUERY = ` - query GetPolicies { - shop { - privacyPolicy { - body - handle - id - title - url - } - refundPolicy { - body - handle - id - title - url - } - termsOfService { - body - handle - id - title - url - } - } - } -` - -export const PAGES_QUERY = ` - query GetPages($first: Int!, $after: String) { - pages(first: $first, after: $after) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - id - handle - title - body - bodySummary - updatedAt - url - } - } - } - } -` diff --git a/packages/gatsby-source-shopify/src/query-builders/bulk-query.ts b/packages/gatsby-source-shopify/src/query-builders/bulk-query.ts new file mode 100644 index 0000000000000..4ecb742bc5887 --- /dev/null +++ b/packages/gatsby-source-shopify/src/query-builders/bulk-query.ts @@ -0,0 +1,30 @@ +export abstract class BulkQuery { + pluginOptions: ShopifyPluginOptions + + constructor(pluginOptions: ShopifyPluginOptions) { + this.pluginOptions = pluginOptions + } + + abstract query(date?: Date): string + + protected bulkOperationQuery(query: string): string { + return ` + mutation INITIATE_BULK_OPERATION { + bulkOperationRunQuery( + query: """ + ${query} + """ + ) { + bulkOperation { + id + status + } + userErrors { + field + message + } + } + } + ` + } +} diff --git a/packages/gatsby-source-shopify/src/query-builders/collections-query.ts b/packages/gatsby-source-shopify/src/query-builders/collections-query.ts new file mode 100644 index 0000000000000..4059506bf4073 --- /dev/null +++ b/packages/gatsby-source-shopify/src/query-builders/collections-query.ts @@ -0,0 +1,100 @@ +import { BulkQuery } from "./bulk-query" + +export class CollectionsQuery extends BulkQuery { + query(date?: Date): string { + const publishedStatus = this.pluginOptions.salesChannel + ? encodeURIComponent(`${this.pluginOptions.salesChannel}=visible`) + : `published` + + const filters = [`published_status:${publishedStatus}`] + if (date) { + const isoDate = date.toISOString() + filters.push(`created_at:>='${isoDate}' OR updated_at:>='${isoDate}'`) + } + + const queryString = filters.map(f => `(${f})`).join(` AND `) + + const query = ` + { + collections(query: "${queryString}") { + edges { + node { + products { + edges { + node { + id + } + } + } + metafields { + edges { + node { + createdAt + description + id + key + legacyResourceId + namespace + ownerType + updatedAt + value + valueType + } + } + } + description + descriptionHtml + feedback { + details { + app { + id + } + link { + label + url + } + messages { + field + message + } + } + summary + } + handle + id + image { + id + altText + height + width + originalSrc + transformedSrc + } + legacyResourceId + productsCount + ruleSet { + appliedDisjunctively + rules { + column + condition + relation + } + } + seo { + description + title + } + sortOrder + storefrontId + templateSuffix + title + updatedAt + } + } + } + } + ` + + return this.bulkOperationQuery(query) + } +} diff --git a/packages/gatsby-source-shopify/src/query-builders/orders-query.ts b/packages/gatsby-source-shopify/src/query-builders/orders-query.ts new file mode 100644 index 0000000000000..32e7b94fb50bc --- /dev/null +++ b/packages/gatsby-source-shopify/src/query-builders/orders-query.ts @@ -0,0 +1,43 @@ +import { BulkQuery } from "./bulk-query" + +export class OrdersQuery extends BulkQuery { + query(date?: Date): string { + const filters = [] + if (date) { + const isoDate = date.toISOString() + filters.push(`created_at:>='${isoDate}' OR updated_at:>='${isoDate}'`) + } + + const queryString = filters.map(f => `(${f})`).join(` AND `) + + const query = ` + { + orders(query: "${queryString}") { + edges { + node { + id + edited + closed + closedAt + refunds { + id + createdAt + } + lineItems { + edges { + node { + id + product { + id + } + } + } + } + } + } + } + }` + + return this.bulkOperationQuery(query) + } +} diff --git a/packages/gatsby-source-shopify/src/query-builders/products-query.ts b/packages/gatsby-source-shopify/src/query-builders/products-query.ts new file mode 100644 index 0000000000000..9acbf1845561e --- /dev/null +++ b/packages/gatsby-source-shopify/src/query-builders/products-query.ts @@ -0,0 +1,205 @@ +import { BulkQuery } from "./bulk-query" + +export class ProductsQuery extends BulkQuery { + query(date?: Date): string { + const publishedStatus = this.pluginOptions.salesChannel + ? encodeURIComponent(`${this.pluginOptions.salesChannel}=visible`) + : `published` + + const filters = [`status:active`, `published_status:${publishedStatus}`] + if (date) { + const isoDate = date.toISOString() + filters.push(`created_at:>='${isoDate}' OR updated_at:>='${isoDate}'`) + } + + const ProductVariantSortKey = `POSITION` + const ProductImageSortKey = `POSITION` + + const queryString = filters.map(f => `(${f})`).join(` AND `) + + const query = ` + { + products(query: "${queryString}") { + edges { + node { + id + storefrontId + createdAt + description + descriptionHtml + featuredImage { + id + altText + height + width + originalSrc + transformedSrc + } + featuredMedia { + alt + mediaContentType + mediaErrors { + details + } + preview { + image { + id + altText + height + width + originalSrc + transformedSrc + } + status + } + status + } + feedback { + details { + app { + id + } + link { + label + url + } + messages { + field + message + } + } + summary + } + giftCardTemplateSuffix + handle + hasOnlyDefaultVariant + hasOutOfStockVariants + isGiftCard + legacyResourceId + mediaCount + onlineStorePreviewUrl + onlineStoreUrl + options { + id + name + position + values + } + priceRangeV2 { + maxVariantPrice { + amount + currencyCode + } + minVariantPrice { + amount + currencyCode + } + } + productType + publishedAt + requiresSellingPlan + sellingPlanGroupCount + seo { + description + title + } + status + tags + templateSuffix + title + totalInventory + totalVariants + tracksInventory + updatedAt + vendor + images(sortKey: ${ProductImageSortKey}) { + edges { + node { + id + altText + src + originalSrc + width + height + } + } + } + metafields { + edges { + node { + createdAt + description + id + key + legacyResourceId + namespace + ownerType + updatedAt + value + valueType + } + } + } + variants(sortKey: ${ProductVariantSortKey}) { + edges { + node { + availableForSale + barcode + compareAtPrice + createdAt + displayName + id + image { + id + altText + height + width + originalSrc + transformedSrc + } + inventoryPolicy + inventoryQuantity + legacyResourceId + position + price + selectedOptions { + name + value + } + sellingPlanGroupCount + sku + storefrontId + taxCode + taxable + title + updatedAt + weight + weightUnit + metafields { + edges { + node { + createdAt + description + id + key + legacyResourceId + namespace + ownerType + updatedAt + value + valueType + } + } + } + } + } + } + } + } + } + } + ` + + return this.bulkOperationQuery(query) + } +} diff --git a/packages/gatsby-source-shopify/src/resolve-gatsby-image-data.ts b/packages/gatsby-source-shopify/src/resolve-gatsby-image-data.ts new file mode 100644 index 0000000000000..188b6ccb5cd10 --- /dev/null +++ b/packages/gatsby-source-shopify/src/resolve-gatsby-image-data.ts @@ -0,0 +1,121 @@ +import { fetchRemoteFile } from "gatsby-core-utils" +import { + generateImageData, + getLowResolutionImageURL, + IGatsbyImageData, + IGatsbyImageHelperArgs, + IImage, + ImageFormat, +} from "gatsby-plugin-image" +import { readFileSync } from "fs" +import { IShopifyImage, urlBuilder } from "./get-shopify-image" + +type ImageLayout = "constrained" | "fixed" | "fullWidth" + +type IImageWithPlaceholder = IImage & { + placeholder: string +} + +async function getImageBase64({ + imageAddress, + cache, +}: { + imageAddress: string + cache: any +}): Promise { + // Downloads file to the site cache and returns the file path for the given image (this is a path on the host system, not a URL) + const filePath = await fetchRemoteFile({ + url: imageAddress, + cache, + }) + const buffer = readFileSync(filePath) + return buffer.toString(`base64`) +} + +/** + * Download and generate a low-resolution placeholder + * + * @param lowResImageFile + */ +function getBase64DataURI({ imageBase64 }: { imageBase64: string }): string { + return `data:image/png;base64,${imageBase64}` +} + +export function makeResolveGatsbyImageData(cache: any) { + return async function resolveGatsbyImageData( + image: Node & IShopifyImage, + { + formats = [`auto`], + layout = `constrained`, + ...options + }: { formats: Array; layout: ImageLayout } + ): Promise { + const remainingOptions = options as Record + let [basename] = image.originalSrc.split(`?`) + + const dot = basename.lastIndexOf(`.`) + let ext = `` + if (dot !== -1) { + ext = basename.slice(dot + 1) + basename = basename.slice(0, dot) + } + + const generateImageSource: IGatsbyImageHelperArgs["generateImageSource"] = ( + filename, + width, + height, + toFormat + ): IImageWithPlaceholder => { + return { + width, + height, + placeholder: ``, + format: toFormat, + src: urlBuilder({ + width, + height, + baseUrl: filename, + format: toFormat, + options: {}, + }), + } + } + const sourceMetadata = { + width: image.width, + height: image.height, + format: ext as ImageFormat, + } + + if (remainingOptions && remainingOptions.placeholder === `BLURRED`) { + // This function returns the URL for a 20px-wide image, to use as a blurred placeholder + const lowResImageURL = getLowResolutionImageURL({ + ...remainingOptions, + aspectRatio: remainingOptions.width / remainingOptions.height, // Workaround - fixes height being NaN; we can remove this once gatsby-plugin-image is fixed + formats, + layout, + sourceMetadata, + pluginName: `gatsby-source-shopify`, + filename: image.originalSrc, + generateImageSource, + }) + const imageBase64 = await getImageBase64({ + imageAddress: lowResImageURL, + cache, + }) + + // This would be your own function to download and generate a low-resolution placeholder + remainingOptions.placeholderURL = getBase64DataURI({ + imageBase64, + }) + } + return generateImageData({ + ...remainingOptions, + formats, + layout, + sourceMetadata, + pluginName: `gatsby-source-shopify`, + filename: image.originalSrc, + generateImageSource, + }) + } +} diff --git a/packages/gatsby-source-shopify/src/rest.ts b/packages/gatsby-source-shopify/src/rest.ts new file mode 100644 index 0000000000000..0bf17c7f59da1 --- /dev/null +++ b/packages/gatsby-source-shopify/src/rest.ts @@ -0,0 +1,48 @@ +import fetch, { Response } from "node-fetch" + +const getBaseUrl = (options: ShopifyPluginOptions): string => + `https://${options.storeUrl}/admin/api/2021-01` + +export function makeShopifyFetch( + options: ShopifyPluginOptions +): (path: string) => Promise { + const baseUrl = getBaseUrl(options) + + async function shopifyFetch( + path: string, + fetchOptions = { + headers: { + "X-Shopify-Access-Token": options.password, + }, + }, + retries = 3 + ): Promise { + /** + * This is kind of a hack, but... + * + * We do this because although callers will use a relative path, + * some responses might have pagination links that get fed back + * to shopifyFetch to retrieve the next page. Those links are + * absolute URLs so we account for both, but not in a very robust + * fashion. + */ + const url = path.includes(options.storeUrl) ? path : `${baseUrl}${path}` + + const resp = await fetch(url, fetchOptions) + + if (!resp.ok) { + if (retries > 0) { + if (resp.status === 429) { + // rate limit + const retryAfter = parseFloat(resp.headers.get(`Retry-After`) || ``) + await new Promise(resolve => setTimeout(resolve, retryAfter)) + return shopifyFetch(path, fetchOptions, retries - 1) + } + } + } + + return resp + } + + return async (path: string): Promise => shopifyFetch(path) +} diff --git a/packages/gatsby-source-shopify/src/static-queries.ts b/packages/gatsby-source-shopify/src/static-queries.ts new file mode 100644 index 0000000000000..855ee575f3c39 --- /dev/null +++ b/packages/gatsby-source-shopify/src/static-queries.ts @@ -0,0 +1,49 @@ +export const OPERATION_STATUS_QUERY = ` + query OPERATION_STATUS { + currentBulkOperation { + id + status + errorCode + createdAt + completedAt + objectCount + fileSize + url + partialDataUrl + query + } + } +` + +export const OPERATION_BY_ID = ` + query OPERATION_BY_ID($id: ID!) { + node(id: $id) { + ... on BulkOperation { + id + status + errorCode + createdAt + completedAt + objectCount + fileSize + url + partialDataUrl + query + } + } + } + ` + +export const CANCEL_OPERATION = ` + mutation CANCEL_OPERATION($id: ID!) { + bulkOperationCancel(id: $id) { + bulkOperation { + status + } + userErrors { + field + message + } + } + } + ` diff --git a/packages/gatsby-source-shopify/tsconfig.json b/packages/gatsby-source-shopify/tsconfig.json new file mode 100644 index 0000000000000..cab823f261c46 --- /dev/null +++ b/packages/gatsby-source-shopify/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": ["src/**/*.ts", "types"], + "exclude": ["node_modules"], + "compilerOptions": { + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "module": "commonjs", + "removeComments": false, + "preserveConstEnums": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "target": "es2017", + "declaration": true, + "lib": ["es2017", "dom", "esnext.asynciterable"] + } +} diff --git a/packages/gatsby-source-shopify/types/interface.d.ts b/packages/gatsby-source-shopify/types/interface.d.ts new file mode 100644 index 0000000000000..a527447da8883 --- /dev/null +++ b/packages/gatsby-source-shopify/types/interface.d.ts @@ -0,0 +1,64 @@ +interface ShopifyPluginOptions { + apiKey: string; + password: string; + storeUrl: string; + downloadImages?: boolean; + shopifyConnections?: string[]; + typePrefix?: string; + salesChannel?: string; +} + +interface NodeBuilder { + buildNode: (obj: Record) => Promise; +} + +type BulkResult = Record; +type BulkResults = BulkResult[]; + +type BulkOperationStatus = + | "CANCELED" + | "CANCELING" + | "COMPLETED" + | "CREATED" + | "EXPIRED" + | "FAILED" + | "RUNNING"; + +interface BulkOperationNode { + status: BulkOperationStatus; + /** + * FIXME: The docs say objectCount is a number, but it's a string. Let's + * follow up with Shopify on this and make sure it's working as intended. + */ + objectCount: string; + url: string; + id: string; + errorCode?: "ACCESS_DENIED" | "INTERNAL_SERVER_ERROR" | "TIMEOUT"; + query: string; +} + +interface CurrentBulkOperationResponse { + currentBulkOperation: { + id: string; + status: BulkOperationStatus; + }; +} + +interface UserError { + field?: string[]; + message: string; +} + +interface BulkOperationRunQueryResponse { + bulkOperationRunQuery: { + userErrors: UserError[]; + bulkOperation: BulkOperationNode; + }; +} + +interface BulkOperationCancelResponse { + bulkOperationCancel: { + bulkOperation: BulkOperationNode; + userErrors: UserError[]; + }; +} diff --git a/renovate.json5 b/renovate.json5 index b62f7091cb066..bfb77da69843e 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -18568,7 +18568,8 @@ "groupName": "minor and patch dependencies for gatsby-source-shopify", "groupSlug": "gatsby-source-shopify-prod-minor", "matchPackageNames": [ - "gatsby-node-helpers" + "sharp", + "shift-left" ], "matchUpdateTypes": [ "patch" @@ -18603,7 +18604,8 @@ "groupName": "major dependencies for gatsby-source-shopify", "groupSlug": "gatsby-source-shopify-prod-major", "matchPackageNames": [ - "gatsby-node-helpers" + "sharp", + "shift-left" ], "matchUpdateTypes": [ "major", diff --git a/yarn.lock b/yarn.lock index a6eb87122d349..90a92392fef64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3486,7 +3486,7 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@mswjs/cookies@^0.1.5": +"@mswjs/cookies@^0.1.4", "@mswjs/cookies@^0.1.5": version "0.1.6" resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.1.6.tgz#176f77034ab6d7373ae5c94bcbac36fee8869249" integrity sha512-A53XD5TOfwhpqAmwKdPtg1dva5wrng2gH5xMvklzbd9WLTSVU953eCRa8rtrrm6G7Cy60BOGsBRN89YQK0mlKA== @@ -4627,6 +4627,14 @@ dependencies: jest-diff "^24.3.0" +"@types/jest@^26.0.20": + version "26.0.23" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" + integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/joi@^14.3.4": version "14.3.4" resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0" @@ -4701,7 +4709,7 @@ dependencies: "@types/node" "*" -"@types/node-fetch@2", "@types/node-fetch@^2.5.10": +"@types/node-fetch@2", "@types/node-fetch@^2.5.10", "@types/node-fetch@^2.5.8": version "2.5.10" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" integrity sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ== @@ -4724,6 +4732,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.1.tgz#5e07e0cb2ff793aa7a1b41deae76221e6166049f" integrity sha512-/tpUyFD7meeooTRwl3sYlihx2BrJE7q9XF71EguPFIySj9B7qgnRtHsHTho+0AUm4m1SvWGm6uSncrR94q6Vtw== +"@types/node@^14.14.34": + version "14.17.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.3.tgz#6d327abaa4be34a74e421ed6409a0ae2f47f4c3d" + integrity sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw== + "@types/node@^8.5.7": version "8.10.59" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.59.tgz#9e34261f30183f9777017a13d185dfac6b899e04" @@ -4884,6 +4897,13 @@ dependencies: "@types/node" "*" +"@types/sharp@^0.28.0": + version "0.28.3" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.28.3.tgz#0e57ede34d3e334632ab7a68a6af070aa0f51ceb" + integrity sha512-y3mxUj3jukIWgdu9CrSTSCo5HruTzDxdjn5SqdIEALdTszkcot9r8HX/7q9FMx3YjuXifTD0OI+d4wA6Pogqmw== + dependencies: + "@types/node" "*" + "@types/signal-exit@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/signal-exit/-/signal-exit-3.0.0.tgz#75e3b17660cf1f6c6cb8557675b4e680e43bbf36" @@ -10774,7 +10794,7 @@ duplexer@0.1.1: version "0.1.1" resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" -duplexer@^0.1.1, duplexer@^0.1.2: +duplexer@^0.1.1, duplexer@^0.1.2, duplexer@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -11604,6 +11624,19 @@ event-source-polyfill@^1.0.15: resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.15.tgz#a28e116281be677af4b055b67d95517e35c92435" integrity sha512-IVmd8jWwX6ag5rXIdVCPBjBChiHBceLb1/7aKPIK7CUeJ5Br7alx029+ZpQlK4jW4Hk2qncy3ClJP97S8ltvmg== +event-stream@=3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -11646,6 +11679,19 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== +execa@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe" + integrity sha1-V7aaWU8IF1nGnlNw8NF7nLEWWP4= + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -12626,6 +12672,11 @@ from2@^2.1.0, from2@^2.1.1: inherits "^2.0.1" readable-stream "^2.0.0" +from@~0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= + fs-access@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" @@ -12789,14 +12840,6 @@ gatsby-interface@^0.0.244: markdown-to-jsx "^7.0.0" theme-ui "^0.2.49" -gatsby-node-helpers@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/gatsby-node-helpers/-/gatsby-node-helpers-0.3.0.tgz#3bdca3b7902a702a5834fef280ad66d51099d57c" - dependencies: - json-stringify-safe "^5.0.1" - lodash "^4.17.4" - p-is-promise "^1.1.0" - gatsby-plugin-webfonts@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/gatsby-plugin-webfonts/-/gatsby-plugin-webfonts-1.1.4.tgz#f6fb8daf0acc4c59511c98964fceca35504014ac" @@ -14809,7 +14852,7 @@ inquirer@^6.2.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.0: +inquirer@^7.0.0, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== @@ -18139,6 +18182,11 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= + map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -19190,6 +19238,30 @@ msw@^0.25.0: strict-event-emitter "^0.1.0" yargs "^16.2.0" +msw@^0.27.1: + version "0.27.2" + resolved "https://registry.yarnpkg.com/msw/-/msw-0.27.2.tgz#c79530e04513e5d491a149c9181bc594bad75265" + integrity sha512-PjxQ06gi2mqNINzVKL/lVWiP6Dd2LDUT3QK9AS2vJMbz/Xa0FgKmd1RF7kyFKiwv6qEazVp74TS0Qc8yjXRUgA== + dependencies: + "@mswjs/cookies" "^0.1.4" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.0" + "@types/inquirer" "^7.3.1" + "@types/js-levenshtein" "^1.1.0" + chalk "^4.1.0" + chokidar "^3.4.2" + cookie "^0.4.1" + graphql "^15.4.0" + headers-utils "^1.2.0" + inquirer "^7.3.3" + js-levenshtein "^1.1.6" + node-fetch "^2.6.1" + node-match-path "^0.6.1" + node-request-interceptor "^0.6.3" + statuses "^2.0.0" + strict-event-emitter "^0.1.0" + yargs "^16.2.0" + msw@^0.29.0: version "0.29.0" resolved "https://registry.yarnpkg.com/msw/-/msw-0.29.0.tgz#7242d575cb01db0c925241587df1fc2b79230d78" @@ -19578,7 +19650,7 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-match-path@^0.6.0, node-match-path@^0.6.3: +node-match-path@^0.6.0, node-match-path@^0.6.1, node-match-path@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/node-match-path/-/node-match-path-0.6.3.tgz#55dd8443d547f066937a0752dce462ea7dc27551" integrity sha512-fB1reOHKLRZCJMAka28hIxCwQLxGmd7WewOCBDYKpyA1KXi68A7vaGgdZAPhY2E6SXoYt3KqYCCvXLJ+O0Fu/Q== @@ -20371,11 +20443,6 @@ p-is-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" -p-iteration@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/p-iteration/-/p-iteration-1.1.8.tgz#14df726d55af368beba81bcc92a26bb1b48e714a" - integrity sha512-IMFBSDIYcPNnW7uWYGrBqmvTiq7W0uB0fJn6shQZs7dlF3OvrHOre+JT9ikSZ7gZS3vWqclVgoQSvToJrns7uQ== - p-limit@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" @@ -20939,6 +21006,13 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= + dependencies: + through "~2.3" + pbkdf2@^3.0.3: version "3.0.16" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c" @@ -21897,6 +21971,13 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" +prettier-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prettier-check/-/prettier-check-2.0.0.tgz#edd086ee12d270579233ccb136a16e6afcfba1ae" + integrity sha512-HZG53XQTJ9Cyi5hi1VFVVFxdlhITJybpZAch3ib9KqI05VUxV+F5Hip0GhSWRItrlDzVyqjSoDQ9KqIn7AHYyw== + dependencies: + execa "^0.6.0" + prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -21913,6 +21994,11 @@ prettier@^2.0.5, prettier@^2.3.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18" integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== +prettier@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + pretty-bytes@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-3.0.1.tgz#27d0008d778063a0b4811bb35c79f1bd5d5fbccf" @@ -22165,6 +22251,13 @@ prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" +ps-tree@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd" + integrity sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA== + dependencies: + event-stream "=3.3.4" + pseudomap@^1.0.1, pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -25154,6 +25247,11 @@ shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" +shift-left@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/shift-left/-/shift-left-0.1.5.tgz#4a61a9d0412b1b32f9abbd97f72466c9c74a6be1" + integrity sha512-55d8QaP1YmuL1D52fhgq8CT1tXksM/2WPZ6980RtkMbl0Cze++kuJy50GLMnXwostk/YG9hasaJmP3r+d3yUtQ== + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -25588,6 +25686,13 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= + dependencies: + through "2" + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -25786,6 +25891,13 @@ stream-combiner2@^1.1.1: duplexer2 "~0.1.0" readable-stream "^2.0.2" +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= + dependencies: + duplexer "~0.1.1" + stream-each@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" @@ -25855,6 +25967,11 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-argv@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738" + integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA== + string-env-interpolation@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz#ad4397ae4ac53fe6c91d1402ad6f6a52862c7152" @@ -26777,7 +26894,7 @@ through2@^4.0.0: dependencies: readable-stream "3" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3.4: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: version "2.3.8" resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -27161,6 +27278,17 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a" integrity sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ== +tsc-watch@^4.2.9: + version "4.4.0" + resolved "https://registry.yarnpkg.com/tsc-watch/-/tsc-watch-4.4.0.tgz#3ebbf1db54bcef6bfe534b330fa87284a4139320" + integrity sha512-+0Yey6ptOOXAnt44OKTk2/EnQnmA0auL7VWXm9d9abMS4tabt0Xdr9B4AK6OJbWAre9ZdLA81+Nk8sz9unptyA== + dependencies: + cross-spawn "^7.0.3" + node-cleanup "^2.1.2" + ps-tree "^1.2.0" + string-argv "^0.1.1" + strip-ansi "^6.0.0" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -27332,6 +27460,11 @@ typescript@^4.1.3, typescript@^4.1.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== +typescript@^4.2.3: + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== + typography-normalize@^0.16.19: version "0.16.19" resolved "https://registry.yarnpkg.com/typography-normalize/-/typography-normalize-0.16.19.tgz#58e0cf12466870c5b27006daa051fe7307780660"