AWS Amplify and GraphQL Interfaces - interface

How would you deal with interfaces and using them for connections in a data model using the AWS Amplify Model Transforms?
interface User #model {
id: ID
email: String
created: AWSTimestamp
}
type ActiveUser implements User {
id: ID
first: String
last: String
email: String
created: AWSTimestamp
}
type InvitedUser implements User {
id: ID
email: String
created: AWSTimestamp
invitedBy: String
}
type Team #model {
users: [User] #connection
}
It seems like my choices are to put #model on the types but then I get separate Dynamo tables and queries on the Query once amplify update api is run.
Can the transformer support interfaces as documented here: https://docs.aws.amazon.com/appsync/latest/devguide/interfaces-and-unions.html
I also found some support tickets, but was wondering if there was anything out there that enabled this feature. Here are the support tickets I found:
https://github.com/aws-amplify/amplify-cli/issues/1037
https://github.com/aws-amplify/amplify-cli/issues/202

You only use #connection to link two databases together (which must be made from type and not interface), so if you don't want to do that then just get rid of the #connection and the Team database will simply have users be of type [User]. I am not entirely what you want to do but I would do something like:
type User #model {
id: ID
first: String!
last: String!
email: String!
created: AWSTimestamp
isActive: boolean
invitedBy: String
team: Team #connection(name: "UserTeamLink")
}
type Team #model {
users: [User!] #connection(name: "UserTeamLink")
}
Where the fields first, last, and email are required when creating a new user, and you can distinguish between an active user with a boolean, and when you query the User database it returns the Team item from the Team database as well (I am guessing you want other fields like team name, etc.?), so when you create a Team object you pass in the teamUserId (not shown below but created when using amplify) that will allow you to attach a newly created Team to an existing user or group of users.

I think you could keep the common fields in User, and extra info in separate type. Not sure if this is the best practice, but it should work for this scenario
enum UserType {
ACTIVE
INVITED
}
type User #model #key(name:"byTeam", fields:["teamID"]){
id: ID!
teamID: ID!
email: String
created: AWSTimestamp
type: UserType
activeUserInfo: ActiveUserInfo #connection(fields:["id"])
invitedUserInfo: InvitedUserInfo #connection(fields:["id"])
}
type ActiveUserInfo #key(fields:["userID"]){
userID: ID!
first: String
last: String
}
type InvitedUserInfo #key(fields:["userID"]){
userID: ID!
invitedBy: String
}
type Team #model {
id:ID!
users: [User!] #connection(keyName:"byTeam", fields:["id"])
}

Related

One way one to many relation

I have a Recipe and a Tag model. Currently, the recipe contains an array of id's belonging to Tag:
#Entity()
export class Recipe extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
public id!: string;
#Column({ type: 'varchar' })
public title!: string;
#Column({ type: 'varchar' })
public description!: string;
#Column({ type: 'simple-array', nullable: true })
public tags: string[];
}
#Entity()
export class Tag extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
public id!: string;
#Column({ type: 'varchar' })
public name!: string;
}
However, I am currently not making use of the relational capabilities of TypeORM. I was wondering though, how would i go about doing this? Since the relation only works one way, i.e. the one Recipe having many Tags.
I could be wrong, but I believe by default, you must declare both ways--even if you only intend to use a single direction of the relationship.
For example, you need to declare that a Recipe has many Tags you also have to set up the Tag to Recipe relationship even if you aren't going to use it.
Given your example, you'll need to set up a one:many and a many:one relationship.
Since Recipe will "own" the tags, it will have the one:many:
// recipe.entity.ts
#OneToMany(() => Tag, (tag) => tag.recipe)
tags: Tag[];
Then the inverse will look like this:
// tag.entity.ts
#ManyToOne(() => Recipe, (recipe) => recipe.tags)
#JoinColumn({
name: 'recipeId',
})
recipe: Recipe;
If you're considering having many recipes own the same tag, you may need to consider using a many:many relationship
EDIT
I suppose you could technically store an array of id's in a column to represent tags for any given recipe. The question here is, what happens if you decide you need further info on any given tag?
IMO, (and it's just that so take all of this with a grain of salt). You are bending your recipe table to also store relationship info.
I have found it to be more helpful to keep my "buckets" (tables) as specific as possible. That'd leave us with:
recipes | tags | recipes_tags
-----------------------------
That way my recipes table just has recipes & that's it. "Just give me all recipes...". Tags is the same, "just show me all tags"
The two things are completely different entities. By setting up a ManyToMany relationship, we're telling TypeORM that these two columns are related--without "muddying" either of their underlying data.
You can add/remove columns on the pivot table should you decide you want more info about the relationship. At that point, you'd still be working with the relationship, not a tag or recipe so your data would still be nice & lean!
Another example from one of my own use cases...
I have an Activity and a Resource any given resource can have one or more Activities. (activities = tags/ resources = recipes)
// activity.entity.ts
...
#PrimaryGeneratedColumn('uuid')
id: string;
#Column()
name: string;
...
#ManyToMany((type) => Resource, (resource) => resource.activities)
resources: Resource[];
// resource.entity.ts
#PrimaryGeneratedColumn('uuid')
id: string;
#Column()
name: string;
...
#JoinTable()
#ManyToMany((type) => Activity, (activity) => activity.resources)
activities: Activity[];
The above generates a resources_activities_activities table.
Within that table is:
resourceId | activityId
------------------------
I could add additional columns here as well. createdBy or status or something else that is specific to the relationship. Each entry in this table has a relationship back to the activity and the resource--which is great!
I realize we've gone outside the scope of your original question, but I think this is a pretty small step outside, for a potential big win later on.
When I make a request to get a resource: example.com/resources/123 I get something like this back:
"id": "123"
...
"activities": [
{
"id": "f79ce066-75ba-43bb-bf17-9e60efa65e25",
"name": "Foo",
"description": "This is what Foo is.",
"createdAt": "xxxx-xx-xxxxx:xx:xx.xxx",
"updatedAt": "xxxx-xx-xxxxx:xx:xx.xxx"
}
]
...
Likewise, any time I get an activity, I also get back any resources that are related to it. In my front-end I can then easily do something like resource.activities.

Updating many-to-many relations in Prisma

I have a group of checkboxes for skin concerns. Users can check/uncheck them before submitting, which means the set of skin concerns submitted can be different every time.
I modeled it in Prisma schema as an 'explicit' many-to-many relation.
model User {
id String #id #default(cuid())
name String?
nickname String? #unique
...
skinConcerns SkinConcernsForUsers[]
...
}
model SkinConcern {
id Int #id #default(autoincrement())
name String #unique
user SkinConcernsForUsers[]
}
model SkinConcernsForUsers {
user User #relation(fields: [userId], references: [id])
userId String
skinConcern SkinConcern #relation(fields: [skinConcernId], references: [id])
skinConcernId Int
##id([userId, skinConcernId])
}
Then, SkinConcerns table is seeded with the following values, using prisma.skinConcern.createMany:
"ACNE",
"DRYNESS",
"OILY_SKIN",
"PIGMENTATION",
"REDNESS",
"WRINKLES",
SkinConcerns in Update mutation input comes in the form of array of strings, e.g. ["PIGMENTATION", "REDNESS"].
I want to update the skin concerns for users (SkinConcernsForUsers) from the prisma.user.update query, but it's tricky, since I'm not merely creating SkinConcerns, but have to connect to existing set of skin concerns.
I've tried directly setting skinConcerns in user, like
await prisma.user.update({
where: { nickname },
data: {
// ... other user data
skinConcerns: {
set: [
{
skinConcern: {
connect: { name: "PIGMENTATION" },
},
},
{
skinConcern: {
connect: { name: "REDNESS" },
},
},
],
},
// ... other user data
}
});
among many other things, but of course this is not a correct argument and fails with error
Unknown arg `connect` in data.skinConcerns.update.0.where.connect for type SkinConcernsForUsersWhereUniqueInput. Did you mean `select`?
Argument data for data.skinConcerns.update.0.data is missing.
Unknown arg `connect` in data.skinConcerns.update.1.where.connect for type SkinConcernsForUsersWhereUniqueInput. Did you mean `select`?
Argument data for data.skinConcerns.update.1.data is missing.
Is there a way to do this? Is it even possible to update this in prisma.user.update?
I guess I could directly update SkinConcernsForUsers. In that case, should I just delete all rows associated to the user that are not in the user input ["PIGMENTATION", "REDNESS"], then create rows that don't already exist? What will it look like in prisma code?
First I would change your schema for SkinConcern. The id field is not necessary and will create complications in queries (you would needlessly need to map each name to id when trying to connect/disconnect records.
The name field is sufficient as the primary key, as it is always unique for a certain record.
The changed schema looks like this
model SkinConcern {
name String #id // name is the new #id.
user SkinConcernsForUsers[]
}
model SkinConcernsForUsers {
user User #relation(fields: [userId], references: [id])
userId String
skinConcern SkinConcern #relation(fields: [skinConcernName], references: [name])
skinConcernName String
##id([userId, skinConcernName])
}
The query you want to do can be executed in two steps with the SkinConcernsForUsers model.
Step 1: Remove existing SkinConcernsForUsers records a user is connected to. These are no longer relevant, as you want to overwrite the previous selection.
Step 2: Create new SkinConcernsForUsers records with the new choices.
Here is what the code looks like
// step 1
await prisma.skinConcernsForUsers.deleteMany({
where: {
userId: "1",
},
});
// step 2
await prisma.skinConcernsForUsers.createMany({
data: [
{
userId: "1",
skinConcernName: "REDNESS",
},
{
userId: "1",
skinConcernName: "PIGMENTATION",
},
],
});

Dql mutation add duplicate records

What I want to do
Prevent the dql mutation to add duplicates records
What I did
I add a graphql schema:
type Product {
id: ID!
name: String! #id #dgraph(pred: "Product.name")
slug: String! #id #dgraph(pred: "Product.slug")
image: String #dgraph(pred: "Product.image")
created_at: DateTime! #dgraph(pred: "Product.created_at")
updated_at: DateTime! #dgraph(pred: "Product.updated_at")
}
the above graphql schema has generated the bellow DQL schema:
<Product.created_at>: datetime .
<Product.image>: string .
<Product.name>: string #index(hash) #upsert .
<Product.slug>: string #index(hash) #upsert .
<Product.updated_at>: datetime .
<dgraph.drop.op>: string .
<dgraph.graphql.p_query>: string #index(sha256) .
<dgraph.graphql.schema>: string .
<dgraph.graphql.xid>: string #index(exact) #upsert .
type <Product> {
Product.name
Product.slug
Product.image
Product.created_at
Product.updated_at
}
type <dgraph.graphql> {
dgraph.graphql.schema
dgraph.graphql.xid
}
type <dgraph.graphql.persisted_query> {
dgraph.graphql.p_query
}
I run a mutation to add some data using: https://github.com/dgraph-io/dgo#running-a-mutation.
But it does not respect the #id added to the schema to some fields like "slug" and "name".
Using the graphql mutation this is working and respect the uniqueness by returning an error:"message": "couldn't rewrite mutation addProduct because failed to rewrite mutation payload because id aaaa already exists for field name inside type Product"
dgraph version v21.03.2
In Dql you have to handle this by your own using Upsert Block.
Or if you don't want the power of dql you can use the graphql which handle this stuff automatically.

Same relation for two fields in one data type

can I write something like this:
type User {
primaryStory: Story! #relation(name: "userStory")
secondaryStories: [Story] #relation(name: "userStory")
}
type Story {
user: User! #relation(name: "userStory")
}
Basically what I want is to have a single relation name for both primary story and secondary stories.
This is not possible. With the name specified in an ambiguous way it is not clear what the userStory relates to.
You could either have 2 different relation names, or have a construct like the following and filter accordingly:
type User {
stories: Story! #relation(name: "userStories")
}
type Story {
author: User! #relation(name: "userStories")
isPrimary: Boolean!
}

Populate a query in Mongoose with Schema First approach and NestJS

First off I want to say this question is similar to this one which references this one. I have the exact same question as the second link except a notable difference. I'm trying to extend a class generated by NestJS which defines a property.
I'm using NestJs with the Schema first approach found here. I'm also generating a classes file based on my GraphQL Schema.
Here is the Schema:
type Location {
name: String!
owner: User!
}
Which generates the class:
export class Location {
name: string;
owner: User;
}
Now, I want to extend this class so I don't have to repeat the data (there are a lot more fields not shown). I also I want to add fields that live on a document but are not in the schema (_id in this example). Here is my LocationDocument and my schema.
export interface LocationDocument extends Location, Document {
_id: Types.ObjectId
}
export const LocationSchema: Schema = new Schema(
{
name: {
type: String,
required: true,
},
owner: {
type: Types.ObjectId,
ref: 'User',
}
);
Now here is my issue. The generated Location class from the GraphQL schema defines the owner property as a User type. But in reality it's a just a mongodb id until it is populated by Mongoose. So it could be a Types.ObjectId or a User on a UserDocument. So I attempted to define it as:
export interface LocationDocument extends Location, Document {
_id: Types.ObjectId
owner: User | Types.ObjectId;
}
But this throws an error in the compiler that LocationDocument incorrectly extends Location. This makes sense. Is there any way to extend the User Class but say that owner property can be a User Type (once populated by Mongoose) or a mongo object ID (as is stored in the database).
I decided that having a property that can be both types, while easy with Mongoose and JS, isn't the typed way. In my schema I have an owner which is a User type. In my database and the document which extends it, I have an OwnerId. So to people accessing the API, they don't care about the ownerId for the relationship. But in my resolver, I use the Id. One is a Mongo ID type, the other is a User type.