GraphQL + Relay: How can I perform authorization for refetching? - express

I am working on a GraphQL server built using Express and attempting to support Relay.
For a regular GraphQL query, I can handle authorization in the resolve function. E.g.:
var queryType = new GraphQLObjectType({
name: 'RootQueryType',
fields: () => ({
foo: {
type: new GraphQLList(bar),
description: 'I should have access to some but not all instances of bar',
resolve: (root, args, request) => getBarsIHaveAccessTo(request.user)
}
})
});
To support Relay refetching on the back-end, Facebook's Relay tutorial instructs us to have GraphQL objects implement a nodeInterface for mapping global ids to objects and objects to GraphQL types. The nodeInterface is defined by the nodeDefinitions function from graphql-relay.
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'bar') {
// since I don't have access to the request object here, I can't pass the user to getBar, so getBar can't perform authorization
return getBar(id);
} else {
return null;
}
},
(obj) => {
// return the object type
}
);
The refetching function that gets passed to nodeDefinitions doesn't get passed the request object, only the global id. How can I get access to the user during refetching so I can authorize those requests?
As a sanity check, I tried querying for nodes that the authenticated user doesn't otherwise have access to (and shouldn't) through the node interface, and got the requested data back:
{node(id:"id_of_something_unauthorized"){
... on bar {
field_this_user_shouldnt_see
}
}}
=>
{
"data": {
"node": {
"field_this_user_shouldnt_see": "a secret"
}
}
}

As it turns out, the request data actually does get passed to resolve. If we look at the source, we see that nodeDefinitions tosses out the parent parameter and passes the global id, the context (containing the request data), and the info arguments from nodeField's resolve function.
Ultimately, where a resolve call would get the following arguments:
(parent, args, context, info)
the idFetcher instead gets:
(id, context, info)
So we can implement authorization as follows:
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId, context) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'bar') {
// get Bar with id==id if context.user has access
return getBar(context.user, id);
} else {
return null;
}
},
(obj) => {
// return the object type
}
);
https://github.com/graphql/graphql-relay-js/blob/master/src/node/node.js#L94-L102

Related

Get result of useQuery with Apollo and Nuxt.js

I'm trying to use the useQuery function (from package '#vue/apollo-composable').
This function doesn't return a promise, just refs to result, loading etc. so I can't directly use this data in my store (Pinia).
Currently I have this code:
fetchArticle: function (id: string) {
// check if article is already in cache
const cache = this.articles.find(a => a.id == id);
if (cache && !cache.partial) return cache;
// fetch article from server
const { result: article } = useQuery<{ article: Article }>(GET_ARTICLE, { id });
// update state, but... when `article` contains data?
},
When I'am in a store I don't know how to wait for request end.
I tried to transform useQuery to return promise but that doesn't work, Nuxt.js freeze on server with this code:
fetchArticle: async function (id: string) {
// check if article is already in cache
const cache = this.articles.find(a => a.id == id);
if (cache && !cache.partial) return cache;
// fetch article from server
const { onResult, result } = useQuery<{ article: Article }>(GET_ARTICLE, { id });
const article = result.value?.article || (await new Promise(r => onResult(({ data }) => r(data.article))));
if (!article) return;
const data = { ...article, partial: false };
this.articles = this.articles.map(a => (a.id == id ? data : a)) as Article[];
// return article
return article;
},
Informations
Store: Pinia
Versions: Nuxt 3.1.2; #vue/apollo-composable 4.0.0-beta.2
I use the apollo client like this:
// The creation of the client
// This part is on a base class, that all of my Service extends
this.apolloClient = new ApolloClient({
link: //the link,
cache: new InMemoryCache(),
name: "My APP name",
version: `v${version.toString()}`,
defaultOptions: defaultOptions //some options I customize
});
provideApolloClient(this.apolloClient);
//The query
// This part is on the Service class
return this.apolloClient.query({ query: query, variables: { id: id }}).then((result) => {
return result.data.myData;
});
I always used like this, never used the useQuery. In the link I use a combination of three, one for the Auth, one for Errors and one for the base URL

Best practice to return ApolloErrors from Nexus resolver

I'm currently struggeling with returning errors from my resolvers created with nexus and typescript.
I know that nexus isn't supposed to handle errors but we still have to define a return type for the mutation/query and that makes it impossible to just return a new ApolloError because nexus does not allow to return either the success value (like a user object) or an error.
export const confirmEmail = mutationField('confirmEmail', {
type: Boolean | ApolloError, // not allowed by nexus
args: {
email: nonNull(stringArg()),
code: nonNull(stringArg()),
},
resolve: async (_, args, ctx) => {
const user = await ctx.prisma.user.findUnique({
where: {
email: args.email,
},
})
if (!user) {
return new ApolloError('User does not exists', 'BAD_USER_INPUT')
}
await sendEmail(
'jonashiltl2003#gmail.com',
`<p>Your confirmation code is: </p> <b>${args.code}</b>`,
'Confirmation Code',
)
return true
},
})
Just to mention, it does work to just define one type for a mutation/query and still return an ApolloError but Typescript complains because then in some cases the ApolloError gets returned and not the defined one.
Is there a cleaner way to return errors from resolvers and stay typesafe?

Nock - Get matched hostname

I want to mock an internal host naming scheme, like this.
nock(/some-internal.(ds1|ds2|ds3).hostname/)
.get("/info")
.reply(200, (???, requestBody) => {
if(??? === "d1") {
// return mock for d1
} else if (??? === "d2") {
// return mock for d2
}
// ...
})
The first parameter of the callback is the path without the base url, so is this even possible?
You can access the ClientRequest instance from inside the callback using the context.
Docs for accessing the original request and headers.
const scope = nock(/some-internal.(ds1|ds2|ds3).hostname/)
.get('/info')
.reply(function (uri, requestBody) {
console.log('host:', this.req.options.host)
// ...
})

How to include info from schema directives as returned data using Apollo Server 2.0 GraphQL?

I'm using schema directives for authorization on fields. Apollo server calls the directives after the resolvers have returned. Because of this the directives don't have access to the output so when authorization fails I can't include relevant information for the user without a convoluted workaround throwing errors that ends up always returning the error data whether the query requests them or not.
I'm hoping someone understands the internals of Apollo better than I and can point out where I can insert the proper information from directives so I don't have to break the standard functionality of GraphQL.
I tried including my output in the context, but that doesn't work despite the directive having access since the data has already been returned from the resolvers and the context version isn't needed after that.
As of right now I throw a custom error in the directive with a code DIRECTIVE_ERROR and include the message I want to return to the user. In the formatResponse function I look for directive errors and filter the errors array by transferring them into data's internal errors array. I know formatResponse is not meant for modifying the content of the data, but as far as I know this is the only place left where I can access what I need. Also frustrating is the error objects within the response don't include all of the fields from the error.
type User implements Node {
id: ID!
email: String #requireRole(requires: "error")
}
type UserError implements Error {
path: [String!]!
message: String!
}
type UserPayload implements Payload {
isSuccess: Boolean!
errors: [UserError]
data: User
}
type UserOutput implements Output {
isSuccess: Boolean!
payload: [UserPayload]
}
/**
* All output responses should be of format:
* {
* isSuccess: Boolean
* payload: {
* isSuccess: Boolean
* errors: {
* path: [String]
* message: String
* }
* data: [{Any}]
* }
* }
*/
const formatResponse = response => {
if (response.errors) {
response.errors = response.errors.filter(error => {
// if error is from a directive, extract into errors
if (error.extensions.code === "DIRECTIVE_ERROR") {
const path = error.path;
const resolverKey = path[0];
const payloadIndex = path[2];
// protect from null
if (response.data[resolverKey] == null) {
response.data[resolverKey] = {
isSuccess: false,
payload: [{ isSuccess: false, errors: [], data: null }]
};
} else if (
response.data[resolverKey].payload[payloadIndex].errors == null
) {
response.data[resolverKey].payload[payloadIndex].errors = [];
}
// push error into data errors array
response.data[resolverKey].payload[payloadIndex].errors.push({
path: [path[path.length - 1]],
message: error.message,
__typename: "DirectiveError"
});
} else {
return error;
}
});
if (response.errors.length === 0) {
return { data: response.data };
}
}
return response;
};
My understanding of the order of operations in Apollo is:
resolvers return data
data filtered based on query parameters?
directives are called on the object/field where applied
data filtered based on query parameters?
formatResponse has opportunity to modify output
formatError has opportunity to modify errors
return to client
What I'd like is to not have to throw errors in the directives in order to create info to pass to the user by extracting it in formatResponse. The expected result is for the client to receive only the fields it requests, but the current method breaks that and returns the data errors and all fields whether or not the client requests them.
You can inject it using destruct:
const { SchemaDirectiveVisitor } = require("apollo-server-express");
const { defaultFieldResolver } = require("graphql");
const _ = require("lodash");
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (parent, args, context, info) {
// You could e.g get something from the header
//
// The verification below its necessary because
// my application runs locally and on Serverless
const authorization = _.has(context, "req")
? context.req.headers.authorization
: context.headers.Authorization;
return resolve.apply(this, [
parent,
args,
{
...context,
user: { authorization, name: "", id: "" }
},
info,
]);
};
}
}
Then on your resolver, you can access it through context.user.

How to broadcast to other controllers when load with module.config or .run in Angularjs

I have a checking when reading the web page,then using the result to refresh sidebar by ng-repeat,but I have errors :
Uncaught Error: Unknown provider: $scope from myModule or
Uncaught Error: Unknown provider: $scope from sharedService
How can I resolve it?
Here is my code
module:
var myModule = angular.module('myModule', []);
service for broadcast:
myModule.factory('mySharedService', function($rootScope) { //service
var sharedService = {};
sharedService.keyHistory = [];
sharedService.linkHistory = [];
sharedService.prepForBroadcast = function(key,link) {
this.keyHistory = key;
this.linkHistory = link;
this.broadcastItem();
};
sharedService.prepForBroadcastAdd =function(key){
console.log(this.keyHistory.push(key));
//this.linkHistory = linkHistory+link;
this.broadcastItem();
};
sharedService.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
return sharedService;
});
config to do Checking:
myModule.config(function($scope,sharedService){
$.ajax({
url:"/fly/AJAX",
type:"POST",
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
datatype:"json",
success:function(data){
if(data!=null){
var loginResult = $.parseJSON(data);
if (loginResult.success == true){
console.log("login success");
$("#userLable").html(loginResult.userName+'('+loginResult.loginID+')');//
if (loginResult.hasHistory==true) {
sharedService.prepForBroadcast(loginResult.searchHistory,[]);
console.log("broadcast");
}
};
}
}
});
});
SideCtrl:
function SideCtrl($scope,sharedService) {
$scope.$on('handleBroadcast', function() {
$scope.keyHistory =sharedService.keyHistory;
$scope.linkHistory = sharedService.linkHistory;
});
}
SideCtrl.$inject = ['$scope', 'mySharedService'];
THX !
The error is due to trying to request a $scope in a config block, which you can't do. If I understand what you're trying to do, then I also think you're over-complicating it. I'd solve the problem a little differently. The details would depend on your requirements and use case, but based on the information you gave...
I'd have a service responsible for communication with the server and storing the state:
app.factory( 'loginService', function ( $http ) {
var result;
function doRequest( data ) {
// just flesh out this post request to suit your needs...
return $http.post( '/fly/ajax', data, {} )
.then( function ( response ) {
// assuming you don't care about the headers, etc.
return response.data;
});
}
// Do it once initially
if ( ! angular.isDefined( result ) ) {
result = doRequest();
}
// return the service's public API
return {
getStatus: function () { return result; },
login: doRequest
};
});
Now the first time this service is requested, the $http request will be made. If you're accessing this from multiple controllers, the post will only occur once because of the isDefined statement. You can then use this in your controllers:
app.controller( 'MainCtrl', function( $scope, loginService ) {
loginService.getStatus().then( function ( data ) {
// do whatever you need to with your data.
// it is only guaranteed to exist as of now, because $http returns a promise
});
});
Every controller accesses it the same way, but it was still only called once! You can set values against the scope and access it from your views, if you want:
app.controller( 'MainCtrl', function( $scope, loginService ) {
loginService.getStatus().then( function ( data ) {
$scope.loginId = data.loginID;
});
});
And in your view:
<h1>Welcome, {{loginId || 'guest'}}!</h1>
And if you need to, you call the function again:
app.controller( 'MainCtrl', function( $scope, loginService ) {
// ...
loginService.login( $scope.user ).then( function ( data ) {
$scope.loginId = data.loginID;
});
// ...
});
As you can see, broadcasting an event is totally unnecessary.
I would do it differently. I would create some sort of more top-level controller, like function MainController($rootScope, $scope, sharedService) and wire it up with body: <body ng-controller='mainController' ng-init='init()'. After that you should create init() method in MainController.
Inside this initialization method I would call sharedService which should make AJAX request (via $http! that's the best practice, and it's very similar to jQuery) and broadcast proper event when required.
That way you make sure to call initialization just once (when MainController is initializing), you stick to the angular's best practices and avoid dodgy looking code.