Implement the shopping cart

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

I. Create implementation file

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

  2. The first two lines import the language SDK and the EventSourcedEntity type from the SDK.

    import as from '@lightbend/akkaserverless-javascript-sdk';
    const EventSourcedEntity = as.EventSourcedEntity;
  3. To connect the implementation to the cart.proto file, add the following code. There are three parameters required to create an Event Sourced entity, an array of protobuf files where the entity can find message definitions, the fully qualified name of the service that provides this entities interface, and the entity type name for all event source entities of this type. Those are the first three lines. The other lines represent additional options you have:

    const entity = new EventSourcedEntity(
        ['cart.proto', 'domain.proto'],
        'ecommerce.CartService',
        'cart',
        {
            // A snapshot will be persisted every time this many events are emitted.
            snapshotEvery: 100,
    
            // The directories to include when looking up imported protobuf files.
            includeDirs: ['./'],
    
            // Whether serialization of primitives should be supported when serializing events
            // and snapshots.
            serializeAllowPrimitives: true,
    
            // Whether serialization should fallback to using JSON if the state can't be serialized
            // as a protobuf.
            serializeFallbackToJson: true
        }
    );
  4. The events and state that are stored in Akka Serverless are in Protobuf format. To make it easier to work with, you can load the protobuf types (as happens in the below code. The Protobuf types, as shown in the code below, are needed so that Akka Serverless knowns how to serialize these objects when they are persisted.

    const pkg = 'ecommerce.persistence.';
    const ItemAdded = entity.lookupType(pkg + 'ItemAdded');
    const ItemRemoved = entity.lookupType(pkg + 'ItemRemoved');
    const Cart = entity.lookupType(pkg + 'Cart');
  5. Akka Serverless needs to create an initial state when the entity is first created and no state exists to load. The method Cart.create comes from the Cart message, which is defined in the cart.proto file and creates a new Cart object.

    entity.setInitial(userId => Cart.create({ items: [] }));
  6. 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. In this example you’ll create one behavior, but you could return multiple different ones depending on state:

    entity.setBehavior(cart => {
        return {
            // Command handlers. The name of the command corresponds to the name of the rpc call in
            // the gRPC service that this entity offers.
            commandHandlers: {
                AddItem: addItem,
                RemoveItem: removeItem,
                GetCart: getCart
            },
            // Event handlers. The name of the event corresponds to the (unqualified) name of the
            // persisted protobuf message.
            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:

    /**
     * addItem is the entry point for the API to add a new item to the cart and emits an
     * ItemAdded event to modify the state.
     * @param {*} addItem the item to add to the cart
     * @param {*} cart the cart to add the item to
     * @param {*} ctx the Akka Serverless context object
     * @returns
     */
    function addItem(addItem, cart, ctx) {
        /**
         * Perform some validation so you cannot add negative quantities. If the
         * validation is successful, a new ItemAdded event is created and emitted
         * using the ctx object.
         */
        if (addItem.quantity < 1) {
            ctx.fail('Cannot add negative quantity to item ' + addItem.productId);
        } else {
            const itemAdded = ItemAdded.create({
                item: {
                    productId: addItem.productId,
                    name: addItem.name,
                    quantity: addItem.quantity
                }
            });
            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:

    /**
     * removeItem is the entry point for the API to remove an item from the cart and emits an
     * ItemRemoved event to modify the state.
     * @param {*} addItem the item to remove from the cart
     * @param {*} cart the cart to remove the item from
     * @param {*} ctx the Akka Serverless context object
     * @returns
     */
    function removeItem(removeItem, cart, ctx) {
        /**
         * Perform some validation so you cannot remove items that aren't in your cart.
         * If the validation is successful, a new ItemRemoved event is created and emitted
         * using the ctx object.
         */
        const existing = cart.items.find(item => {
            return item.productId === removeItem.productId;
        });
    
        // If not, fail the command.
        if (!existing) {
            ctx.fail('Item ' + removeItem.productId + ' not in cart');
        } else {
            // Otherwise, emit an item removed event.
            const itemRemoved = ItemRemoved.create({
                productId: removeItem.productId
            });
            ctx.emit(itemRemoved);
            return {};
        }
    }
  3. Add a function getCart that handles the GetCart remote procedure call:

    /**
     * getCart is the entry point for the API to get a cart for a specific user
     * @param {*} request the request from the API
     * @param {*} cart the cart that was requested
     * @returns
     */
    function getCart(request, cart) {
        /**
         * This simply returns the cart without making any modifications
         */
        return cart;
    }
  4. Add a function itemAdded that handles the ItemAdded event:

    /**
     * itemAdded reacts to the ItemAdded events emitted and adds the item to the state of the cart
     * @param {*} added the item to be added
     * @param {*} cart the current cart
     * @returns
     */
    function itemAdded(added, cart) {
        /**
         * If there is an existing item with that product id, we need to increment its quantity.
         * Otherwise, we just add the item to the existing list, and return the new state
         */
    
        const existing = cart.items.find(item => {
            return item.productId === added.item.productId;
        });
    
        if (existing) {
            existing.quantity = existing.quantity + added.item.quantity;
        } else {
            cart.items.push(added.item);
        }
    
        return cart;
    }
  5. Add a function itemRemoved that handles the ItemRemoved event:

    /**
     * itemRemoved reacts to the ItemRemoved events emitted and removes the item from the state of the cart
     * @param {*} added the item to be added
     * @param {*} cart the current cart
     * @returns
     */
    function itemRemoved(removed, cart) {
        /**
         * Filter the removed item from the items by product id and return the new state
         */
    
        cart.items = cart.items.filter(item => {
            return item.productId !== removed.productId;
        });
    
        return cart;
    }
  6. Add an export at the end of the file to export the entity object

    export default entity;

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. The first two lines import the language SDK and the AkkaServerless type from the SDK.

    import as from '@lightbend/akkaserverless-javascript-sdk';
    import entity from './cart.js';
  3. To create a new Akka Serverless server and bind the entity from cart.js to it, you’ll need the following code. The server will listen on port 8080 and bind to all interfaces. This makes it possible to test your service locally and deploy it to Akka Serverless without any changes:

    const server = new as.AkkaServerless();
    server.addComponent(entity);
    server.start({ bindAddress: '0.0.0.0', bindPort: '8080' });

The complete shoppingcart.js file

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

import as from '@lightbend/akkaserverless-javascript-sdk';
const EventSourcedEntity = as.EventSourcedEntity;

const entity = new EventSourcedEntity(
    ['cart.proto', 'domain.proto'],
    'ecommerce.CartService',
    'cart',
    {
        // A snapshot will be persisted every time this many events are emitted.
        snapshotEvery: 100,

        // The directories to include when looking up imported protobuf files.
        includeDirs: ['./'],

        // Whether serialization of primitives should be supported when serializing events
        // and snapshots.
        serializeAllowPrimitives: true,

        // Whether serialization should fallback to using JSON if the state can't be serialized
        // as a protobuf.
        serializeFallbackToJson: true
    }
);

const pkg = 'ecommerce.persistence.';
const ItemAdded = entity.lookupType(pkg + 'ItemAdded');
const ItemRemoved = entity.lookupType(pkg + 'ItemRemoved');
const Cart = entity.lookupType(pkg + 'Cart');

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

entity.setBehavior(cart => {
    return {
        // Command handlers. The name of the command corresponds to the name of the rpc call in
        // the gRPC service that this entity offers.
        commandHandlers: {
            AddItem: addItem,
            RemoveItem: removeItem,
            GetCart: getCart
        },
        // Event handlers. The name of the event corresponds to the (unqualified) name of the
        // persisted protobuf message.
        eventHandlers: {
            ItemAdded: itemAdded,
            ItemRemoved: itemRemoved
        }
    };
});
/**
 * addItem is the entry point for the API to add a new item to the cart and emits an
 * ItemAdded event to modify the state.
 * @param {*} addItem the item to add to the cart
 * @param {*} cart the cart to add the item to
 * @param {*} ctx the Akka Serverless context object
 * @returns
 */
function addItem(addItem, cart, ctx) {
    /**
     * Perform some validation so you cannot add negative quantities. If the
     * validation is successful, a new ItemAdded event is created and emitted
     * using the ctx object.
     */
    if (addItem.quantity < 1) {
        ctx.fail('Cannot add negative quantity to item ' + addItem.productId);
    } else {
        const itemAdded = ItemAdded.create({
            item: {
                productId: addItem.productId,
                name: addItem.name,
                quantity: addItem.quantity
            }
        });
        ctx.emit(itemAdded);
        return {};
    }
}
/**
 * removeItem is the entry point for the API to remove an item from the cart and emits an
 * ItemRemoved event to modify the state.
 * @param {*} addItem the item to remove from the cart
 * @param {*} cart the cart to remove the item from
 * @param {*} ctx the Akka Serverless context object
 * @returns
 */
function removeItem(removeItem, cart, ctx) {
    /**
     * Perform some validation so you cannot remove items that aren't in your cart.
     * If the validation is successful, a new ItemRemoved event is created and emitted
     * using the ctx object.
     */
    const existing = cart.items.find(item => {
        return item.productId === removeItem.productId;
    });

    // If not, fail the command.
    if (!existing) {
        ctx.fail('Item ' + removeItem.productId + ' not in cart');
    } else {
        // Otherwise, emit an item removed event.
        const itemRemoved = ItemRemoved.create({
            productId: removeItem.productId
        });
        ctx.emit(itemRemoved);
        return {};
    }
}
/**
 * getCart is the entry point for the API to get a cart for a specific user
 * @param {*} request the request from the API
 * @param {*} cart the cart that was requested
 * @returns
 */
function getCart(request, cart) {
    /**
     * This simply returns the cart without making any modifications
     */
    return cart;
}
/**
 * itemAdded reacts to the ItemAdded events emitted and adds the item to the state of the cart
 * @param {*} added the item to be added
 * @param {*} cart the current cart
 * @returns
 */
function itemAdded(added, cart) {
    /**
     * If there is an existing item with that product id, we need to increment its quantity.
     * Otherwise, we just add the item to the existing list, and return the new state
     */

    const existing = cart.items.find(item => {
        return item.productId === added.item.productId;
    });

    if (existing) {
        existing.quantity = existing.quantity + added.item.quantity;
    } else {
        cart.items.push(added.item);
    }

    return cart;
}
/**
 * itemRemoved reacts to the ItemRemoved events emitted and removes the item from the state of the cart
 * @param {*} added the item to be added
 * @param {*} cart the current cart
 * @returns
 */
function itemRemoved(removed, cart) {
    /**
     * Filter the removed item from the items by product id and return the new state
     */

    cart.items = cart.items.filter(item => {
        return item.productId !== removed.productId;
    });

    return cart;
}
export default entity;

The complete index.js file

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

import as from '@lightbend/akkaserverless-javascript-sdk';
import entity from './cart.js';
const server = new as.AkkaServerless();
server.addComponent(entity);
server.start({ bindAddress: '0.0.0.0', bindPort: '8080' });