Hello. I can't figure out how to create multiple levels of nested queries with #ResolveFiled. I hope for your help. 🙏
What I'm doing and Context:
I have a product. The product has a supplier. A vendor-specific product contains product variants. Variants contain options.
I need to make a request in 4 levels:
Product
ProductHasProvider
Product Variants
Variant Options
I use the "Code First" approach, created an ObjectType for each entity. Next, I create a "Product" resolver.
Creating a second level "ProductHasProvider" with #ResolveField
When adding a ResolveField ("Providers") - it appears inside the main resolver "Product" and resolves the Providers ObjectType. Okay, it works, I can make requests at the 2nd level correctly.
#ResolveField('Providers', () => [ProductHasProvider])
async getProductProviders (#Parent() product: Product) {
const { id } = product;
return await this.productListService.ProductsProviders( { id });
}
I want to make third level where ProductHasProvider has Variants. I decorate ProductHasProvider as the parent.
#ResolveField(('variants'), type => [Variant])
async getVariants (#Parent() productHasProvider: ProductHasProvider) {
const { id } = productHasProvider;
return await this.productListService.getVariants({ id });
}
In this case, this ResolveField defines the ObjectType for [Variants], but for some reason at the first level. That is, in Apollo studio, the field is displayed in "Product". I can't query Variants for ProductHasProvider.
query Products {
getProducts {
Providers {
id
}
variants {
id
options {
id
}
}
}
}
Expected behavior:
I add a new #ResolveField(() => [Variants]) with "ProductHasProvider" parent (Which is already #ResorveField for Product). And I can do 3rd and 4th level queries.
query Products {
getProducts {
id
Providers {
id
variants {
id
options {
id
}
}
}
}
}
Please tell me what I'm doing wrong and how to achieve what I want. Thank you.🙏
#ResolveField is to be put in a Resolver, to specify how to return a specific field for a specific entity.
In your case, you have a Resolver for Products, in which you specify a #ResolveField for the field Providers.
I'm guessing that you are adding another #ResolveField in the same Resolver, and it will specify how to return another field of Products.
What you want is to create another Resolver, for Providers, in which you specify how to return the field variants.
Here is how it is working for me :
#Resolver('Product')
export class ProductsResolver {
#ResolveField('Providers', () => [ProductHasProvider])
async getProductProviders (#Parent() product: Product) {
const { id } = product;
return await this.productListService.ProductsProviders( { id });
}
}
#Resolver('Provider')
export class ProvidersResolver {
#ResolveField('variants', () => [Variant])
async getProductProviders (#Parent() provider: ProductHasProvider) {
const { id } = provider;
return await this.variantsService.getVariantForProvider( { id });
}
}
Related
I'm trying to get all the records from the table.
controller:
#Get('types')
async getTypes(): Promise<PageTypeRO[]> {
return this.pageService.findTypes();
};
service:
async findTypes(): Promise<PageTypeRO[]> {
return await this.pageTypePropsRepository.find();
}
interface (RO):
export interface PageTypeRO {
readonly id: number
}
I expect to get an array with objects in which only the "id" field, but teach all the fields from the table.
You have to set columns you want to get,
To make it work for you, you should edit FindTypes function:
async findTypes(): Promise<PageTypeRO[]> {
return await this.pageTypePropsRepository.find({ select: ["id"] });
}
I am trying to write a transaction that first query documents by documentId from a list of ids, then makes some updates.
I am getting the error:
The corresponding value for FieldPath.documentId() must be a string or a DocumentReference.
For example:
const indexArray = [..list of doc ids...]
const personQueryRef = db.collection("person").where(admin.firestore.FieldPath.documentId(), "in", indexArray)
return db.runTransaction(transaction => {
return transaction.get(personQueryRef).then(personQuery => {
return personQuery.forEach(personRef => {
transaction.update(personRef, { ...update values here })
//more updates etc
})
})
})
I am wanting to do this in an onCreate and onUpdate trigger. Is there another approach I should be taking?
Update
The error still persists when not using a transaction, so this is unrelated to the problem.
The problem does not occur when the query is .where(admin.firestore.FieldPath.documentId(), "==", "just_one_doc_id"). So, the problem is with using FieldPath.documentId() and in.
It sounds like the type of query you're trying to do just isn't supported by the SDK. Whether or not that's intentional, I don't know. But if you want to transact with multiple documents, and you already know all of their IDs, you can use getAll(...) instead:
// build an array of DocumentReference objects
cost refs = indexArray.map(id => db.collection("person").doc(id))
return db.runTransaction(transaction => {
// pass the array to getAll()
return transaction.getAll(refs).then(docs => {
docs.forEach(doc => {
transaction.update(doc.ref, { ...update values here })
})
})
})
I have an article table, where an article can cite multiple articles. Each article can also have a single author.
Article model :
class Article extends DefaultModel {
static get tableName() {
return "articles";
}
static get relationMappings() {
return {
author: {
relation: DefaultModel.BelongsToOneRelation,
modelClass: "Author",
join: {
from: "articles.authorId",
to: "authors.id"
}
},
mentions: {
relation: DefaultModel.ManyToManyRelation,
modelClass: "Article",
join: {
from: "articles.id",
through: {
from: "mentions.citingArticle",
to: "mentions.citedArticle"
},
to: "articles.id"
}
}
};
}
}
Mention model :
class Mention extends DefaultModel {
static get tableName() {
return "mentions";
}
static get idColumn() {
return ["citingArticle", "citedArticle"];
}
}
What I'm trying to do, is insertGraph, the main article + the articles that are mentioned in it, here is what I have :
async function insertData(fullArticle) {
const articleTx = await transaction(Article.knex(), async tx => {
const references = fullArticle.references
.map(articleObj => {
return {
...articleObj.article,
author: articleObj.author
};
});
const article = await Article.query(tx).insertGraph({
...fullArticle.article,
author: fullArticle.author,
mentions: references[0]
});
return article;
});
console.log(articleTx);
}
I know this is only inserting the main row + the first row, but I ran into multiple problems :
The mention model has a composite key, when I try to insert using the code above I get a (node:5944) UnhandledPromiseRejectionWarning: error: column "citedArticle" of relation "mentions" does not exist.
I also got the same error when I try to add "#id" to the main article mentions relation, and "#ref" to the cited article, and insert them in 2 different objects.
A third problem I ran into here, was a unique constraint on both the citing and the cited article authors. What if both of them were written by the same author? I have a unique string column in the authors table that isn't the id, when I tried to insert I got duplicate key value violates unique constraint "author_isn". I'm not sure how I can update the row to reference the existing author id if that happens.
In an update to our GraphQL API only the models _id field is required hence the ! in the below SDL language code. Other fields such as name don't have to be included on an update but also cannot have null value. Currently, excluding the ! from the name field allows the end user to not have to pass a name in an update but it allows them to pass a null value for the name in, which cannot be allowed.
A null value lets us know that a field needs to be removed from the database.
Below is an example of a model where this would cause a problem - the Name custom scalar doesn't allow null values but GraphQL still allows them through:
type language {
_id: ObjectId
iso: Language_ISO
auto_translate: Boolean
name: Name
updated_at: Date_time
created_at: Date_time
}
input language_create {
iso: Language_ISO!
auto_translate: Boolean
name: Name!
}
input language_update {
_id: ObjectId!
iso: Language_ISO!
auto_translate: Boolean
name: Name
}
When a null value is passed in it bypasses our Scalars so we cannot throw a user input validation error if null isn't an allowed value.
I am aware that ! means non-nullable and that the lack of the ! means the field is nullable however it is frustrating that, as far as I can see, we cannot specify the exact values for a field if a field is not required / optional. This issue only occurs on updates.
Are there any ways to work around this issue through custom Scalars without having to start hardcoding logic into each update resolver which seems cumbersome?
EXAMPLE MUTATION THAT SHOULD FAIL
mutation tests_language_create( $input: language_update! ) { language_update( input: $input ) { name }}
Variables
input: {
_id: "1234",
name: null
}
UPDATE 9/11/18: for reference, I can't find a way around this as there are issues with using custom scalars, custom directives and validation rules. I've opened an issue on GitHub here: https://github.com/apollographql/apollo-server/issues/1942
What you're effectively looking for is custom validation logic. You can add any validation rules you want on top of the "default" set that is normally included when you build a schema. Here's a rough example of how to add a rule that checks for null values on specific types or scalars when they are used as arguments:
const { specifiedRules } = require('graphql/validation')
const { GraphQLError } = require('graphql/error')
const typesToValidate = ['Foo', 'Bar']
// This returns a "Visitor" whose properties get called for
// each node in the document that matches the property's name
function CustomInputFieldsNonNull(context) {
return {
Argument(node) {
const argDef = context.getArgument();
const checkType = typesToValidate.includes(argDef.astNode.type.name.value)
if (checkType && node.value.kind === 'NullValue') {
context.reportError(
new GraphQLError(
`Type ${argDef.astNode.type.name.value} cannot be null`,
node,
),
)
}
},
}
}
// We're going to override the validation rules, so we want to grab
// the existing set of rules and just add on to it
const validationRules = specifiedRules.concat(CustomInputFieldsNonNull)
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules,
})
EDIT: The above only works if you're not using variables, which isn't going to be very helpful in most cases. As a workaround, I was able to utilize a FIELD_DEFINITION directive to achieve the desired behavior. There's probably a number of ways you could approach this, but here's a basic example:
class NonNullInputDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
const { args: { paths } } = this
field.resolve = async function (...resolverArgs) {
const fieldArgs = resolverArgs[1]
for (const path of paths) {
if (_.get(fieldArgs, path) === null) {
throw new Error(`${path} cannot be null`)
}
}
return resolve.apply(this, resolverArgs)
}
}
}
Then in your schema:
directive #nonNullInput(paths: [String!]!) on FIELD_DEFINITION
input FooInput {
foo: String
bar: String
}
type Query {
foo (input: FooInput!): String #nonNullInput(paths: ["input.foo"])
}
Assuming that the "non null" input fields are the same each time the input is used in the schema, you could map each input's name to an array of field names that should be validated. So you could do something like this as well:
const nonNullFieldMap = {
FooInput: ['foo'],
}
class NonNullInputDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field
const visitedTypeArgs = this.visitedType.args
field.resolve = async function (...resolverArgs) {
const fieldArgs = resolverArgs[1]
visitedTypeArgs.forEach(arg => {
const argType = arg.type.toString().replace("!", "")
const nonNullFields = nonNullFieldMap[argType]
nonNullFields.forEach(nonNullField => {
const path = `${arg.name}.${nonNullField}`
if (_.get(fieldArgs, path) === null) {
throw new Error(`${path} cannot be null`)
}
})
})
return resolve.apply(this, resolverArgs)
}
}
}
And then in your schema:
directive #nonNullInput on FIELD_DEFINITION
type Query {
foo (input: FooInput!): String #nonNullInput
}
I want to execute a query like this:
{
houses(owner: "Thomas") {
id
color
cars(type: "Sports Car") {
name
year
}
}
}
But this returns an error:
"message": "Unknown argument \"type\" on field \"cars\" of type \"House\".",
However, I'm able to execute this properly:
cars(type: "Sports Car") {
name
year
}
Is what I'm trying to do even possible?
Thanks in advance!
Make sure that you have your resolver for cars set up as a sub query of houses. The result from the houses query should be passed as the root argument to the cars sub query.
type House {
id
color
cars( type: String! ): [ Car ]
}
type Car {
name
year
}
Your resolver might look like:
Query: {
async houses( root, args, context ) {
return { ... houses ... }
}
},
houses: {
async cars( root, args, context ) {
return { ... cars ... }
}
}
Then create a resolver for cars that is nested underneath the houses query. Here's an article on it if you are using graphql-tools from Apollo: Writing resolvers with graphql-tools
Hope this helps!