updateMany can't find argument - orm

I have a query:
createNotification: async (_, args, {req, res}) => {
const followedBy = await prisma.user.updateMany({
where: {
following: {
some: {
id: req.userId
},
},
},
data: {
notifications: {
create: {
message: args.message,
watched: false,
},
},
},
})
And User and Notification models:
model User {
id Int #id #default(autoincrement())
email String #unique
name String
user_name String #unique
password String
movies Movie[]
notifications Notification[]
followedBy User[] #relation("UserFollows", references: [id])
following User[] #relation("UserFollows", references: [id])
}
model Notification {
id Int #id #default(autoincrement())
link String?
movie_id Int?
message String
icon String?
thumbnail String?
user User #relation(fields: [userId], references: [id])
userId Int
watched Boolean
}
When I run my query I get an error:
Unknown arg `notifications` in data.notifications for type UserUpdateManyMutationInput. Did you mean `email`? Available args:
type UserUpdateManyMutationInput {
email?: String | StringFieldUpdateOperationsInput
name?: String | StringFieldUpdateOperationsInput
user_name?: String | StringFieldUpdateOperationsInput
password?: String | StringFieldUpdateOperationsInput
}
The strange thing is that this works:
const followedBy = await prisma.user.findUnique({
where: {id: req.userId},
include: {
followedBy: true,
},
});
followedBy.followedBy.map(async(user) => {
await prisma.user.update({
where: {id: user.id},
data: {
notifications: {
create: {
message: args.message,
watched: false,
},
},
},
});
});
But this isn't making the best of what Prisma offers.

As of September 2021, Prisma does not support mutating nested relations in a top-level updateMany query. This is what the typescript error is trying to tell you, that you can only access email, name, user_name and password fields inside data. There's an open feature request for this which you could follow if you're interested.
For the schema that you have provided, here's a possible workaround that's slightly less readable but more optimized than your current solution.
createNotification: async (_, args, {req, res}) => {
// get the followers array of req.userId
const followedBy = await prisma.user.findUnique({
where: { id: req.userId },
include: {
followedBy: true,
},
});
// array of notification objects to be created, one for each follower of req.userId
let messageDataArray = followedBy.followedBy.map((user) => {
return {
userId: user.id,
message: args.message,
watched: false,
};
});
// do a bulk createMany.
// Since it's one query, it should be more optimized than running an update for each user in a loop.
await prisma.notification.createMany({
data: messageDataArray,
});
};
If you're interested, here's the docs reference for the kinds of nested updates that are possible.

Related

Mapping raw sql result to graphql compatible structure in typeorm

post resolver fetches posts from postgres database and return them in response to graphql request.
The return value is array of Post entity objects. Post entity structure is given below.
{
id: 29,
title: 'Turkey Earthquake kills 1000 plus people',
text: "Body is saying and don't go to the dark path",
points: 0,
creatorId: 44,
createdAt: 2023-02-07T06:53:39.453Z,
updatedAt: 2023-02-08T07:01:54.409Z,
creator: User {
id: 44,
username: 'prashant',
email: 'prashant#gmail.com',
password: '$argon2id$v=19$m=65536,t=3,p=4$LqsF/Gb3E8J4+vm5TszyWQ$d+LhZ6mNOE4eLqWvdwItQ3qYMZr17+CfvPt9l0Iqk3M',
createdAt: 2023-02-06T07:32:11.717Z,
updatedAt: 2023-02-06T07:32:11.717Z
}
}
Now I have a subquery in SELECT section of the query.
#Query(() => PaginatedPosts)
async posts(
#Arg('limit', () => Int) limit: number,
#Arg('cursor', () => String, { nullable: true }) cursor: string | null,
#Ctx() { req, dataSource }: MyContext
): Promise<PaginatedPosts> {
const realLimit = Math.min(30, limit)
const realLimitPlusOne = realLimit + 1
const qb = dataSource
.createQueryBuilder()
.select('p')
.from(Post, 'p')
.innerJoinAndSelect('p.creator', 'u')
.orderBy('p.createdAt', 'DESC')
.take(realLimitPlusOne)
if (req.session.userId)
qb.addSelect((qb) => {
return qb
.subQuery()
.select('d.value')
.from(Updoot, 'd')
.where('"userId" = :userId AND "postId" = p.id', {
userId: req.session.userId
})
}, 'voteStatus')
else qb.addSelect('null', 'voteStatus')
if (cursor)
qb.where('p.createdAt < :cursor', {
cursor: new Date(parseInt(cursor))
})
const posts = await qb.getMany() // this is returning the above structure without `voteStatus` field in it.
console.log('posts: ', posts[0])
return {
posts: posts.slice(0, realLimit),
hasMore: posts.length === realLimitPlusOne
}
}
But instead of using await qb.getMany() if I use
const posts = await qb.getRawMany()
I get the data in following structure
{
p_id: 29,
p_title: 'Turkey Earthquake kills 1000 plus people',
p_text: "Body is saying and don't go to the dark path",
p_points: 0,
p_creatorId: 44,
p_createdAt: 2023-02-07T06:53:39.453Z,
p_updatedAt: 2023-02-08T07:01:54.409Z,
u_id: 44,
u_username: 'prashant',
u_email: 'prashant#gmail.com',
u_password: '$argon2id$v=19$m=65536,t=3,p=4$LqsF/Gb3E8J4+vm5TszyWQ$d+LhZ6mNOE4eLqWvdwItQ3qYMZr17+CfvPt9l0Iqk3M',
u_createdAt: 2023-02-06T07:32:11.717Z,
u_updatedAt: 2023-02-06T07:32:11.717Z,
voteStatus: -1
}
Which is not compatible with the graphql type I intend to respond with. So I am confused whether there is another way I can use queryBuilder to return the result with the compatible type or do I have to explicitly create a mapping function to map each object to desired structure of Post entity.

Apollo-client: How to add cache fragment in different Query?

when I create feed in Screen A,
I add and in order to apply this change to screen directly I change cache like below.
in this Screen A, I use seeAllFeeds query.
So I add this added feed to seeAllFeeds.
const updateUploadPhoto = (cache, result) => {
const {
data: { createFeed },
} = result;
if (createFeed.id) {
cache.modify({
id: "ROOT_QUERY",
fields: {
seeAllFeeds(prev) {
return [createFeed, ...prev];
},
},
});
}
}
So it works well.
But problem is I use seeCertainUserFeedpoem query in Screen B.
And here, I also need to add this added feed info.
However this screen is not applied unless I refresh the screen.
(Due to flatlist, I can't refresh because if so, scroll goes to top.)
So I add this cache.modify once again below.
I also match the data with seeCertainUserFeedpoem manually and update.
const updateUploadPhoto = (cache, result) => {
const {
data: { createFeed },
} = result;
const FeedpomeId = `Feedpoem:${createFeed.id}`;
const FeedpoemFragment = {
caption: createFeed.caption,
commentNumber: createFeed.commentNumber,
createdAt: createFeed.createdAt,
id: createFeed.id,
isLiked: createFeed.isLiked,
isMine: createFeed.isMine,
likeNumber: createFeed.likeNumber,
photos: createFeed.photos,
poemCaption: null,
poemCommentNumber: 0,
poemLikeNumber: 0,
poemTitle: null,
updatedAt: createFeed.updatedAt,
};
if (createFeed.id) {
cache.modify({
id: "ROOT_QUERY",
fields: {
seeAllFeeds(prev) {
return [createFeed, ...prev];
},
},
});
cache.modify({
id: FeedpomeId,
data: FeedpoemFragment,
});
navigation.navigate("Tabs", { screen: "일상" });
}
};
So I also try this way, not cache.modify but client.writeFragment.
const updateUploadPhoto = (cache, result) => {
const {
data: { createFeed },
} = result;
const FeedpomeId = `Feedpoem:${createFeed.id}`;
const FeedpoemFragment = {
caption: createFeed.caption,
commentNumber: createFeed.commentNumber,
createdAt: createFeed.createdAt,
id: createFeed.id,
isLiked: createFeed.isLiked,
isMine: createFeed.isMine,
likeNumber: createFeed.likeNumber,
photos: createFeed.photos,
poemCaption: null,
poemCommentNumber: 0,
poemLikeNumber: 0,
poemTitle: null,
updatedAt: createFeed.updatedAt,
};
if (createFeed.id) {
cache.modify({
id: "ROOT_QUERY",
fields: {
seeAllFeeds(prev) {
return [createFeed, ...prev];
},
},
});
client.writeFragment({
id: FeedpomeId,
data: FeedpoemFragment,
});
navigation.navigate("Tabs", { screen: "일상" });
}
};
But both dont' work.
And 2nd way throws me this error.
LogBox.js:173 Possible Unhandled Promise Rejection (id: 0): Error:
Cannot read properties of undefined (reading 'definitions') Error:
Cannot read properties of undefined (reading 'definitions')
what is the problem here?
I can edit / delete with cache.
Because I understand that first find the exact cache by id.
However creating is hard.
I want to share my cache as well.
But this is too long.
chat is also welcome, please help me.

GraphQL & Sequelize: Users and followers/following

I'm attempting to set up my User GraphQL model to have followers and following attributes to query on. However I'm having trouble setting up the relationship in Sequelize. I'm trying to use a Follower model as a Join Table and setup a BelongsToMany association, but haven't been able to get it working. Can anyone suggest what to do or point out what I'm doing wrong?
I've come up with a temporary solution by manually querying, which you can see in my User.model.ts, but I believe there is a better way to do it using proper configuration.
I'm using typescript wrappers around GraphQL and Sequelize, TypeGraphQL and sequelize-typescript respectively, as well as PostgreSQL.
User.model.ts
// VENDOR
import { ObjectType, Field, ID } from 'type-graphql';
import { Model, Table, Column, PrimaryKey, Unique, IsUUID, HasMany, DefaultScope, AllowNull, DataType, BelongsToMany } from 'sequelize-typescript';
// APP
import Post from '../post/post.types';
import Follower from '../follower/follower.types';
/** User model for GraphQL & Database */
#Table({ timestamps: false, tableName: 'users' }) // tell sequelize to treat class as table model
#DefaultScope(() => ({ include: [{ model: Post.scope(), as: 'posts' }] })) // tell sequelize to include posts in its default queries
#ObjectType() // tell GraphQL to treat class as GraphQL model
export default class User extends Model<User>{
#PrimaryKey
#Unique
#AllowNull(false)
#IsUUID(4)
#Column(DataType.UUID)
#Field(() => ID)
id: string;
#Unique
#AllowNull(false)
#Column
#Field()
ci_username: string;
#AllowNull(false)
#Column
#Field()
username: string;
#AllowNull(false)
#Column
#Field()
first_name: string;
#Column
#Field()
last_name: string;
#Column
#Field({ nullable: true })
profile_picture?: string;
// #BelongsToMany(() => User, { otherKey: 'user_id', as: 'followers', through: () => Follower })
// #Field(() => [User])
// followers: User[];
// MY TEMPORARY SOLUTION USING MANUAL QUERYING
#Field(() => [User])
get followers(): Promise<User[]> {
return Follower.findAll({ where: { user_id: this.id } })
.then(records => records.map(record => record.follower_id))
.then((follower_ids: string[]) => {
return User.findAll({ where: { id: follower_ids }});
})
}
// DOES NOT WORK, BUT I BELIEVE COULD POTENTIALLY LEAD TO BETTER SOLUTION
#BelongsToMany(() => User, { otherKey: 'follower_id', as: 'following', through: () => Follower })
#Field(() => [User])
following: User[];
#HasMany(() => Post)
#Field(() => [Post])
posts: Post[];
}
Follower.model.ts
// VENDOR
import { Model, Table, Column, PrimaryKey, Unique, IsUUID, AllowNull, DataType, Index, ForeignKey, AutoIncrement } from 'sequelize-typescript';
// APP
import User from '../user/user.types';
/** Follower model for Database */
#Table({ timestamps: false, tableName: 'followers' }) // tell sequelize to treat class as table model
export default class Follower extends Model<Follower>{
#PrimaryKey
#AutoIncrement
#Unique
#AllowNull(false)
#Column
id: number;
#AllowNull(false)
#IsUUID(4)
#Index
#ForeignKey(() => User)
#Column(DataType.UUID)
user_id: string;
#AllowNull(false)
#IsUUID(4)
#Index
#ForeignKey(() => User)
#Column(DataType.UUID)
follower_id: string;
}
GraphQL Query
{
users: allUsers {
id
username
first_name
last_name
following {
username
id
}
}
}
GraphQL Response / Error
{
"errors": [
{
"message": "Cannot return null for non-nullable field User.following.",
"locations": [
{
"line": 7,
"column": 5
}
],
"path": [
"users",
0,
"following"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: Cannot return null for non-nullable field User.following.",
" at completeValue (/Users/jsainz237/Projects/trueview/trueview-api/node_modules/graphql/execution/execute.js:560:13)",
" at /Users/jsainz237/Projects/trueview/trueview-api/node_modules/graphql/execution/execute.js:492:16"
]
}
}
}
],
"data": null
}
Any help is appreciated.
You need to write a #FieldResolver manually that will resolve the relation and return proper data.
Another solution is to rely on ORM capabilities and lazy relations - when the returned base entity contains a promise as a field, so when .then() is called, it automatically fetches the relation for the database.

GraphQL queries with tables join using Node.js

I am learning GraphQL so I built a little project. Let's say I have 2 models, User and Comment.
const Comment = Model.define('Comment', {
content: {
type: DataType.TEXT,
allowNull: false,
validate: {
notEmpty: true,
},
},
});
const User = Model.define('User', {
name: {
type: DataType.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
},
phone: DataType.STRING,
picture: DataType.STRING,
});
The relations are one-to-many, where a user can have many comments.
I have built the schema like this:
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: {
type: GraphQLString
},
name: {
type: GraphQLString
},
phone: {
type: GraphQLString
},
comments: {
type: new GraphQLList(CommentType),
resolve: user => user.getComments()
}
})
});
And the query:
const user = {
type: UserType,
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve(_, {id}) => User.findById(id)
};
Executing the query for a user and his comments is done with 1 request, like so:
{
User(id:"1"){
Comments{
content
}
}
}
As I understand, the client will get the results using 1 query, this is the benefit using GraphQL. But the server will execute 2 queries, one for the user and another one for his comments.
My question is, what are the best practices for building the GraphQL schema and types and combining join between tables, so that the server could also execute the query with 1 request?
The concept you are refering to is called batching. There are several libraries out there that offer this. For example:
Dataloader: generic utility maintained by Facebook that provides "a consistent API over various backends and reduce requests to those backends via batching and caching"
join-monster: "A GraphQL-to-SQL query execution layer for batch data fetching."
To anyone using .NET and the GraphQL for .NET package, I have made an extension method that converts the GraphQL Query into Entity Framework Includes.
public static class ResolveFieldContextExtensions
{
public static string GetIncludeString(this ResolveFieldContext<object> source)
{
return string.Join(',', GetIncludePaths(source.FieldAst));
}
private static IEnumerable<Field> GetChildren(IHaveSelectionSet root)
{
return root.SelectionSet.Selections.Cast<Field>()
.Where(x => x.SelectionSet.Selections.Any());
}
private static IEnumerable<string> GetIncludePaths(IHaveSelectionSet root)
{
var q = new Queue<Tuple<string, Field>>();
foreach (var child in GetChildren(root))
q.Enqueue(new Tuple<string, Field>(child.Name.ToPascalCase(), child));
while (q.Any())
{
var node = q.Dequeue();
var children = GetChildren(node.Item2).ToList();
if (children.Any())
{
foreach (var child in children)
q.Enqueue(new Tuple<string, Field>
(node.Item1 + "." + child.Name.ToPascalCase(), child));
}
else
{
yield return node.Item1;
}
}}}
Lets say we have the following query:
query {
getHistory {
id
product {
id
category {
id
subCategory {
id
}
subAnything {
id
}
}
}
}
}
We can create a variable in "resolve" method of the field:
var include = context.GetIncludeString();
which generates the following string:
"Product.Category.SubCategory,Product.Category.SubAnything"
and pass it to Entity Framework:
public Task<TEntity> Get(TKey id, string include)
{
var query = Context.Set<TEntity>();
if (!string.IsNullOrEmpty(include))
{
query = include.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Aggregate(query, (q, p) => q.Include(p));
}
return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}

In Rally SDK 2, how do I update a hash field?

In Rally SDK 2, how do I update a hash field, like the Author field for a changeset? I read how to update the Message field, but I can't figure out how to update Author["DisplayName"] hash.
var new_message = settings.message;
Rally.data.ModelFactory.getModel({
type: 'Changeset',
success: function(model) {
model.load( '1234', {
fetch: [ 'Artifacts' ],
callback: function(result, operation) {
if ( operation.wasSuccessful() ){
var message = new_message;
record.set( 'Message', message);
record.save( {
callback: function( resultset, operation ) {
console.log( "After saving:", resultset );
if ( operation.wasSuccessful() ) {
var that = tree.ownerCt.ownerCt.ownerCt.ownerCt;
that._getChangesets();
}
}
} );
}
}
})
}
});
The Author property on Changeset is of type User. Like any other object associations on Rally's WSAPI you just set this property to the ref of the object you'd like to link. You set this the same way as you're currently setting Message in your above code snippet. (Assuming author is writable after the changeset has already been created).
record.set('Author', '/user/123456');
You can probably also avoid the deeply nested structure of your code a little bit by specifying scope on your callbacks and using member functions in your app definition:
_loadChangesetModel: function() {
//If you already have a changeset record you can get the model
//via record.self. Otherwise, load it fresh.
Rally.data.ModelFactory.getModel({
type: 'Changeset',
success: this._onChangesetModelLoaded,
scope: this
});
},
_onChangesetModelLoaded: function(model) {
model.load( '1234', {
fetch: [ 'Artifacts' ],
callback: this._onChangesetLoaded,
scope: this
});
},
_onChangesetLoaded: function(record, operation) {
if ( operation.wasSuccessful() ){
var message = settings.message;
record.set( 'Message', message);
record.save( {
callback: this._onChangesetSaved,
scope: this
} );
}
},
_onChangesetSaved: function( resultset, operation ) {
console.log( "After saving:", resultset );
if ( operation.wasSuccessful() ) {
//You shouldn't need to do this now that the scope is correct.
//I'm guessing 'that' was referring to the app itself?
//var that = tree.ownerCt.ownerCt.ownerCt.ownerCt;
this._getChangesets();
}
},
_getChangesets: function() {
//refresh
}