aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
89 stars 79 forks source link

Unique Constraints #286

Open jcbdev opened 4 years ago

jcbdev commented 4 years ago

Is your feature request related to a problem? Please describe. I have lots of scenarios where I require unique values on non-primary key fields. Especially for public apps where you might want to not allow two people with the same phone number or email address and so forth

Describe the solution you'd like It would be possible to add a directive such as @unique to a field or @unique(fields: ["email", "phone"]) to the type

First of all this would cause resources to be added:

when this is added you can then change the VTL generated to do the following:

This would then only succeed if the field is successful. You would also need the update and delete templates to follow the equivalent logic

If the "value" is a complex type you could potentially use a hash of the value

Describe alternatives you've considered I have used custom lambda resolvers and custom resolvers to implement this myself but it tedious for something I have to do over and over again

jamesone commented 4 years ago

Does anyone know if this is currently being developed by the amplify team?

half2me commented 4 years ago

I've only started out with Amplify, but it seems like a really basic requirement. There are plenty of times you would need to make sure a field(s) is unique. Having a directive like @unique seems to make sense.

WillDent commented 4 years ago

This is the best I could find specific to what you're asking for.

https://medium.com/@fullstackpho/ensuring-usernames-are-unique-in-your-aws-amplify-app-6fff963274

connelhooley commented 3 years ago

A real shame this hasn't been picked up, the medium link posted above works by running a Lambda that validates the data before (or just after?) it is inserted into the database. Uniqueness should be handled by the data store so it is guaranteed to be enforced.

jcbdev commented 3 years ago

@WillDent I tried similar methods but the method I suggested works better. I have modified my VTL template to use the method and it works perfectly (and is not that diffficult to do). It just takes a long time to do for every template and field combination when we could easily add directive to the schema that changes the vtl template generation slightly

matansagee commented 3 years ago

+1

droidgeek1 commented 3 years ago

Even key field does not ensure uniqueness. I have following and duplicate email accepted even though I have indicated that as key. Worse part is that the backend corresponding dynamo table does not seem to take the duplicate record. So now, my client side sqlite is out of sync with dynamoDB. So I have to write code to check if the record exists..Sounds ugly unless someone can enlighten me please..

type AddUserToT @model @key(fields: ["userEmail"]) id: ID! email: ID!

mm87642 commented 3 years ago

+1

shizhaojingszj commented 3 years ago

+1

DerekFei commented 3 years ago

+1

graniczny commented 3 years ago

+1

Wyfy0107 commented 2 years ago

+1

JohnLamHK commented 2 years ago

+1

iltumio commented 2 years ago

+1

hackmajoris commented 2 years ago

+1

ilkerburakkurt commented 2 years ago

+1

acusti commented 2 years ago

i notice this issue has the graphql-transformer-v1 label on it, but i want this feature for an amplify project i am working on that is using graphql transformer v2. should i create a new issue for that, or should this issue have the graphql-transformer-v2 label?

nxia416 commented 2 years ago

+1

acusti commented 2 years ago

did the behavior of attribute_exists / attribute_not_exists dynamodb condition expressions change at some point? i was using attribute_not_exists in a CreateItem mutation (via condition: { field: { attributeExists: false } }), and i thought i had verified that it worked to ensure that the new item’s value for the specified field would be unique amongst all other items in the table, but i can verify that is not the case right now.

according to the dynamodb docs on comparison operators and functions, attribute_exists and attribute_not_exists are used to check only whether an item has or doesn’t have an attribute. it seems to have nothing to do with whether any other items in the table have or don’t have the same value for that attribute.

@jcbdev do you know if your technique of maintaining a UniqueConstraints table with the id of each item being a hash of table + fieldname + value still works? or maybe it only works because it’s being used as the primary key, and primary keys enforce uniqueness, so it has nothing to do with the attribute_not_exists(id) condition? in which case i suppose that condition passes because the item hasn’t been created yet so it doesn’t have an id? and lastly, have you added aDelete item for the previous value of the unique field on Update and Delete operations? it would be great to see a more complete example of how i could implement this manually by modifying the VTL templates.

Voyager-Two commented 2 years ago

This seems like major missing feature IMO.

For future readers: One work-around is to set unique field as primary key:

username: String! @primaryKey

You can also do something like this (e.g. Discord usernames Bob#0001):

discordUsername: String! @primaryKey(sortKeyFields: ["discordNumber"])
discordNumber: Int!
jcbdev commented 2 years ago

did the behavior of attribute_exists / attribute_not_exists dynamodb condition expressions change at some point? i was using attribute_not_exists in a CreateItem mutation (via condition: { field: { attributeExists: false } }), and i thought i had verified that it worked to ensure that the new item’s value for the specified field would be unique amongst all other items in the table, but i can verify that is not the case right now.

according to the dynamodb docs on comparison operators and functions, attribute_exists and attribute_not_exists are used to check only whether an item has or doesn’t have an attribute. it seems to have nothing to do with whether any other items in the table have or don’t have the same value for that attribute.

@jcbdev do you know if your technique of maintaining a UniqueConstraints table with the id of each item being a hash of table + fieldname + value still works? or maybe it only works because it’s being used as the primary key, and primary keys enforce uniqueness, so it has nothing to do with the attribute_not_exists(id) condition? in which case i suppose that condition passes because the item hasn’t been created yet so it doesn’t have an id? and lastly, have you added aDelete item for the previous value of the unique field on Update and Delete operations? it would be great to see a more complete example of how i could implement this manually by modifying the VTL templates.

@acusti It did when I was using it but yes i think its because the column is the primary key. The trick was to create a uniqueconstraints table where all the unique constraints are written to and not to write the unique constraints to the same table your data is in (the dynamodb put requests can go to different tables in the transaction). But I do not use amplify anymore. I rewrote the platform on cdk and have never looked back. I found aws amplify terrible at scaling and just couldn't get to work on any project that wasn't a toy project. Too many limitations, compromises and assumptions that I spent more time maintaining and working around amplify in the project than developing new features.

lewisdonovan commented 2 years ago

Hi all. Just stumbled across this and think I might have found the answer. If you look at the generated schemas in AppSync, you'll see the schemas for the condition expression objects as well. For example, the schema to create a condition on a String input is:

input ModelStringInput {
    ne: String
    eq: String
    le: String
    lt: String
    ge: String
    gt: String
    contains: String
    notContains: String
    between: [String]
    beginsWith: String
    attributeExists: Boolean
    attributeType: ModelAttributeTypes
    size: ModelSizeInput
}

The first item in that schema ne means not equal to, which can be used to run a uniqueness comparison when putting an item. An example mutation would look like this:

mutation CreateTodoGraphQL($input: CreateTodoInput!, $condition: ModelTodoConditionInput) {
  createTodoGraphQL(input: $input, condition: $condition) {
    id
    name
    priority
    completedAt
  }
}

and the input variables would look like:

{
  "input": {
    "name": "test",
    "priority": "1",
    "completedAt": "2022-09-11T09:00:00.000Z"
  },
  "condition": { 
    "name": {
      "ne": "test"
    }
  }
}

If an item already exists with the name test then the condition expression would fail, and the item would not be added to the DB.

Meerkov commented 1 year ago

@lewisdonovan , isn't the condition specified on the client side? In other words, a client can cheat your system by removing that part of the request.

lewisdonovan commented 1 year ago

@lewisdonovan , isn't the condition specified on the client side? In other words, a client can cheat your system by removing that part of the request.

Yes, absolutely. But AppSync can be run on the back-end too. You could create a custom mutation, pass name as a parameter (rather than a filter variable), map the mutation to a Lambda and run the filter logic there.

gavmck commented 1 year ago

@lewisdonovan I'm not sure the condition method works, just given it a spin and it still lets me create/update the user.