Actually I'm trying to cancel a hook to avoid duplicate pair entity-name/subname - by a server-side check.
My example is, if an entity already exists with the same name and subname, I'd like it not to be created/persisted.
Here's my code so far in my entity.js:
module.exports = function (ContactType) {
ContactType.observe('before save', function filterSameEntities(ctx, next) {
if (ctx.instance) {
ContactType.find({where: {name: ctx.instance.name, subname: crx.instance.subname}}, function (err, ct) {
if (ct.length > 0) {
//I'd like to exit and not create/persist the entity.
next(new Error("There's already an entity with this name and subname"));
}
});
}
next();
});
};
Actually the error is correctly displayed, but the entity is still created and I would like that it wouldn't be the case.
Your last next(); statement is always called, hence the save-action always happens.
You can end further execution using return.
Keep in mind that .find() is async, so just adding return inside the callback would still cause that last next(); statement to run.
Please try this:
module.exports = function (ContactType) {
ContactType.observe('before save', function filterSameEntities(ctx, next) {
if (!ctx.instance) {
return next();
}
ContactType.find({where: {name: ctx.instance.name, subname: ctx.instance.subname}}, function (err, ct) {
if (err) { // something went wrong with our find
return next(err);
}
if (ct.length > 0) {
//I'd like to exit and not create/persist the entity.
return next(new Error("There's already an entity with this name and subname"));
}
return next();
});
});
};
Related
I'm using apollo link in schema stitching as an access control layer. I'm not quite sure how to make the link return error response if a user does not have permissions to access a particular operation. I know about such packages as graphql-shield and graphql-middleware but I'm curious whether it's possible to achieve basic access control using apollo link.
Here's what my link looks like:
const link = setContext((request, previousContext) => merge({
headers: {
...headers,
context: `${JSON.stringify(previousContext.graphqlContext ? _.omit(previousContext.graphqlContext, ['logger', 'models']) : {})}`,
},
})).concat(middlewareLink).concat(new HttpLink({ uri, fetch }));
The middlewareLink has checkPermissions that returns true of false depending on user's role
const middlewareLink = new ApolloLink((operation, forward) => {
const { operationName } = operation;
if (operationName !== 'IntrospectionQuery') {
const { variables } = operation;
const context = operation.getContext().graphqlContext;
const hasAccess = checkPermissions({ operationName, context, variables });
if (!hasAccess) {
// ...
}
}
return forward(operation);
});
What should I do if hasAccess is false. I guess I don't need to forward the operation as at this point it's clear that a user does not have access to it
UPDATE
I guess what I need to do is to extend the ApolloLink class, but so far I didn't manage to return error
Don't know if anyone else needs this, but I was trying to get a NetworkError specifically in the onError callback using Typescript and React. Finally got this working:
const testLink = new ApolloLink((operation, forward) => {
let fetchResult: FetchResult = {
errors: [] // put GraphQL errors here
}
let linkResult = Observable.of(fetchResult).map(_ => {
throw new Error('This is a network error in ApolloClient'); // throw Network errors here
});
return linkResult;
});
Return GraphQL errors in the observable FetchResult response, while throwing an error in the observable callback will produce a NetworkError
After some digging I've actually figured it out. But I'm not quite sure if my approach is correct.
Basically, I've called forward with a subsequent map where I return an object containing errors and data fields. Again, I guess there's a better way of doing this (maybe by extending the ApolloLink class)
const middlewareLink = new ApolloLink((operation, forward) => {
const { operationName } = operation;
if (operationName !== 'IntrospectionQuery') {
const { variables } = operation;
const context = operation.getContext().graphqlContext;
try {
checkPermissions({ operationName, context, variables });
} catch (err) {
return forward(operation).map(() => {
const error = new ForbiddenError('Access denied');
return { errors: [error], data: null };
});
}
}
return forward(operation);
});
I have Ember code where the backend API calls are abstracted into a
separate service. This service uses ember-ajax library for making
backend calls.
This service sets up the common headers, handles the
timeout errors, and 4xx/5xx errors. And anything else like 422
(validation errors) are left to be handled by the calling code.
-
getCustomerProfile (authenticationToken) {
const backendService = this.get('callBackendService');
return backendService.callEndpoint(GET_METHOD,
getCustomerProfileAPI.url,
{'X-Auth-Token': authenticationToken}).then((customerProfileData) => {
if (!backendService.get('didAnybodyWin') && customerProfileData) {
backendService.set('didAnybodyWin', true);
return customerProfileData.profiles[0];
}
}).catch((error) => {
if (isInvalidError(error)) {
if (!backendService.get('didAnybodyWin')) {
backendService.set('didAnybodyWin', true);
backendService.transitionToErrorPage();
return;
}
}
});
}
and the call-backend-service looks like this
callEndpoint (httpVerb, endPoint, headersParameter, data = {},
timeoutInMillisecs = backendCallTimeoutInMilliseconds) {
const headersConst = {
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json',
'Brand': 'abc'
};
var headers = Ember.assign(headersParameter, headersConst);
var promiseFunctionWrapper;
this.set('didAnybodyWin', false);
if (httpVerb.toUpperCase() === GET_METHOD) {
Ember.Logger.warn('hit GET Method');
promiseFunctionWrapper = () => {
return this.get('ajax').request(endPoint, {headers});
};
} else if (httpVerb.toUpperCase() === POST_METHOD) {
Ember.Logger.warn('hit POST Method');
promiseFunctionWrapper = () => {
return this.get('ajax').post(endPoint, {data: data, headers: headers});
};
}
return RSVP.Promise.race([promiseFunctionWrapper(), this.delay(timeoutInMillisecs).then(() => {
if (!this.get('didAnybodyWin')) {
this.set('didAnybodyWin', true);
Ember.Logger.error('timeout of %s happened when calling the endpoint %s', backendCallTimeoutInMilliseconds, endPoint);
this.transitionToErrorPage();
return;
}
})]).catch((error) => {
if (!this.get('didAnybodyWin')) {
if (isTimeoutError(error)) {
this.set('didAnybodyWin', true);
Ember.Logger.warn('callBackEndService: isTimeoutError(error) condition is true');
this.transitionToErrorPage();
return;
} else if (isAjaxError(error) && !isInvalidError(error)) { //handles all errors except http 422 (inValid request)
this.set('didAnybodyWin', true);
Ember.Logger.warn('callBackEndService: isAjaxError(error) && !isInvalidError(error) [[ non timeout error]] condition is true');
this.transitionToErrorPage();
return;
} else {
throw error; // to be caught by the caller
}
}
});
},
The callEndpoint does a RSVP.Promise.race call to make sure the called backend API comes back before a timeout happens. It runs two promises and whichever resolves first is the one that wins. didAnybodyWin is the flag that guards both the promises from getting executed.
Up to this part is all fine.
But this didAnybodyWin becomes the shared state of this call-backend-service because it has to convey back to the caller whether it ran the default set of then or catch blocks or does it expect the caller to run its then/catch blocks.
The problem is when model() hook is run, I am doing
RSVP.all {
backendAPICall1(),
backendAPICall2(),
backendAPICAll3()
}
This RSVP.all is going to execute all 3 calls one after another, so they will hit the call-backend-service in an interleaved fashion and hence run the risk of stepping over each other (when it comes to the didAnybodyWin shared state).
How can this situation be avoided ?
Is there any other better way for the callee to signal to the caller whether or not its supposed to do something with the returned promise.
I have two models and both of them have Users. Let's call them:
Users_A
Users_B
I have to find a user in Users_A, but if it doesn't exist in Users_A then I have to search on Users_B.
If the user exists on User_B then I want to return it when I call Users_A.findOne()
Is there a way to do this?
You can create remote method to achieve this task and define async waterfall method to run code through several models.
for example:
in remote method execution:
async.waterfall([
function(callback){
//find results on USERS_A
},
function(data,callback){
//find results on USERS_B
}],
function(err){
if(err) console.log(err);
//return final result from either USERS_A or USERS_B
}
);
hope this method will helpful.
cheers.
I solved it in this way:
Users_A.afterRemote('findOne' function(ctx, instance, next) {
if (ctx.result) {
return next();
} else {
Users_A.app.models.Users_B.findOne(ctx.req.query.filter,
(error, data) => {
if (data) {
//some logic here
return next();
} else {
//instance the error
return next(error);
}
}
}
}
That worked for me, hope it helps
I've been developing node js app which use mongodb to manipulate static data. I'm using webstorm to my main editor program.
I wrote a function which use mongodb to find a data. Actually, this is sample code in the mongodb's getting started document.
var findRestaurants = function(db, callback) {
var cursor =db.collection('restaurants').find( );
cursor.each(function(err, doc) {
assert.equal(err, null);
if (doc != null) {
console.dir(doc);
} else {
callback();
}
});
};
In the 3rd line, each is the method of the cursor object which defined in mongodb driver api. This method operate the callback function to the returned records which the cursor pointing to, and there's no problem to run this code.
But, in the webstorm editor window, the program gives warning to each method, saying that this symbol is deprecated. It says that javascript has deprecated this each method. It may seems that webstorm doesn't know about the api information of node js or mongodb. Of course, I could ignore this message, but it makes me a little irritated.
Is there a way to update warning information of webstorm program? I think that there's a way to register node js or mongodb api list to webstorm program, but I can't find it by searching.
Thanks.
I was experiencing this same problem, and I have found it to be a reference to the javascript .each() method which is in fact deprecated. The simplest (read quick fix) way to resolve this, is to place "//noinspection JSDeprecatedSymbols" above the line giving the error. You can alternatively click on the lightbulb icon on the left, goto "inspection Deprecated Javascript symbol options", then Suppress for statement (or any other option you may wish to use to disable this warning)
var findRestaurants = function(db, callback) {
var cursor =db.collection('restaurants').find( );
//noinspection JSDeprecatedSymbols
cursor.each(function(err, doc) {
assert.equal(err, null);
if (doc != null) {
console.dir(doc);
} else {
callback();
}
});
};
I took a look at the list of functions present in MongoDB engine for Node.JS. There two function that can replace "each" function: "next" and "hasNext".
So for your example, I would write this code:
var findRestaurants = function(db, callback) {
var cursor = db.collection('restaurants').find( );
var parseRestaurant = function() {
cursor.next(function(err, restaurant){
if (err)
return console.error(err);
console.dir(doc);
hasNextRestaurant();
});
};
var hasNextRestaurant = function() {
cursor.hasNext(function(err, result){
if (err)
return console.error(err);
if (result)
parseRestaurant();
else {
//Here is the last point of iterations
//We can use this for statistics output
return console.log('That\'s all');
}
});
};
hasNextRestaurant();
}
Or something like this:
var findRestaurants = function(db, callback) {
var cursor = db.collection('restaurants').find( );
var parseRestaurant = function(err, doc) {
if (err)
return console.error(err);
console.dir(doc);
cursor.hasNext(checkNext);
};
var checkNext = function(err) {
if (err)
return console.error(err);
if (result)
parseRestaurant();
else {
//Here is the last point of iterations
//We can use this for statistics output
return console.log('That\'s all');
}
cursor.next(parseRestaurant);
};
cursor.hasNext(checkNext);
}
I implemented a "before save" operation hook in my code to compare the new instance about to be saved with the old one already in the database.
For that, I compare the value given in the ctx.data with the one given by a query in the database.
The problem is the returned values are always similar, as if the new instance has already been saved in the database.
Have I totally missed the point of the "before save" hook, or is there a way to compare the two values ?
module.exports = function(app) {
var Like = app.models.Like;
Like.observe('before save', function(ctx, next) {
var count = 0;
if (ctx.instance) { // create operation
console.log('create operation);
}
else { // update operation
// Query for the existing model in db
Like.findById(ctx.where.id,
function(err, item) {
if (err)
console.log(err);
else {//compare query value and instance value
if (item.value != ctx.data.value) {
// Always false
}
else {
//Always true
}
}
}
);
}
next();
I can't understand why item.value always similar to ctx.data.value as the first one is supposed to be the actual value in the db and the second one the value about to be saved.
They way you have next() at the bottom doesn't seem right and might be giving enough time for the save to actually happen before the findById call returns. Once you call next the save can actually happen so findById can race with your save.
Try it like this where your next() is within the callback from the findById which will block saving until you've done your comparison.
module.exports = function(app) {
var Like = app.models.Like;
Like.observe('before save', function(ctx, next) {
var count = 0;
if (ctx.instance) { // create operation
console.log('create operation);
next();
}
else { // update operation
// Query for the existing model in db
Like.findById(ctx.where.id,
function(err, item) {
if (err)
console.log(err);
else {//compare query value and instance value
if (item.value != ctx.data.value) {
// Always false
}
else {
//Always true
}
}
next();
}
);
}