From 8acfa87cef1454f57591a5172fd8814f34c906f2 Mon Sep 17 00:00:00 2001 From: Mina Smart Date: Fri, 8 Sep 2017 14:53:59 -0400 Subject: [PATCH] namespace product resource --- src/client.js | 115 ++------------------------------ src/paginators.js | 21 ++++++ src/product-helpers.js | 2 +- src/product-resource.js | 99 +++++++++++++++++++++++++++ src/resource.js | 6 ++ test/client-integration-test.js | 96 +++++++++++++------------- 6 files changed, 179 insertions(+), 160 deletions(-) create mode 100644 src/paginators.js create mode 100644 src/product-resource.js create mode 100644 src/resource.js diff --git a/src/client.js b/src/client.js index f2b1cf9ea..dfc9ca5b0 100644 --- a/src/client.js +++ b/src/client.js @@ -4,15 +4,13 @@ import handleCheckoutMutation from './handle-checkout-mutation'; import productHelpers from './product-helpers'; import imageHelpers from './image-helpers'; import defaultResolver from './default-resolver'; -import fetchResourcesForProducts from './fetch-resources-for-products'; +import {paginateCollectionsProductConnectionsAndResolve} from './paginators'; +import ProductResource from './product-resource'; import {version} from '../package.json'; // GraphQL import types from '../schema.json'; import checkoutNodeQuery from './graphql/checkoutNodeQuery.graphql'; -import productNodeQuery from './graphql/productNodeQuery.graphql'; -import productConnectionQuery from './graphql/productConnectionQuery.graphql'; -import productByHandleQuery from './graphql/productByHandleQuery.graphql'; import collectionNodeQuery from './graphql/collectionNodeQuery.graphql'; import collectionNodeWithProductsQuery from './graphql/collectionNodeWithProductsQuery.graphql'; import collectionConnectionQuery from './graphql/collectionConnectionQuery.graphql'; @@ -25,26 +23,6 @@ import checkoutLineItemsAddMutation from './graphql/checkoutLineItemsAddMutation import checkoutLineItemsRemoveMutation from './graphql/checkoutLineItemsRemoveMutation.graphql'; import checkoutLineItemsUpdateMutation from './graphql/checkoutLineItemsUpdateMutation.graphql'; -function paginateProductConnectionsAndResolve(client) { - return function(products) { - return fetchResourcesForProducts(products, client).then(() => { - return products; - }); - }; -} - -function paginateCollectionsProductConnectionsAndResolve(client) { - return function(collectionOrCollections) { - const collections = [].concat(collectionOrCollections); - - return Promise.all(collections.reduce((promiseAcc, collection) => { - return promiseAcc.concat(fetchResourcesForProducts(collection.products, client)); - }, [])).then(() => { - return collectionOrCollections; - }); - }; -} - /** * The JS Buy SDK Client. * @class @@ -97,6 +75,8 @@ class Client { } } }); + + this.product = new ProductResource(this.graphQLClient, productHelpers); } /** @@ -132,61 +112,6 @@ class Client { .then(defaultResolver('shop')); } - /** - * Fetches all products on the shop. - * - * @example - * client.fetchAllProducts().then((products) => { - * // Do something with the products - * }); - * - * @param {Client.Queries.productConnectionQuery} [query] Callback function to specify fields to query on the products. - * @return {Promise|GraphModel[]} A promise resolving with an array of `GraphModel`s of the products. - */ - fetchAllProducts(pageSize = 20) { - return this.graphQLClient - .send(productConnectionQuery, {pageSize}) - .then(defaultResolver('shop.products')) - .then(paginateProductConnectionsAndResolve(this.graphQLClient)); - } - - /** - * Fetches a single product by ID on the shop. - * - * @example - * client.fetchProduct('Xk9lM2JkNzFmNzIQ4NTIY4ZDFi9DaGVja291dC9lM2JkN==').then((product) => { - * // Do something with the product - * }); - * - * @param {String} id The id of the product to fetch. - * @param {Client.Queries.productNodeQuery} [query] Callback function to specify fields to query on the product. - * @return {Promise|GraphModel} A promise resolving with a `GraphModel` of the product. - */ - fetchProduct(id) { - return this.graphQLClient - .send(productNodeQuery, {id}) - .then(defaultResolver('node')) - .then(paginateProductConnectionsAndResolve(this.graphQLClient)); - } - - /** - * Fetches a single product by handle on the shop. - * - * @example - * client.fetchProductByHandle('my-product').then((product) => { - * // Do something with the product - * }); - * - * @param {String} handle The handle of the product to fetch. - * @return {Promise|GraphModel} A promise resolving with a `GraphModel` of the product. - */ - fetchProductByHandle(handle) { - return this.graphQLClient - .send(productByHandleQuery, {handle}) - .then(defaultResolver('shop.productByHandle')) - .then(paginateProductConnectionsAndResolve(this.graphQLClient)); - } - /** * Fetches a collection by handle on the shop. * @@ -301,38 +226,6 @@ class Client { }); } - /** - * Fetches all products on the shop that match the query. - * - * @example - * client.fetchQueryProducts({sortBy: 'title', limit: 10}).then((products) => { - * // Do something with the first 10 products sorted by title in ascending order - * }); - * - * @param {Object} [queryObject] An object specifying the query data containing zero or more of: - * @param {String} [queryObject.title] The title of the product to fetch. - * @param {String} [queryObject.updatedAtMin] Products updated since the supplied timestamp (format: `2016-09-25T21:31:33`). - * @param {String} [queryObject.createdAtMin] Products created since the supplied timestamp (format: `2016-09-25T21:31:33`). - * @param {String} [queryObject.productType] The type of products to fetch. - * @param {Number} [queryObject.limit=20] The number of products to fetch. - * @param {String} [queryObject.sortBy] The field to use to sort products. Possible values are `title`, `updatedAt`, and `createdAt`. - * @param {String} [queryObject.sortDirection] The sort direction of the products. - * Will sort products by ascending order unless `'desc'` is specified. - * @param {Client.Queries.productConnectionQuery} [query] Callback function to specify fields to query on the products. - * @return {Promise|GraphModel[]} A promise resolving with an array of `GraphModel`s of the products. - */ - fetchQueryProducts({first = 20, sortKey = 'ID', query, reverse}) { - return this.graphQLClient - .send(productConnectionQuery, { - first, - sortKey: this.graphQLClient.enum(sortKey), - query, - reverse - }) - .then(defaultResolver('shop.products')) - .then(paginateProductConnectionsAndResolve(this.graphQLClient)); - } - /** * Fetches all collections on the shop that match the query. * diff --git a/src/paginators.js b/src/paginators.js new file mode 100644 index 000000000..688af4c18 --- /dev/null +++ b/src/paginators.js @@ -0,0 +1,21 @@ +import fetchResourcesForProducts from './fetch-resources-for-products'; + +export function paginateProductConnectionsAndResolve(client) { + return function(products) { + return fetchResourcesForProducts(products, client).then(() => { + return products; + }); + }; +} + +export function paginateCollectionsProductConnectionsAndResolve(client) { + return function(collectionOrCollections) { + const collections = [].concat(collectionOrCollections); + + return Promise.all(collections.reduce((promiseAcc, collection) => { + return promiseAcc.concat(fetchResourcesForProducts(collection.products, client)); + }, [])).then(() => { + return collectionOrCollections; + }); + }; +} diff --git a/src/product-helpers.js b/src/product-helpers.js index 4fbef7185..c7a84fb67 100644 --- a/src/product-helpers.js +++ b/src/product-helpers.js @@ -6,7 +6,7 @@ export default { /** * Returns the variant of a product corresponding to the options given. * - * @memberof Client.Product.Helpers + * @memberof Client.product.Helpers * @method variantForOptions * @param {GraphModel} product The product to find the variant on. Must include `variants`. * @param {Object} options An object containing the options for the variant. diff --git a/src/product-resource.js b/src/product-resource.js new file mode 100644 index 000000000..54932d0e3 --- /dev/null +++ b/src/product-resource.js @@ -0,0 +1,99 @@ +import Resource from './resource'; +import defaultResolver from './default-resolver'; +import {paginateProductConnectionsAndResolve} from './paginators'; + +// GraphQL +import productNodeQuery from './graphql/productNodeQuery.graphql'; +import productConnectionQuery from './graphql/productConnectionQuery.graphql'; +import productByHandleQuery from './graphql/productByHandleQuery.graphql'; + +export default class ProductResource extends Resource { + + /** + * Fetches all products on the shop. + * + * @example + * client.fetchAllProducts().then((products) => { + * // Do something with the products + * }); + * + * @param {Client.Queries.productConnectionQuery} [query] Callback function to specify fields to query on the products. + * @return {Promise|GraphModel[]} A promise resolving with an array of `GraphModel`s of the products. + */ + fetchAll(pageSize = 20) { + return this.graphQLClient + .send(productConnectionQuery, {pageSize}) + .then(defaultResolver('shop.products')) + .then(paginateProductConnectionsAndResolve(this.graphQLClient)); + } + + /** + * Fetches a single product by ID on the shop. + * + * @example + * client.fetchProduct('Xk9lM2JkNzFmNzIQ4NTIY4ZDFi9DaGVja291dC9lM2JkN==').then((product) => { + * // Do something with the product + * }); + * + * @param {String} id The id of the product to fetch. + * @param {Client.Queries.productNodeQuery} [query] Callback function to specify fields to query on the product. + * @return {Promise|GraphModel} A promise resolving with a `GraphModel` of the product. + */ + fetch(id) { + return this.graphQLClient + .send(productNodeQuery, {id}) + .then(defaultResolver('node')) + .then(paginateProductConnectionsAndResolve(this.graphQLClient)); + } + + /** + * Fetches a single product by handle on the shop. + * + * @example + * client.fetchProductByHandle('my-product').then((product) => { + * // Do something with the product + * }); + * + * @param {String} handle The handle of the product to fetch. + * @return {Promise|GraphModel} A promise resolving with a `GraphModel` of the product. + */ + fetchByHandle(handle) { + return this.graphQLClient + .send(productByHandleQuery, {handle}) + .then(defaultResolver('shop.productByHandle')) + .then(paginateProductConnectionsAndResolve(this.graphQLClient)); + } + + /** + * Fetches all products on the shop that match the query. + * + * @example + * client.fetchQueryProducts({sortBy: 'title', limit: 10}).then((products) => { + * // Do something with the first 10 products sorted by title in ascending order + * }); + * + * @param {Object} [queryObject] An object specifying the query data containing zero or more of: + * @param {String} [queryObject.title] The title of the product to fetch. + * @param {String} [queryObject.updatedAtMin] Products updated since the supplied timestamp (format: `2016-09-25T21:31:33`). + * @param {String} [queryObject.createdAtMin] Products created since the supplied timestamp (format: `2016-09-25T21:31:33`). + * @param {String} [queryObject.productType] The type of products to fetch. + * @param {Number} [queryObject.limit=20] The number of products to fetch. + * @param {String} [queryObject.sortBy] The field to use to sort products. Possible values are `title`, `updatedAt`, and `createdAt`. + * @param {String} [queryObject.sortDirection] The sort direction of the products. + * Will sort products by ascending order unless `'desc'` is specified. + * @param {Client.Queries.productConnectionQuery} [query] Callback function to specify fields to query on the products. + * @return {Promise|GraphModel[]} A promise resolving with an array of `GraphModel`s of the products. + */ + fetchQuery({first = 20, sortKey = 'ID', query, reverse}) { + return this.graphQLClient + .send(productConnectionQuery, { + first, + sortKey: this.graphQLClient.enum(sortKey), + query, + reverse + }) + .then(defaultResolver('shop.products')) + .then(paginateProductConnectionsAndResolve(this.graphQLClient)); + } + +} diff --git a/src/resource.js b/src/resource.js new file mode 100644 index 000000000..df334b479 --- /dev/null +++ b/src/resource.js @@ -0,0 +1,6 @@ +export default class Resource { + constructor(client, helpers) { + this.graphQLClient = client; + this.helpers = helpers; + } +} diff --git a/test/client-integration-test.js b/test/client-integration-test.js index c717df040..a3dd7be49 100644 --- a/test/client-integration-test.js +++ b/test/client-integration-test.js @@ -38,10 +38,10 @@ suite('client-integration-test', () => { fetchMock.restore(); }); - test('it resolves with an array of products on Client#fetchAllProducts', () => { + test('it resolves with an array of products on Client.product#fetchAllProducts', () => { fetchMock.postOnce(apiUrl, shopWithProductsFixture); - return client.fetchAllProducts().then((products) => { + return client.product.fetchAll().then((products) => { assert.ok(Array.isArray(products), 'products is an array'); assert.equal(products.length, 2, 'there are two products'); @@ -51,49 +51,24 @@ suite('client-integration-test', () => { }); }); - test('it resolves with a single product on Client#fetchProduct', () => { + test('it resolves with a single product on Client.product#fetch', () => { fetchMock.postOnce(apiUrl, singleProductFixture); const id = singleProductFixture.data.node.id; - return client.fetchProduct(id).then((product) => { + return client.product.fetch(id).then((product) => { assert.ok(!Array.isArray(product), 'product is not an array'); assert.equal(product.id, id); assert.ok(fetchMock.done()); }); }); - test('it resolves with an array of collections on Client#fetchAllCollections', () => { - fetchMock.postOnce(apiUrl, shopWithCollectionsFixture); - - return client.fetchAllCollections().then((collections) => { - assert.ok(Array.isArray(collections), 'collections is an array'); - assert.equal(collections.length, 2, 'there are two collections'); - - assert.equal(collections[0].id, shopWithCollectionsFixture.data.shop.collections.edges[0].node.id); - assert.equal(collections[1].id, shopWithCollectionsFixture.data.shop.collections.edges[1].node.id); - assert.ok(fetchMock.done()); - }); - }); - - test('it resolves with a single collection on Client#fetchCollection', () => { - fetchMock.postOnce(apiUrl, singleCollectionFixture); - - const id = singleCollectionFixture.data.node.id; - - return client.fetchCollection(id).then((collection) => { - assert.ok(Array.isArray(collection) === false, 'collection is not an array'); - assert.equal(collection.id, id); - assert.ok(fetchMock.done()); - }); - }); - - test('it fetches all images on products on Client#fetchAllProducts', () => { + test('it fetches all images on products on Client.product#fetchAll', () => { fetchMock.postOnce(apiUrl, productWithPaginatedImagesFixture) .postOnce(apiUrl, secondPageImagesFixture) .postOnce(apiUrl, thirdPageImagesFixture); - return client.fetchAllProducts().then((products) => { + return client.product.fetchAll().then((products) => { const images = products[0].images; assert.ok(Array.isArray(images), 'images is an array'); @@ -106,12 +81,12 @@ suite('client-integration-test', () => { }); }); - test('it fetches all variants on Client#fetchProduct', () => { + test('it fetches all variants on Client.product#fetch', () => { fetchMock.postOnce(apiUrl, productWithPaginatedVariantsFixture) .postOnce(apiUrl, secondPageVariantsFixture) .postOnce(apiUrl, thirdPageVariantsFixture); - return client.fetchProduct(productWithPaginatedVariantsFixture.data.node.id).then((product) => { + return client.product.fetch(productWithPaginatedVariantsFixture.data.node.id).then((product) => { const variants = product.variants; assert.ok(Array.isArray(variants), 'variants is an array'); @@ -124,7 +99,7 @@ suite('client-integration-test', () => { }); }); - test('it does not fetch paginated images if the images query result was empty on Client#fetchProduct', () => { + test('it does not fetch paginated images if the images query result was empty on Client.product#fetch', () => { fetchMock.postOnce(apiUrl, { data: { node: { @@ -139,24 +114,60 @@ suite('client-integration-test', () => { } }); - return client.fetchProduct('an-id').then((product) => { + return client.product.fetch('an-id').then((product) => { assert.equal(product.images.length, 0); assert.ok(fetchMock.done()); }); }); - test('it can fetch a product by handle through Client#fetchProductByHandle', () => { + test('it can fetch a product by handle through Client.product#fetchByHandle', () => { fetchMock.postOnce(apiUrl, productByHandleFixture); const handle = productByHandleFixture.data.shop.productByHandle.handle; - return client.fetchProductByHandle(handle).then((product) => { + return client.product.fetchByHandle(handle).then((product) => { assert.equal(product.id, productByHandleFixture.data.shop.productByHandle.id); assert.equal(product.handle, handle); assert.ok(fetchMock.done()); }); }); + test('it resolves products with Client.product#fetchQuery', () => { + fetchMock.postOnce(apiUrl, shopWithProductsFixture); + + return client.product.fetchQuery({}).then((products) => { + assert.equal(products.length, 2); + assert.equal(products[0].id, shopWithProductsFixture.data.shop.products.edges[0].node.id); + assert.equal(products[1].id, shopWithProductsFixture.data.shop.products.edges[1].node.id); + assert.ok(fetchMock.done()); + }); + }); + + test('it resolves with an array of collections on Client#fetchAllCollections', () => { + fetchMock.postOnce(apiUrl, shopWithCollectionsFixture); + + return client.fetchAllCollections().then((collections) => { + assert.ok(Array.isArray(collections), 'collections is an array'); + assert.equal(collections.length, 2, 'there are two collections'); + + assert.equal(collections[0].id, shopWithCollectionsFixture.data.shop.collections.edges[0].node.id); + assert.equal(collections[1].id, shopWithCollectionsFixture.data.shop.collections.edges[1].node.id); + assert.ok(fetchMock.done()); + }); + }); + + test('it resolves with a single collection on Client#fetchCollection', () => { + fetchMock.postOnce(apiUrl, singleCollectionFixture); + + const id = singleCollectionFixture.data.node.id; + + return client.fetchCollection(id).then((collection) => { + assert.ok(Array.isArray(collection) === false, 'collection is not an array'); + assert.equal(collection.id, id); + assert.ok(fetchMock.done()); + }); + }); + test('it can fetch a collection by handle through Client#fetchCollectionByHandle', () => { fetchMock.postOnce(apiUrl, collectionByHandleFixture); @@ -169,17 +180,6 @@ suite('client-integration-test', () => { }); }); - test('it resolves products with Client#fetchQueryProducts', () => { - fetchMock.postOnce(apiUrl, shopWithProductsFixture); - - return client.fetchQueryProducts({}).then((products) => { - assert.equal(products.length, 2); - assert.equal(products[0].id, shopWithProductsFixture.data.shop.products.edges[0].node.id); - assert.equal(products[1].id, shopWithProductsFixture.data.shop.products.edges[1].node.id); - assert.ok(fetchMock.done()); - }); - }); - test('it can fetch collections with the query arg on Client#fetchQueryCollections', () => { fetchMock.postOnce(apiUrl, shopWithCollectionsFixture);