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

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.

Related

Nuxt 3 - Server not ready on mount, Volar engine confused

I have two issues that may or may not be related.
Overview
Folder Structure
pages
|---user.vue
server
|---api
|---profile.get.ts
|---profile.post.ts
The tech is Nuxt3 using the server and Supabase.
profile.get.ts
import { serverSupabaseClient, serverSupabaseUser } from "#supabase/server"
import { Database } from "~~/types/supabase"
export default defineEventHandler(async (event) => {
try {
const supabase = serverSupabaseClient<Database>(event)
const user = await serverSupabaseUser(event)
const query = getQuery(event)
const { data, error } = await supabase.from('profiles').select('*').eq('email', query.email).single()
if (error) throw { status: error.code, message: error.message }
return { displayName: data.display_name, avatarUrl: data.avatar_url }
} catch (err) {
console.error('Handled Error:', err)
}
})
profile.post.ts
import { serverSupabaseClient } from "#supabase/server"
import { Database } from "~~/types/supabase"
export default defineEventHandler(async (event) => {
const supabase = serverSupabaseClient<Database>(event)
const { displayName, avatarUrl, email }: { displayName: string, avatarUrl: string, email: string } = await readBody(event)
const { error } = await supabase.from('profiles').update({ display_name: displayName, avatar_url: avatarUrl }).match({ email })
if (error) throw new Error(error.message)
return { status: 200 }
})
user.vue Snippet
onMounted(() => {
setTimeout(() => {
getProfile()
}, 100) // Fails when around 50 or less
})
async function getProfile() {
const { data, error } = await useFetch('/api/profile', { method: 'GET', params: { email: user.value?.email } })
console.log(data.value)
console.log(error.value)
displayName.value = data.value!.displayName || ''
avatarUrl.value = data.value!.avatarUrl || ''
}
Problem 1
When user.vue mounts, I want to call my Nuxt API (profile.get.ts) and fetch user data (display name, avatar url) from the Supabase database. However, I receive this error when fetching on mount: FetchError: 404 Cannot find any route matching /api/profile. (/api/profile). However, if I use setTimeout to 100ms, it fetches fine. That makes me think the API server is simply not ready, but the documentation doesn't mention that and encourages fetching during lifecycle.
Problem 2
Volar seems to be confused about the typing of data from getProfile().
Property 'displayName' does not exist on type '{ status: number; } | { displayName: string | null; avatarUrl: string | null; }'.
Property 'displayName' does not exist on type '{ status: number; }'.ts(2339)
However, this is the typing from profile.post.ts even though I'm using profile.get.ts.
Current Behavior
Without setTimeout at 100ms or greater, it will fail with the 404 message
With setTimeout at 100ms or greater, or with getProfile() called from a button, there is no issue, even with the TypeScript errors, etc.
Desired Behavior
TypeScript correctly recognizes the proper endpoint (profiles.get.ts since I'm calling it with get)
Data can be fetched on mount from the API without the use of setTimeout

AWS Graphql with Vue JS - pass parameter to a query

I have a query in AppSync that works and I now want to add this to my Vue app:
query MyQuery {getOrder(id: "704ffe56-f975-4068-b736-664a8f7fd7c3") {
id
orders {
items {
createdAt
order {
id
}
}
}
}
}
The query is protected by incognito user pool (am logged into Appsync with a valid user).
I have this, but get complaints:
async getOrder() {
const { data } = await API.graphql(
graphqlOperation(getOrder, {
input: { id: "704ffe56-f975-4068-b736-664a8f7fd7c3",
},
authMode: 'AMAZON_COGNITO_USER_POOLS'
})
);
console.log(data);
}
The error:
message: "Variable 'id' has coerced Null value for NonNull type 'ID!'"
There seems to be various ways/styles to call Graphql operations but I cannot see a Vue JS example similar to the the one I am trying to execute.
Thanks

How to set vue-i18n in a component's property reactively

I am trying to display some messages or errors depends on an async function result.
When a response's request doesn't include the message property I should use my own.
The problem is that when I use this.$t("message") inside the someMethod method the response.message (data) it isn't reactive. So it won't change after the locale change.
<v-alert v-if="response.error || response.message"
:type="response.error ? 'error' : 'info'">
{{response.error ? response.error : response.message}}
</v-alert>
export default {
data() {
return {
response: {
message: "",
error: ""
}
};
},
methods: {
async someMethod() {
const apiResponse = await sendRequestToAnApi();
if (apiResponse.message) {
this.response.message = apiResponse.message;
} else {
this.response.message = this.$t("translation")
}
}
}
}
I guess I should use computed property to make it work, but I don't have a clue how to mix response object with possible API response.
Any help will be appreciated
You could have your API return not only a message but also a code, where the message could be a textual string based on user locale settings, and the code a slug that will always be English, like users.added or project.created. You can store this code in response.code and create a computed property checking this code and returning the correct vue-i18n translation.
export default {
data() {
return {
response: {
code: "",
error: ""
}
};
},
methods: {
async someMethod() {
const apiResponse = await sendRequestToAnApi();
if (apiResponse.code) {
this.response.code = apiResponse.code;
} else {
this.response.code = null
}
}
},
computed: {
translatedMessage () {
if (this.response.code === 'users.added') {
return this.$t('translate')
}
return 'unknown code'
}
}
}
Of course you could also make this work with a textual string coming from the API, but personally I like working with a specific code that is independent from language settings.

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

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

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.