0

I've reached out on the AWS forums but am hoping to get some attention here with a broader audience. I'm looking for any guidance on the following question.

I'll post the question below:

Hello, thanks in advance for any help.

I'm new to Amplify/GraphQL and am struggling to get mutations working. Specifically, when I add a connection to a Model, they never appear in the mock api generator. If I write them out, they say "input doesn't exist". I've searched around and people seem to say "Create the sub item before the main item and then update the main item" but I don't want that. I have a large form that has several many-to-many relationships and they all need to be valid before I can save the main form. I don't see how I can create every sub item and then the main.

However, the items are listed in the available data for the response. In the example below, addresses, shareholders, boardofdirectors are all missing in the input.

None of the fields with '@connection' appear in the create api as inputs. I'll take any help/guidance I can get. I seem to not be understanding something core here.

Here's my Model:

type Company @model(queries: { get: "getEntity", list: "listEntities" }, subscriptions: null) {
id: ID!
name: String!
president: String
vicePresident: String
secretary: String
treasurer: String
shareholders: Shareholder @connection
boardOfDirectors: BoardMember @connection
addresses: [Address]! @connection
...
}

type Address @model{
id: ID!
line1: String!
line2: String
city: String!
postalCode: String!
state: State!
type: AddressType!
}

type BoardMember @model{
id: ID!
firstName: String!
lastName: String!
email: String!
}

type Shareholder @model {
id: ID!
firstName: String!
lastName: String!
numberOfShares: String!
user: User!
}

----A day later----

I have made some progress, but still lacking some understanding of what's going on.

I have updated the schema to be:

type Company @model(queries: { get: "getEntity", list: "listEntities" }, subscriptions: null) {
id: ID!
name: String!
president: String
vicePresident: String
secretary: String
treasurer: String
...
address: Address @connection
...
}

type Address @model{
id: ID!
line1: String!
line2: String
city: String!
postalCode: String!
state: State!
type: AddressType!
}

I removed the many-to-many relationship that I was attempting and now I'm limited to a company only having 1 address. I guess that's a future problem. However, now in the list of inputs a 'CompanyAddressId' is among the list of inputs. This would indicate that it expects me to save the address before the company. Address is just 1 part of the company and I don't want to save addresses if they aren't valid and some other part of the form fails and the user quits.

I don't get why I can't write out all the fields at once? Going along with the schema above, I'll also have shareholders, boardmembers, etc. So I have to create the list of boardmembers and shareholders before I can create the company? This seems backwards.

Again, any attempt to help me figure out what I'm missing would be appreciated.

Thanks

--Edit-- What I'm seeing in explorer enter image description here

-- Edit 2-- Here is the newly generated operations based off your example. You'll see that Company takes an address Id now -- which we discussed prior. But it doesn't take anything about the shareholder. In order to write out a shareholder I have to use 'createShareholder' which needs a company Id, but the company hasn't been created yet. Thoroughly confused.

enter image description here

@engam I'm hoping you can help out the new questions. Thank you very much!

Thomas
  • 368
  • 1
  • 7
  • 19

1 Answers1

0

Here are some concepts that you can try out:

For the @model directive, try it out without renaming the queries. AWS Amplify gives great names for the automatically generated queries. For example to get a company it will be getCompany and for list it will be listCompanys. If you still want to give it new names, you may change this later.

For the @connection directive: The @connection needs to be set on both tables of the connection. Also if you want many-to-many connections you need to add a third table that handles the connections. It is also usefull to give the connection a name, when you have many connections in your schema.

Only Scalar types that you have created in the schema, standard schalars like String, Int, Float and Boolean, and AWS specific schalars (like AWSDateTime) can be used as schalars in the schema. Check out this link: https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html

Here is an example for some of what I think you want to achieve:

type Company @model {
   id: ID!
   name: String
   president: String
   vicePresident: String
   secretary: String
   treasurer: String
   shareholders: [Shareholder] @connection(name: "CompanySharholderConnection")
   address: Address @connection(name: "CompanyAdressConnection") #one to many example
   # you may add more connections/attributes ...
}

# table handling many-to-many connections between users and companies, called Shareholder.
type Shareholder @model {
   id: ID!
   company: Company @connection(name: "CompanySharholderConnection")
   user: User @connection(name: "UserShareholderConnection")
   numberOfShares: Int #or String
}

type User @model {
   id: ID!
   firstname: String
   lastname: String
   company: [Shareholder] @connection(name: "UserShareholderConnection")
   #... add more attributes / connections here
}

# address table, one address may have many companies
type Address @model {
  id: ID!
  street: String
  city: String
  code: String
  country: String
  companies: [Company] @connection(name: "CompanyAdressConnection") #many-to-one connection
}

Each of this type...@model generates a new dynamoDB table. This example will make it possible for u to create multiple companies and multiple users. To add users as shareholders to a company, you only need to create a new item in the Shareholder table, by creating a new item with the ID in of the user from the User table and the ID of the company in the Company table + adding how many shares.

Edit

Be aware that when you generate a connection between two tables, the amplify cli (which uses cloudformation to do backend changes), will generate a new global index to one or more of the dynamodb tables, so that appsync can efficient give you data.

Limitations in dynamodb, makes it only possible to generate one index (@connection) at a time, when you edit a table. I think you can do more at a time when you create a new table (@model). So when you edit one or more of your tables, only remove or add one connection at a time, between each amplify push / amplify publish. Or else cloudformation will fail when you push the changes. And that can be a mess to clean up. I have had to, multiple times, delete a whole environment because of this, luckily not in a production environment.

Update

(I also updated the Address table in the schema with som values); To connect a new address when you are creating a new company, you will first have to create a new address item in the Address table in dynamoDb.

The mutation for this generated from appsync is probably named createAddress() and takes in a createAddressInput.

After you create the address you will recieve back the whole newly createdItem, including the automatically created ID (if you did not add one yourself).

Now you may save the new company that you are creating. One of the attributes the createCompany mutation takes is the id of the address that you created, probably named as companyAddressId. Store the address Id here. When you then retrieves your company with either getCompany or listCompanys you will get the address of your company.

Javascript example:

const createCompany = async (address, company) => {
   // api is name of the service with the mutations and queries
   try {
      const newaddress = await this.api.createAddress({street: address.street, city: address.city, country: address.country});
      const newcompany = await this.api.createCompany({
        name: company.name,
        president: company.president,
        ...
        companyAddressId: newaddress.id
      })
   } catch(error) {
      throw error
   }
   
  
}

// and to retrieve the company including the address, you have to update your graphql statement for your query:

const statement = `query ListCompanys($filter: ModelPartiFilterInput, $limit: Int, $nextToken: String) {
   listCompanys(filter: $filter, limit: $limit, nextToken: $nextToken) {
      __typename
      id
      name
      president
      ...
      address {
         __typename
         id
         street
         city
         code
         country
      }
   }

}

`

AppSync will now retrive all your company (dependent on your filter and limit) and the addresses of those companies you have connected an address to.

Edit 2 Each type with @model is a referance to a dynamoDb table in aws. So when you are creating a one-to-many relationship between two tables, when both items are new you first have to create the the 'many' in the one-to-many realationships. In the dynamoDb Company tables when an address can have many companies, and one company only can have one address, you have to store the id (dynamoDB primary key) for the address on the company. You could of course generate the address id in frontend, and using that for the id of the address and the same for the addressCompanyId in for the company and use await Promise.all([createAddress(...),createCompany(...)) but then if one fails the other one will be created (but generally appsync api's are very stable, so if the data you send is correct it won't fail).

Another solution, if you generally don't wont to have to create/update multiple items in multiple tables, you could store the address directly in the company item.

type Company @model {
  name: String
  ...
  address: Address # or [Address] if you want more than one Address on the company
}
type Address {
   street: String
   postcode: String
   city: string
}

Then the Address type will be part of the same item in the same table in dynamoDb. But you will loose the ability to do queries on addresses (or shareholders) to look up a address and see which companies are located there (or simulary look up a person and see which companies that person has a share in). Generally i don't like this method because it locks your application to one specific thing and it's harder to create new features later on.

As far as I'm aware of, it is not possible to create multiple items in multiple dynamoDb tables in one graphql (Amplify/AppSync) mutation. So async await with Promise.all() and you manually generate the id attributes frontendside before creating the items might be your best option.

Engam
  • 1,051
  • 8
  • 17
  • Thank you very much for the response! I ended up just using your example to get things working and I'm seeing the same thing in the explorer. The list of inputs is taking an addressid, not an actual address object. I added a screenshot showing what I see. I think I'm expecting "address: { "address": "1234 st. City State Zip"} as an input of createCompany -- not an id that would imply the address already exists. – Thomas Jan 21 '21 at 02:18
  • Could it be that I’m thinking too much in terms of a RDS? Maybe rather than separate tables would the nosql design be better to flatten out the data into a single key/value pair? – Thomas Jan 21 '21 at 04:41
  • Or would this problem get easier if I were to use graphql with rds instead of dynamo? – Thomas Jan 21 '21 at 04:44
  • If I understand you correctly, in my example, when your adding a new address to a company, you first have to create the address Item in the address table in dynamodb. The name and input of generated graphQl mutation you get from amplify is probably createAddress(createAddressInput). When you have successfully created the new address, you need to use the id for that new item (address.id) and then store only the id from the address on the new company item (probably named as companyAddressId). I will add more detailed explanation to my answer – Engam Jan 22 '21 at 21:16
  • Actually, I’m trying to create the address at the same time as the company — not prior. Like the json for address would be nested under the company – Thomas Jan 22 '21 at 21:30
  • look at the update to the answer, if you are storing it in multiple databases so that it can be used multiple times, you have to create the address item first. – Engam Jan 22 '21 at 21:42
  • Ok thank you again! One more question — if the address gets created, but something later in the transaction fails, or even the company fails... should I go delete what had been created? – Thomas Jan 22 '21 at 21:44
  • yes, that sounds like a good idea, but you don't have to. It could be used again if another company have the same address or when you try to create the same company again. – Engam Jan 22 '21 at 21:46
  • I marked yours as the answer. I was trying to avoid creating things before the company ultimately was created but I can see how this makes sense. Thanks again! – Thomas Jan 22 '21 at 21:48
  • sorry but something still isn't clicking. After playing around with your example, in order to add a Shareholder I need the Company to exist. Now it seems like I need to 1) Create the entity 2) create the addresses 3) create the shareholders 4) (create other necessary relations) 5) go back and write them all back to the entity. Can you help me understand why I can't just write it all out at once? I'm struggling with the fact to handle these relationships I have to create them before the company is even created. What am I not getting? Thanks – Thomas Jan 24 '21 at 20:31
  • I have added a 2nd edit with what I'm seeing to hopefully help explain my confusion. – Thomas Jan 24 '21 at 20:37
  • (I typed entity but meant company above) – Thomas Jan 24 '21 at 21:06
  • For more context as to where I'm coming from: This is a big form on the front end react application that currently calls to Rails. I'm trying to migrate off Rails to serverless, so when the schema created 'CreateCompany' for me I expected it to be similar to the POST /company route with a json load sent to Rails. What this is turning out to be is a mystery to me to get a brand new Company created. – Thomas Jan 24 '21 at 21:35
  • Hoping you can follow up on this. Thank you! – Thomas Jan 27 '21 at 16:17
  • Sorry for the late reply. I added some more content in 'edit2' with some explenation and an options you can consider. I'm not entirely sure if that explains your question, so let me know if you have more questions :) – Engam Jan 30 '21 at 19:06
  • also if you havn't noticed, when a the amplify cli is finished with amplify push, it has generated som files for you in your src folder, i'm using angular not react so the files might be different, but it should have generated a mutation and a query file and an API.service.ts file (but the last one might be angular specific, since it's typescript) where you see examples of what mutations and queries you can make and what inputs they need. – Engam Jan 30 '21 at 19:14
  • So to save a complex form, something like this would be acceptable? createCompany, createAddress, createShareholder, create....whatever else..., then finally updateCompany with all the generated IDs? That can be a ton of calls rather than just 1 -- and that's what the pattern *should* be? Thanks so much for all your advice. – Thomas Jan 30 '21 at 19:16
  • yes i think so, you might be able to optimize by generating the id's manually and providing them to the item you are created so that you can minimize calls to as few as possible. The use cases I have used amplify mostly for, usually have been more staged, where a user signs up, then they gets taken to a new page where they provide some information and so on. So I don't have had this problem so much. But when your data is generated you will get really powerfull and flexible queries that you can make. Expecially if you add @searchable later on (but beware that searchable can be expensive). – Engam Jan 30 '21 at 19:27