Add computed field to graphql results within Strapi 4 - graphql

I'm using Strapi 4, and I try to add computed field to my custom resolver. (I'm not a graphql expert). I've followed this tutorial to do it.
https://www.theitsolutions.io/blog/how-to-add-custom-graphql-query-to-strapi-v4
I’m also using the “toEntityResponseCollection” methods to send the datas and display it in graphql playground.
But, when I send it back, I get a null result.
Here is my custom resolver
"use strict";
module.exports =
(strapi, toEntityResponseCollection, toEntityResponse) =>
({ nexus }) => ({
typeDefs: `
type PopularityResponse {
id: ImpressionEntityResponseCollection
startDate: String
endDate: String
branding: String
}
extend type Query {
popularity(id: ID!, startDate: String, endDate: String, branding: String): PopularityResponse
}
`,
resolvers: {
Query: {
popularity: {
resolve: async (parent, args, context) => ({
id: args.id,
startDate: args.startDate,
endDate: args.endDate,
branding: args.branding,
}),
},
},
PopularityResponse: {
id: {
resolve: async (parent, args) => {
let query = {
value: await strapi.entityService.findMany(
"api::impression.impression",
{
filters: {
googleid: {
id: {
$eq: parent.id,
},
},
date_debut: {
$gte: parent.startDate,
},
date_fin: {
$lte: parent.endDate,
},
},
},
args
),
};
console.log(query.value);
console.log(parent);
let aggregate = query.value.reduce(
(acc, key) => {
// vérifie si la campagne est dans la liste
if (
[parent.branding].some((elem) => {
let reg = new RegExp(elem);
return reg.test(key.campaignName);
})
) {
let brandingIndex = acc.branding.findIndex(
(el) => el.date_debut == key.date_debut
);
if (brandingIndex !== -1) {
// si on a un élément
acc.branding[brandingIndex].search_impression_share +=
parseInt(key.search_impression_share);
} else {
acc.branding.push({
search_impression_share: parseInt(
key.search_impression_share
),
date_debut: key.date_debut,
});
}
} else {
let nobrandingIndex = acc.nobranding.findIndex(
(el) => el.date_debut == key.date_debut
);
if (nobrandingIndex !== -1) {
// si on a un élément
acc.nobranding[nobrandingIndex].search_impression_share +=
parseInt(key.search_impression_share);
} else {
acc.nobranding.push({
search_impression_share: parseInt(
key.search_impression_share
),
date_debut: key.date_debut,
});
}
}
return acc;
},
{ branding: [], nobranding: [] }
);
console.log("==========>>>>",aggregate);
let y = [query.value[0]];
return toEntityResponseCollection([aggregate]);
},
},
},
},
resolversConfig: {
"Query.popularity": {
auth: {
scope: [
"api::impression.impression.findOne",
"api::impression.impression.find",
],
},
},
},
});
Here is my graphql query
query GetPopularity {
popularity(id: "37", startDate:"2022-06-13",endDate:"2022-07-15",branding:"brand") {
myData {
data {
attributes {
googleid {
data {
attributes {
g_customer_id
}
}
}
}
}
}
}
}
When I log the result ssr, I get my computed datas, but when I look at grapql Playground, I get null.
{
"data": {
"popularity": {
"id": {
"data": [
{
"attributes": {
"search_impression_share": null,
"search_top_impression_share": null
}
}
]
}
}
}
}
I don't know what to do to make it work.
I do it like this, because I need to fetch a huge amount of datas. I know that strapi has a 100 limit result from graphql. Even if I can manualy increase it in the config file, I understand it's not a good practice.
If you have any idea how to solve this, please let me know.
Thanks
Fabien

I found how to solve my issue.
I’ve created a specific type which aggregate the datas.
Now I’m able to fetch my computed elements.
"use strict";
module.exports =
(strapi, toEntityResponseCollection, toEntityResponse) =>
({ nexus }) => ({
typeDefs: `
type PopularityResponse {
id: ImpressionEntityResponseCollection
startDate: String
endDate: String
branding: String
aggregated: aggregateInput
}
type aggregateInput {
brand: [singleAggregate]
nobrand: [singleAggregate]
}
type singleAggregate {
date_debut: String
search_impression_share: Int
}
extend type Query {
popularity(id: ID!, startDate: String, endDate: String, branding: String): PopularityResponse
}
`,
resolvers: {
Query: {
popularity: {
resolve: async (parent, args, context) => ({
id: args.id,
startDate: args.startDate,
endDate: args.endDate,
branding: args.branding,
}),
},
},
PopularityResponse: {
aggregated: {
resolve: async (parent, args, ctx) => {
let compile = await strapi.entityService.findMany(
"api::impression.impression",
{
filters: {
googleid: {
id: {
$eq: parent.id,
},
},
date_debut: {
$gte: parent.startDate,
},
date_fin: {
$lte: parent.endDate,
},
},
},
args
);
// console.log(compile);
let aggregate = compile.reduce(
(acc, key) => {
// vérifie si la campagne est dans la liste
if (
[parent.branding].some((elem) => {
let reg = new RegExp(elem);
return reg.test(key.campaignName);
})
) {
let brandingIndex = acc.branding.findIndex(
(el) => el.date_debut == key.date_debut
);
if (brandingIndex !== -1) {
// si on a un élément
acc.branding[brandingIndex].search_impression_share +=
parseInt(key.search_impression_share);
} else {
acc.branding.push({
search_impression_share: parseInt(
key.search_impression_share
),
date_debut: key.date_debut,
});
}
} else {
let nobrandingIndex = acc.nobranding.findIndex(
(el) => el.date_debut == key.date_debut
);
if (nobrandingIndex !== -1) {
// si on a un élément
acc.nobranding[nobrandingIndex].search_impression_share +=
parseInt(key.search_impression_share);
} else {
acc.nobranding.push({
search_impression_share: parseInt(
key.search_impression_share
),
date_debut: key.date_debut,
});
}
}
return acc;
},
{ branding: [], nobranding: [] }
);
return {
brand: aggregate.branding,
nobrand: () => {
return aggregate.nobranding;
},
};
},
},
},
},
resolversConfig: {
"Query.popularity": {
auth: {
scope: [
"api::impression.impression.findOne",
"api::impression.impression.find",
],
},
},
},
});
````

Related

Prisma2: How to solve n +1 Problem with Paljs

thx for any help.
Im using at the frontend the apollo-client and at the backend graphql-nexus,prisma2 and graphql-yoga server.
I want to solve the n + 1 problem with #paljs/plugins.
At the frontend I have a query posts like:
query posts{
posts {
id
favoritedBy(where: { id: { equals: $currentUserId } }) {
id
}
author {
id
avatar {
id
}
}
link {
id
}
games {
id
}
tags {
id
}
likes(where: { user: { id: { equals: $currentUserId } } }) {
id
}
}
}
Posts resolver:
import { PrismaSelect } from '#paljs/plugins'
export const posts = queryField('posts', {
type: 'Post',
list: true,
args: {
...
},
resolve: async (_parent, args, { prisma, request }, info) => {
const select = new PrismaSelect(info).value
let opArgs: FindManyPostArgs = {
take: 10,
orderBy: {
[args.orderBy]: 'desc',
},
...select
}
const post = await prisma.post.findMany(opArgs)
//The result I want to return with the "sub-models" like likes, author tags...
console.log(JSON.stringify(post, undefined, 2))
return post
},
})
I logging the queries
const prisma = new PrismaClient({
log: ['query'],
})
My Problem: With PrismaSelect, I have 5 queries more than without and If I check the request-time at the frontend I need 300-400ms longer with PrismaSelect. So what I'm doing wrong?
I saw in the #paljs/plugins doc the select in the context. Maybe that is my mistake. How can I use the select in the context?
Here ist my Context:
import { PrismaClient, PrismaClientOptions } from '#prisma/client'
import { PubSub } from 'graphql-yoga'
import { PrismaDelete, onDeleteArgs } from '#paljs/plugins'
class Prisma extends PrismaClient {
constructor(options?: PrismaClientOptions) {
super(options)
}
async onDelete(args: onDeleteArgs) {
const prismaDelete = new PrismaDelete(this)
await prismaDelete.onDelete(args)
}
}
export const prisma = new PrismaClient({
log: ['query'],
})
export const pubsub = new PubSub()
export interface Context {
prisma: PrismaClient
request: any
pubsub: PubSub
}
export function createContext(request: any): Context {
return { prisma, request, pubsub }
}
You need to know that to use my PrismaSelect plugin you need to remove the nexus-prisma-plugin package and use my Pal.js CLI to create your CRUD and ObjectType for nexus and using #paljs/nexus plugin to add in mackSchema function
import { makeSchema } from '#nexus/schema';
import * as types from './graphql';
import { paljs } from '#paljs/nexus'; // import our plugin
export const schema = makeSchema({
types,
plugins: [paljs()],// here our plugin don't use nexus-prisma-plugin
outputs: {
schema: __dirname + '/generated/schema.graphql',
typegen: __dirname + '/generated/nexus.ts',
},
typegenAutoConfig: {
sources: [
{
source: require.resolve('./context'),
alias: 'Context',
},
],
contextType: 'Context.Context',
},
});
Now add this type to your Context
export interface Context {
prisma: PrismaClient
request: any
pubsub: PubSub
select: any // here our select type
}
export function createContext(request: any): Context {
// our paljs plugin will add select object before resolver
return { prisma, request, pubsub, select: {} }
}
after you add our plugin your query will log like this
extendType({
type: 'Query',
definition(t) {
t.field('findOneUser', {
type: 'User',
nullable: true,
args: {
where: arg({
type: 'UserWhereUniqueInput',
nullable: false,
}),
},
resolve(_, { where }, { prisma, select }) {
// our plugin add select object into context for you
return prisma.user.findOne({
where,
...select,
});
},
});
},
});
Can you please try to use my pal c command to start an example from my list and try your schema and make tests with it
It is working, thx Ahmed your plugin is AWESOME!!!!!
I changed my Post-Object from
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.authorId()
t.model.tags()
t.model.games()
t.model.link()
t.model.report()
t.model.notifications()
t.model.author()
t.model.favoritedBy({
filtering: {
id: true,
},
})
t.model.likes({
filtering: {
user: true,
},
})
}
})
to
const Post = objectType({
name: 'Post',
definition(t) {
t.string('id')
t.field('tags', {
nullable: false,
list: [true],
type: 'Tag',
resolve(parent: any) {
return parent['tags']
},
})
t.field('games', {
list: [true],
type: 'Game',
resolve(parent: any) {
return parent['games']
},
})
t.field('link', {
type: 'Link',
nullable: true,
resolve(parent: any) {
return parent['link']
},
})
t.field('notifications', {
list: [true],
type: 'Notification',
resolve(parent: any) {
return parent['notifications']
},
})
t.field('author', {
nullable: false,
type: 'User',
resolve(parent: any) {
return parent['author']
},
})
t.field('favoritedBy', {
nullable: false,
list: [true],
type: 'User',
args: {
where: 'UserWhereInput',
},
resolve(parent: any) {
return parent['favoritedBy']
},
})
t.field('likes', {
list: [true],
type: 'Like',
args: {
where: 'LikeWhereInput',
},
resolve(parent: any) {
return parent['likes']
},
})
},
})
And I also used the nexus-prisma-plugin and paljs-plugin at the same time

Is there a way to filter json data format field in strapi?

Hi Guys I'm trying to filter post with data json format field?
"categoryList": ["cat", "cat1"]
For anyone still looking for a solution, this is what I have done for a json type field called tags of a collection type called Articles.
I have two articles in the database with one article having the following values set:
title: "lorem ipsum 1",
tags: [
"test",
"rest"
]
The other article has the following values set:
title: "lorem ipsum 2",
tags: [
"test",
"graphql"
]
My graphql query looks like this:
query {
articlesByTag(limit: 2, where: {tags_include: ["test", "rest"]}, start: 0, sort: "title:asc") {
title,
tags
}
}
While my rest query looks like this:
http://localhost:1337/articlesByTag?limit=2&tags_include[]=test&tags_include[]=rest
This is my articles.js service file:
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
const _ = require('lodash');
const { convertToParams, convertToQuery } = require('../../../node_modules/strapi-plugin-graphql/services/utils');
module.exports = {
async findByTag(ctx) {
let tags_include;
if (ctx.where && ctx.where.tags_include && ctx.where.tags_include.length > 0) {
tags_include = ctx.where.tags_include;
delete ctx.where.tags_include;
} else if (ctx.query && ctx.query.tags_include && ctx.query.tags_include.length > 0) {
tags_include = ctx.query.tags_include;
delete ctx.query.tags_include;
}
if (!Array.isArray(tags_include)) {
tags_include = [tags_include];
}
let filters = null;
if (ctx.query) {
filters = convertRestQueryParams({
...convertToParams(ctx.query)
});
} else {
filters = convertRestQueryParams({
...convertToParams(_.pick(ctx, ['limit', 'start', 'sort'])),
...convertToQuery(ctx.where),
});
}
const entities = await strapi.query('articles').model.query(qb => {
buildQuery({ model: strapi.query('articles').model, filters: filters })(qb);
if (tags_include.length > 0) {
tags_include.forEach((tag) => {
if (tag && tag.length > 0) {
const likeStr = `%"${tag}"%`;
qb.andWhere('tags', 'like', likeStr);
}
});
}
}).fetchAll();
return entities;
},
};
This is the entry needed in routes.js
{
"method": "GET",
"path": "/articlesByTag",
"handler": "articles.findByTag",
"config": {
"policies": []
}
}
This is the controller articles.js
const { sanitizeEntity } = require('strapi-utils');
module.exports = {
async findByTag(ctx) {
const entities = await strapi.services.articles.findByTag(ctx);
return entities.map(entity => sanitizeEntity(entity, { model: strapi.models.articles }));
},
};
And finally this is the schema.graphql.js
module.exports = {
query: `
articlesByTag(sort: String, limit: Int, start: Int, where: JSON): [Articles]
`,
resolver: {
Query: {
articlesByTag: {
description: 'Return articles filtered by tag',
resolverOf: 'application::articles.articles.findByTag',
resolver: async (obj, options, ctx) => {
return await strapi.api.articles.controllers.articles.findByTag(options);
},
},
},
},
};
There is not currently a way to filter the JSON fields yet as of beta.17.8 (latest)
Probably something like that?
strapi.query('cool_model').find({ categoryList: { $all: [ "cat" , "cat1" ] } })

How to concat array of observables into one. throw error Property 'pipe' does not exist on type 'Observable<Message>[]'

angular v 6.1.10
typescript v 2.9.2
rxjs v 6.3.3
ng2-stmompjs v 7.0.0
I am using ng2-stomp library for web sockets which create observable of will initiate a subscription which is observable. In my requirements, I am creating multiple channel subscriptions based on application id and now want to subscribe all these channels all in once or we can say higher order observable so tried to use the various rxjs operator merge, mergeAll, concat but nothing works so far. Here is what I have done so far.
Right now this one is working
appList = [{appID: '123'}, {appID: '345'}];
const appList$ = appList.map((appID: string, idx: number) => {
const headers = Object.assign({}, this.headers, { id: `app_${idx}` });
const watcher = this.rxStompService.watch(`/topic/${appID}`, headers);
console.log({ watcher }); // This is observable
return watcher;
});
appList$.forEach((app$) => {
app$.subscribe((message: Message) => {
const notification: Notification = JSON.parse(message.body);
this.totalNotificationCount++;
if (Object.keys(notification).length) {
this.notificationMessages.push(notification);
}
});
});
{
"watcher": { "_isScalar": false, "source": { "source": { "_isScalar": false } }, "operator": { "connectable": { "source": { "_isScalar": false } } } }
}
BUT I think we can concat all observables in one and can subscribe all. Note that I am unable to use ForkJoin because appList is dynamic and so the number of WebSocket. followings are my trail to convert multiple observable into once.
Trial 1: using concat and map operator
const batch = appList.map((appID, idx) => {
console.log({ appID, idx });
const headers = Object.assign({}, this.headers, { id: `app_${idx}` });
const watcher = this.rxStompService.watch(`/topic/${appID}`, headers);
return watcher;
});
concat(...batch).pipe( map (i => i)).subscribe({ });
this gives error:
Property 'pipe' does not exist on type 'MonoTypeOperatorFunction'.
trial 2: use subscribe all after concat
concat(...batch).subscribe({
next: (v: any) => console.log(v),
complete: () => console.log('Complete')
});
Error: Property 'subscribe' does not exist on type 'MonoTypeOperatorFunction'.
Trail 3: using pipe
const appList$ = appList.map((appID: string, idx: number) => {
const headers = Object.assign({}, this.headers, { id: `app_${idx}` });
const watcher = this.rxStompService.watch(`/topic/${appID}`, headers);
return watcher;
});
console.log({ appList$ });
appList$.pipe(
takeUntil(this.ngUnsubscribe),
tap((i) => {
console.log('tapping', i);
})
);
console.log({appList$}) return this
{
"appList$": [
{
"_isScalar": false,
"source": {
"source": {
"_isScalar": false
}
},
"operator": {
"connectable": {
"source": {
"_isScalar": false
}
}
}
},
{
"_isScalar": false,
"source": {
"source": {
"_isScalar": false
}
},
"operator": {
"connectable": {
"source": {
"_isScalar": false
}
}
}
},
{
"_isScalar": false,
"source": {
"source": {
"_isScalar": false
}
},
"operator": {
"connectable": {
"source": {
"_isScalar": false
}
}
}
},
{
"_isScalar": false,
"source": {
"source": {
"_isScalar": false
}
},
"operator": {
"connectable": {
"source": {
"_isScalar": false
}
}
}
},
{
"_isScalar": false,
"source": {
"source": {
"_isScalar": false
}
},
"operator": {
"connectable": {
"source": {
"_isScalar": false
}
}
}
},
{
"_isScalar": false,
"source": {
"source": {
"_isScalar": false
}
},
"operator": {
"connectable": {
"source": {
"_isScalar": false
}
}
}
}
]
}
Error: Property 'pipe' does not exist on type 'Observable[]'
So my question is how to merge all observable into once and subscribe in once
This is amazing; whenever I write the question here and try again and I found the solution myself.
I have solved this way using from and mergeMap and thanks to this angular in depth article
private watchApplications(appList: string[]) {
const appList$ = from(appList).pipe(
mergeMap((appID, idx) => {
const headers = Object.assign({}, this.headers, { id: `app_${idx}` });
const watcher = this.rxStompService.watch(`/topic/${appID}`, headers);
return watcher;
})
);
appList$
.pipe(
takeUntil(this.ngUnsubscribe),
tap((f: Frame) => {
console.log('tapping Frame', f);
})
)
.subscribe((message: Message) => {
const notification: Notification = JSON.parse(message.body);
console.log({ notification });
this.totalNotificationCount++;
if (Object.keys(notification).length) {
this.notificationMessages.push(notification);
}
});
}

How can I select a part of a array of objects in a GraphQL query?

My resolver get
{ adminMsg:
[
{active: “y”, text1: “blah1" } ,
{active: “n”, text1: “blah2" }
] };
My query:
{
adminWarn {
adminMsg {
active, text1
}
}
}
I want only array-elements with condition: active = 'y'
I find in GQL Dokumentation no way to write this condition im my query.
Is there any solution in GQL?
Use of resolve args can solve the problem:
const adminWarnList = new GraphQLObjectType({
name: 'adminWarnReportList',
fields: () => ({
adminMsg: {
type: new GraphQLList(adminWarnFields),
},
}),
});
const adminWarn = {
type: adminWarnList,
args: {
active: { type: GraphQLString },
},
resolve: (parent, args, context) => {
...
let reportdata = context.loadData();
if (args.active == 'y') {
let filteredItems = reportdata.filter(function(item) {
return item.active != null && item.active != 'y';
});
reportdata = filteredItems;
}
return { adminMsg: reportdata };
},
};

Variable value in graphql mutation isn't saving

I have simple mutation with variables as:
Mutation Query
mutation M($name: String) {
Adduser(name: $name) {
_id
name
}
}
Query variables
{
"name":"user1"
}
And in graphql API:
app.post('/graphql', (req, res) => {
const query = req.body.query;
const vars = req.body.variables;
//console.log(vars);/*showing { "name":"user1" }*/
graphql(Schema, query, null, vars).then(result => {
res.send(result);
});
});
but the output i am getting is:
{
"data": {
"Adduser": {
"_id": "593a2cd4cf057a07d073e971",
"name": null
}
}
}
Updated
userType:
const Adduser = {
type: userType,
args: {
name: {
type: graphql.GraphQLString
},
age: {
type: graphql.GraphQLInt
}
},
resolve: (obj, args) => {
return new Promise((resolve, reject) => {
var user=new User({
name:args.name
});
user.save(function(err,usr){
if(err){
console.log(err);
}
else{
resolve(usr);
}
});
})
}
};
mutation:
const Rootmutation = new graphql.GraphQLObjectType({
name: 'Rootmutation',
fields: {
Adduser: Adduser
}
});
const schema = new graphql.GraphQLSchema({
query: Rootquery,
mutation: Rootmutation
});
It means the variable value is not binding with mutation query.
Any suggestion here??

Resources