diff --git a/lib/models/checkout-model.js b/lib/models/checkout-model.js new file mode 100644 index 000000000..e528b144e --- /dev/null +++ b/lib/models/checkout-model.js @@ -0,0 +1,9 @@ +import BaseModel from 'buy-button-sdk/models/base-model'; + +const CartModel = BaseModel.extend({ + constructor() { + this.super(...arguments); + } +}); + +export default CartModel; diff --git a/lib/serializers/checkout-serializer.js b/lib/serializers/checkout-serializer.js new file mode 100644 index 000000000..0f09fdf9f --- /dev/null +++ b/lib/serializers/checkout-serializer.js @@ -0,0 +1,39 @@ +import CoreObject from 'buy-button-sdk/metal/core-object'; +import CheckoutModel from 'buy-button-sdk/models/checkout-model'; + +const CheckoutSerializer = CoreObject.extend({ + constructor() { + }, + + rootKeyForType(type) { + return type.slice(0, -1); + }, + + modelForType(/* type */) { + return CheckoutModel; + }, + + deserializeSingle(type, singlePayload, metaAttrs) { + const modelAttrs = singlePayload[this.rootKeyForType(type)]; + const model = this.modelFromAttrs(type, modelAttrs, metaAttrs); + + return model; + }, + + modelFromAttrs(type, attrs, metaAttrs) { + const Model = this.modelForType(type); + + return new Model(attrs, metaAttrs); + }, + + serialize(type, model) { + const root = this.rootKeyForType(type); + const payload = {}; + + payload[root] = model.attrs; + + return payload; + } +}); + +export default CheckoutSerializer; diff --git a/lib/shop-client.js b/lib/shop-client.js index e27fe6f11..05ab3369b 100644 --- a/lib/shop-client.js +++ b/lib/shop-client.js @@ -77,6 +77,27 @@ const ShopClient = CoreObject.extend({ }); }, + create(type, attrs = {}) { + const adapter = new this.adapters[type](this.config); + + return adapter.create(type, attrs).then(payload => { + return this.deserialize(type, payload, adapter, { single: true }); + }); + }, + + update(type, updatedModel) { + const adapter = updatedModel.adapter; + const serializer = updatedModel.serializer; + const serializedModel = serializer.serialize(type, updatedModel); + const id = updatedModel.attrs[adapter.idKeyForType(type)]; + + return adapter.update(type, id, serializedModel).then(payload => { + const meta = { shopClient: this, adapter, serializer, type }; + + return serializer.deserializeSingle(type, payload, meta); + }); + }, + fetchQuery(type, query) { const adapter = new this.adapters[type](this.config); diff --git a/tests/unit/api/shop-client-test.js b/tests/unit/api/shop-client-test.js index d5fb27ccd..61af72560 100644 --- a/tests/unit/api/shop-client-test.js +++ b/tests/unit/api/shop-client-test.js @@ -3,6 +3,7 @@ import { step, resetStep } from 'buy-button-sdk/tests/helpers/assert-step'; import ShopClient from 'buy-button-sdk/shop-client'; import Config from 'buy-button-sdk/config'; import Promise from 'promise'; +import CheckoutModel from 'buy-button-sdk/models/checkout-model'; const configAttrs = { myShopifyDomain: 'buckets-o-stuff', @@ -25,6 +26,16 @@ function FakeAdapter() { resolve({}); }); }; + this.create = function () { + return new Promise(function (resolve) { + resolve({}); + }); + }; + this.update = function () { + return new Promise(function (resolve) { + resolve({}); + }); + }; } function FakeSerializer() { @@ -34,6 +45,9 @@ function FakeSerializer() { this.deserializeMultiple = function () { return [{}]; }; + this.serialize = function () { + return {}; + }; } module('Unit | ShopClient', { @@ -394,3 +408,153 @@ test('it forwards "fetchQueryNouns" to "fetchQuery(\'nouns\', ...)"', function ( shopClient.fetchQueryCollections(fetchedQuery); }); + +test('it inits a type\'s adapter with the config during #create', function (assert) { + assert.expect(2); + + const done = assert.async(); + + shopClient.adapters = { + checkouts: function (localConfig) { + assert.equal(localConfig, config); + FakeAdapter.apply(this, arguments); + } + }; + shopClient.serializers = { + checkouts: FakeSerializer + }; + + shopClient.create('checkouts').then(() => { + assert.ok(true, 'it resolves the promise'); + done(); + }).catch(() => { + assert.ok(false); + done(); + }); +}); + +test('it inits a type\'s serializer with the config during #create', function (assert) { + assert.expect(2); + + const done = assert.async(); + + shopClient.adapters = { + checkouts: FakeAdapter + }; + + shopClient.serializers = { + checkouts: function (localConfig) { + assert.equal(localConfig, config); + FakeSerializer.apply(this, arguments); + } + }; + + shopClient.create('checkouts').then(() => { + assert.ok(true); + done(); + }).catch(() => { + assert.ok(false); + done(); + }); +}); + +test('it chains the result of the adapter\'s create through the type\'s serializer on #create', function (assert) { + assert.expect(6); + + const done = assert.async(); + + const inputAttrs = { someProps: 'prop' }; + const rawModel = { props: 'some-object' }; + const serializedModel = { attrs: 'serialized-model' }; + + shopClient.adapters = { + checkouts: function () { + this.create = function (type, attrs) { + step(1, 'calls create on the adapter', assert); + + assert.equal(attrs, inputAttrs); + + return new Promise(function (resolve) { + resolve(rawModel); + }); + }; + } + }; + + shopClient.serializers = { + checkouts: function () { + this.deserializeSingle = function (type, results) { + step(2, 'calls deserializeSingle', assert); + + assert.equal(results, rawModel); + + return serializedModel; + }; + } + }; + + shopClient.create('checkouts', inputAttrs).then(products => { + step(3, 'resolves after fetch and serialize', assert); + assert.equal(products, serializedModel); + + done(); + }).catch(() => { + assert.ok(false, 'promise should not reject'); + done(); + }); +}); + +test('it utilizes the model\'s adapter and serializer during #update', function (assert) { + assert.expect(9); + + const done = assert.async(); + const serializedModel = { serializedProps: 'some-values' }; + const updatedPayload = { rawUpdatedProps: 'updated-values' }; + const updatedModel = { updatedProps: 'updated-values' }; + const model = new CheckoutModel({ + token: 'abc123', + someProp: 'some-prop' + }, { + shopClient, + adapter: { + update(type, id, payload) { + step(2, 'calls update on the models adapter', assert); + assert.equal(id, model.attrs.token, 'client extracts the token'); + assert.equal(payload, serializedModel); + + return new Promise(function (resolve) { + resolve(updatedPayload); + }); + }, + idKeyForType() { + return 'token'; + } + }, + serializer: { + serialize(type, localModel) { + step(1, 'calls serialize on the models serializer', assert); + assert.equal(localModel, model, 'serializer recieves model'); + + return serializedModel; + }, + deserializeSingle(type, singlePayload) { + step(3, 'calls deserializeSingle with the result of adapter#update', assert); + assert.equal(singlePayload, updatedPayload); + + return updatedModel; + } + } + }); + + // shopClient.adapters = { checkouts: FakeAdapter }; + // shopClient.serializers = { products: FakeSerializer }; + + shopClient.update('checkouts', model).then(localUpdatedModel => { + step(4, 'resolves update with the deserialized model', assert); + assert.equal(localUpdatedModel, updatedModel); + done(); + }).catch(e => { + assert.ok(false); + done(); + }); +}); diff --git a/tests/unit/serializers/checkout-serializer-test.js b/tests/unit/serializers/checkout-serializer-test.js new file mode 100644 index 000000000..834583482 --- /dev/null +++ b/tests/unit/serializers/checkout-serializer-test.js @@ -0,0 +1,75 @@ +import { module, test } from 'qunit'; +import CheckoutSerializer from 'buy-button-sdk/serializers/checkout-serializer'; +import CheckoutModel from 'buy-button-sdk/models/checkout-model'; + +let serializer; + +module('Unit | CheckoutSerializer', { + setup() { + serializer = new CheckoutSerializer(); + }, + teardown() { + serializer = null; + } +}); + + +const checkoutFixture = { + checkout: { + line_items: [] + } +}; + +test('it discovers the root key from the type', function (assert) { + assert.expect(1); + + assert.equal(serializer.rootKeyForType('checkouts'), 'checkout'); +}); + +test('it returns CheckoutModel for checkout type', function (assert) { + assert.expect(1); + + assert.equal(serializer.modelForType('checkouts'), CheckoutModel); +}); + +test('it transforms a single item payload into a checkout object.', function (assert) { + assert.expect(2); + + const model = serializer.deserializeSingle('checkouts', checkoutFixture); + + assert.notOk(Array.isArray(model), 'should not be an array'); + assert.deepEqual(model.attrs, checkoutFixture.checkout); +}); + +test('it attaches a reference of the passed serializer to the model on #deserializeSingle', function (assert) { + assert.expect(1); + + const model = serializer.deserializeSingle('checkouts', checkoutFixture, { serializer }); + + assert.deepEqual(model.serializer, serializer); +}); + +test('it attaches a reference of the passed shopClient to the model on #deserializeSingle', function (assert) { + assert.expect(1); + + const shopClient = 'some-shop-client'; + + const model = serializer.deserializeSingle('checkouts', checkoutFixture, { shopClient }); + + assert.equal(model.shopClient, shopClient); +}); + +test('it transforms a model into a payload on #serialize using the root key', function (assert) { + const updatedModel = new CheckoutModel({ + line_items: [ + { + variant_id: 123456789, + quantity: 1 + } + ] + }); + + const payload = serializer.serialize('checkouts', updatedModel); + + assert.deepEqual(payload, { checkout: updatedModel.attrs }); +});