AppSync: pipeline resolver #return null result - aws-appsync

I'm successfully using a pipeline resolver to persist a parent/child relationship, except when the list of child items is empty and I #return early.
I'm guessing the issue is around my response mappers and use of $ctx.prev vs $ctx.result but I can't figure it out.
The pipeline looks like this:
BEFORE template: {}
Function 1:
request = PutItem the parent
response = $utils.toJson($ctx.result)
Function 2:
request = TransactWriteItems (foreach UpdateItem) the children
response = $utils.toJson($ctx.prev.result)
AFTER template: $utils.toJson($ctx.prev.result)
When I call the mutation with
{"parentAttribute":"foo", "children": [{"childAttribute": "bar"}]}
I get a good response like:
{
"data": {
"createFoo": {
"parentAttribute": "foo",
"children": [
{
"childAttribute": "bar"
}
]
}
}
}
If no children, Function 2 request mapper does #return to avoid "TransactWriteItems must have at least one operation" error.
In this scenario I am hoping for the above response to the mutation, just with children: []
Instead, I get:
{
"data": {
"createFoo": null
}
}
The data has been written correctly; if I query it I get back the parent with empty list of children.
How do I get this pipeline to execute so that it returns the combined parent+child data whether the child array is populated or not?
Detail
The schema is something like:
type Foo {
id: String!
attr1: String
bars: [Bar]
}
type Bar {
id: String!
attr2: String
}
type Mutation {
createFoo(foo: Foo): Foo
}
And a dynamodb representation like this:
pk
sk
attr1
attr2
FOO#1
METADATA#FOO#1
Lorem
FOO#1
BAR#1
Ipsum
While the pipeline looks like:
before.vtl
{}
createParent-request.vtl
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
"pk" : $util.dynamodb.toDynamoDBJson(...),
"sk" : $util.dynamodb.toDynamoDBJson(...)
},
"attributeValues" : {
"data" : $util.dynamodb.toDynamoDBJson(...)
}
}
createParent-response.vtl
#if($ctx.error)
$utils.error($ctx.error.message, $ctx.error.type)
#end
$utils.toJson($ctx.result)
createChildren-request.vtl
#if($ctx.args.fooInput.children.size() > 0)
{
"version": "2018-05-29",
"operation": "TransactWriteItems",
"transactItems": [
#foreach( $child in $ctx.args.fooInput.children )
{
"table": "${table}",
"operation": "UpdateItem",
"key": {
"pk" : $util.dynamodb.toDynamoDBJson(...),
"sk" : $util.dynamodb.toDynamoDBJson(...)
},
"update": {
"expression": "SET #data = :data",
"expressionNames": {
"#data": "data"
},
"expressionValues": {
":data":
$util.dynamodb.toDynamoDBJson(...)
}
}
}
#if( $foreach.hasNext ),#end
#end
]
}
#else
#return
#end
createChildren-response.vtl
#if($ctx.error)
$utils.error($ctx.error.message, $ctx.error.type)
#end
$utils.toJson($ctx.prev.result)
after.vtl
#if($ctx.error)
$utils.error($ctx.error.message, $ctx.error.type)
#end
$utils.toJson($ctx.prev.result)

I figured it out. For the expected behaviour, one needs the 'after' mapper to return the necessary JSON to populate the overall mutation response. In my example above, after.vtl needs to return a parent and nothing else matters (in particular, the result of the individual function response mappers).
I ended up putting the output of the 'create parent' operation into ctx.stash then returning ctx.stash in after.vtl, setting the other resolvers to {}.
Note that, if your response has subtypes (with their own resolvers) and you return it sparse, AppSync will call the resolver. In the context of my example, it's enough to return the parent without any children and then the normal query resolver for "get children of a parent" will execute to populate the final response.

Related

how can I set the value of objectId to another property different that _id when creating a document?

I'm trying to create an object that looks like this:
const userSettingsSchema = extendSchema(HistorySchema, {
user: //ObjectId here,
key:{type: String},
value:{type: String}
});
this is the post method declared in the router
app.post(
"/api/user/settings/:key",
userSettingsController.create
);
and this is the method "create":
async create(request, response) {
try {
const param = request.params.key;
const body = request.body;
console.log('body', body)
switch (param) {
case 'theme':
var userSettings = await UserSettings.create(body) // user:objecId -> missing...
response.status(201).send(userSettings);
break;
}
} catch (e) {
return response.status(400).send({ msg: e.message });
}
}
I don't know how to assign the value of ObjectId to the user property, because ObjectId is generate when the doc is created, thus, I can not do this: userSettings.user = userSettings._id, because the objectr is already. I only manage to get something like this created:
{
"_id": "60c77565f1ac494e445cccfe",
"key": "theme",
"value": "dark",
}
But it should be:
{
"user": "60c77565f1ac494e445cccfe",
"key": "theme",
"value": "dark",
}
_id is the only mandatory property of a document. It is unique identifier of the document and you cannot remove it.
If you provide document without _id the driver will generate one.
You can generate ObjectId as
let id = mongoose.Types.ObjectId();
and assign it to as many properties as you want.
You can even generate multiple different ObjectIds for different properties of the same document.
Now, you don't really need to assign ObjectId to "user" property. _id will do just fine. In fact it is most likely you don't need user's settings in separate collection and especially as multiple documents with single key-value pair.
You should be good by embedding "settings" property to your "user" collection as a key-value json.

Mongoose - can't insert subDocuments of a Dictionary Type

I have a Mongoose schema for the document Company, that has several fields. One of these (documents_banks) is a "free" field, of dictionary type, because I don't know the names of the keys in advance.
The problem is that, when I save the document (company.save()) even if the resulting saved document has the new sub_docs, in the DB no new sub_docs are actually saved.
var Company = new Schema({
banks: [{ type: String }], // array of Strings
documents_banks: {} // free field
});
Even if documents_banks is not restricted by the Schema, it will have this structure (in my mind):
{
"bank_id1": {
"doc_type1": {
"url": { "type": "String" },
"custom_name": { "type": "String" }
},
"doc_type2": {
"url": { "type": "String" },
"custom_name": { "type": "String" }
}
},
"bank_id2": {
"doc_type1": {
"url": { "type": "String" },
"custom_name": { "type": "String" }
}
}
}
But I don't know in advance names of keys bank_id neither doc_type, so I used the Dictionary type (documents_banks:{}).
Now, this below is the function I use to save new sub_docs in documents_banks. The same logic I always use to save new sub_docs.. Anyway this time, it seems saved, but it's not.
function addBankDocument(company_id, bank_id, doc_type, url, custom_name) {
// retrieve the company document
Company.findById(company_id)
.then(function(company) {
// create empty sub_docs if needed
if (!company.documents_banks) {
company.documents_banks = {};
}
if (!company.documents_banks[bank_id]) {
company.documents_banks[bank_id] = {};
}
// add the new sub_doc
company.documents_bank[bank_id][doc_type] = {
"url": url,
"custom_name": custom_name
};
return company.save();
})
.then(function(saved_company) {
// I try to check if the new obj has been saved
console.log(saved_company.documents_bank[bank_id][doc_type]);
// and it actually prints the new obj!!
});
}
The saved_company returned by the .save() actually has the new sub_docs, but if I check the DB there is not the new sub_doc! I can save just the first one, all the others are not stored.
So, the console.log() always print the new sub_docs, but actually in the DataBase, just the first sub_doc is saved, not the others. So at the end, saved_company always has 1 sub_doc, the first one.
It seems very strange to me, since saved_company has the new sub_docs. What can be happened?
This below is a real extract from by DB, and it will contains forever just the sub_doc "doc_bank#1573807781414", others will be not present in the DB.
{
"_id": "5c6eaf8efdc21500146e289c", // company_id
"banks": [ "MPS" ],
"documents_banks": {
"5c5ac3e025acd98596021a9a": // bank_id
{
"doc_bank#1573807781414": // doc_type
{
"url": "http://...",
"custom_name": "file1"
}
}
}
}
Versions:
$ npm -v
6.4.1
$ npm show mongoose version
5.7.11
$ node -v
v8.16.0
It seems that, since mongoose doesn't know the exact model of the subdoc, it can't know when it changes. So I have to use markModified to notify changes of the "free field" (also known as dictionary or MixedType) with this:
company_doc.documents_banks["bank_id2"]["doc_type3"] = obj; // modify
company_doc.markModified('documents_banks'); // <--- notify changes
company_doc.save(); // save changes
As I understood, markModified force the model to 'update' that field during the save().

Appsync missing resolver

I'm using AWS appsync + DynamoDB.
The problem: I created the new field 'rating' in my 'Users' schema:
type Users {
id: ID!
first: String!
last: String!
rating: String #<----The new field
}
AppSync created all the resources and I can create new records with Mutations and that works like a charm.
mutation createUsers{
createUsers(input:{
first:"John"
last:"Smith"
rating:"B" #<---Writing new field without problem
}){
id
first
last
rating #<---Confirming that is recorded in DynamoDB
}
}
The problem is that I can't figure out how to write the resolver to make the following query work.
query{
queryUsersByRating(rating: "B"){
items{
id
username
rating
}
}
}
The result is this:
{
"data": {
"queryUsersByRating": null
}
}
The problem is clearly identified here under "Missing Resolver", but there's no clear solution.
I tried attaching the following Resolver directly in AppSync interface but is not working:
{
"version" : "2017-02-28",
"operation" : "Query",
"query" : {
"expression": "rating = :rating",
"expressionValues" : {
":rating" : $util.dynamodb.toDynamoDBJson($ctx.args.rating)
}
}
}
Any help would be appreciated, THANKS!
You don't have to write your own resolver for querying by rating, Appsync wrapped all the fields inside filter.
query{
queryUsersByRating(filter: {rating: "B"}){
items{
id
username
rating
}
}
}

Relay/Mongodb Error - Cannot Read Property of Undefined

I have this test relay-starter-kit project with an autocomplete search form that renders quotes to the browser. When I use the search form, I get an error in the browser saying "app.js:8284 Uncaught TypeError: Cannot read property 'edges' of undefined". I cant understand why its populating the search fields the first time but not on typing in the search form. Of note, if you hard code the searchTerm variable, it will render the search results. In other words, It can read the edges when its hard coded. Any guidance on this would be great. Thanks.
The schema is here
This is the component that the valid graphql query for the search term wont render to.
import React from 'react';
import Relay from 'react-relay';
import { debounce } from 'lodash';
import SearchForm from '../search-form';
import Quote from '../quote';
class App extends React.Component {
constructor(props) {
super(props)
this.search = debounce(this.search.bind(this), 300)
}
search(searchTerm) {
this.props.relay.setVariables({ searchTerm });
}
render() {
return (
<div className="quotes-library">
<SearchForm searchAction={this.search} />
<div className="quotes-list">
{this.props.viewer.quotes.edges.map(edge =>
<Quote key={edge.node.id} quote={edge.node} />
)}
</div>
</div>
)
}
}
export default Relay.createContainer(App, {
initialVariables: {
searchTerm: '',
},
fragments: {
viewer: () => Relay.QL`
fragment on User {
quotes(first:100, searchTerm: $searchTerm) {
edges {
node {
id
${Quote.getFragment('quote')}
}
}
}
}
`,
},
});
Update: This is the query as shown in Chrome DevTools network tab. Note the 'p' input to the search form is being queried.
query App_ViewerRelayQL($id_0:ID!) {
node(id:$id_0) {
...F2
}
}
fragment F0 on Quote {
id,
likesCount
}
fragment F1 on Quote {
text,
author,
id,
...F0
}
fragment F2 on User {
_quotes3UfnML:quotes(first:100,searchTerm:"pri") {
edges {
node {
id,
...F1
},
cursor
},
pageInfo {
hasNextPage,
hasPreviousPage
}
},
id
}
Adding console.log to render() shows the searchTerm input:
app.js:17
{
"viewer": {
"__dataID__": "VXNlcjox",
"__status__": 4
},
"relay": {
"pendingVariables": null,
"route": {
"name": "AppHomeRoute",
"params": {},
"queries": {}
},
"variables": {
"searchTerm": "pri"
}
}
}
The following error occurs at line 24 in app.js, which is {this.props.library.quotes.edges.map(edge =>
<Quote key={edge.node.id} quote={edge.node} />
)}:
app.js:8285 Uncaught TypeError: Cannot read property 'edges' of undefined
at App.render (app.js:8285)
at app.js:43197
at measureLifeCyclePerf (app.js:42476)
at ReactCompositeComponentWrapper._renderValidatedComponentWithoutOwnerOrContext (app.js:43196)
at ReactCompositeComponentWrapper._renderValidatedComponent (app.js:43223)
at ReactCompositeComponentWrapper._updateRenderedComponent (app.js:43147)
at ReactCompositeComponentWrapper._performComponentUpdate (app.js:43125)
at ReactCompositeComponentWrapper.updateComponent (app.js:43046)
at ReactCompositeComponentWrapper.receiveComponent (app.js:42948)
at Object.receiveComponent (app.js:35341)
UPDATE 2:
So check this out. I'm doing something wrong with my { ObjectID }. I pulled a user and quote from the db and there is an '_id' but no 'id' property on the object. This is the output:
> db.users.find({})
{ "_id" : ObjectId("586253169465191cb812066c"), "name" : "me", "id" : ObjectId("586253169465191cb812066c"), "errors" : [ ] }
> db.quotes.find({})
{ "_id" : ObjectId("586693333ff93f3581c0ca05"), "text" : "Hi Prisc", "author" : "H. Jackson Brown", "likesCount" : 24 }
{ "_id" : ObjectId("586693333ff93f3581c0ca06"), "text" : "If opportunity doesn't knock, build a door", "author" : "Milton Berle", "likesCount" : 2 }
{ "_id" : ObjectId("586693333ff93f3581c0ca07"), "text" : "Try to be a rainbow in...", "author" : "Maya Angelou" }
If I log the id from the globalIdFetcher(), the logged id for the Jackson Brown Quote object show two id's as expected but they are both the same and different from the one in the db. Output in console is:
{
"viewer": {
"__dataID__": "VXNlcjo=",
"quotes": {
"__dataID__": "client:6068631311_first(100),searchTerm()",
"edges": [
{
"__dataID__": "client:client:6068631311:UXVvdGU6NTg2NjkzMzMzZmY5M2YzNTgxYzBjYTA1",
"node": {
"__dataID__": "UXVvdGU6NTg2NjkzMzMzZmY5M2YzNTgxYzBjYTA1",
"id": "UXVvdGU6NTg2NjkzMzMzZmY5M2YzNTgxYzBjYTA1",
"__fragments__": {
"1::client": [
{
"showLikes": false
}
]
}
}
},
Any ideas on fixing this syntax?
It's very difficult to solve the problem with the information provided in question. As you requested guidance, here's my suggestions :)
I cant understand why its populating the search fields the first time but not on typing in the search form.
Because Relay fetches the result for the initial value of searchTerm, which is an empty string in your case. You have to deal with this case on the server side (by checking the input searchTerm) and client side (by checking if the current value of searchTerm is empty, for example).
When you type in search form, search() isn't called and searchTerm is not updated. Check SearchForm component.
When I use the search form, I get an error in the browser saying "app.js:8284 Uncaught TypeError: Cannot read property 'edges' of undefined".
Start debugging what the server side returns for quotes field under viewer GraphQL object type. What's the output of db.collection('quotes').find(findParams).toArray() in your schema? In addition, put a console.log(JSON.stringify(this.props, null, 4)) in render() function of App class. Check what you see. You can also inspect the HTTP request and response on the Chrome DevTools Network tab. Filter by graphql.
Solution was to take the Quote type out of the nodeInterface so only running the top level object through the nodeInterface, let relay generate the id for the objectId from the GraphQLID or fromGlobalId as defined in the individual itemType.

sub iterator object in mongodb C driver

Here's the structure part of my collection :
{
_id: {
id:"6a6ca923517f304900badd98",
target:"00badd6a6ca923517f304998e4df"
},
...
}
The use of :
if(bson_find(iterator, mongo_cursor_bson(cursor), "_id")){
bson_iterator_subiterator(iterator, sub);
id = (char*)bson_iterator_string(sub);
}
is "working" but in reality simply returns me the result of the first field of the array found...
How to recover precisely the value of the "id" or "target" field please ?
You can also use bson_iterator_more and bson_iterator_next upon the sub-iterator(It was still an iterator).
try this:
if(bson_find(iterator, mongo_cursor_bson(cursor), "_id"))
{
bson_iterator_subiterator(iterator, sub);
while(bson_iterator_more(sub))
{
if (bson_iterator_next(sub) != BSON_EOO)
{
printf("%s: %s\n", bson_iterator_key(sub), bson_iterator_string(sub));
}
}
}