What is wrong with my findOne() implementation? - mongodb

I have followed all basic tutorials in search to fix this. My implementation of collection.findOne({_id: ctx.params.id}) is just not working.
// import statements for version transparancy, they are not really used here
import { MongoClient } from "https://deno.land/x/mongo#v0.29.2/mod.ts";
import { Application, Router } from "https://deno.land/x/oak#v10.4.0/mod.ts";
// ...
const recipes = db.collection<Recipe>("recipes");
// ...
router.get("/rec/:id", async (ctx) => {
const id = ctx.params.id;
const recipe = await recipes.findOne({ _id: { $oid: id } });
// const recipe = await recipes.findOne({_id: id});
if (recipe) {
ctx.response.status = 200;
ctx.response.body = recipe;
} else {
ctx.response.status = 404;
ctx.response.body = { message: "No recipe found" };
}
});
Is there a current bug that I am not aware of?
Once I call that endpoint with http://localhost:5000/rec/622b6be81089abbc4b4144a1 (and yes, that ID is in my database), I get an internal server error (500) with the following message:
[uncaught application error]: Error - MongoError: {"ok":0,"errmsg":"unknown operator: $oid","code":2,"codeName":"BadValue"}
request: {
url: "http://localhost:5000/rec/622b6be81089abbc4b4144a1",
method: "GET",
hasBody: false
}
response: { status: 404, type: undefined, hasBody: false, writable: true }
at WireProtocol.commandSingle (https://deno.land/x/mongo#v0.29.2/src/protocol/protocol.ts:44:13)
at async FindCursor.executor (https://deno.land/x/mongo#v0.29.2/src/collection/commands/find.ts:17:24)
at async FindCursor.execute (https://deno.land/x/mongo#v0.29.2/src/protocol/cursor.ts:34:21)
at async FindCursor.next (https://deno.land/x/mongo#v0.29.2/src/protocol/cursor.ts:48:7)
at async file:///home/andy/dev/deno/denodb/routes.ts:42:20
at async dispatch (https://deno.land/x/oak#v10.4.0/middleware.ts:41:7)
at async dispatch (https://deno.land/x/oak#v10.4.0/middleware.ts:41:7)
at async dispatch (https://deno.land/x/oak#v10.4.0/middleware.ts:41:7)
at async Application.#handleRequest (https://deno.land/x/oak#v10.4.0/application.ts:376:9)
Downgrading the version did not help either.

Related

MongoDB transaction with #NestJs/mongoose not working

I really need your help. My MongoDB transaction with #NestJs/mongoose not working...When My stripe payment fails rollback is not working... Still, my order collection saved the data...How can I fix this issue..?
async create(orderData: CreateOrderServiceDto): Promise<any> {
const session = await this.connection.startSession();
session.startTransaction();
try {
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save();
await this.stripeService.charge(
orderData.amount,
orderData.paymentMethodId,
orderData.stripeCustomerId,
);
await session.commitTransaction();
return order;
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
I had the same issue and i found that on github: Mongo DB Transactions With Mongoose & Nestjs
So I think, according this issue, you have to call the create method of your model, like that:
const order = await this.orderModel.create(orderData, { session });
as you can see, the Model.create method has an overload with SaveOptions as parameter:
create(docs: (AnyKeys<T> | AnyObject)[], options?: SaveOptions): Promise<HydratedDocument<T, TMethodsAndOverrides, TVirtuals>[]>;
it takes an optional SaveOptions parameter that can contain the session:
interface SaveOptions {
checkKeys?: boolean;
j?: boolean;
safe?: boolean | WriteConcern;
session?: ClientSession | null;
timestamps?: boolean;
validateBeforeSave?: boolean;
validateModifiedOnly?: boolean;
w?: number | string;
wtimeout?: number;
}
Please note that Model.save() can also take a SaveOptions parameter.
So you can also do as you did like that:
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save({ session });
A little further...
As i do many things that require a transaction, I came up with this helper to avoid many code duplication:
import { InternalServerErrorException } from "#nestjs/common"
import { Connection, ClientSession } from "mongoose"
export const mongooseTransactionHandler = async <T = any>(
method: (session: ClientSession) => Promise<T>,
onError: (error: any) => any,
connection: Connection, session?: ClientSession
): Promise<T> => {
const isSessionFurnished = session === undefined ? false : true
if (isSessionFurnished === false) {
session = await connection.startSession()
session.startTransaction()
}
let error
let result: T
try {
result = await method(session)
if (isSessionFurnished === false) {
await session.commitTransaction()
}
} catch (err) {
error = err
if (isSessionFurnished === false) {
await session.abortTransaction()
}
} finally {
if (isSessionFurnished === false) {
await session.endSession()
}
if (error) {
onError(error)
}
return result
}
}
Details
the optional parameter session is in case you are doing nested nested transaction.
that's why i check if the session is provided. If it is, it means we are in a nested transaction. So we'll let the main transaction commit, abort and end the session.
Example
for example: you delete a User model, and then the user's avatar which is a File model.
/** UserService **/
async deleteById(id: string): Promise<void> {
const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
const user = await this.userModel.findOneAndDelete(id, { session })
await this.fileService.deleteById(user.avatar._id.toString(), session)
}
const onError = (error: any) => {
throw error
}
await mongooseTransactionHandler<void>(
transactionHandlerMethod,
onError,
this.connection
)
}
/** FileService **/
async deleteById(id: string, session?: ClientSession): Promise<void> {
const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
await this.fileModel.findOneAndRemove(id, { session })
}
const onError = (error: any) => {
throw error
}
await mongooseTransactionHandler<void>(
transactionHandlerMethod,
onError,
this.connection,
session
)
}
So, in short:
You can use it like this:
async create(orderData: CreateOrderServiceDto): Promise<any> {
const transactionHandlerMethod = async (session: ClientSession): Promise<Order> => {
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save({ session });
await this.stripeService.charge(
orderData.amount,
orderData.paymentMethodId,
orderData.stripeCustomerId,
);
return order
}
const onError = (error: any): void => {
throw error
}
const order = await mongooseTransactionHandler<Order>(
transactionHandlerMethod,
onError,
this.connection
)
return order
}
Hope it'll help.
EDIT
Do not abuse of the model.save({ session }) of the same model in nested transcations.
For some reasons it will throw an error the model is updated too many times.
To avoid that, prefer using model embeded methods that update and return a new instance of your model (model.findOneAndUpdate for example).

How to access mongodb via the plugin fastify-mongodb

My plugin looks like
import fp from 'fastify-plugin';
import mongodb from 'fastify-mongodb';
export default fp(async (fastify) => {
fastify.register(mongodb, {
url: 'mongodb+srv://dbuser:password#cluster0.otigz.mongodb.net/myapp?retryWrites=true&w=majority',
});
});
and my handler looks like
const postJoinHandler = async (
request: any,
reply: any
): Promise<{ id: string; name: string }> => {
try {
const { username, password } = request.body;
const test = await reply.mongo.db.users.insertOne({
username,
password,
});
console.log(test);
return reply.code(201).send(username);
} catch (error) {
request.log.error(error);
return reply.send(400);
}
};
Expected it to insert the username and password into the collection named users, but it didn't? and the error is Cannot read property 'db' of undefined
I also tried
reply.mongodb.users.insertOne({...
and
const test = await request.mongodb.collection('users');
test.insertOne({
username,
password,
});
console.log(test);
and
const test = await this.mongo.db.collection('users'); //<= Object is possibly 'undefined'
Routes look like
import { FastifyPluginAsync } from 'fastify';
import { postJoinSchema, postLoginSchema } from '../schemas/auth';
const auth: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post('/auth/join', postJoinSchema);
fastify.post('/auth/login', postLoginSchema);
};
export default auth;
The mongo decorator is attached to the fastify instance, not to the request nor reply object.
You should move your handlers into the routes file and read for fastify.mongo or use a named function as the handler.
In the latter case, the handler has this bounded to the fastify instance.
async function postJoinHandler (
request,
reply
) {
try {
const { username, password } = request.body;
const test = await this.mongo.db.users.insertOne({
username,
password,
});
console.log(test);
reply.code(201)
return username
} catch (error) {
request.log.error(error);
reply.code(400);
return {}
}
};

Jest mock mongoose.startSession() throws error

i'm implemented transaction in the post method. it was work fine. But now I have to update unit test case for that method. I tried to mock startSession() and startTransaction() to check toHaveBeenCalled.But while running test case i got like MongooseError: Connection 0 was disconnected when calling startSession``. I am new to that so i don't know how to mock that?.
Method:
static post = (funcCall: Promise<Document>) => async (response: Response, fields?: string[]) => {
const session = await startSession();
session.startTransaction();
try {
const dbResponse = await funcCall; // model.save(request.body)
// commit the changes if everything was successful
await session.commitTransaction();
success(pick(dbResponse, fields ? fields : ['_id']), 201)(response);
} catch (error) {
// this will rollback any changes made in the database
await session.abortTransaction();
throwError(error);
} finally {
// ending the session
session.endSession();
}
};
My Test case:
it('should perform post when valid parameters is passed.', async () => {
// Preparing
const mockSaveReturn = {
_id: objectID,
test_name: 'sample',
};
jest.spyOn(mongoose, 'startSession')
const spySave = jest.spyOn(modelPrototype.prototype, 'save').mockReturnValueOnce(mockSaveReturn);
const document = new modelPrototype(mockSaveReturn);
// Executing
await post(document.save())(response as Response);
expect(response.send).toHaveBeenCalledWith({ _id: '54759eb3c090d83494e2d804' });
expect(spySave).toHaveBeenCalled();
// Cleaning
spySave.mockClear();
});

Best practice for using React hooks and Context API to update global state and fetch/provide data from multiple endpoints

I am new to React hooks/Context API. I have read the React hook/context docs, and I am still having trouble with the following:
My attempts to update global state by multiple consumer components
currently causes frequent overwriting of context state due to
rerendering (e.g., activity or details state is sometimes
null/undefined). This probably is why...
... I am getting 400 (bad request) and/or 500 (server) errors on random refreshes of the page (~30% of the time content loads as
expected, ~70% errors are thrown. I believe this is happening
because we have various context states that are being called
asynchronously).
I am not sure how to implement Axios Cancellation, given that our useEffect hooks are calling dispatch functions (e.g.,
getActivities()) in different files. The examples I've seen
involve fetching data within the component (rather than in context).
I am seeking assistance for #1 specifically. I would love guidance on how to accurately fetch data and store in context as global state, and then provide that context to child components, allowing them to consume/update context state without unnecessary rerendering.
Tried to only provide relevant code snippets below:
ActivityState.js -- should fetch activity data
...
const ActivityState = props => {
const initialState = {
activities: [],
isLoading: false,
isError: false
};
const HEADERS = {
'Content-Type': 'application/json',
'user_id': 1
}
const [state, dispatch] = useReducer(ActivityReducer, initialState);
const userContext = useContext(UserContext);
const getActivities = async () => {
const { loggedIn } = contactContext;
let didCancel = false; // attempts to start implementing axios cancellation
try {
const res = await axios.get(url);
dispatch({ type: GET_ACTIVITIES, payload: res.data.data.activities });
} catch (err) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
}
const updateActivity = (path, data) => { //update context state
dispatch({ type: UPDATE_ACTIVITY, payload: { path: path, data: data } });
};
const saveActivity = () => { //send new activity data to the backend
const postData = {
actions: [{"293939": []}],
activities: state.activities
};
try {
const res = axios.post(url,{ data: postData }, { headers: HEADERS });
} catch (err) {
console.log(err);
}
}
return (
<ActivityContext.Provider
value={{
activities: state.activities,
data: state.data,
backup_data: state.backup_data,
getActivities,
updateActivity,
saveActivity,
}}
>
{props.children}
</ActivityContext.Provider>
);
};
export default ActivityState;
ActivityReducer.js -- switch statements to be dispatched by ActivityState.js
...
export default (state, action) => {
switch (action.type) {
case GET_ACTIVITIES:
return {
...state,
activities: action.payload,
isLoading: true
};
case FETCH_FAILURE:
return {
...state,
isLoading: false,
isError: true
};
case UPDATE_ACTIVITY:
const { payload: { path }, payload } = action;
const data = state;
if (!data.activities)
return { data };
const index = data.activities.findIndex(e => e.socium_tracking_number == path.id);
if(index === -1)
return { data };
_.set(data, `activities[${index}].${path.field}`, payload.data);
return {
data,
};
...
DetailsState.js -- dispatch functions to fetch details
const DetailsState = props => {
const initialState = {
details: null,
};
const [state, dispatch] = useReducer(DetailsReducer, initialState);
const getDetails = async () => {
try {
const res = await axios.get(url);
dispatch({ type: GET_DETAILS, payload: res.data.data[0].details});
}catch(err) {
console.log(err)
}
};
return (
<DetailsContext.Provider
value={{ details: state.details, getDetails }}
>
{ props.children }
</DetailsContext.Provider>
);
}
export default SchemaState;
DetailsReducer.js -- switch statement
export default (state, action) => {
switch (action.type) {
case GET_DETAILS:
return {
...state,
details: action.payload,
};
default:
return state;
}
};
ActivityTable.js -- component that consumes Activity Info
...
const ActivityTable = ({ activity }) => {
const activityContext = useContext(ActivityContext);
const { activities, filtered, getActivities } = activityContext;
const [order, setOrder] = React.useState('asc');
const [orderBy, setOrderBy] = React.useState(activities.wait_time);
// Get activity data on mount
useEffect(() => {
async function fetchData() {
await getActivities()
}
fetchData();
}, []);
...
CreateActivity.js -- component that consumes Activity and Details data
...
const CreateActivity = props => {
const activityContext = useContext(ActivityContext);
const { activities, filtered, getActivities, addActivity } = activityContext;
const detailsContext = useContext(DetailsContext);
const { details, getDetails } = detailsContext;
// Get activity and details data on mount
useEffect(() => {
async function fetchData() {
await getActivities();
await getSchema();
}
fetchData();
}, []);
...
I really tried to get smarter on these issues before approaching the SO community, so that my question(s) was more defined. But this is what I have. Happy to provide any info that I missed or clarify confusion. Thank you for your time

hapi lab AssertionError [ERR_ASSERTION]: Plugin crumb already registered

i'm not sure why i am receiving this. I am trying to create a simple test while using #hapi/crumb. i am only registering it once in my server.js.
const Path = require("path");
const hapi = require("hapi");
const inert = require("inert");
const vision = require("vision");
const Ejs = require("ejs");
const Crumb = require("#hapi/crumb");
const Blankie = require("blankie");
const Scooter = require("#hapi/scooter");
const routes = require("./routes");
// Configure the server
const server = hapi.Server({
host: "0.0.0.0",
port: process.env.PORT || 3000,
routes: {
files: {
relativeTo: Path.join(__dirname, "..", "public")
},
state: {
parse: true,
failAction: "ignore"
},
security: {
xframe: true,
noOpen: false
},
cors: {
origin: ["banglarelief.org"],
headers: ["Authorization"], // an array of strings - 'Access-Control-Allow-Headers'
exposedHeaders: ["Accept"], // an array of exposed headers - 'Access-Control-Expose-Headers',
additionalExposedHeaders: ["Accept"], // an array of additional exposed headers
maxAge: 60,
credentials: true // boolean - 'Access-Control-Allow-Credentials'
}
}
});
const plugins = async () => {
const pluginsToRegister = [
inert,
vision,
require("hapi-mobile-views"),
{ plugin: Crumb, options: { cookieOptions: { isSecure: false } } },
Scooter,
{
plugin: Blankie,
options: {} // specify options here
}
];
await server.register(pluginsToRegister);
};
const init = async () => {
await plugins();
server.state("player", {
ttl: null,
clearInvalid: true,
isSecure: false
});
server.views({
engines: { ejs: Ejs },
path: `${__dirname}/views`,
layout: "layout"
});
await server.route(routes);
return server;
};
const start = async () => {
try {
await init();
await server.start();
} catch (err) {
console.log(err);
process.exit(1);
}
};
module.exports = { init, start };
My test file is very basic and i have tried to move around where the start should be called but it keep throwing same error.
'use strict';
const Lab = require('#hapi/lab');
const { expect } = require('#hapi/code');
const { afterEach, beforeEach, describe, it } = exports.lab = Lab.script();
const { init, start } = require('../src/server');
let server = start();
describe('GET /', () => {
//let server;
//server = start();
beforeEach(async () => {
//server = start();
});
afterEach(async () => {
//await server.stop();
});
it('responds with 200', async () => {
const res = await server.inject({
method: 'get',
url: '/'
});
expect(res.statusCode).to.equal(200);
});
});
I have been following https://hapijs.com/tutorials/testing?lang=en_US
The solution seems to work if you break up your plugins function into two parts. One part will init 3rd party plugins like #Hapi/*. The other function will init your 1st party plugins that you wrote. You will only init the 3rd party plugins in your start function.
It's critical that you include { once: true } because that will prevent your error. It will only initialize the plugin once, which will prevent your error. You cannot always specify { once: true } on 3rd party plugins. Thus, we have to handle that a different way. Since we moved all the 3rd party plugins to their own function, which is invoked on start, that should prevent 3rd party plugins from causing an issue of being reinitialized.
const hapiPlugins = async () => {
const pluginsToRegister = [
inert,
vision,
require("hapi-mobile-views"),
{ plugin: Crumb, options: { cookieOptions: { isSecure: false } } },
Scooter,
{
plugin: Blankie,
options: {} // specify options here
}
];
};
const myPlugins = async () => {
await server.register([
allOfMyPlugins...
],
{
once: true //critical so that you don't re-init your plugins
});
};
const init = async () => {
server.state("player", {
ttl: null,
clearInvalid: true,
isSecure: false
});
server.views({
engines: { ejs: Ejs },
path: `${__dirname}/views`,
layout: "layout"
});
await server.route(routes);
return server;
};
const start = async () => {
try {
await hapiPlugins();
await init();
await server.start();
} catch (err) {
console.log(err);
process.exit(1);
}
};
Then, you should be able to call init in your test's before function. Use that server object to inject.