In Apollo Server, one could use a schema directive to implement a resolver middleware like such:
adminGetUsers(getUsersPL: GetUsersPL!): [User] #hasRole(role: "ADMIN")
#hasRole(role: "ADMIN") serves as a middleware to prevent any non-admin user from using this mutation.
So how would one sanitize/transform input data? For example,
getUser(userId: String! #transform): [User]
#transform will take in userId as a hashed id (ie: xyfd), and transform it to a numbered ID (ie: 12). This syntax is not allowed of course. Basically I want a way to modify input data before it goes into resolver.
That actually is valid syntax. You can define a directive that's applied to argument definitions like this:
directive #test on ARGUMENT_DEFINITION
type Query {
foo(input: String #test): String
}
Schema directives are not middleware. They are just ways of altering individual definitions inside your schema. Most commonly they are used to alter field definitions, but you can alter other definitions like object types, input object types, enums, unions, etc. When using a directive with a field, you can wrap the existing resolve function inside another one (or replace it altogether) -- in doing so, we can create "middleware" for resolvers. However, that's not the purpose of schema directives.
That aside, you can't use an argument directive to alter the value the argument is passed. At best, you can change the type of the argument to something else (like a custom scalar). However, you can just use a field directive to do what you're trying to accomplish:
class ExampleDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
field.resolve = async function (
source,
args,
context,
info,
) {
args.someArg = doSomething(args.someArg)
return resolve.call(this, source, args, context, info);
}
}
}
Related
I'm using lighthouse on laravel to create APIs for a portal for which I only deal with the development of the backend.
Basically I have to extract a list from a table in the db and so far everything is ok: in the schema file I define the type and the query itself
type StandardLibrary #guard{
id: ID!
code: String!
title: String!
....
}
type Query{
...
standardLibraries: [StandardLibrary!] #all
...
}
At this point, however, I need to get the translations of the title field from the dedicated json files, and I should have solved it by making a #translate directive that I call next to the field that interests me and implemented as follows
type StandardLibrary #guard{
id: ID!
code: String!
title: String! #translate
....
}
namespace App\GraphQL\Directives;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class TranslateDirective extends BaseDirective implements FieldMiddleware
{
public static function definition(): string
{
return /** #lang GraphQL */ <<<'GRAPHQL'
directive #example on FIELD_DEFINITION
GRAPHQL;
}
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$resolver = $fieldValue->getResolver();
// If you have any work to do that does not require the resolver arguments, do it here.
// This code is executed only once per field, whereas the resolver can be called often.
$fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) {
// Do something before the resolver, e.g. validate $args, check authentication
// Call the actual resolver
$result = $resolver($root, $args, $context, $resolveInfo);
// Do something with the result, e.g. transform some fields
return __($result,[],'en');
});
// Keep the chain of adding field middleware going by calling the next handler.
// Calling this before or after ->setResolver() allows you to control the
// order in which middleware is wrapped around the field.
return $next($fieldValue);
}
}
It works and if it doesn't find the corresponding translation it returns the contents of the field in the db.
But my problem is: how do I dynamically give the language in the query? I've tried declaring a custom client directive but I can't figure out how to implement it, could someone help me out? At some point I'll also have to fetch from the json other fields (such as the description) not present in the db, so I'd need to fetch the translations via the record id and not directly looking for the column content.
You can either do it based on client locale (using the Accept-Language header), or ask API client to explicitly specify expected locales, or even mix both a choose the first-one as a fallback of the second one.
For the header part, I would recommend a Laravel middleware that would simply set the app()->setLocale() based on available locales, and header values.
For the argument, schema would look like this :
title(locale: string): String! #translate
(Yes, argument can exist at any level, not only Query/Mutation)
Value is retrieved as following on directive class:
return __($result,[],$args['locale'] ?? app()->getLocale());
For an Apollo GraphQL Server custom data source, what's the best way to add a default where clause to every database query e.g.
where isDeleted = false
Using Apollo GraphQL Server, I have created a custom data source. The data source simply gets data from a database.
class CustomDataSource extends DataSource {
I've extended CustomDataSource so that project specific details can be added to it. i.e. CustomDataSource should be vanilla and re-usable across projects while MyProjectDataSource can contain project specific business rules.
class MyProjectDataSource extends CustomDataSource {
My GraphQL queries accept a filter parameter and CustomDataSource applies the filter to the database query.
type Query {
users(filter: JSON): [User]
}
My customDataSource is top heavy so does contain joins to return all query data.
Implementation Ideas
MyProjectDataSource to change/override filter argument and then call CustomDataSource. Changing the query argument is straight forward but it's unclear if it's possible (or advisable) to change child field arguments. I'm using npm module graphql-parse-resolve-info for field look ahead.
async get(args, info) {
args.filter = 'where isDeleted = false'
return super.get(args, info);
}
MyProjectDataSource to override CustomDataSource functions. Con: seems overly complicated. Maybe exposing a "getFilter" function that could be overwritten could work but overriding functions feels like a sledge hammer approach.
Add a new defaultFilter parameter. Con: we don't want to expose this parameter to the app.
Create a defaultFilter directive. Con: Complicates schema. Needs to be manually added across entire schema. Unclear how to implement in MyProjectDataSource and has the same implementation cons as 2 above.
Use NodeJS eventEmitter to allow CustomDataSource to override filter argument. Con: Must bake in hooks to CustomDataSource for all project specific rules.
// CustomDataSource to emit event (and pass filter argument).
eventEmitter.emit('filter', ...
// MyDataSource to listen for event (and override filter argument).
eventEmitter.on('filter', ...
Create a custom filter data type (instead of being of type JSON). Maybe this filter class could expose a defaultFilter property. Con: Feels like a complex road for adding a simple requirement.
I wasn't able to find any Apollo Server architecture to help implement this feature. So I decided to use this simplified approach:
class CustomDataSource extends DataSource {
// Override this method in MyProjectDataSource (optional).
static onFilterEvent(args, info, eargs) {
return eargs;
}
// Add filter.
eargs = { filter: get(args, 'filter') };
eargs = this.constructor.onFilterEvent(this.args, this.info, eargs);
if (eargs.filter) {
// Run database query with modified filter in eargs.filter.
}
}
class MyProjectDataSource extends CustomDataSource {
static onFilterEvent(args, info, eargs) {
eargs.filter = { isDeleted: false }; // TODO: If existing filter then use and clause.
return eargs;
}
}
How can we populate an object and pass it in a request parameter in GraphQL. For example, i want to get rid of sequence of parameters here in the GraphQL request and want to create an object and pass it as a single parameter.
{
allBooks(first:20, orderBy:"myRating"){
isn
title
authors
publisher
}
}
You can use GraphQL Input Types.
In your schema definition you need to define the input into something like:
input BooksInput {
first: Int
orderBy: String
}
And require it as the args type in your query, then use it as:
{
allBooks($input: {first:20, orderBy:"myRating"}){
isn
title
authors
publisher
}
}
I've the following structure in my schema:
type gn_Feature implements Some_Interface {
s_description: String
s_id: URL!
some_parent: gn_Feature
}
As you can see, each gn_Feature has an another linked gn_Feature object (the linking is handled elsewhere, it doesn't really matter). By my current understanding, you only need to define the resolvers for the return types, so my resolvers look like the following:
export const resolvers = Object.assign(
{},
{
DateTime: DateTime,
Date: DateTime,
Time: RegularExpression("Time", /^\d{2}:\d{2}(:\d{2})?$/),
URL,
Query: {
gn_Feature: gn_FeatureResolver
},
gn_Feature: gn_FeatureResolver
}
);
But, my queries fail with the following error if I don't explicitly define the resolver for the nested field, like so:
gn_Feature: {some_parent: gn_FeatureResolver}
Error:
"message": "Resolve function for \"gn_Feature.s_description\"
returned undefined"
My resolver function doesn't even get invoked for my nested object when I don't specify it like the above.
My backend consists of some voodoo transpiling of GraphQL queries into SparQL queries which return data back so I won't post the resolver code as I utilize one universal resolver for many fields. I'd like to avoid having to specify resolvers for each nested field as that's going be extremely tedious (I have dozens of types with dozens of fields). Any clarifications are welcome, as I'm completely baffled.
Example GraphQL query:
gn_Feature(some_field:"AD", last:2){
s_description,
s_id
some_parent{
s_description
}
}
When executing a query, only the value of any particular field is initially unknown and needs to be resolved... everything else, like the types, selection sets, etc. is already known. A resolver is a function called to resolve a specific field. In GraphQL.js, there are no resolvers for types.
Apollo muddies the waters a bit in this regard, since for convenience, the API for makeExecutableSchema allows you to define custom scalars by including them in the resolver map. Additionally, even though interfaces and unions don't have resolvers either, Apollo lets you use the resolver map to specify a __resolveType function for these abstract types as well. Apollo's docs actually define a resolver as "a function that connects schema fields and types to various backends." However, in the context of GraphQL in general, you should think of a resolvers as "a functions that resolves the value of a field".
So, given a type called gn_Feature and a field called some_parent, this is the correct way to structure your resolvers:
const resolvers = {
gn_Feature: {
some_parent: someResolverFunction
}
}
Also note that Query and Mutation are both types themselves, so if you have a query called gn_Feature, you are actually resolving a field called gn_Feature for the Query type.
I created a field in a model named "Listing". The field in here is "category". This is set to enum of some values (screenshot below). Does graphql allow to query to fetch the enum list?
This is possible using a GraphQL introspection query.
Enum types on Graphcool are named "MODEL_FIELD", so in your case you can run this query:
{
__type(name: "LISTING_CATEGORY") {
name
enumValues {
name
}
}
}
You can find more information in this answer to a similar question.
An easier approach, when using Nexus.js, is to just create a new root type, and have it resolve to your array of ENUM values. This works because with Nexus.js you're writing your schema with code (TypeScript), which means you're able to import your array of ENUM values into the resolver — it's just code.