GraphQL resolver with DataLoader and nested endpoints - graphql

Adapting the example of GraphQL best practices created by the Apollo Team (https://github.com/apollographql/GitHunt-API/tree/master/api), I'm having hard time to come up with a resolver that would result in a list of Person using DataLoaders.
Here's an example of the api (data from: https://github.com/steveluscher/zero-to-graphql/tree/master/zero-node)
Given the output of /people/ endpoint like:
{
"people": [
{
"username": "steveluscher",
"id": "1",
},
{
"username": "aholovaty",
"id": "2",
},
{
"username": "swillison",
"id": "3",
},
{
"username": "gvr",
"id": "4",
}
]
}
And a person from the endpoint /people/1/
{
"person": {
"last_name": "Luscher",
"username": "steveluscher",
"friends": [
"/people/2/",
"/people/3/"
],
"id": "1",
"email": "steveluscher#fb.com",
"first_name": "Steven"
}
I would like to have a resolver what would give me a list of Person like:
[
{
"person": {
"last_name": "Luscher",
"username": "steveluscher",
"friends": [
"/people/2/",
"/people/3/"
],
"id": "1",
"email": "steveluscher#fb.com",
"first_name": "Steven"
}
},
{
"person": {
"last_name": "Holovaty",
"username": "aholovaty",
"friends": [
"/people/1/",
"/people/4/"
],
"id": "2",
"email": "a.holovaty#django.com",
"first_name": "Adrian"
}
},
...
]
This is what I got so far:
server.js
import { ApiConnector } from './api/connector';
import { People } from './api/models';
import schema from './schema';
export function run() {
const PORT = 3000;
const app = express();
app.use(bodyParser.json());
app.use('/graphql', graphqlExpress((req) => {
const query = req.query.query || req.body.query;
if (query && query.length > 2000) {
throw new Error('Query too large.');
}
const apiConnector = new ApiConnector();
return {
schema,
context: {
People: new People({ connector: apiConnector }),
},
};
}));
app.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
}));
const server = createServer(app);
server.listen(PORT, () => {
console.log(`API Server is now running on http://localhost:${PORT}`);
});
return server;
}
models.js
export class People {
constructor({ connector }) {
this.connector = connector;
}
getPeople() {
return this.connector.get(`/people/`);
}
getPerson(id) {
return this.connector.get(`/people/${id}/`);
}
}
connector.js
const API_ROOT = 'http://localhost:8080';
export class ApiConnector {
constructor() {
this.rp = rp;
this.loader = new DataLoader(this.fetch.bind(this));
}
fetch(urls) {
const options = {
json: true,
resolveWithFullResponse: true,
headers: {
'user-agent': 'Request-Promise',
},
};
return Promise.all(urls.map((url) => {
return new Promise((resolve) => {
this.rp({
uri: url,
...options,
}).then((response) => {
const body = response.body;
resolve(body);
}).catch((err) => {
console.error(err);
resolve(null);
});
});
}));
}
get(path) {
return this.loader.load(API_ROOT + path);
}
And the resolver in the schema would have something like:
const rootResolvers = {
Query: {
people(root, args, context) {
return context.People.getPeople();
},
person(root, { id }, context) {
return context.People.getPerson(id)
}
},
};
Until now I can get the first endpoint /people/ and a person from /people/id/. But how to change it to have a list of person? I'm not quite sure how/where should this code be.
Thanks a lot!

You could change your people resolver to something like the code bellow:
const rootResolvers = {
Query: {
people(root, args, context) {
const list = context.People.getPeople();
if (list && list.length > 0) {
return list.map(item => context.People.getPerson(item.id))
}
},
...
},
};
Ps: You said that you are using dataLoader, so i think your API calls is just being cached, but if it is not the case, you need to implement some cache to avoid calling same endpoints a lot times.

Related

How to pass variables into a graphql query?

I am using the storefront API to create a headless e commerce site. I am having an issue passing variables into the query - where if I hard code the values it seems to work but as soon as I try and pass variables into the query it fails and says
Argument 'lineItems' on InputObject 'CheckoutCreateInput' has an invalid value ([[object, Object]]). Expected type '[CheckoutLineItemInput!]'
I am pretty sure the problem has to do with the passing of the variables becuase I have tried most things - and come to this conclusion.
Here is the query and action function
export const createCheckout = async (items: IProductVariant[]) => {
const query = `
mutation {
checkoutCreate(input:
lineItems: ${items}
}) {
checkout {
id
webUrl
lineItems(first: 5) {
edges {
node {
title
quantity
}
}
}
}
}
}
`;
export const action: ActionFunction = async ({request}) => {
// get the form data from the POST
const formData = await request.formData()
const id = Array.from(formData.getAll('id'))
const quantity = Array.from(formData.getAll('quantity'))
let items = id.map((item , idx) => {
const newObj: IProductVariant = {}
newObj["variantId"] = item as string
newObj["quantity"] = Number(quantity[idx]) as number
return newObj
})
const res = await createCheckout(items)
return {res}
}
I think i need to modify the query to take a variable ?
Hmm.. I'm not sure that's how you call the GraphQL end point.
https://shopify.dev/api/storefront/2022-01/mutations/checkoutCreate
import Shopify from '#shopify/shopify-api';
const client = new Shopify.Clients.Storefront('your-development-store.myshopify.com', storefrontAccessToken);
// define your mutation (note how you define the input type first, then call
// the mutation with the input variable
const query =
`mutation checkoutCreate($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout {
id
webUrl
lineItems(first: 5) {
edges {
node {
title
quantity
}
}
}
}
}
}`;
// define the input data (this is from the docs, pass what is needed here)
const input = {
{
"allowPartialAddresses": true,
"buyerIdentity": {
"countryCode": ""
},
"customAttributes": [
{
"key": "",
"value": ""
}
],
"email": "",
"lineItems": [
{
"customAttributes": {
"key": "",
"value": ""
},
"quantity": 1,
"variantId": ""
}
],
"note": "",
"presentmentCurrencyCode": "",
"shippingAddress": {
"address1": "",
"address2": "",
"city": "",
"company": "",
"country": "",
"firstName": "",
"lastName": "",
"phone": "",
"province": "",
"zip": ""
}
};
// this is where you call the API passing in the query and variables
const data = await client.query({
data: {
"query": query,
"variables": {
"input": input,
"queueToken": queueToken
},
},
});

shorten the graphql field value

I am trying to fetch image path in an alias field using graphql, I am able to get output like this:
Output:
{
"data": {
"leaders": [
{
"partyImg": {
"image": {
"url": "/uploads/17a5f020cc974679ac52e56a22b74dd6.png"
}
}
},
{
"partyImg": {
"image": {
"url": "/uploads/70bd673d41654058847e39c14cda5fef.png"
}
}
},
{
"partyImg": {
"image": {
"url": "/uploads/c54a0ace0bb34da3985c67945b1d0bf0.png"
}
}
}
]
}
}
When I used the following graphql code:
Input:
query Leaders{
leaders{
partyImg: party{image: Image{ url }},
}
}
The output I am trying to get is:
Expected output:
{
"data": {
"leaders": [
{
"partyImg": "/uploads/17a5f020cc974679ac52e56a22b74dd6.png"
},
{
"partyImg": "/uploads/70bd673d41654058847e39c14cda5fef.png"
},
{
"partyImg": "/uploads/c54a0ace0bb34da3985c67945b1d0bf0.png"
}
]
}
}
Please help me to prepare the graphql input which could generate the expected output.
It appears the graphql schema prevents you from getting the data in the format that you want. Based on your input query, I expect the schema looks something like this:
query {
leaders: [Leader]
}
type Leader {
party: Party
}
type Party {
Image: Image
}
type Image {
url: String
}
To get the data in the format that you want, you would need a schema that looks more like:
query {
leaders: [Leader]
}
type Leader {
party: Party,
imageUrl: String
}
Then you could do:
query Leaders {
leaders {
partyImg: imageUrl
}
}
I assume you don't control the schema, so you would have to do post processing. If you are using javascript, the following could work for the above output as a simple mapping exercise.
(function() {
var output = {
"data": {
"leaders": [{
"partyImg": {
"image": {
"url": "/uploads/17a5f020cc974679ac52e56a22b74dd6.png"
}
}
},
{
"partyImg": {
"image": {
"url": "/uploads/70bd673d41654058847e39c14cda5fef.png"
}
}
},
{
"partyImg": {
"image": {
"url": "/uploads/c54a0ace0bb34da3985c67945b1d0bf0.png"
}
}
}
]
}
};
var transformed = {
data: {
leaders: output.data.leaders.map(function flattenUrl(item) {
return {
partyImg: item.partyImg.image.url
};
})
}
}
document.getElementById('transformedOutput').innerHTML = JSON.stringify(transformed);
}());
<div id="transformedOutput"></div>
If you are the author of this graphql schema, you can structure it in whatever way makes the most sense to your applications and/or consumers.

Creating a custom adapter in Botframework for Actions on Google

I'm currently writing a custom adapter in Typescript to connect Google Assistant to Microsoft's Botframework. In this adapter I'm attempting to capture the Google Assistant conversation object through a webook call and change it using my bot.
At this moment the only thing that my bot is doing is receive the request from Actions on Google and parsing the request body into an ActionsOnGoogleConversation object. After this I call conv.ask() to try a simple conversation between the two services.
Api Endpoint:
app.post("/api/google", (req, res) => {
googleAdapter.processActivity(req, res, async (context) => {
await bot.run(context);
});
});
Adapter processActivity function:
public async processActivity(req: WebRequest, res: WebResponse, logic: (context: TurnContext) => Promise<void>): Promise<void> {
const body = req.body;
let conv = new ActionsSdkConversation();
Object.assign(conv, body);
res.status(200);
res.send(conv.ask("Boo"));
};
When I try to start the conversation I get the following error in the Action on Google console.
UnparseableJsonResponse
API Version 2: Failed to parse JSON response string with
'INVALID_ARGUMENT' error: "availableSurfaces: Cannot find field." HTTP
Status Code: 200.
I've already checked the response and I can find a field called availableSurfaces in the AoG console and when I call my bot using Postman.
Response:
{
"responses": [
"Boo"
],
"expectUserResponse": true,
"digested": false,
"noInputs": [],
"speechBiasing": [],
"_responded": true,
"_ordersv3": false,
"request": {},
"headers": {},
"_init": {},
"sandbox": false,
"input": {},
"surface": {
"capabilities": [
{
"name": "actions.capability.MEDIA_RESPONSE_AUDIO"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.ACCOUNT_LINKING"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
}
]
},
"available": {
"surfaces": {
"list": [],
"capabilities": {
"surfaces": []
}
}
},
"user": {
"locale": "en-US",
"lastSeen": "2019-11-14T12:40:52Z",
"userStorage": "{\"data\":{\"userId\":\"c1a4b8ab-06bb-4270-80f5-958cfdff57bd\"}}",
"userVerificationStatus": "VERIFIED"
},
"arguments": {
"parsed": {
"input": {},
"list": []
},
"status": {
"input": {},
"list": []
},
"raw": {
"list": [],
"input": {}
}
},
"device": {},
"screen": false,
"body": {},
"version": 2,
"action": "",
"intent": "",
"parameters": {},
"contexts": {
"input": {},
"output": {}
},
"incoming": {
"parsed": []
},
"query": "",
"data": {},
"conversation": {
"conversationId": "ABwppHEky66Iy1-qJ_4g08i3Z1HNHe2aDTrVTqY4otnNmdOgY2CC0VDbyt9lIM-_WkJA8emxbMPVxS5uutYHW2BzRQ",
"type": "NEW"
},
"inputs": [
{
"intent": "actions.intent.MAIN",
"rawInputs": [
{
"inputType": "VOICE",
"query": "Talk to My test app"
}
]
}
],
"availableSurfaces": [
{
"capabilities": [
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
},
{
"name": "actions.capability.WEB_BROWSER"
}
]
}
]
}
Does anyone know what might be causing this? I personally feel that creating the ActionsSdkConversation could be the cause, but I've not found any examples of using Google Assistant without getting the conv object from the standard intent handeling setup.
So I managed to fix it by changing the approach, instead of having an API point that fits the structure of bot framework I changed it to the intenthandler of AoG.
Google controller
export class GoogleController {
public endpoint: GoogleEndpoint;
private adapter: GoogleAssistantAdapter;
private bot: SampleBot;
constructor(bot: SampleBot) {
this.bot = bot;
this.adapter = new GoogleAssistantAdapter();
this.endpoint = actionssdk();
this.setupIntents(this.endpoint);
};
private setupIntents(endpoint: GoogleEndpoint) {
endpoint.intent(GoogleIntentTypes.Start, (conv: ActionsSdkConversation) => {
this.sendMessageToBotFramework(conv);
});
endpoint.intent(GoogleIntentTypes.Text, conv => {
this.sendMessageToBotFramework(conv);
});
};
private sendMessageToBotFramework(conv: ActionsSdkConversation) {
this.adapter.processActivity(conv, async (context) => {
await this.bot.run(context);
});
};
};
interface GoogleEndpoint extends OmniHandler, BaseApp , ActionsSdkApp <{}, {}, ActionsSdkConversation<{}, {}>> {};
Once the conv object was in the adapter, I used the conv object to create an activity which the bot used to do its things and saved it in state using context.turnState()
Adapter ProcessActivity
public async processActivity(conv: ActionsSdkConversation, logic: (context: TurnContext) => Promise<void>): Promise<ActionsSdkConversation> {
const activty = this.createActivityFromGoogleConversation(conv);
const context = this.createContext(activty);
context.turnState.set("httpBody", conv);
await this.runMiddleware(context, logic);
const result = context.turnState.get("httpBody");
return result;
};
Bot
export class SampleBot extends ActivityHandler {
constructor() {
super();
this.onMessage(async (context, next) => {
await context.sendActivity(`You said: ${context.activity.text}`);
await next();
});
}
Once the bot send a response, I used the result to modify the conv object, save it and then return it in processActivity().
private createGoogleConversationFromActivity(activity: Partial<Activity>, context: TurnContext) {
const conv = context.turnState.get("httpBody");
if (activity.speak) {
const response = new SimpleResponse({
text: activity.text,
speech: activity.speak
});
conv.ask(response);
} else {
if (!activity.text) {
throw Error("Activity text cannot be undefined");
};
conv.ask(activity.text);
};
context.turnState.set("httpBody", conv);
return;
};
That resulted into a simple conversation between Google Assistant and Bot Framework.

How to populate only with content of getter

I have some problem in mongoose project.
I try to populate and use getter but not all data
But now all virtuals appear in document.
I'm using mongoose.Schema and mongoose.Model
Here is example of my test code
const GroupsSchema = schema({
title: String,
users: [{
type: schema.Types.ObjectId,
ref: 'Users'
}]
});
const UsersSchema = schema({
name: String,
avatar: String
}, {
toJSON: {
virtuals: true
}
});
class Users extends Model {
get name() {
return {
name: this.name
};
}
get avatar() {
return {
avatar: this.avatar
};
}
}
Populating document
const groups = await Groups.find({}).populate('users').exec();
My current result:
[
{
"_id": "5c9bb51626924f0a08aa8c3d",
"title": "GroupName"
"users": [
{
"_id": "5c8e37169fc1f9000f8c333b",
"name": "Jack",
"avatar": "avatar.jpg",
"name": {
"name": "Jack",
},
"avatar": {
"avatar": "avatar.jpg"
}
}
]
}
]
How can I populate document with content of only name getter?
Desired result:
[
{
"_id": "5c9bb51626924f0a08aa8c3d",
"title": "GroupName"
"users": [
{
"name": "Jack"
}
]
}
]

DynamoDB query shows UnepectedParameter

I'm trying to create a query in Lambda for a DynamoDB table.
I have followed the Instructions and examples from the aws developer handbook but still get an error
//+++++++++++++++++++++++++++++++++++++++++++
//The database Structure
"Table": {
"AttributeDefinitions": [
{
"AttributeName": "Score",
"AttributeType": "N"
},
{
"AttributeName": "gtin_RecBook",
"AttributeType": "S"
},
{
"AttributeName": "gtin_RefBook",
"AttributeType": "S"
}
],
"TableName": "Recommendation_test_2",
"KeySchema": [
{
"AttributeName": "gtin_RefBook",
"KeyType": "HASH"
},
{
"AttributeName": "gtin_RecBook",
"KeyType": "RANGE"
}
]
}
//+++++++++++++++++++++++++++++++++++++++++++
// The Lambda Code
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB({region:'eu-central-1', apiVersion:'2012-08-10'});
exports.handler = (event, context, callback) => {
const params = {
TableName: "Recommendation_test_2",
KeyConditionExpression: "gtin_RefBook = :a",
ExpressionAttributeValues: {
":a": "9783453471092"
}
};
dynamodb.query(params, function(err, data){
if (err) {
console.log(err);
callback(err);
} else{
console.log(data);
callback(null, data);
}
});
};
The database is populated and should provide some results.
However Lambda throws the following error:
"errorMessage": "There were 14 validation errors:\n* InvalidParameterType: Expected params.ExpressionAttributeValues[':a'] to be a structure"
Could you please tell me what to change to make the query work?
You need to use the DynamoDB Document Client or change the ExpresionAttributeValue to
ExpressionAttributeValues: {
":a": {
"S": "9783453471092"
}
}

Resources