Write Conflict mongoose transactions - javascript

In my application i am trying to build a route for creating orders the flow should go like this:
1- reserve the products for the user
2- empty the user's cart
3- take payment
however if one operation fails the database should return to its original state before updating any documents
I'm using mongodb
this is what i tried:
export default async function order(userId) {
const User = mongoose.model('user');
const Product = mongoose.model('product');
const Order = mongoose.model('order');
const session = await mongoose.startSession();
session.startTransaction();
try {
const user = await User.findById(userId).session(session);
// reserve the product for the user
const results = await Product.bulkWrite(
user.cart.map((item) => ({
updateOne: {
filter: {
_id: item.product,
'combinations.size': item.size,
'combinations.color': item.color,
'combinations.qty': { $gte: item.qty },
},
update: {
$inc: { 'combinations.$.qty': -item.qty },
},
},
})),
{ session }
);
if (results.nModified !== user.cart.length) {
throw new Error('Not all products available, transaction aborted');
}
// empty user's cart
await User.findByIdAndUpdate(userId, { cart: [] }).session(session);
// take payment from the user
const payment = (successed) => {
if (successed) return { id: 'charge id' };
throw new Error('payment failed');
};
const charge = payment(true);
const productsInCart = await Product.find({
_id: { $in: user.cart.map((item) => item.product.toString()) },
});
// create the order
await Order.create([
{
user: userId,
products: user.cart.map((item) => ({
...item,
price: productsInCart.find(
(product) => product._id.toString() === item.product.toString()
).price.curr1,
})),
currency: 'curr1',
status: 'placed',
chargeId: charge.id,
},
]);
await session.commitTransaction();
} catch (err) {
console.log(err);
await session.abortTransaction();
} finally {
await session.endSession();
}
}
however when testing it i get this error
these are my models:
User:
import { Schema, model } from 'mongoose';
const UserSchema = new Schema({
email: String,
cart: [
{
product: { type: Schema.Types.ObjectId, ref: 'product' },
color: String,
size: String,
qty: Number,
},
],
orders: [{ type: Schema.Types.ObjectId, ref: 'order' }],
});
model('user', UserSchema);
Product
import { Schema, model } from 'mongoose';
const ProductSchema = new Schema({
title: String,
price: {
curr1: Number, // price in a region
curr2: Number, // price in another region
},
combinations: [
{
size: String,
color: String,
qty: Number,
},
],
});
model('product', ProductSchema);
Order:
import { Schema, model } from 'mongoose';
const OrderSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'user' },
products: [
{
product: { type: Schema.Types.ObjectId, ref: 'product' },
size: String,
color: String,
qty: Number,
price: Number,
},
],
currency: String,
status: String,
chargeId: String,
});
model('order', OrderSchema);
and my test:
import async from 'async';
import mongoose from 'mongoose';
import order from '../src/order.js';
const User = mongoose.model('user');
const Order = mongoose.model('order');
const Product = mongoose.model('product');
describe('Race condition for orders', async () => {
let products;
let users;
beforeEach(async () => {
products = await Product.insertMany([
{
title: 'test product 1',
price: { curr1: 10, curr2: 10 },
combinations: [
{
size: 'large',
color: 'black',
qty: 1,
},
{
size: 'small',
color: 'red',
qty: 2,
},
],
},
{
title: 'test product 2',
price: { curr1: 10, curr2: 10 },
combinations: [
{
size: 'medium',
color: 'yellow',
qty: 1,
},
],
},
]);
users = await User.insertMany([
{
email: 'test#test.test',
cart: products.map((product) => ({
product: product._id.toString(),
size: product.combinations[0].size,
color: product.combinations[0].color,
qty: 1,
})),
},
{
email: 'test2#test.test',
cart: products.map((product) => ({
product: product._id.toString(),
size: product.combinations[0].size,
color: product.combinations[0].color,
qty: 1,
})),
},
]);
});
it('calls order twice at the same time', async () => {
await new Promise((resolve, reject) =>
async.parallel(
[
async () => {
await order(users[0]._id.toString());
},
async () => {
await order(users[1]._id.toString());
},
],
(err, res) => {
if (err) return reject(err);
resolve(res);
}
)
);
console.log(await Order.find({}));
});
});

Related

Add field on mongoose find

I have a mongoose find like this
Persona.find(query)
.sort({ order:1 }).exec(async function (err, data) {
if (err) {
return res.status(400).send(err);
}
let dataform = [];
await asyncForEach(data, async persona => {
persona.newField = "Test";
dataform.push(persona);
}
});
res.json(dataform);
});
With the debugger the value of dataform on res.json(dataform); is this
0:{
_id: 123,
name: 'persona1',
newField: 'Test'
},
1:{
_id: 1234,
name: 'persona2',
newField: 'Test'
}
But when my controller gets the api response, the "newField" doesn't exist
0:{
_id: 123,
name: 'persona1'
},
1:{
_id: 1234,
name: 'persona2'
}

how to test a deadlock using jest.js?

I want to have a test that is running concurrently and I am using it.concurrent in jestjs.
The problem is the mockData is not created in each it.concurrent test when I'm running the test. And also I want each test to share the data so I can see if it has a deadlock. By the way, I am using PostgreSQL and knex.js here. Thanks in advance.
describe('#Concurrent tests', () => {
const knex: Knex = container.get(Types.Knex);
const orderDataSource: IOrderDataSource = container.get(Types.OrderDataSource);
const branchDataSource: IBranchDataSource = container.get(Types.BranchDataSource);
const brandDataSource: IBrandDataSource = container.get(Types.BrandDataSource);
const variantDataSource: IVariantDataSource = container.get(Types.VariantDataSource);
const categoryDataSource: ICategoryDataSource = container.get(Types.CategoryDataSource);
const productDataSource: IProductDataSource = container.get(Types.ProductDataSource);
const branchVariantDataSource: IBranchVariantDataSource = container.get(Types.BranchVariantDataSource);
const basketDataSource: IBasketDataSource = container.get(Types.BasketDataSource);
const mockBrand2 = Factory.build('brand',{
id: 'asdf',
name: 'Brand 2',
});
const mockBranch2 = Factory.build('branch',{
id: 'branch-2',
brandId: mockBrand2.id,
}) as IBranch;
const mockCategory2 = Factory.build('category.activated', {
id:'category-2',
brandId: mockBrand2.id,
}) as ICategory;
const mockProduct2 = Factory.build('product', {
id: 'product-2',
categoryId: mockCategory2.id,
}) as IProduct;
const mockVariant2 = Factory.build('variant', {
id: 'variant-2',
productId: mockProduct2.id,
}) as IVariant;
const mockBranchVariant2 = Factory.build('branchVariant', {
id: 'branch-variant-2',
branchId: mockBranch2.id,
variantId: mockVariant2.id,
stock: 100,
});
const mockBasket = Factory.build('basket', {
id: 'basket-1',
branchId: mockBranch2.id,
orderItems: [
{
variantId: mockVariant2.id,
quantity: 10,
}
]
}) as IBasket;
const mockBasket2 = Factory.build('basket-3',{
id: 'basket-2',
branchId: mockBranch2.id,
orderItems: [
{
variantId: mockVariant2.id,
quantity: 20,
}
]
}) as IBasket;
beforeEach(async () => {
await brandDataSource.create(mockBrand2);
await categoryDataSource.create(mockCategory2);
await branchDataSource.create(mockBranch2);
await productDataSource.create(mockProduct2);
await variantDataSource.create(mockVariant2);
await branchVariantDataSource.create(mockBranchVariant2);
await basketDataSource.create(mockBasket);
await basketDataSource.create(mockBasket2);
});
afterEach(async () => {
await brandDataSource.truncate();
await branchDataSource.truncate();
await productDataSource.truncate();
await variantDataSource.truncate();
await categoryDataSource.truncate();
await branchVariantDataSource.truncate();
await basketDataSource.truncate();
await orderDataSource.truncate();
});
it.concurrent('creates multiple baskets', async () => {
const params = {
items: [
{
variantId: mockVariant2.id,
},
],
address: {
latitude: 14.6930497078559,
longitude: 120.99470307301551,
addressDetails: '713 Gold Eagle St., Gen. T. De Leon, Valenzuela City',
},
mobileNum: '09198003996',
paymentMethod: 'cod',
deliveryId: 'iahdsfiuahdsnfiuchnaiuhiudfhiahfhalifnhcasf',
shippingFee: 150.0,
customerName: 'David Tan',
email: 'd.tan#gmail.com',
};
const event = {
requestContext: {
authorizer: {
lambda: {
principalId: 'lkjfi123nOPIUJnss',
}
}
},
pathParameters: {
branchId: mockBranch2.id,
},
body: JSON.stringify(params),
isTesting: true,
};
const res = await handler(event);
const body = JSON.parse(res.body);
});
it.concurrent('creates multiple baskets-2', async () => {
const params = {
items: [
{
variantId: mockVariant2.id,
},
],
address: {
latitude: 14.6930497078559,
longitude: 120.99470307301551,
addressDetails: '713 Gold Eagle St., Gen. T. De Leon, Valenzuela City',
},
mobileNum: '09198003996',
paymentMethod: 'cod',
deliveryId: 'iahdsfiuahdsnfiuchnaiuhiudfhiahfhalifnhcasf',
shippingFee: 150.0,
customerName: 'Mark Uy',
email: 'mark#gmail.com',
};
const event = {
requestContext: {
authorizer: {
lambda: {
principalId: 'AsdioSDOIj123kj',
}
}
},
pathParameters: {
branchId: mockBranch2.id,
},
body: JSON.stringify(params),
isTesting: true,
};
const res = await handler(event);
const body = JSON.parse(res.body);
});
});

GraphQL server not starting due to module.exports failure

I am learning GraphQL and I faced an error due while trying to start a server.
const graphql = require('graphql');
const _ = require('lodash');
const {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLSchema
} = graphql;
const Users = [
{ id: '23', firstName: 'Bill', age: 20 },
{ id: '41', firstName: 'Jane', age: 18 }
];
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLString },
firstName: { type: GraphQLString },
age: { type: GraphQLInt }
}
});
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
user: {
type: {
type: UserType,
args: { id: { type: GraphQLString } },
resolve(parentValue, args) {
return _.find(Users, { id: args.id });
}
}
}
}
});
module.exports = new GraphQLSchema({
query: RootQuery
});
and the server.js code is here:
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const schema = require('./schema/schema');
const app = express();
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}));
app.listen(4000, () => {
console.log("Server running at port 4000.");
});
The error seems to be coming from the "module.exports" section as when I comment it out the server starts without a problem.
There is syntax confusion. In RootQuery the type part under user completely covers the whole field structure, error is here.
Just remove top level type: {}.
Try this out:
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
user: {
type: UserType,
args: { id: { type: GraphQLString } },
resolve(parentValue, args) {
return _.find(Users, { id: args.id });
}
}
}
});

Avoid Code Duplication with GraphQL Cursor based Pagination

I've been looking all over for an answer to this and I've been banging my head on the wall. I wrote a cursor based pagination example that works well with graphql and the thing is I thought I would do the same thing with authors, that I did with books and the only way I can figure out how to do this is to completely duplicate everything. On the root query there is quite a long chunk of code handling the pagination and I would hate to do that all over for the authors endpoint but I can't seem to find a way to do this while reusing the code
Here is the code
const express = require('express')
const { graphqlHTTP } = require('express-graphql')
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLList,
GraphQLInt,
GraphQLNonNull
} = require('graphql')
const {
PageType,
convertNodeToCursor,
convertCursorToNodeId
} = require('./pagination')
const app = express()
const authors = [
{ id: 1, name: "Author 1"},
{ id: 2, name: "Author 2"},
{ id: 3, name: "Author 3"}
]
const books = [
{ id: 1, title: "Book 1", authorId: 1 },
{ id: 2, title: "Book 2", authorId: 1 },
{ id: 3, title: "Book 3", authorId: 1 },
{ id: 4, title: "Book 4", authorId: 2 },
{ id: 5, title: "Book 5", authorId: 2 },
{ id: 6, title: "Book 6", authorId: 2 },
{ id: 7, title: "Book 7", authorId: 3 },
{ id: 8, title: "Book 8", authorId: 3 },
{ id: 9, title: "Book 9", authorId: 3 }
]
const Book = new GraphQLObjectType({
name: 'Book',
description: 'this is a book',
fields: () => ({
id: { type: GraphQLNonNull(GraphQLInt) },
title: { type: GraphQLNonNull(GraphQLString) },
authorId: { type: GraphQLNonNull(GraphQLInt) },
author: {
type: Author,
resolve: ({authorId}) => {
return authors.find(author => author.id === authorId)
}
}
})
})
const Author = new GraphQLObjectType({
name: 'Author',
description: 'this represents the author of a book',
fields: () => ({
id: { type: GraphQLNonNull(GraphQLInt) },
name: { type: GraphQLNonNull(GraphQLString) },
books: {
type: GraphQLList(Book),
resolve: ({id}) => {
return books.filter(book => book.authorId === id)
}
}
})
})
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
description: 'this is the root query',
fields: () => ({
book: {
type: Book,
description: 'a single book',
args: {
id: { type: GraphQLInt }
},
resolve: (_, { id }) => {
return books.find(book => book.id === id)
}
},
author: {
type: Author,
description: 'a single author',
args: {
id: { type: GraphQLInt },
},
resolve: (_, { id }) => {
return authors.find(author => author.id === id)
}
},
books: {
type: PageType(Book),
description: 'a list of books',
args: {
first: { type: GraphQLInt },
afterCursor: { type: GraphQLString }
},
resolve: (_, { first, afterCursor }) => {
let afterIndex = 0
if (typeof afterCursor === 'string') {
let nodeId = convertCursorToNodeId(afterCursor)
let nodeIndex = books.findIndex(book => book.id === nodeId)
if (nodeIndex >= 0) {
afterIndex = nodeIndex + 1
}
}
const slicedData = books.slice(afterIndex, afterIndex + first)
console.log('sliced data: ', slicedData)
const edges = slicedData.map(node => ({
node,
cursor: convertNodeToCursor(node)
}))
let startCursor = null
let endCursor = null
if (edges.length > 0) {
startCursor = convertNodeToCursor(edges[0].node)
endCursor = convertNodeToCursor(edges[edges.length - 1].node)
}
let hasNextPage = books.length > afterIndex + first
return {
totalCount: books.length,
edges,
pageInfo: {
startCursor,
endCursor,
hasNextPage
}
}
}
}
})
})
const schema = new GraphQLSchema({
query: RootQuery
})
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}))
app.listen(3000, () => console.log('app running at http://localhost:3000/graphql'))
and I handle the pagination in another file here:
const {
GraphQLString,
GraphQLInt,
GraphQLBoolean,
GraphQLObjectType,
GraphQLList,
} = require('graphql')
const Edge = (itemType) => {
return new GraphQLObjectType({
name: 'EdgeType',
fields: () => ({
node: { type: itemType },
cursor: { type: GraphQLString }
})
})
}
const PageInfo = new GraphQLObjectType({
name: 'PageInfoType',
fields: () => ({
startCursor: { type: GraphQLString },
endCursor: { type: GraphQLString },
hasNextPage: { type: GraphQLBoolean }
})
})
const PageType = (itemType) => {
return new GraphQLObjectType({
name: 'PageType',
fields: () => ({
totalCount: { type: GraphQLInt },
edges: { type: new GraphQLList(Edge(itemType)) },
pageInfo: { type: PageInfo }
})
})
}
const convertNodeToCursor = (node) => {
// Encoding the cursor value to Base 64 as suggested in GraphQL documentation
return Buffer.from((node.id).toString()).toString('base64')
}
const convertCursorToNodeId = (cursor) => {
// Decoding the cursor value from Base 64 to integer
return parseInt(Buffer.from(cursor, 'base64').toString('ascii'))
}
module.exports = {
PageType,
convertNodeToCursor,
convertCursorToNodeId
}
Now if I copy and paste the books endpoint and change it to authors, and change the type to PageType(Author) then I get another error:
Schema must contain uniquely named types but contains multiple types named "PageType".
So this clearly isn't a solution either
You cannot have one EdgeType that contains Authors and another EdgeType that contains Books. Instead, you will need one AuthorEdge and one BookEdge type.
The same holds for the PageType - there can't be two different types with different fields but the same name.
The solution is relatively simple though - if you dynamically generated these types in a function, also name them dynamically:
const Edge = (itemType) => {
return new GraphQLObjectType({
name: itemType.name + 'Edge',
// ^^^^^^^^^^^^^^^^^^^^^^
fields: () => ({
node: { type: itemType },
cursor: { type: GraphQLString }
})
})
}
const PageInfo = new GraphQLObjectType({
name: 'PageInfo',
fields: () => ({
startCursor: { type: GraphQLString },
endCursor: { type: GraphQLString },
hasNextPage: { type: GraphQLBoolean }
})
})
const PageType = (itemType) => {
return new GraphQLObjectType({
name: itemType.name + 'sPage',
// ^^^^^^^^^^^^^^^^^^^^^^^
fields: () => ({
totalCount: { type: GraphQLInt },
edges: { type: new GraphQLList(Edge(itemType)) },
pageInfo: { type: PageInfo }
})
})
}

Use findOneAndUpdate $pull and populate does not update or Populate

This is my data:
{
"_id" : ObjectId("594e762b03cc52508686ceef"),
"_members" : [
"59527bd5e521801bf07eab98",
"594ecca47a699d1775c2a2db"
],
}
I want to delete 59527bd5e521801bf07eab98 from _members.
PostSchema:
const PostsSchema = new Schema({
// ...
_members: [{ type: Schema.Types.ObjectId, ref: 'user' }],
// ...
})
UserSchema:
const UserSchema = new Schema({
// ...
posts : [{ type: Schema.Types.ObjectId, ref: 'post' }],
// ...
})
Find And Update:
let id = mongoose.mongo.ObjectID(req.params.id)
let member = mongoose.mongo.ObjectID(req.params.member)
let populateQuery = [
{path:'_members', select:'_id'}
]
Post.
findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': { '_id': member }}
},
{'new': true}
).
populate(populateQuery).
exec(
function (err, data) {
res.send(data)
}
);
Interestingly "population" only seems to work by specifying the model option to .populate() in this case.
Also you only need supply the member directly to $pull as the _id is not actually a property, until "populated". The "_members" array is simply an array of ObjectId values within the Post itself. So supply the input directly:
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
)
.populate({path:'_members', select:'_id', model: 'user' })
.exec(function(err,post) {
if (err) throw err; // or something
res.send(post)
})
Or with Promises
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
)
.populate({path:'_members', select:'_id', model: 'user' })
.then(post => res.send(post))
.catch(err => console.error(err)); // or something
Alternately call Model.populate() instead:
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
).exec(function(err,post) {
if (err) throw err; // or something
Post.populate(post, {path:'_members', select:'_id', model: 'user' },function(err,post) {
if (err) throw err; // or something
res.send(post); // now populated
}
})
Or alternately using Promises:
Post.findOneAndUpdate(
{'_id' : id} ,
{
$pull: { '_members': member }
},
{'new': true}
)
.then(post => Post.populate(post, {path:'_members', select:'_id', model: 'user' }))
.then(post => res.send(post)) // also populated
.catch(err => console.error(err)) // or whichever
Somewhat unsure about why you want to call .populate() when you are only asking to return the _id field which is already embedded in the document, but that's another case. With the model option the population actually takes place here.
As a self contained demonstration:
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/test');
const relSchema = new Schema({ c: String });
const testSchema = new Schema({
a: String,
b: [{ type: Schema.Types.ObjectId, rel: 'Rel' }]
})
const Test = mongoose.model('Test', testSchema);
const Rel = mongoose.model('Rel', relSchema);
function log(data) {
console.log(JSON.stringify(data, undefined, 2))
}
async.series(
[
(callback) =>
async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
(callback) =>
async.waterfall(
[
(callback) => Rel.create([{ c: 2 },{ c: 3 },{ c: 4 }],callback),
(rels,callback) => Test.create({ a: 1, b: rels },(err,test) => {
if (err) callback(err);
log(test);
callback(err,test.b.slice(1,3))
}),
(rels,calback) =>
Test.findOneAndUpdate(
{ 'a': 1 },
{ '$pull': { 'b': rels[0] } },
{ 'new': true }
)
.populate({ path: 'b', model: 'Rel' })
.exec((err,test) => {
if (err) callback(err);
log(test);
callback(err);
})
],
callback
)
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
)
And the output:
Mongoose: tests.remove({}, {})
Mongoose: rels.remove({}, {})
Mongoose: rels.insert({ c: '2', _id: ObjectId("595714579afd8860e56d2ec7"), __v: 0 })
Mongoose: rels.insert({ c: '3', _id: ObjectId("595714579afd8860e56d2ec8"), __v: 0 })
Mongoose: rels.insert({ c: '4', _id: ObjectId("595714579afd8860e56d2ec9"), __v: 0 })
Mongoose: tests.insert({ a: '1', _id: ObjectId("595714579afd8860e56d2eca"), b: [ ObjectId("595714579afd8860e56d2ec7"), ObjectId("595714579afd8860e56d2ec8"), ObjectId("595714579afd8860e56d2ec9") ], __v: 0 })
{
"__v": 0,
"a": "1",
"_id": "595714579afd8860e56d2eca",
"b": [
"595714579afd8860e56d2ec7",
"595714579afd8860e56d2ec8",
"595714579afd8860e56d2ec9"
]
}
Mongoose: tests.findAndModify({ a: '1' }, [], { '$pull': { b: ObjectId("595714579afd8860e56d2ec8") } }, { new: true, upsert: false, remove: false, fields: {} })
Mongoose: rels.find({ _id: { '$in': [ ObjectId("595714579afd8860e56d2ec7"), ObjectId("595714579afd8860e56d2ec9") ] } }, { fields: {} })
{
"_id": "595714579afd8860e56d2eca",
"a": "1",
"__v": 0,
"b": [
{
"_id": "595714579afd8860e56d2ec7",
"c": "2",
"__v": 0
},
{
"_id": "595714579afd8860e56d2ec9",
"c": "4",
"__v": 0
}
]
}

Categories

Resources