Prisma 2 query to return records only that are associated with ALL of the provided tag IDs - react-apollo

I have tables Principles and Tags. And there is a many-to-many relation between them (joined implicitly).
Without using prisma.raw, how can I run the following query?
SELECT p.id, p.title, p.description, p.createdAt, p.modifiedAt
FROM principle p
WHERE EXISTS (SELECT NULL
FROM _PrincipleToTag pt
WHERE pt.B IN (${tagIds.join(',')})
AND pt.A = p.id
GROUP BY pt.A
HAVING COUNT(DISTINCT pt.B) = ${tagIds.length})
How can I update this Prisma 2 query such that the principles returned are only principles that are associated with ALL of the provided tagIds?
export const principles = ({ tagIds }) => {
const payload = {
where: {
//TODO filter based on tagIds
},
}
return db.principle.findMany(payload)
}
The docs mention contains and in and every, but I can't find examples of what I'm trying to do.
I'm using RedwoodJs, Prisma 2, Apollo, GraphQL.
Update in response to comment: here is the SDL:
input CreatePrincipleInput {
title: String!
description: String
}
input CreatePrincipleWithTagsInput {
title: String!
description: String
tagIdsJson: String
}
input CreateTagInput {
title: String!
description: String
}
# A date string, such as 2007-12-03, compliant with the `full-date` format
# outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for
# representation of dates and times using the Gregorian calendar.
scalar Date
# A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the
# `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO
# 8601 standard for representation of dates and times using the Gregorian calendar.
scalar DateTime
type Mutation {
createPrinciple(input: CreatePrincipleInput!): Principle
createPrincipleWithTags(input: CreatePrincipleWithTagsInput!): Principle
updatePrinciple(id: Int!, input: UpdatePrincipleInput!): Principle!
deletePrinciple(id: Int!): Principle!
createTag(input: CreateTagInput!): Tag!
updateTag(id: Int!, input: UpdateTagInput!): Tag!
deleteTag(id: Int!): Tag!
}
type Principle {
id: Int!
title: String!
description: String!
tags: [Tag]
createdAt: DateTime!
modifiedAt: DateTime!
}
type Query {
redwood: Redwood
principles(searchQuery: String, tagIds: [Int]): [Principle!]!
tags: [Tag!]!
tagsByLabel(searchTerm: String): [TagCount!]!
tag(id: Int!): Tag!
}
type Redwood {
version: String
}
type Tag {
id: Int!
title: String!
principles: [Principle]
description: String
createdAt: DateTime!
modifiedAt: DateTime!
}
type TagCount {
id: Int!
title: String!
count: Int!
principles: [Principle]
description: String
createdAt: DateTime!
modifiedAt: DateTime!
}
# A time string at UTC, such as 10:15:30Z, compliant with the `full-time` format
# outlined in section 5.6 of the RFC 3339profile of the ISO 8601 standard for
# representation of dates and times using the Gregorian calendar.
scalar Time
input UpdatePrincipleInput {
title: String
description: String
}
input UpdateTagInput {
title: String
description: String
}

It doesn't look like you are using prisma 2. Prisma 2 uses models (not types) and has arrays classified like Principles[] vs [Principles]. Maybe Redwood does the conversion(Never used it).
I created your model in Prisma 2 and used the following command to get a single principle that has the two tags associated with it. Keep in mind the IDs in there are from my test dataset. Hopefully, you can modify this to your code. If not, please create a sandbox/playground with minimal code for us to test.
export const principles = async ({ searchQuery, tagIds }) => {
const payload = {
where: {
OR: [
{ title: { contains: searchQuery } },
{ description: { contains: searchQuery } },
],
userId: userIdFromSession,
},
}
if (tagIds.length) {
const whereAnd = []
tagIds.forEach((tagId) => {
whereAnd.push({
tags: { some: { id: tagId } },
})
})
payload.where.AND = whereAnd
}
const result = await db.principle.findMany(payload)
return result
}

You could try something like this
export const principles = ({ searchQuery, tagIds }) => {
const payload = {
where: {
OR: [
{ title: { contains: searchQuery } },
{ description: { contains: searchQuery } },
],
// using the `in` operator like this
tagId: { in: tagIds },
userId: userIdFromSession,
},
}
console.log('db.principle.findMany(payload)', payload)
return db.principle.findMany(payload)
}
That should do the trick!

I had to resort to using AND for something similar - hope this helps!
const tagIds = [9,6];
where: {
// ...
AND: tagIds.map(tagId => ({
tags: {
some: {
id: {
equals: tagId,
},
},
},
})),
}

Related

Can you apply sorting to a lists of models inside another model?

Using AWS Amplify, can we apply sorting to the messages in the Conversation model?
When fetching the conversation, it would be nice that the messages come sorted based on the generated createdAt date.
Currently these are the models used.
type Conversation #model {
id: ID!
messages: [Message!]! #hasMany
...
}
type Message #model {
id: ID!
authorId: String!
content: String!
conversation: Conversation #belongsTo
}
Ideally want to place sorting on the hasMany directive, but this is not possible.
type Conversation #model {
id: ID!
messages: [Message!]! #hasMany(sortKeys:['createdAt'])
...
}
Created a secondary index on the Message model with a sort field on createdAt.
type Message #model {
id: ID!
authorId: String! #index(name: "byAuthorId", queryField: "getMessagesByAuthorId", sortKeyFields: [ "createdAt" ])
content: String!
conversation: Conversation #belongsTo
}
Amplify created a new query to fetch the messages and apply sorting. Following example uses react-query to fetch the messages from an authorId with sorting.
export function useMessagesPerAuthorId({
id,
filter,
enabled = true,
}: {
id: string | undefined;
filter: any;
enabled?: boolean;
}) {
return useQuery(
['conversations', 'messages', id, filter],
() => fetchMessagesByAuthorId({ id: id!, filter }),
{ enabled: enabled && !!id }
);
}
async function fetchMessagesByAuthorId({ id, filter }: { id: string; filter: any }) {
const query: any = await API.graphql({
query: getMessagesByAuthorId,
variables: { authorId: id, sortDirection: 'DESC', filter },
});
const data: Message[] = query.data?.getMessagesByAuthorId.items;
return data;
}
Now we can call that hook in our view component and pass the filters.
const { isLoading, data: messages = [] } = useMessagesPerAuthorId({
id: profile?.id,
filter: {
and: [{ conversationMessagesId: { eq: conversationId } }, { deleted: { eq: false } }],
},
enabled: !!profile?.id,
});

Prisma graphql computed fields on relations

I have the following datamodel:
type Tvshow {
id: ID! #unique
title: String!
pricing: [Pricing]
startDate: DateTime!
endDate: DateTime!
subscribers: [Tvshowsubscription!]
.....
}
type FavoriteTvshow {
id: ID! #unique
tvshow: Tvshow!
user: User!
}
type User {
id: ID! #unique
name: String
email: String! #unique
password: String
googleID: String #unique
resetToken: String
resetTokenExpiry: String
permissions: [Permission]
address: Address
phone: String
favorites: [FavoriteTvshow!]
tvshowSubscriptions: [Tvshowsubscription!]
}
I have my custom Tvshow resolver using addFragmentToInfo:
resolver-queries.js
const Query = {
...
favoriteTvshows: forwardTo('db'),
tvshow: (parent, args, ctx, info) => {
const fragment = `fragment EnsureComputedFields on Tvshow { pricing { minQuantity maxQuantity unitPrice} subscribers { id }}`
return ctx.db.query.tvshow({}, addFragmentToInfo(info, fragment))
},
....
};
tvshow-resolver.js
const Tvshow = {
countSubscribers: (parent) => {
return parent.subscribers.length;
},
}
This is an example, I have more computed fields for Tvshow
I can query Tvshows with countSubscribers, It works fine doing something like this:
query SINGLE_TVSHOW_QUERY($id: ID!) {
tvshow(where: { id: $id }) {
id
title
pricing {
minQuantity
maxQuantity
unitPrice
}
startDate
endDate
countSubscribers
}
}
But what I want to do is to get all the favorite Tvshows from an user returning the countSubscribers, a query for that could be something like this:
query FAVORITES_FROM_USER($userId: ID!) {
favoriteTvshows(where: { user: {id: $userId} }) {
tvshow {
id
title
startDate
endDate
countSubscribers
}
}
}
The problem is that when I query this, in the tvshow-resolver.js I mentioned before, the parent doesn’t have any subscribers object
The error was very silly but I will post it anyway. I needed subscribers in the query
query FAVORITES_FROM_USER($userId: ID!) {
favoriteTvshows(where: { user: {id: $userId} }) {
tvshow {
id
title
startDate
endDate
subscribers { <---
id
quantity
}
countSubscribers
}
}
}
That way the parent in tvshow-resolver.js will have subscribers object

graphql, how to design input type when there are "add" and "update" mutation?

Here are my requirements:
"add" mutation, every field(or called scalar) of BookInput input type should have additional type modifiers "!" to validate the non-null value. Which means when I add a book, the argument must have title and author field, like {title: "angular", author: "novaline"}
"update" mutation, I want to update a part of fields of the book, don't want to update whole book(MongoDB document, And, I don't want front-end to pass graphql server a whole big book mutation argument for saving bandwidth). Which means the book argument can be {title: "angular"} or {title: "angular", author: "novaline"}.
Here are my type definitions:
const typeDefs = `
input BookInput {
title: String!
author: String!
}
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]!
}
type Mutation{
add(book: BookInput!): Book
update(id: String!, book: BookInput!): Book
}
`;
For now, "add" mutation works fine. But "update" mutation cannot pass the non-null check if I pass {title: "angular"} argument
Here is a mutation which does not pass the non-null check, lack of "author" field for BookInput input type.
mutation {
update(id: "1", book: {title: "angular"}) {
id
title
author
}
}
So, graphql will give me an error:
{
"errors": [
{
"message": "Field BookInput.author of required type String! was not provided.",
"locations": [
{
"line": 2,
"column": 24
}
]
}
]
}
How do I design the BookInput input type? Don't want to define addBookInput and updateBookInput. It's duplicated.
A very common pattern is to have separate input types for each mutation. You may also want to create one mutation query per operation. Perhaps something like this:
const typeDefs = `
input AddBookInput {
title: String!
author: String!
}
input UpdateBookInput {
# NOTE: all fields are optional for the update input
title: String
author: String
}
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]!
}
type Mutation{
addBook(input: AddBookInput!): Book
updateBook(id: String!, input: UpdateBookInput!): Book
}
`;
Some people also like to include the update ID as part of the update input:
const typeDefs = `
input AddBookInput {
title: String!
author: String!
}
input UpdateBookInput {
# NOTE: all fields, except the 'id' (the selector), are optional for the update input
id: String!
title: String
author: String
}
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]!
}
type Mutation{
addBook(input: AddBookInput!): Book
updateBook(input: UpdateBookInput!): Book
}
`;
Finally, you may want to use a 'payload' type for the return type - for added flexibility (gives you more wiggle room to change the return type later without breaking your API):
const typeDefs = `
input AddBookInput {
title: String!
author: String!
}
input UpdateBookInput {
# NOTE: all fields, except the 'id' (the selector), are optional for the update input
id: String!
title: String
author: String
}
type Book {
id: ID!
title: String!
author: String!
}
type AddBookPayload {
book: Book!
}
type UpdateBookPayload {
book: Book!
}
type Query {
books: [Book!]!
}
type Mutation{
addBook(input: AddBookInput!): AddBookPayload!
updateBook(input: UpdateBookInput!): UpdateBookPayload!
}
`;
Hope this helps!
Here is my solution, I write a helper function to generate "create" input type and "update" input type.
const { parse } = require('graphql');
/**
* schema definition helper function - dynamic generate graphql input type
*
* #author https://github.com/mrdulin
* #param {string} baseSchema
* #param {object} options
* #returns {string}
*/
function generateInputType(baseSchema, options) {
const inputTypeNames = Object.keys(options);
const schema = inputTypeNames
.map(inputTypeName => {
const { validator } = options[inputTypeName];
const validatorSchema = Object.keys(validator)
.map(field => `${field}: ${validator[field]}\n`)
.join(' ');
return `
input ${inputTypeName} {
${baseSchema}
${validatorSchema}
}
`;
})
.join(' ')
.replace(/^\s*$(?:\r\n?|\n)/gm, '');
parse(schema);
return schema;
}
schema.js:
${generateInputType(
`
campaignTemplateNme: String
`,
{
CreateCampaignTemplateInput: {
validator: {
channel: 'ChannelUnionInput!',
campaignTemplateSharedLocationIds: '[ID]!',
campaignTemplateEditableFields: '[String]!',
organizationId: 'ID!',
},
},
UpdateCampaignTemplateInput: {
validator: {
channel: 'ChannelUnionInput',
campaignTemplateSharedLocationIds: '[ID]',
campaignTemplateEditableFields: '[String]',
organizationId: 'ID',
},
},
},
)}

How to pass params to child property in GraphQL

i am pretty new to GraphQL, getting to become a huge fan :)
But, something is not clear to me. I am using Prisma with and GraphQL-Yoga with Prisma bindings.
I do not know how to pass params from my graphQL server to sub properties. Don't know if this is clear, but i will show it with code, thats hopefully easier :)
These are my types
type User {
id: ID! #unique
name: String!
posts: [Post!]!
}
type Post {
id: ID! #unique
title: String!
content: String!
published: Boolean! #default(value: "false")
author: User!
}
My schema.graphql
type Query {
hello: String
posts(searchString: String): [Post]
users(searchString: String, searchPostsTitle: String): [User]
me(id: ID): User
}
and my users resolver:
import { Context } from "../../utils";
export const user = {
hello: () => "world",
users: (parent, args, ctx: Context, info) => {
return ctx.db.query.users(
{
where: {
OR: [
{
name_contains: args.searchString
},
{
posts_some: { title_contains: args.searchPostsTitle }
}
]
}
},
info
);
},
me: (parent, args, ctx: Context, info) => {
console.log("parent", parent);
console.log("args", args);
console.log("info", info);
console.log("end_________________");
return ctx.db.query.user({ where: { id: args.id } }, info);
}
};
and my posts resolver
import { Context } from "../../utils";
export const post = {
posts: (parent, args, ctx: Context, info) => {
return ctx.db.query.posts(
{
where: {
OR: [
{
title_contains: args.searchString
},
{
content_contains: args.searchString
}
]
}
},
info
);
}
};
so, now :)
I am able to do the following when i am in the GraphQL playground on my prisma service:
{
user(where: {id: "cjhrx5kaplbu50b751a3at99d"}) {
id
name
posts(first: 1, after: "cjhweuosv5nsq0b75yc18wb2v") {
id
title
content
}
}
}
but i cant do it on the server, if i do something like that.. i am getting the error:
"error": "Response not successful: Received status code 400"
this is what i am trying:
{
me(id: "cjhrx5kaplbu50b751a3at99d") {
id
name
posts(first:1) {
id
title
content
}
}
}
does somebody know how i could do that?
since i have a custom type of user, posts does not have params like the generated one. Either i am using the the generated one, or modifying it to look like this:
type User {
id: ID!
name: String!
posts(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Post!]
}
EDIT 2018 June 4th
# import Post from './generated/prisma.graphql'
type Query {
hello: String
posts(searchString: String): [Post]
users(searchString: String, where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]
me(id: ID): User
}
type Mutation {
createUser(name: String!): User
createPost(
title: String!
content: String!
published: Boolean!
userId: ID!
): Post
}
I copied the params over from prisma.graphql manually.

Query.products is defined in resolvers but not in schema

Hi I defined rootQuery in Customer schema and then in Product schema I extended query. I wrote resolvers for product schema but then I got following error: Error: Query.products defined in resolvers, but not in schema.
When I move product queries to customer query definition it works.
I dont understand why I'm getting this error. Do I need implement some rootQuery and insert it into typeDefs array and then extend queries in Customer and Product ?
Customer schema
import CustomerPhoto from "./customerPhoto";
const Customer = `
type Customer {
customerID: ID!
firstname: String
lastname: String
phone: String
email: String
CustomerPhoto: CustomerPhoto
}
input CustomerInput {
firstname: String!
lastname: String!
phone: String!
email: String!
}
type Query {
customers(cursor: Int!):[Customer]
customer(id: Int!): Customer
}
type Mutation {
createCustomer(photo: String!, input: CustomerInput): Customer
updateCustomer(customerID: ID!, photo: String, input: CustomerInput): Customer
deleteCustomer(customerID: ID!): Customer
}
`;
export default [Customer, CustomerPhoto];
Product Schema
import ProductPhoto from "./productPhoto";
const Product = `
type Product {
productID: ID!
name: String!
description: String!
pricewithoutdph: Float!
pricewithdph: Float!
barcode: Int!
ProductPhoto: ProductPhoto
}
extend type Query {
products: [Product]
product(productID: ID!): Product
}
`;
export default [Product, ProductPhoto]
Here Im importing both schemas. Is there something missing ?
const schema = makeExecutableSchema({
typeDefs: [...Customer,...Product],
resolvers: merge(CustomerResolvers, ProductResolvers),
logger: {
log: e => {
console.log("schemaError", e);
}
},
resolverValidationOptions: {
requireResolversForNonScalar: true
}
});
Product Resolvers
const ProductResolvers = {
Query: {
products: (_, { cursor }) => {
return models.Product.findAndCountAll({
include: {
model: models.ProductPhoto,
attributes: ["productPhotoID", "photo"],
required: true
},
offset: cursor,
limit: 10,
attributes: ["productID", "name", "description", "pricewithoutdph", "pricewithdph", "barcode"]
}).then(response => {
return response.rows;
});
}
};
export default ProductResolvers;
Customer Resolvers
const CustomerResolvers = {
Query: {
customers: (_, {cursor}) => {
return models.Customer.findAndCountAll({
include: {
model: models.CustomerPhoto,
attributes: ["customerPhotoID", "photo"],
required: true
},
offset: cursor,
limit: 10,
attributes: ["customerID", "firstname", "lastname", "phone", "email"]
}).then(response => {
return response.rows;
});
}
......
}
};

Resources