Implement the shopping cart

Next, create your .js file to implement the shopping cart.

I. Create implementation file

  1. Create a shoppingcart.js file in the shoppingcart directory.

  2. Add a const EventSourced = require("cloudstate").EventSourced; as the first line in the file. This line specifies which type of state model is used, in this case EventSourced

  3. To connect the implementation to the cart.proto file, add the following code:

    const entity = new EventSourced(
      "cart.proto", // This is the name of your proto file
      "com.example.shoppingcart.ShoppingCart", // This is the name of the package and service in your proto file
      {
        persistenceId: "shopping-cart",
        snapshotEvery: 5,
        includeDirs: ["./"],
        serializeFallbackToJson: true // This allows you to test your implementation with Mocha later on
      }
    );
  4. To create the initial state, set a callback, which will create the state if there is no snapshot to load.

    entity.setInitial(userId => ({items: []}));
  5. Set a callback to create the behavior given the current state. This callback will be invoked after an event is handled to get the current behavior for the current state. The names of the commandHandlers need to match the names of the remote procedure calls in your proto file. To test, you’ll use a JSON payload and the name of the event corresponds to the value of the names of the eventHandlers:

    entity.setBehavior(cart => {
      return {
        commandHandlers: {
          AddItem: addItem,
          RemoveItem: removeItem,
          GetCart: getCart
        },
        eventHandlers: {
          ItemAdded: itemAdded,
          ItemRemoved: itemRemoved
        }
      };
    });

II. Add functions for the remote procedure calls

  1. Add a function addItem that handles the AddItem remote procedure call:

    /**
     * Handler for add item commands.
     */
    function addItem(addItem, cart, ctx) {
      console.log("addItem", addItem);
      const itemAdded = {
        type: "ItemAdded",
        item: {
          productId: addItem.productId,
          name: addItem.name,
          quantity: addItem.quantity
        }
      };
      console.log("addItem::emit event", itemAdded);
      ctx.emit(itemAdded);
      return {};
    }
    This example function takes addItem, cart, and ctx as parameters. The addItem parameter contains the data that is passed in from the remote procedure call.
  2. Add a function removeItem that handles the RemoveItem remote procedure call:

    /**
     * Handler for remove item commands.
     */
    function removeItem(removeItem, cart, ctx) {
      console.log("removeItem", removeItem);
      // Check that the item that we're removing actually exists.
      const existing = cart.items.find(item => {
        console.log("removeItem:: return existing");
        return item.productId === removeItem.productId;
      });
    
      if (existing) {
        const itemRemoved = {
          type: "ItemRemoved",
          productId: removeItem.productId
        };
        ctx.emit(itemRemoved);
      }
      return {};
    }
  3. Add a function getCart that handles the GetCart remote procedure call:

    /**
     * Handler for get cart commands.
     */
    function getCart(request, cart) {
      console.log("getCart", cart);
      return cart;
    }
  4. Add a function itemAdded that handles the ItemAdded event:

    /**
     * Handler for item added events.
     * If there is an existing item with that product id, we need to increment its quantity.
     */
    function itemAdded(added, cart) {
      console.log("itemAdded");
      const existing = cart.items.find(item => {
        console.log("itemAdded::return existing");
        return item.productId === added.item.productId;
      });
    
      if (existing) {
        existing.quantity = existing.quantity + added.item.quantity;
      } else {
        console.log("itemAdded::push");
        cart.items.push(added.item);
      }
    
      console.log("return state");
      return cart;
    }
  5. Add a function itemRemoved that handles the ItemRemoved event:

    /**
     * Handler for item removed events.
     */
    function itemRemoved(removed, cart) {
      cart.items = cart.items.filter(item => {
        return item.productId !== removed.productId;
      });
    
      return cart;
    }
  6. Add module.exports = entity; at the end of the file to export the entity object

III. Implement the entry point

An entry point tells the Node engine where to find the Entity implementation. Use index.js as the entry point to start the shopping cart:

  1. Create a index.js file in the shoppingcart directory.

  2. Add require("./shoppingcart.js").start(); as the first and only line to the file. This line will tell the node engine where to find the entity implementation.

The complete shoppingcart.js file

Your complete shoppingcart.js file should look like the following:

const EventSourced = require("cloudstate").EventSourced;

const entity = new EventSourced(
    "cart.proto", // This is the name of your proto file
    "com.example.shoppingcart.ShoppingCart", // This is the name of the package and service in your proto file
    {
        persistenceId: "shopping-cart",
        snapshotEvery: 5,
        includeDirs: ["./"],
        serializeFallbackToJson: true // This allows you to test your implementation with Mocha later on
    }
);

entity.setInitial(userId => ({ items: [] }));

entity.setBehavior(cart => {
    return {
        commandHandlers: {
            AddItem: addItem,
            RemoveItem: removeItem,
            GetCart: getCart
        },
        eventHandlers: {
            ItemAdded: itemAdded,
            ItemRemoved: itemRemoved
        }
    };
});

/**
* Handler for add item commands.
*/
function addItem(addItem, cart, ctx) {
    console.log("addItem", addItem);
    const itemAdded = {
        type: "ItemAdded",
        item: {
            productId: addItem.productId,
            name: addItem.name,
            quantity: addItem.quantity
        }
    };
    console.log("addItem::emit event", itemAdded);
    ctx.emit(itemAdded);
    return {};
}

/**
* Handler for remove item commands.
*/
function removeItem(removeItem, cart, ctx) {
    console.log("removeItem", removeItem);
    // Check that the item that we're removing actually exists.
    const existing = cart.items.find(item => {
        console.log("removeItem:: return existing");
        return item.productId === removeItem.productId;
    });

    if (existing) {
        const itemRemoved = {
            type: "ItemRemoved",
            productId: removeItem.productId
        };
        ctx.emit(itemRemoved);
    }
    return {};
}

/**
* Handler for get cart commands.
*/
function getCart(request, cart) {
    console.log("getCart", cart);
    return cart;
}

/**
* Handler for item added events.
* If there is an existing item with that product id, we need to increment its quantity.
*/
function itemAdded(added, cart) {
    console.log("itemAdded");
    const existing = cart.items.find(item => {
        console.log("itemAdded::return existing");
        return item.productId === added.item.productId;
    });

    if (existing) {
        existing.quantity = existing.quantity + added.item.quantity;
    } else {
        console.log("itemAdded::push");
        cart.items.push(added.item);
    }

    console.log("return state");
    return cart;
}

/**
* Handler for item removed events.
*/
function itemRemoved(removed, cart) {
    cart.items = cart.items.filter(item => {
        return item.productId !== removed.productId;
    });

    return cart;
}

module.exports = entity;