How to merge a type whose fields come from two different GraphQL subschemas? - graphql

I'm running a gateway with apollo-server v2 with graphql-tools v7 and have the following services and subschemas, which I've set up as a (contrived) proof-of-concept:
Service 1:
Query {
getUser(input: GetUserInput!): User
}
input GetUserInput {
id: ID!
}
type User {
id: ID!
contacts: Contacts
}
type Contacts {
id: ID!
primary: String
}
Service 2:
Query {
getUser(input: GetUserInput!): User
}
input GetUserInput {
id: ID!
}
type User {
id: ID!
contacts: Contacts
}
type Contacts {
id: ID!
secondary: String
}
The gateway has the combined schema like this:
Query {
getUser(input: GetUserInput!): User
}
input GetUserInput {
id: ID!
}
type User {
id: ID!
contacts: Contacts
}
type Contacts {
id: ID!
primary: String
secondary: String
}
The gateway is configured to stitch the schemas and merge the User type like this:
import { stitchSchemas } from "#graphql-tools/stitch"
...
const schema = stitchSchemas({
subschemas: [
{
schema: service1Schema,
executor: service1Executor,
merge: {
User: {
fieldName: "getUser",
selectionSet: "{ id }",
args: (originalObject) => ({
input: {
id: originalObject.id
}
}),
},
}
},
{
schema: service2Schema,
executor: service2Executor,
merge: {
User: {
fieldName: "getUser",
selectionSet: "{ id }",
args: (originalObject) => ({
input: {
id: originalObject.id
}
}),
},
}
},
]
})
However, when I send a query to fetch the contacts field for a User, only Service 2 is called by the gateway. Instead, I would have expected the gateway to call Service 1 to get its representation of User.contacts and also Service 2 to get its representation of User.contacts and then merge the two into a combined result.
The query I executed:
query GetContacts {
getUser(input: { id: "123" }) {
contacts {
primary
secondary
}
}
}
The result I got (and I see in logs that Service 1 was not called at all):
{
"data": {
"getUser": {
"contacts": {
"primary": null,
"secondary": "Secondary contact from Service 2"
}
}
}
}
The result I expected:
{
"data": {
"getUser": {
"contacts": {
"primary": "Primary contact from Service 1", <-- This should be included in the result.
"secondary": "Secondary contact from Service 2"
}
}
}
}
I have confirmed that I'm able to fetch other fields (e.g. User.name) from Service 1 successfully, so the subschema for Service 1 is configured correctly on the gateway (though probably not the merge options for it).
Since this is similar to the example from the graphql-tools documentation, I'm confused about why the fields of the Contacts type aren't merged as expected in the query response.

Related

Can you apply sorting to a lists of models inside another model?

Using AWS Amplify, can we apply sorting to the messages in the Conversation model?
When fetching the conversation, it would be nice that the messages come sorted based on the generated createdAt date.
Currently these are the models used.
type Conversation #model {
id: ID!
messages: [Message!]! #hasMany
...
}
type Message #model {
id: ID!
authorId: String!
content: String!
conversation: Conversation #belongsTo
}
Ideally want to place sorting on the hasMany directive, but this is not possible.
type Conversation #model {
id: ID!
messages: [Message!]! #hasMany(sortKeys:['createdAt'])
...
}
Created a secondary index on the Message model with a sort field on createdAt.
type Message #model {
id: ID!
authorId: String! #index(name: "byAuthorId", queryField: "getMessagesByAuthorId", sortKeyFields: [ "createdAt" ])
content: String!
conversation: Conversation #belongsTo
}
Amplify created a new query to fetch the messages and apply sorting. Following example uses react-query to fetch the messages from an authorId with sorting.
export function useMessagesPerAuthorId({
id,
filter,
enabled = true,
}: {
id: string | undefined;
filter: any;
enabled?: boolean;
}) {
return useQuery(
['conversations', 'messages', id, filter],
() => fetchMessagesByAuthorId({ id: id!, filter }),
{ enabled: enabled && !!id }
);
}
async function fetchMessagesByAuthorId({ id, filter }: { id: string; filter: any }) {
const query: any = await API.graphql({
query: getMessagesByAuthorId,
variables: { authorId: id, sortDirection: 'DESC', filter },
});
const data: Message[] = query.data?.getMessagesByAuthorId.items;
return data;
}
Now we can call that hook in our view component and pass the filters.
const { isLoading, data: messages = [] } = useMessagesPerAuthorId({
id: profile?.id,
filter: {
and: [{ conversationMessagesId: { eq: conversationId } }, { deleted: { eq: false } }],
},
enabled: !!profile?.id,
});

Relationships with AwsCdk, DynamoDB and AppSync - Typescript and lambda functions

we are currently studying the stack: cdk, appsync and amplify to migrate our applications.
In our initial tests, we were able to upload a graphql api with only appsync wit relationships and it was very smooth, nice and fast.
When testing to build with cdk, we are having difficulties to create the relationships.
Here my code:
Schema
type Person {
id: ID!
name: String!
}
input PersonInput {
id: ID!
name: String!
}
input UpdatePersonInput {
id: ID!
name: String
}
type Client {
id: ID!
type: String!
personId: String
# Person: PersonConnection
Person: Person #connection(fields: ["personId"])
}
input ClientInput {
id: ID!
type: String!
personId: String!
}
input UpdateClientInput {
id: ID!
type: String
personId: String
}
My function
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient();
async function list() {
const params = {
TableName: process.env.CLIENT_TABLE,
}
try {
const data = await docClient.scan(params).promise()
return data.Items
} catch (err) {
console.log('DynamoDB error: ', err)
return null
}
}
export default list;
My table
const clientTable = new dynamodb.Table(scope, 'ClientTable', {
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: 'id',
type: dynamodb.AttributeType.STRING,
},
});
clientTable.addGlobalSecondaryIndex({
indexName: 'client-by-person-id',
partitionKey: {
name: 'personId',
type: dynamodb.AttributeType.STRING
},
sortKey: {
name: 'createdAt',
type: dynamodb.AttributeType.STRING
}
})
My query
query MyQuery {
listClients {
id
personId
type
Person {
name
}
}
}
However, my return to Person connection is null
"listClients": [
{
"id": "1",
"personId": "1",
"type": "PJ",
"Person": null
}
]
I would appreciate it if could point out our mistake
Solution of the problem based on the response of the Thorsten.
First, add resolver to the Person field in Client
export const clientResolvers = [{ typeName: "Client", fieldName: "Person" },...]
clientResolvers.map(((resolver: clientTypeResolver) => dataSource2.createResolver(resolver)))
Map function to the Person field in its lambda function
type AppSyncEvent = {
...
source: {personId: string,}
...
}
exports.handler = async (event:AppSyncEvent) => {
switch (event.info.fieldName) {
...
case "Person":
return await getPerson(event.source.personId);
}
}```
Function to solve the person field
async function getPerson(personId: string) {
console.log("CONTEXT\n" + JSON.stringify(personId, null, 2))
// console.log(context.source)
const params = {
TableName: process.env.PERSON_TABLE,
Key: { id: personId }
}
try {
const { Item } = await docClient.get(params).promise()
console.log("DATA\n" + JSON.stringify(Item, null, 2))
return Item
} catch (err) {
console.log('DynamoDB error: ', err)
}

Prisma binding Nested filtering

I'm working on a food order platform with Prisma, Prisma-binding and Apollo Server on the backend. A customer can choose a restaurant in his neighbourhood and add one or more dishes to his cart. It is possible that when a dish from restaurant x is already added, the customer decides to order from another restaurant, restaurant y. Therefore I need to filter the added dishes when making an order based on the customer id and the final chosen restaurant in the backend first before creating the order and payment url.
I've got three data types inside my prisma datamodel: Customer, CartItem and Dish
type Customer {
id: ID! #id
createdAt: DateTime! #createdAt
updatedAt: DateTime! #updatedAt
name: String
email: String
phone: String
user: User
cart: [CartItem]!
orders: [Order]!
}
type CartItem {
id: ID! #id
quantity: Int! #default(value: 1)
dish: Dish!
customer: Customer! #relation(link: INLINE)
}
type Dish {
id: ID! #id
name: String!
price: String!
description: String!
isAvailable: Boolean! #default(value: true)
category: String
restaurant: Restaurant!
}
In the Prisma GraphQL playground that is directly connected to the database I can filter the cartItems that I need to create the order like this:
query {
customer(where: { id: "ck8zwslgs00da0712cq88e3oh" } ) {
id
cart(where: { dish: { restaurant: { id: "ck904gwl400mz0712v0azegm3" } } }) {
quantity
dish {
name
price
restaurant {
id
name
}
}
}
}
}
output:
{
"data": {
"customer": {
"id": "ck8zwslgs00da0712cq88e3oh",
"cart": [
{
"quantity": 2,
"dish": {
"name": "Nachos Plate Hawaii",
"price": "1150",
"restaurant": {
"id": "ck904gwl400mz0712v0azegm3",
"name": "Taco Bell"
}
}
},
{
"quantity": 1,
"dish": {
"name": "Nachos Plate Vulcano",
"price": "1250",
"restaurant": {
"id": "ck904gwl400mz0712v0azegm3",
"name": "Taco Bell"
}
}
}
]
}
}
}
So far so good but now I need the same query in the Apollo Server using prisma-binding. I tried a few things but none of them are working. The first two are returning an error "Field \"cart\" is not defined by type CustomerWhereUniqueInput". The last two are just returning every cartItem without the restaurant filter.
const data = await ctx.db.query.customer({
where: {
AND: [
{
id: args.customerID
},
{
cart: {
dish : {
restaurant: {
id: args.restaurantID
}
}
}
}
]
}
}, info);
const data = await ctx.db.query.customer({
where: {
id: args.customerID
cart: {
dish : {
restaurant: {
id: args.restaurantID
}
}
}
}
}, info);
const data = await ctx.db.query.customer({
where: {
id: args.customerID
},
cart: {
where: {
dish : {
restaurant: {
id: args.restaurantID
}
}
}
}
}, info);
const data = await ctx.db.query.customer({
where: {
id: args.customerID
},
cart: {
dish : {
restaurant: {
where: {
id: args.restaurantID
}
}
}
}
}, info);
Can someone help me out with the right way to filter on the customer id and the restaurant id?

apollo service:push returns Error: Unknown type: "User" however locally it runs fine. How to solve this mystery?

The original code comes from here: https://github.com/apollographql/federation-demo/blob/master/services/reviews/index.js
My code is this:
const typeDefs = gql`
extend type User #key(fields: "id") {
id: ID! #external
numberOfReviews: Int
reviews: [Review]
}
type Review #key(fields: "id") {
id: ID!
body: String
author: User
product: Product
}
extend type Product #key(fields: "upc") {
upc: String! #external
reviews: [Review]
}
`;
const resolvers = {
Review: {
author(review) {
// Review.author -> provides the field `username` and requires User.username to be marked as #external.
console.log("review service returning author")
return { __typename: "User", id: review.authorID };
}
},
User: {
reviews(user, _args, { dataSources }) {
console.log("review service returning reviews for user " + user.id)
return reviews.filter(review => review.authorID === user.id);
},
numberOfReviews(user, _args, { dataSources }) {
console.log("review service returning numberOfReviews for user " + user.id)
return reviews.filter(review => review.authorID === user.id).length;
}
},
Product: {
reviews(product, _args, { dataSources }) {
console.log("review service returning reviews for product " + product.upc)
return reviews.filter(review => review.product.upc === product.upc);
}
}
};
I've other services very similar and they resolve types without issue.
What have I done to desserve this error?
Fetching info from federated service
✔ Loading Apollo Project
✔ Loading Apollo Project
✖ Uploading service to Engine
→ Unknown type: "User".
Error: Unknown type: "User".

Querying NOT NULL GraphQL with Prisma

Schema:
type TrackUser {
id: ID! #unique
createdAt: DateTime!
user: User #note there is no `!`
}
type User {
id: ID! #unique
name: String! #unique
}
I want to get Alls TrackUser where User is not null. What would be the query?
This would be a possible query:
query c {
trackUsers(where: { NOT: [{ user: null }] }) {
name
}
}
Here you can see how it looks in the Playground. I added a name to Trackuser in the datamodel in order to be able to create it from that side without a user.
this works, but I guess it is just a hack..
query TrackUsersQuery($orderBy: TrackUserOrderByInput!, $where: TrackUserWhereInput, $first: Int, $skip: Int) {
trackUsers(where: $where, orderBy: $orderBy, first: $first, skip: $skip) {
id
createdAt
user {
id
name
}
}
}
variables = {
where: {
user: {
name_contains: ''
}
}
}
UPDATE:
For Prisma2, here you have the possibilities:
For products that have no invoice, you can use the following:
const data = await prisma.product.findMany({
where: {
invoices: {
none: {
id: undefined,
},
},
},
})
And for Invoices that do not have a product associated:
const data = await prisma.invoice.findMany({
where: {
productId: null,
},
})
more details here: https://github.com/prisma/prisma/discussions/3461

Resources