Building a gateway with nodeJs - javascript

I have to build a layer for an API using nodeJs and create some endpoints for the frontend.
I'm new to nodeJS and I've builded some small servers creating models and making some CRUD with Mongo.
This is the first time I have to make an intermediate layer to an existing API to filter the results.
I did it, and in the expected format, but I'm getting an error from node and I can't figure out how to resolve it.
The original API is kind like this:
{
id:'some id',
pagination: {...},
results: [...],
other_results: [...],
additional_info: [
{
id:'someid',
info:'someinfo',
values:[
{
id: 'someid',
name: 'some category name',
count: 999
},
{...},
{...}
],
},
{...},
{...}
]
}
and I have to "extract" the data from "results" and the first array of "additional_info".
My endpoint has to return data in this format:
{
brand: {name: "Any brand", country: "Germany"},
categories: ["category one", "category two", "category three"],
items: [
0: {id: "olmk23238777", name: "item one", imgUrl: 'img/34341.jpg', price: {total:424, currency: "USD"}, shipping: 'fast'},
1: {id: "olmk23239348", name: "item two", imgUrl: 'img/34764.jpg', price: {total:47, currency: "USD"}, shipping: 'slow'},
…]
}
I could achieved with this:
const axios = require('axios');
exports.products = async query => {
const url = `${process.env.BASE_URL}${query}`;
let productsData = {
brand: {
name: 'Any Brand',
country: 'Germany',
},
};
try {
const result = await axios({
method: 'GET',
url,
});
const data = result.data;
productsData.categories = data.additional_info[0].values.map(
({ categoryName }) => categoryName
);
productsData.items = data.results.map(item => ({
id: item.id,
name: item.name,
imgUrl: item.imgSrc,
price: {
total: parseInt(item.price),
currency: item.currency,
},
shipping: item.shipping_method,
}));
return productsData;
} catch (error) {
console.log(error);
}
};
This is the controller:
const { products } = require('../utils/products');
exports.searchItem = async (req, res) => {
const { search } = req.query;
try {
const response = await products(search);
res.send(response).json();
} catch (error) {
console.log(error);
}
};
and the endpoint look like this:
http://localhost:4000/api/products?search=shirt
This is the error
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
I tried different things but I can't fixe it

First of all, your error has an explanation here:
Error: Can't set headers after they are sent to the client
And for your code:
look this:
res.send(response).json();
it should be one of these:
res.send(response);
// or
res.json(response);
For the parameter type/structure, please refer to documentation of your selected library.

Related

Pushing into Nested array in mongodb

This is my mongoose Schema
const notesSchema = mongoose.Schema({
college: {type:String},
year: {type:String},
sem: {type:String},
branch:{type:String},
subjects: [
{
subject_id:{type:String},
name:{type:String},
codename: {type:String},
notes: [
{
notes_id:{type:String},
title: {type:String},
material: [
{
material_id:{type:String},
heading:{type:String},
link: {type:String}
}
]
}
]
}
]
})
I want to insert a object into 'material' array which is inside notes array and notes is inside subjects array.
I tried many syntax, but none of them worked for me. Recently I tried this.
try {
notes.updateOne({
$and:[ {_id: '62a61949dc0f920ae99fc687'}, {'subjects.notes.notes_id':'221fad-f35c-ee2a-65b3-8531dbfcf732'}]
},
{$push:{'subjects.0.notes.$.material':
[{ material_id: "hfklahfhoabfoab", heading: "Prime", link: "wwo.prime.com" }]
}}
This is full function code:-
router.get('/populate',async (req,res)=>{
// const {data} = req.body
// const link = "www.wiki.com"
try {
notes.updateOne({
_id: '62a61949dc0f920ae99fc687',
'subjects.notes.notes_id': '221fad-f35c-ee2a-65b3-8531dbfcf732',
},
{$push:{'subjects.0.notes.$.material':
[{ material_id: "hfklahfhoabfoab", heading: "Prime", link: "wwo.prime.com" }]
}}
)
console.log("posted")
const alreadyexist = await notes.find({$and:[{"year":'3'},{"sem":'2'}]})
res.send(alreadyexist)
// console.log(updata)
} catch (error) {
console.log(error)
}
} )
This is my current Database status.
enter image description here
Try to change your code like this:
router.get('/populate', async (req, res) => {
try {
await notes.updateOne(
{
_id: '62a61949dc0f920ae99fc687',
'subjects.notes.notes_id': '221fad-f35c-ee2a-65b3-8531dbfcf732',
},
{
$push: {
'subjects.0.notes.$.material': {
material_id: 'hfklahfhoabfoab',
heading: 'Prime',
link: 'wwo.prime.com',
},
},
}
);
console.log('posted');
const alreadyexist = await notes.find({ year: '3', sem: '2' });
res.send(alreadyexist);
} catch (error) {
console.log(error);
}
});

How to display unique items and the amount of occurances from an array of objects in Vuejs/JS?

I'm working on an inventory app with Vuejs for this project, but my problem is specifically a JS related one.
In my data I have the user data which is the logged userData which is taken from a form and then the static categories and locations which you can choose from below that.
I am trying to pull out the unique categories and how many times the appear in a new array of objects that would look like :
[{title: 'Books', amount: 3 }, {title: 'Recods', amount: 1 }, ...]
I believe what I have is a scope issue.
Data
userData: {
items: [
{
itemName: 'test book',
category: 'Books',
location: 'Kitchen',
},
{
itemName: 'test book 2',
category: 'Books',
location: 'Garage',
},
{
itemName: 'test book 3',
category: 'Books',
location: 'Basement',
},
{
itemName: 'test record',
category: 'Records',
location: 'Basement',
},
{
itemName: 'test furniture',
category: 'Furniture',
location: 'Garage',
},
],
categories: ['Books', 'Movies', 'Records', 'Furniture'],
locations: ['Basement', 'Garage', 'Kitchen'],
},
I'm trying to get this to work like I have here with 'Books', but for all categories.
This is what I have displaying with the code below. It reads 'Items: 3' because I have 3 Books in my userData, but I need it to display the amount for each unique category.
I cannot figure out what to place in the code below
filteredItems = items.filter((item) => item.category === 'Books').length
method/function
convertedCategories() {
const items = this.userData.items
const filteredItems = items.filter((item) => item.category === 'Books').length
function getItemCountOfCategory(categoryName) {
return filteredItems
}
function convertCategoriesIntoCards(categories) {
return categories.map((el) => {
return {
title: el,
amount: getItemCountOfCategory(el),
}
})
}
return convertCategoriesIntoCards(this.userData.categories)
},
I apologize if I haven't broken this down clear enough; it's still very hard for me to extract a particular line that pertains to the question out of so much code.
If anything is unclear, please let me know!
Something like this should work.
convertedCategories() {
const items = this.userData.items
function getItemCountOfCategory(categoryName) {
return items.filter((item) => item.category === categoryName).length
}
function convertCategoriesIntoCards(categories) {
return categories.map((el) => {
return {
title: el,
amount: getItemCountOfCategory(el),
}
})
}
return convertCategoriesIntoCards(this.userData.categories)
},

Authentication failed due to invalid authentication credentials or a missing Authorization header. in nodejs paypal sdk

I am trying to implement paypal subscription api in react-nodejs project.i refer https://developer.paypal.com/docs/api/subscriptions/v1/. after that i got id wit "P-*********".
What i have tried is:
in Ui Side(React) i created an event for requesting server side to create the billing plans.
in server side(nodejs) i do billingPlan create and update actions.
The code is:(nodejs)
export const paypalSubscribe = async (user, data) => {
const customerId = user.customer,
{ invoice: invoiceId } = data;
try {
const billingPlanAttributes = {
description: "Create Plan for Regular",
merchant_preferences: {
auto_bill_amount: "yes",
cancel_url: "http://www.cancel.com",
initial_fail_amount_action: "continue",
max_fail_attempts: "1",
return_url: "http://www.success.com",
setup_fee: {
currency: "USD",
value: "25"
}
},
name: "Testing1-Regular1",
payment_definitions: [
{
amount: {
currency: "USD",
value: order.price.recurringAmount
},
charge_models: [
{
amount: {
currency: "USD",
value: "10.60"
},
type: "SHIPPING"
},
{
amount: {
currency: "USD",
value: "20"
},
type: "TAX"
}
],
cycles: "0",
frequency: "MONTH",
frequency_interval: order.billingCycle,
name: "Regular 1",
type: "REGULAR"
}
],
type: "INFINITE"
};
const createdBillingPlan = await new Promise((resolve, reject) => {
Paypal.billingPlan.create(billingPlanAttributes, function (
error,
billingPlan
) {
if (error) {
reject(error);
} else {
resolve(billingPlan);
}
});
});
console.log("data123....", createdBillingPlan);
// update
var billing_plan_update_attributes = [
{
op: "replace",
path: "/",
value: {
state: "ACTIVE"
}
}
];
console.log(
"billing_plan_update_attributes",
billing_plan_update_attributes
);
const updateBillingPlan = await new Promise((resolve, reject) => {
Paypal.billingPlan.update(
createdBillingPlan.id,
billing_plan_update_attributes,
function (error, response) {
if (error) {
reject(error);
} else {
resolve(response);
}
}
);
});
const getBillingPlan = await new Promise((resolve, reject) => {
Paypal.billingPlan.get(createdBillingPlan.id, function (
error,
updatedBillingPlan
) {
if (error) {
console.log("errr", error.response);
reject(error);
} else {
console.log("updatedBillingPlan", JSON.stringify(updatedBillingPlan));
resolve(updatedBillingPlan);
updatedBillingPlan.redire
}
});
});
console.log("getBillingPlan", getBillingPlan);
return { ok: true, data: getBillingPlan };
} catch (error) {
console.log("error", error);
}
};
And i got getBillingPlan is like this:
{ id: 'P-**************',
state: 'ACTIVE',
name: 'Testing1-Regular1',
description: 'Create Plan for Regular',
type: 'INFINITE',
payment_definitions:
[ { id: 'PD-0EF41434TA3045459BCMIRMA',
name: 'Regular 1',
type: 'REGULAR',
frequency: 'Month',
amount: [Object],
cycles: '0',
charge_models: [Array],
frequency_interval: '1' } ],
merchant_preferences:
{ setup_fee: { currency: 'USD', value: '25' },
max_fail_attempts: '1',
return_url: 'http://www.success.com',
cancel_url: 'http://www.cancel.com',
auto_bill_amount: 'YES',
initial_fail_amount_action: 'CONTINUE' },
create_time: '2020-07-01T04:18:01.008Z',
update_time: '2020-07-01T04:18:02.031Z',
links:
[ { href:
'https://api.sandbox.paypal.com/v1/payments/billing-plans/P-***********',
rel: 'self',
method: 'GET' } ],
httpStatusCode: 200
}
And when i trying to open the links in links array
ie,https://api.sandbox.paypal.com/v1/payments/billing-plans/P-***********' i got the error:
"Authentication failed due to invalid authentication credentials or a missing Authorization
header."
Where i went wrong? How can i resolve this and implement subscription of paypal in my Project.
REST Api
i changed my code to rest api calls finaly i got response like this:
{ status: 'APPROVAL_PENDING',
id: 'I-1FU83BNMBCFS',
create_time: '2020-07-06T09:47:02Z',
links:
[ { href:
'https://www.sandbox.paypal.com/webapps/billing/subscriptions? ba_token=BA-3D945638N1691194P',
rel: 'approve',
method: 'GET' },
{ href:
'https://api.sandbox.paypal.com/v1/billing/subscriptions/I- 1FU83BNMBCFS',
rel: 'edit',
method: 'PATCH' },
{ href:
'https://api.sandbox.paypal.com/v1/billing/subscriptions/I-1FU83BNMBCFS',
rel: 'self',
method: 'GET' } ],
responseCode: 201 }
and in my ui side i opened the approval link in new window after submitting it shows 404. Why?
code:
window.open(URL, "_blank");
UPDATE:Subscription:
const subscriptionString = {
plan_id: result.id,
start_time: "2021-11-01T00:00:00Z",
shipping_amount: {
currency_code: "USD",
value: "10.00"
},
subscriber: {
name: {
given_name: "John",
surname: "Doe"
},
email_address: "customer#example.com",
shipping_address: {
name: {
full_name: "John Doe"
},
address: {
address_line_1: "2211 N First Street",
address_line_2: "Building 17",
admin_area_2: "San Jose",
admin_area_1: "CA",
postal_code: "95131",
country_code: "US"
}
}
},
application_context: {
brand_name: "walmart",
locale: "en-US",
shipping_preference: "SET_PROVIDED_ADDRESS",
user_action: "SUBSCRIBE_NOW",
payment_method: {
payer_selected: "PAYPAL",
payee_preferred: "IMMEDIATE_PAYMENT_REQUIRED"
},
return_url: "https://example.com/returnUrl",
cancel_url: "https://example.com/cancelUrl"
}
},
options = {
url: "https://api.sandbox.paypal.com/v1/billing/subscriptions",
method: "POST",
headers: headers,
body: JSON.stringify(subscriptionString)
},
activateResult = await payment.callPayaplApi(options);
return {
ok: true,
data: activateResult
};
}
There is no SDK for a Subscriptions API integration, you need to implement direct REST API calls.
The PayPal-Node-SDK never supported the Subscriptions API, only the previous billing APIs which are not compatible. Also, the PayPal-Node-SDK is no longer maintained.
You particular authentication error is due to something else, but due to the above issues it is not worth troubleshooting.
Start over and integrate correctly with direct REST API calls.

Collections Missing with Fawn Transactions

In index.js, i create my database correctly, and i add a genre collection in the db, and is added fine.
However, when i add my rental collection, it isn't added or viewed in mongodb compass
My code for rental.js:
const mongoose = require('mongoose')
const joi = require('joi')
const rentalSchema = new mongoose.Schema({
customer: {
type: new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 2,
maxlength: 255
},
phone: {
type: String,
required: true,
minlength: 2,
maxlength: 255
},
isGold: {
type: Boolean,
default: false,
required: false
},
}),
movie: {
type: new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
minlength: 2,
maxlength: 500
},
dailyRentalRate: {
type: Number,
min: 2,
required: true,
max: 255
}
}),
},
dateOut: {
type: Date,
required: true,
default: Date.now
},
dateReturned: {
type: Date
},
rentalFee: {
type: Number,
min: 0
}
}
})
const Rental = mongoose.model('Rental', rentalSchema)
function validate(obj) {
const schema = {
customerId: joi.string().required(),
movieId: joi.string().required()
}
return joi.validate(obj, schema)
}
exports.Rental = Rental
exports.validate = validate
My index.js Code (Where i initialise the database):
const mongoose = require('mongoose')
const movies = require('./routes/movies')
const rentals = require('./routes/rentals')
mongoose.connect('mongodb://localhost/vidly', { useNewUrlParser: true})
.then(() => console.log('Connected to mongodb..'))
.catch(() => console.error('Error connecting...'))
This is unusual, as i do the same thing for genre, but it is added and viewed in mongodb compass!
[The image of mongo db compass is here: ]
Here is my rentals.js file, that uses rental.js for models:
const express = require('express')
const router = express.Router()
const {Customer} = require('../models/customer')
const Fawn = require('fawn')
const mongoose = require('mongoose')
const {Movie} = require('../models/movie')
const {Rental, validate} = require('../models/rental')
Fawn.init(mongoose)
router.get('/rentals', async (req, res) => {
const rentals = await Rental.find().sort('-dateOut')
res.send (rentals)
})
router.post('/rentals', async (req, res) => {
const {error} = validate(req.body)
if (error) return res.status(400).send('Error')
// Makes sure the customerId/customer sends us is valid
const customer = await Customer.findById(req.body.customerId)
if (!customer) return res.status(404).send('Invalid customerId')
const movie = await Movie.findById(req.body.movieId)
if (!movie) return res.status(404).send('Invalid movieId')
let rental = new Rental({
customer: {
_id: customer._id,
name: customer.name,
phone: customer.phone
},
movie: {
_id: movie._id,
title: movie.title,
dailyRentalRate: movie.dailyRentalRate
}
})
// This is for our success scenario
try {
// All args in here treated all together as unit
new Fawn.Task()
// First arg is collection we work with, and second is obj we wanna save
.save('rentals', rental)
// Update movies collection Second Arg is movie that should be updated Third is we increment the numInstock prop, and decrement by 1
.update('movies', { _id: movie._id}, {
$inc: { numberInStock: -1}
})
.run()
res.send(rental)
}
catch(ex) {
// 500 means Internal server error
res.status(500).send('Something failed.')
}
})
module.exports = router
Here is mongodb compass, and the collections seen
Using Fawn
The issue is one of usage with the Fawn library and comes from some misconceptions about the naming of mongoose models and how these interact with the library itself. As such the best way to demonstrate is with a minimal example of working code:
const { Schema } = mongoose = require('mongoose');
const Fawn = require('fawn');
const uri = 'mongodb://localhost:27017/fawndemo';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const oneSchema = new Schema({
name: String
});
const twoSchema = new Schema({
counter: Number
});
// don't even need vars since we access model by name
mongoose.model('One', oneSchema);
mongoose.model('Two', twoSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// init fawm
Fawn.init(mongoose);
// Clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
)
// run test
let task = Fawn.Task();
let results = await task
.save('One', { name: 'Bill' })
.save('Two', { counter: 0 })
.update('Two', { }, { "$inc": { "counter": 1 } })
.run({ useMongoose: true });
log(results);
// List objects in models
for ( [k,m] of Object.entries(conn.models) ) {
let result = await m.find();
log(result);
}
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
Note how the mongoose models are registered here:
mongoose.model('One', oneSchema);
mongoose.model('Two', twoSchema);
That first argument is the registered name which mongoose uses for the model in it's internal logic. From the perspective of mongoose itself, once you have registered the model name with the schema as above, you can actually call an instance of the model as follows:
const One = mongoose.model('One');
Typically people export the result of the initial registration and then just use the returned value which is a reference to mongoose's own internal storage of the model details and attached schema. But the line of code is equivalent to that same thing as long as the registration code has already been run.
A typical exports considering this can therefore be used as:
require('./models/one');
require('./models/two');
let results = await mongoose.model('One').find();
So you might not see that often in other code examples, but that is really to show what is actually happening from the perspective of the Fawn library with later code.
With that knowledge you can consider the following code in the listing:
let task = Fawn.Task();
let results = await task
.save('One', { name: 'Bill' })
.save('Two', { counter: 0 })
.update('Two', { }, { "$inc": { "counter": 1 } })
.run({ useMongoose: true });
Here the methods of update() and save() familiar to mongoose and MongoDB users actually have a different first argument specific to their implementation on the Fawn.Task() result. That first argument is the "registered model name" for mongoose, which is what we just explained with the previous example.
What the Fawn library is actually doing is calling similar code to:
mongoose.model('One').save({ name: 'Bill' })
Well actually it's doing something a lot more complicated than that as is evidenced in the output of the example listing. It's actually doing a lot of other things related to two phase commits and writing temporary entries in another collection and eventually moving those over to the target collections. But when it does actually go to the collections for the registered models, then that is basically how it is doing it.
So the core issue in the code in the question is that you are not using the names that were actually registered to the mongoose models, and a few other things are missing from the documentation steps.
You're also not awaiting asynchronous functions correctly, and the try..catch within the question code is not doing anything with calls in this context. The listing here however demonstrates how to do that correctly using async/await.
You can alternately just use the native Promise.then(...).catch(...) aproach if your NodeJS version does not have async/await support, but there really is little other change than doing that and of course removing the try..catch since promises in that form will ignore it. Which is why you catch() instead.
NOTE - With some brief testing there appear to be a number of things which are supported mongoose/mongodb features which are not actually implemented and supported on this library's methods. Notably "upserts" was a prime example of a useful and common thing which the "two phase commit" system implemented here does not appear to support at all.
This partly seems an oversight in the code of the library where certain "options" to the methods are actually being ignored or stripped completely. This is a concern for getting the most out of MongoDB features.
Transactions
The whole usage of this library though at least seems suspicious to me that you picked it up because you "thought" this was "Transactions". Put plainly the two phase commit is NOT a transaction. Furthermore the implementation of any attempt at such control and rollback etc seem very loose at best.
If you have a modern MongoDB 4.0 server or above, and where you actually configured it to be named as a "replica set" ( which you can also do for a single member, where a common misconception is you need more than one ) then there is support for real transactions, and they are very easy to implement:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/trandemo';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const orderSchema = new Schema({
name: String
});
const orderItemsSchema = new Schema({
order: { type: Schema.Types.ObjectId, ref: 'Order' },
itemName: String,
price: Number
});
const Order = mongoose.model('Order', orderSchema);
const OrderItems = mongoose.model('OrderItems', orderItemsSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
)
let session = await conn.startSession();
session.startTransaction();
// Collections must exist in transactions
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.createCollection())
);
let [order] = await Order.create([{ name: 'Bill' }], { session });
let items = await OrderItems.insertMany(
[
{ order: order._id, itemName: 'Cheese', price: 1 },
{ order: order._id, itemName: 'Bread', price: 2 },
{ order: order._id, itemName: 'Milk', price: 3 }
],
{ session }
);
// update an item
let result1 = await OrderItems.updateOne(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ session }
);
log(result1);
// commit
await session.commitTransaction();
// start another
session.startTransaction();
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
/*
* $lookup join - expect Milk to be price: 4
*
*/
let joined = await Order.aggregate([
{ '$match': { _id: order._id } },
{ '$lookup': {
'from': OrderItems.collection.name,
'foreignField': 'order',
'localField': '_id',
'as': 'orderitems'
}}
]);
log(joined);
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
That is really just a simple listing with the class Order and related OrderItems. There really is nothing special in the code and you should see that it's basically the same as most listing examples you will see with a few small changes.
Notably we initialize a session and also session.startTransaction() as an indicator that a transaction should be in progress. Note that session would generally have a wider scope where you would typically re-use that object for more than just a few operations.
Now you have session and the transaction is started, this is simply added to the "options" of the various statements being executed:
let [order] = await Order.create([{ name: 'Bill' }], { session });
let items = await OrderItems.insertMany(
[
{ order: order._id, itemName: 'Cheese', price: 1 },
{ order: order._id, itemName: 'Bread', price: 2 },
{ order: order._id, itemName: 'Milk', price: 3 }
],
{ session }
);
Admittedly this is a brief example that does not fully cover all write error possibilities and how to handle that within separate try..catch blocks. But as a very basic example should any error occur before the session.commitTransaction() is called, then none of the operations since the transaction was started will actually be persisted within the session.
Also there is "causal consistency" in that once a normal write acknowledgement has been confirmed, then within the scope of the session the data appears written to the respective collections right up until the transaction commit or rollback.
In the event of a rollback ( as demonstrated in the final operation ):
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
These writes though reported to be made as seen in the operation result, are indeed "rolled back" and further operations see the state of the data before these changes were made.
The full example code demonstrates this by adding the items with another update action in one transaction, then beginning another to alter data and read it then abort the transaction. The final data state shows of course only what was actually committed.
NOTE Operations like find() and findOne() or anything that retrieves data must include the session whilst a transaction is active in order to see the current state, just in the same way that write operations are doing as shown in the listing.
Without including the session, these changes in state are not visible in the "global" scope until the transaction is resolved.
Listing Outputs
Code listings given produce the following output when run, for reference.
fawndemo
Mongoose: ones.deleteMany({}, {})
Mongoose: twos.deleteMany({}, {})
Mongoose: ojlinttaskcollections.deleteMany({}, {})
Mongoose: ojlinttaskcollections.insertOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a"), steps: [ { dataStore: [], _id: ObjectId("5bf765f7e5c71c5fae77030d"), index: 0, type: 'save', state: 0, name: 'One', data: { name: 'Bill' } }, { dataStore: [], _id: ObjectId("5bf765f7e5c71c5fae77030c"), index: 1, type: 'save', state: 0, name: 'Two', data: { counter: 0 } }, { dataStore: [], _id: ObjectId("5bf765f7e5c71c5fae77030b"), index: 2, type: 'update', state: 0, name: 'Two', data: { '*_**ojlint**escape$*__tx__00***___string$inc': { counter: 1 } } } ], __v: 0 })
Mongoose: ojlinttaskcollections.updateOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a") }, { '$set': { 'steps.0.state': 1 } })
Mongoose: ones.insertOne({ _id: ObjectId("5bf765f7e5c71c5fae77030e"), name: 'Bill', __v: 0 })
Mongoose: ojlinttaskcollections.updateOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a") }, { '$set': { 'steps.0.state': 2 } })
Mongoose: ojlinttaskcollections.updateOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a") }, { '$set': { 'steps.1.state': 1 } })
Mongoose: twos.insertOne({ _id: ObjectId("5bf765f7e5c71c5fae77030f"), counter: 0, __v: 0 })
Mongoose: ojlinttaskcollections.updateOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a") }, { '$set': { 'steps.1.state': 2 } })
Mongoose: twos.find({})
Mongoose: ojlinttaskcollections.updateOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a") }, { '$set': { 'steps.2.state': 1 } })
Mongoose: twos.update({}, { '$inc': { counter: 1 } }, {})
(node:24494) DeprecationWarning: collection.update is deprecated. Use updateOne, updateMany, or bulkWrite instead.
Mongoose: ojlinttaskcollections.updateOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a") }, { '$set': { 'steps.2.state': 2 } })
Mongoose: ojlinttaskcollections.deleteOne({ _id: ObjectId("5bf765f7e5c71c5fae77030a") })
[
{
"_id": "5bf765f7e5c71c5fae77030e",
"name": "Bill",
"__v": 0
},
{
"_id": "5bf765f7e5c71c5fae77030f",
"counter": 0,
"__v": 0
},
{
"n": 1,
"nModified": 1,
"opTime": {
"ts": "6626877488230301707",
"t": 139
},
"electionId": "7fffffff000000000000008b",
"ok": 1,
"operationTime": "6626877488230301707",
"$clusterTime": {
"clusterTime": "6626877488230301707",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
]
Mongoose: ones.find({}, { projection: {} })
[
{
"_id": "5bf765f7e5c71c5fae77030e",
"name": "Bill",
"__v": 0
}
]
Mongoose: twos.find({}, { projection: {} })
[
{
"_id": "5bf765f7e5c71c5fae77030f",
"counter": 1,
"__v": 0
}
]
Mongoose: ojlinttaskcollections.find({}, { projection: {} })
[]
transdemo
Mongoose: orders.deleteMany({}, {})
Mongoose: orderitems.deleteMany({}, {})
Mongoose: orders.insertOne({ _id: ObjectId("5bf7661c3f60105fe48d076e"), name: 'Bill', __v: 0 }, { session: ClientSession("e146c6074bb046faa7b70ed787e1a334") })
Mongoose: orderitems.insertMany([ { _id: 5bf7661c3f60105fe48d076f, order: 5bf7661c3f60105fe48d076e, itemName: 'Cheese', price: 1, __v: 0 }, { _id: 5bf7661c3f60105fe48d0770, order: 5bf7661c3f60105fe48d076e, itemName: 'Bread', price: 2, __v: 0 }, { _id: 5bf7661c3f60105fe48d0771, order: 5bf7661c3f60105fe48d076e, itemName: 'Milk', price: 3, __v: 0 } ], { session: ClientSession("e146c6074bb046faa7b70ed787e1a334") })
Mongoose: orderitems.updateOne({ order: ObjectId("5bf7661c3f60105fe48d076e"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("e146c6074bb046faa7b70ed787e1a334") })
{
"n": 1,
"nModified": 1,
"opTime": {
"ts": "6626877647144091652",
"t": 139
},
"electionId": "7fffffff000000000000008b",
"ok": 1,
"operationTime": "6626877647144091652",
"$clusterTime": {
"clusterTime": "6626877647144091652",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
Mongoose: orderitems.findOneAndUpdate({ order: ObjectId("5bf7661c3f60105fe48d076e"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("e146c6074bb046faa7b70ed787e1a334"), upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf7661c3f60105fe48d0771",
"order": "5bf7661c3f60105fe48d076e",
"itemName": "Milk",
"price": 5,
"__v": 0
}
Mongoose: orders.aggregate([ { '$match': { _id: 5bf7661c3f60105fe48d076e } }, { '$lookup': { from: 'orderitems', foreignField: 'order', localField: '_id', as: 'orderitems' } } ], {})
[
{
"_id": "5bf7661c3f60105fe48d076e",
"name": "Bill",
"__v": 0,
"orderitems": [
{
"_id": "5bf7661c3f60105fe48d076f",
"order": "5bf7661c3f60105fe48d076e",
"itemName": "Cheese",
"price": 1,
"__v": 0
},
{
"_id": "5bf7661c3f60105fe48d0770",
"order": "5bf7661c3f60105fe48d076e",
"itemName": "Bread",
"price": 2,
"__v": 0
},
{
"_id": "5bf7661c3f60105fe48d0771",
"order": "5bf7661c3f60105fe48d076e",
"itemName": "Milk",
"price": 4,
"__v": 0
}
]
}
]
const { Rentals, validateRentals } = require("../models/rentals");
const { Movie } = require("../models/movie");
const { Customer } = require("../models/customer");
const Fawn = require("fawn");
const express = require("express");
const router = express.Router();
Fawn.init("mongodb://127.0.0.1:27017/vidly");
router.get("/", async (req, res) => {
const rentals = await Rentals.find().sort("-dateOut");
res.send(rentals);
});
router.get("/:id", async (req, res) => {
const rentals = await Rentals.findById(req.params.id);
if (!rentals)
return res.status(404).send("The rental with the given ID was not found.");
res.send(rentals);
});
router.delete("/:id", async (req, res) => {
const rentals = await Rentals.findByIdAndRemove(req.params.id);
if (!rentals)
return res.status(404).send("The rental with the given ID was not found.");
res.send(rentals);
});
router.post("/", async (req, res) => {
const { error } = validateRentals(req.body);
if (error) return res.status(400).send(error.detais[0].message);
const movie = await Movie.findById(req.body.movieId);
if (!movie)
return res.status(404).send("The rental with the given ID was not found.");
const customer = await Customer.findById(req.body.customerId);
if (!customer)
return res.status(404).send("The rental with the given ID was not found.");
if (movie.numberInStock === 0)
return res.status(400).send("Movie not in stock");
let rentals = new Rentals({
customer: {
_id: customer._id,
name: customer.name,
isGold: customer.isGold,
phone: customer.phone,
},
movie: {
_id: movie._id,
title: movie.title,
dailyRentalRate: movie.dailyRentalRate,
},
});
try {
new Fawn.Task()
.save("rentals", rentals)
.update("movies", { _id: movie._id }, { $inc: { numberInStock: -1 } })
.run();
res.send(rentals);
} catch (ex) {
res.status(500).send("Something failed");
}
// rentals = await rentals.save();
// movie.numberInStock--;
// movie.save();
// res.send(rentals);
//implementing transaction
});
router.put("/:id", async (req, res) => {
const { error } = validateRentals(req.body);
if (error) return res.status(400).send(error.detais[0].message);
const movie = await Movie.findById(req.body.movieId);
if (!movie)
return res.status(404).send("The rental with the given ID was not found.");
const customer = await Customer.findById(req.body.customerId);
if (!customer)
return res.status(404).send("The rental with the given ID was not found.");
let rentals = await Rentals.findByIdAndUpdate(
req.params.id,
{
customer: {
_id: customer._id,
name: customer.name,
isGold: customer.isGold,
phone: customer.phone,
},
movie: {
_id: movie._id,
title: movie.title,
dailyRentalRate: movie.dailyRentalRate,
},
},
{ new: true }
);
if (!rentals)
return res.status(404).send("The rentals with the given ID was not found.");
res.send(rentals);
});
module.exports = router;
Instead of using
Fawn.init(mongoose)
try using
Fawn.init('mongodb://localhost/yourDataBaseName')

How to make a GrahpQL Mutation with nested schema

I have been writing an API that uses GraphQL. I am still pretty new to it, and have been running into some problems regarding mutations. A simplistic form of my API has two record types. There is a contact record and a tag record. A contact record can have multiple tag records associated with it.
The schema I wrote for each of these record types are below:
const Tag = new graphQL.GraphQLObjectType({
name: 'Tag',
description: 'Categorizes records into meaningful groups',
fields: () => ({
_id: {
type: graphQL.GraphQLID
},
name: {
type: graphQL.GraphQLString
}
})
});
const Contact = new graphQL.GraphQLObjectType({
name: 'Contact',
description: 'Contact record',
fields: () => ({
_id: {
type: graphQL.GraphQLID
},
name: {
type: graphQL.GraphQLString
},
tags: {
type: new graphQL.GraphQLList(Tag),
resolve: function(src, args, context) {
return TagModel.findByContactId(src._id)
.then(tags => {
return Promise.map(tags, (tag) => {
return TagModel.findById(tag.tag_id);
});
});
}
}
})
});
I can make a mutation easy enough on records such as tags since they don't contain nested records of their own, but I'm not sure how to make a mutation on a record like contacts since it can contain tags as well. The mutation code I put in place looks like this:
const Mutation = new graphQL.GraphQLObjectType({
name: 'Mutation',
fields: {
createContact: {
type: Contact,
description: "Create Contact",
args: {
name: {type: new graphQL.GraphQLNonNull(graphQL.GraphQLString)},
tags: {type: new graphQL.GraphQLList(Tag)}
},
resolve: function(source, args) {
return ContactModel.save(args.name);
}
}
}
});
I'm not sure how to complete the resolver in the mutation in order to be able to save a contact and tag records at the same time. For instance, if I made a mutation query to save a new contact record with a new tag like this:
{"query": "mutation createNewContact {
contact: createContact (name: "John Smith", tags { name: "family" } )
{_id, text, tags { name } } }" }
Is there something special that I need to do in my mutation schema in order to allow for this type of mutation to happen?
You can't use Tag as an input object type, you would have to create a type like TagInput
const TagInput = new GraphQLInputObjectType({
name: 'TagInput',
fields: {
_id: { type: GraphQLID },
name: { type: GraphQLString }
}
});
It is recommended to always create Input version of your normal type. You could do the same with Contact by creating ContactInput. Then you could create a mutation in very similar way you did it
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createContact: {
type: Contact,
args: {
contact: { type: new GraphQLNonNull(ContactInput) },
tags: { type: new GraphQLList(TagInput) }
},
resolve: (root, args, context) => {
console.log(args);
// this would console something like
// { contact: { name: 'contact name' },
// tags: [ { name: 'tag#1' }, { name: 'tag#2' } ] }
// here create contact with tags
}
}
});
The query you would run would look like that
{
"operationName": "createContact",
"query": "mutation createContact($contact: ContactInput!, $tags: [TagInput])
{
createContact(contact: $contact, tags: $tags) {
_id
text
tags {
name
}
}
}",
"variables": {
contact: { name: "contact name" },
tags: [ { name: "tag#1" }, { name: "tag#2" } ]
}
}

Categories