Skip to content

Commit

Permalink
feat: add an api to add items to a cart atomically
Browse files Browse the repository at this point in the history
Signed-off-by: Raymond Feng <[email protected]>
  • Loading branch information
raymondfeng committed Sep 19, 2018
1 parent 6fda110 commit ec06800
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 74 deletions.
118 changes: 68 additions & 50 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@
"src"
],
"dependencies": {
"@loopback/boot": "^0.13.2",
"@loopback/boot": "^0.13.5",
"@loopback/context": "^0.12.8",
"@loopback/core": "^0.11.8",
"@loopback/core": "^0.11.9",
"@loopback/dist-util": "^0.3.7",
"@loopback/openapi-v3": "^0.13.2",
"@loopback/repository": "^0.16.2",
"@loopback/rest": "^0.21.1",
"@loopback/openapi-v3": "^0.14.2",
"@loopback/repository": "^0.16.5",
"@loopback/rest": "^0.22.2",
"bcryptjs": "^2.4.3",
"isemail": "^3.1.3",
"loopback-connector-kv-redis": "^3.0.0",
"loopback-connector-mongodb": "^3.6.0"
"loopback-connector-mongodb": "^3.7.1"
},
"devDependencies": {
"@commitlint/cli": "^7.1.2",
Expand Down
25 changes: 23 additions & 2 deletions src/controllers/shopping-cart.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {put, get, del, param, requestBody, HttpErrors} from '@loopback/rest';
import {
put,
get,
del,
param,
requestBody,
HttpErrors,
post,
} from '@loopback/rest';
import {repository} from '@loopback/repository';
import {ShoppingCartRepository} from '../repositories';
import {ShoppingCart} from '../models';
import {ShoppingCart, ShoppingCartItem} from '../models';

/**
* Controller for shopping cart
Expand Down Expand Up @@ -59,4 +67,17 @@ export class ShoppingCartController {
async remove(@param.path.string('userId') userId: string) {
await this.shoppingCartRepository.delete(userId);
}

/**
* Add an item to the shopping cart for a given user
* @param userId User id
* @param cart Shopping cart item to be added
*/
@post('/shoppingCarts/{userId}/items')
async addItem(
@param.path.string('userId') userId: string,
@requestBody({description: 'shopping cart item'}) item: ShoppingCartItem,
) {
await this.shoppingCartRepository.addItem(userId, item);
}
}
8 changes: 6 additions & 2 deletions src/models/shopping-cart.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {Entity, model, property} from '@loopback/repository';
* Item in a shopping cart
*/
@model()
export class ShoppingCartItem {
export class ShoppingCartItem extends Entity {
/**
* Product id
*/
@property()
@property({id: true})
productId: string;
/**
* Quantity
Expand All @@ -25,6 +25,10 @@ export class ShoppingCartItem {
*/
@property()
price?: number;

constructor(data?: Partial<ShoppingCartItem>) {
super(data);
}
}

@model()
Expand Down
57 changes: 56 additions & 1 deletion src/repositories/shopping-cart.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,69 @@
// License text available at https://opensource.org/licenses/MIT

import {DefaultKeyValueRepository} from '@loopback/repository';
import {ShoppingCart} from '../models/shopping-cart.model';
import {ShoppingCart, ShoppingCartItem} from '../models/shopping-cart.model';
import {RedisDataSource} from '../datasources/redis.datasource';
import {inject} from '@loopback/context';
import {promisify} from 'util';

export class ShoppingCartRepository extends DefaultKeyValueRepository<
ShoppingCart
> {
constructor(@inject('datasources.redis') ds: RedisDataSource) {
super(ShoppingCart, ds);
}

/**
* Add an item to the shopping cart with optimistic lock to allow concurrent
* `adding to cart` from multiple devices
*
* @param userId User id
* @param item Item to be added
*/
addItem(userId: string, item: ShoppingCartItem) {
const addItemToCart = (cart: ShoppingCart | null) => {
cart = cart || new ShoppingCart({userId});
cart.items = cart.items || [];
cart.items.push(item);
return cart;
};
return this.checkAndSet(userId, addItemToCart);
}

/**
* Use Redis WATCH and Transaction to check and set against a key
* See https://redis.io/topics/transactions#optimistic-locking-using-check-and-set
*
* Ideally, this method should be made available by `KeyValueRepository`.
*
* @param userId User id
* @param check A function that checks the current value and produces a new
* value. It returns `null` to abort.
*/
async checkAndSet(
userId: string,
check: (current: ShoppingCart | null) => ShoppingCart | null,
) {
const connector = this.kvModelClass.dataSource!.connector!;
// tslint:disable-next-line:no-any
const execute = promisify((cmd: string, args: any[], cb: Function) => {
return connector.execute!(cmd, args, cb);
});
/**
* - WATCH userId
* - GET userId
* - check(cart)
* - MULTI
* - SET userId
* - EXEC
*/
await execute('WATCH', [userId]);
let cart: ShoppingCart | null = await this.get(userId);
cart = check(cart);
if (!cart) return null;
await execute('MULTI', []);
await this.set(userId, cart);
await execute('EXEC', []);
return cart;
}
}
Loading

0 comments on commit ec06800

Please sign in to comment.