Strapi custom service overwrite find method - strapi

I'm using strapi v4 and I want to populate all nested fields by default when I retrieve a list of my objects (contact-infos). Therefore I have overwritten the contact-info service with following code:
export default factories.createCoreService('api::contact-info.contact-info', ({ strapi }): {} => ({
async find(...args) {
let { results, pagination } = await super.find(...args)
results = await strapi.entityService.findMany('api::contact-info.contact-info', {
fields: ['locale'],
populate: {
sections: {
populate: { link: true }
}
}
})
return { results, pagination }
},
}));
That works well, but I execute a find all entries on the database twice, I guess, which I want to avoid, but when I try to return the result from the entityService directly I'm getting following response:
data": null,
"error": {
"status": 404,
"name": "NotFoundError",
"message": "Not Found",
"details": {}
}
also, I have no idea how I would retrieve the pagination information if I don't call super.find(). Is there any way to find all contents with the option to populate nested objects?

the recommended way of doing this, would be a middleware (do it once apply for all controllers). There would be an video Best Practice Session 003 where it's describes exactly this scenario (Not sure if it's discord only, but on moment of writing this it wasn't yet published).
So regarding rest of your question:
async find(...args) {
let { results, pagination } = await super.find({...args, populate: {section: ['link']})
}
should be sufficient to fix that up in one query
custom pagination example:
async findOne(ctx) {
const { user, auth } = ctx.state;
const { id } = ctx.params;
const limit = ctx.query?.limit ?? 20;
const offset = ctx.query?.offset ?? 0;
const logs = await strapi.db.query("api::tasks-log.tasks-log").findMany({
where: { task: id },
limit,
offset,
orderBy: { updatedAt: "DESC" },
});
const total = await strapi.db
.query("api::tasks-log.tasks-log")
.count({ where: { task: id } });
return { data: logs, meta: { total, offset, limit } };
}

one small addition to the accepted answer, the answer didn't work completely since args is an array with an object inside, so I had to do it like this:
async find(...args) {
const argsObj = args[0]
let { results, pagination } = await super.find({...argsObj, populate: {section: ['link']})
}

Related

Shopify storefront API GraphQL: How to get specific product data?

Note: I'm new to GraphQL.
Challenge: I use the Shopify Storefront API to create a selectbox of all our products. When a user selects a product in this selectbox, its metafields should be displayed on the page.
I managed to create that selectbox. But how would i display the product-specific data when a choice was made in the selectbox? See current code:
function apiCall(productQuery) {
return fetch('https://store//api/2022-04/graphql.json',
{
method: 'POST',
headers: {
'Content-Type': 'application/graphql',
'X-Shopify-Storefront-Access-Token': "xxx"
},
"body": productQuery
}
)
.then(
response => response.json()
);
}
function getProducts() {
const productQuery = `{ products(first: 250) { edges { node { id handle title } } } }`;
return apiCall(productQuery);
}
$(document).ready(function() {
const product_selector_container = $('.product_selector_container');
getProducts().then(response => {
product_selector_container.prepend("<select name='product_compatibility_selector' id='product_compatibility_selector'></select>");
const productSelect = $('#product_compatibility_selector');
const productSelectResult = $("#product_compatibility_result");
response.data.products.edges.forEach(product => {
const optionValues = `<option value="${product.node.handle}">${product.node.title}<option>`;
productSelect.append(optionValues);
});
$("#product_compatibility_selector").on('change', function() {
var selected = $(this).find('option:selected').text();
var selectedVal = $(this).find('option').val();
$(".chosen_product_title").text(selected);
response.data.products.edges.forEach(product => {
// HOW DO I REFERENCE THE CURRENT CHOSEN PRODUCT TO OUTPUT VARIOUS NODES?
const compatibility_result = `${product.node.title}`;
productSelectResult.append(compatibility_result);
});
});
});
});
Now that you have the handle of the selected produt to retrieve all the metafields of that produt you need to run another query, using the "query" parameter, something like this
{
products(first: 1, query:"handle:your-handle"){
edges{
node{
metafields(first:10){
edges{
node{
value
key
}
}
}
}
}
}
}
or
{
product(handle:"your_handle"){
title
metafield(key:"your_key", namespace:"your_space"){
value
}
}
}
If you want to parametrize your handle you may want to introduce variables in your query, like this
query($handle:String){
product(handle:$handle){
title
metafield(key:"x",namespace:"y"){
id
value
}
}
}
and with the variable object being like
{"handle":"your-handle"}
In the request instead of just sending the query you send an object like
{"query" : your-query, "variables" : variable-object}

apollo client cache for nested queries

I have a nested query (query inside query) with apollo client.
Everything works great, I do the request and get the correct data, but the issue is when I'm trying to use the cache, the cache returns undefined for the nested query prop.
My query:
query GetStudents($first: Int!, $after: String) {
me {
id
email
firstName
lastName
students(first: $first, after: $after) {
edges {
node {
id
created
number
status
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
When I try to use the inMemoryCache, the students is always undefined :
new InMemoryCache({
typePolicies: {
Query: {
fields: {
me: {
keyArgs: false,
merge(existing = {}, incoming = {}, { readField }) {
const id = readField("id", incoming);
const email = readField("email", incoming);
const students = readField("students", incoming);
return {
...
};
},
},
}
}
}
});
I can read correctly the id and email from the cache, but the students (which is the nested query) will be always undefined.
Do I need to read the cache students in a different way because it is a query?

How to get random records from Strapi content API

I have records in strapi. I am using strapi content API. In my front-end, I need to display only 2 records randomly. For limiting, I have used limit query from content API. But random fetching what keyword I need to use. The official documentation doesn't provide any details regarding this - https://strapi.io/documentation/v3.x/content-api/parameters.html#available-operators
There's no official Strapi API parameter for random. You have to implement your own. Below is what I've done previously, using Strapi v3:
1 - Make a service function
File: api/mymodel/services/mymodel.js
This will contain our actual random query (SQL), and wrapping it in a service is handy because it can be used in many places (cron jobs, inside other models, etc).
module.exports = {
serviceGetRandom() {
return new Promise( (resolve, reject) => {
// There's a few ways to query data.
// This example uses Knex.
const knex = strapi.connections.default
let query = knex('mydatatable')
// Add more .select()'s if you want other fields
query.select('id')
// These rules enable us to get one random post
query.orderByRaw('RAND()')
query.limit(1)
// Initiate the query and do stuff
query
.then(record => {
console.log("getRandom() record: %O", record[0])
resolve(record[0])
})
.catch(error => {
reject(error)
})
})
}
}
2 - Use the service somewhere, like a controller:
File: api/mymodel/controllers/mymodel.js
module.exports = {
//(untested)
getRandom: async (ctx) => {
await strapi.services.mymodel.serviceGetRandom()
.then(output => {
console.log("getRandom output is %O", output.id)
ctx.send({
randomPost: output
}, 200)
})
.catch( () => {
ctx.send({
message: 'Oops! Some error message'
}, 204) // Place a proper error code here
})
}
}
3 - Create a route that points to this controller
File: api/mymodel/config/routes.json
...
{
"method": "GET",
"path": "/mymodelrandom",
"handler": "mymodel.getRandom",
"config": {
"policies": []
}
},
...
4 - In your front-end, access the route
(However you access your API)
e.g. ajax call to /api/mymodelrandom
There is no API parameter for getting a random result.
So: FrontEnd is the recommended solution for your question.
You need to create a random request range and then get some random item from this range.
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
const firstID = getRandomInt(restaurants.length);
const secondID = getRandomInt(3);
const query = qs.stringify({
id_in:[firstID,secondID ]
});
// request query should be something like GET /restaurants?id_in=3&id_in=6
One way you can do this reliably is by two steps:
Get the total number of records
Fetch the number of records using _start and _limit parameters
// Untested code but you get the idea
// Returns a random number between min (inclusive) and max (exclusive)
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
const { data: totalNumberPosts } = await axios.get('/posts/count');
// Fetch 20 posts
const _limit = 20;
// We need to be sure that we are not fetching less than 20 posts
// e.g. we only have 40 posts. We generate a random number that is 30.
// then we would start on 30 and would only fetch 10 posts (because we only have 40)
const _start = getRandomArbitrary(0, totalNumberPosts - _limit);
const { data: randomPosts } = await axios.get('/posts', { params: { _limit, _start } })
The problem with this approach is that it requires two network requests but for my needs, this is not a problem.
This seem to work for me with Strapi v.4 REST API
Controller, Get 6 random entries
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries].sort(() => 0.5 - Math.random());
ctx.body = randomEntries.slice(0, numberOfEntries);
},
};
});
Route
random.js
"use strict";
module.exports = {
routes: [
{
method: "GET",
path: "/artwork/random",
handler: "artwork.random",
config: {
auth: false,
},
},
],
};
API
http://localhost:1337/api/artwork/random
To match default data structure of Strapi
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries]
.sort(() => 0.5 - Math.random())
.slice(0, numberOfEntries);
const structureRandomEntries = {
data: randomEntries.map((entry) => {
return {
id: entry.id,
attributes: entry,
};
}),
};
ctx.body = structureRandomEntries;
},
};
});
There is also a random sort plugin.
https://www.npmjs.com/package/strapi-plugin-random-sort
This seem to work for me with Strapi v4.3.8 and graphql
src/index.js
"use strict";
module.exports = {
register({ strapi }) {
const extensionService = strapi.service("plugin::graphql.extension");
const extension = ({ strapi }) => ({
typeDefs: `
type Query {
randomTestimonial: Testimonial
}
`,
resolvers: {
Query: {
randomTestimonial: async (parent, args) => {
const entries = await strapi.entityService.findMany(
"api::testimonial.testimonial"
);
const sanitizedRandomEntry =
entries[Math.floor(Math.random() * entries.length)];
return sanitizedRandomEntry;
},
},
},
resolversConfig: {
"Query.randomTestimonial": {
auth: false,
},
},
});
extensionService.use(extension);
},
bootstrap({ strapi }) {},
};
graphql query:
query GetRandomTestimonial {
randomTestimonial {
__typename
name
position
location
description
}
}
generate random testimonial on route change/refresh
https://jungspooner.com/biography

Apollo GraphQL server: filter (or sort) by a field that is resolved separately

I might be facing a design limitation of Apollo GraphQL server and I'd like to ask if there is a workaround.
My schema contains type Thing, that has field flag. I'd like to be able to filter things by the value of flag, but there is appears to be impossible if this field is resolved separately. The same problem would arise if I wanted to sort things. Here’s an example:
type Thing {
id: String!
flag Boolean!
}
type Query {
things(onlyWhereFlagIsTrue: Boolean): [Thing!]!
}
const resolvers = {
Thing: {
flag: async ({id}) => {
const value = await getFlagForThing(id);
return value;
}
},
Query: {
async things(obj, {onlyWhereFlagIsTrue = false}) {
let result = await getThingsWithoutFlags();
if (onlyWhereFlagIsTrue) {
// ↓ this does not work, because flag is still undefined
result = _.filter(result, ['flag', true]);
}
return result;
}
}
}
Is there any way of filtering things after all the async fields are resolved? I know I can call getFlagForThing(id) inside things resolver, but won't that be just repeating myself? The logic behind resolving flag can be a bit more complex than just calling one function.
UPD: This is the best solution I could find so far. Pretty ugly and hard to scale to other fields:
const resolvers = {
Thing: {
flag: async ({id, flag}) => {
// need to check if flag has already been resolved
// to avoid calling getThingsWithoutFlags() twice
if (!_.isUndefined(flag)) {
return flag;
}
const value = await getFlagForThing(id);
return value;
}
},
Query: {
async things(obj, {onlyWhereFlagIsTrue = false}) {
let result = await getThingsWithoutFlags();
if (onlyWhereFlagIsTrue) {
// asynchroniously resolving flags when needed
const promises = _.map(result, ({id}) =>
getFlagForThing(id)
);
const flags = await Promise.all(promises);
for (let i = 0; i < flags.length; i += 1) {
result[i].flag = flags[i];
}
// ↓ this line works now
result = _.filter(result, ['flag', true]);
}
return result;
}
},
};
I think that the issue here is not really a limitation of Apollo server, and more to do with the fact that you have a primitive field with a resolver. Generally, it's best to use resolvers for fields only when that field is going to return a separate type:
Thing {
id: ID!
flag: Boolean!
otherThings: OtherThing
}
Query {
things(onlyWhereFlag: Boolean): [Thing!]!
}
In this example, it would be fine to have a separate resolver for otherThings, but if a field is a primitive, then I would just resolve that field along with Thing.
Using your original schema:
const filterByKeyValuePair = ([key, value]) => obj => obj[key] === value;
const resolvers = {
Query: {
async things(parent, { onlyWhereFlag }) {
const things = await Promise.all(
(await getThings()).map(
thing =>
new Promise(async resolve =>
resolve({
...thing,
flag: await getFlagForThing(thing)
})
)
)
);
if (onlyWhereFlag) {
return things.filter(filterByKeyValuePair(['flag', true]));
} else {
return things;
}
}
}
};
What if flag wasn't a primitive? Well, if you want to filter by it, then you would have a couple of different options. These options really depend on how you are fetching the "flag" data. I'd be happy to elaborate if you can provide more details about your schema and data models.

GraphQL how to mutate data

I have a basic schema for mutating some data which looks like
const schema = new graphql.GraphQLSchema({
mutation: new graphql.GraphQLObjectType({
name: 'Remove',
fields: {
removeUser: {
type: userType,
args: {
id: { type: graphql.GraphQLString }
},
resolve(_, args) {
const removedData = data[args.id];
delete data[args.id];
return removedData;
},
},
},
})
});
Looking around google I cant find a clear example of the example query which needs to be sent to mutate.
I have tried
POST -
localhost:3000/graphql?query={removeUser(id:"1"){id, name}}
This fails with error:
{
"errors": [
{
"message": "Cannot query field \"removeUser\" on type \"Query\".",
"locations": [
{
"line": 1,
"column": 2
}
]
}
]
}
In order to post requests from the front-end application it is recommended to use apollo-client package. Say i wanted to validate a user login information:
import gql from 'graphql-tag';
import ApolloClient, {createNetworkInterface} from 'apollo-client';
client = new ApolloClient({
networkInterface: createNetworkInterface('http://localhost:3000/graphql')
});
remove(){
client.mutate({
mutation: gql`
mutation remove(
$id: String!
) {
removeUser(
id: $id
){
id,
name
}
}
`,
variables: {
id: "1"
}
}).then((graphQLResult)=> {
const { errors, data } = graphQLResult;
if(!errors && data){
console.log('removed successfully ' + data.id + ' ' + data.name);
}else{
console.log('failed to remove');
}
})
}
More information about apollo-client can be found here
Have you tried using graphiql to query and mutate your schema?
If you'd like to create a POST request manually you might wanna try to struct it in the right form:
?query=mutation{removeUser(id:"1"){id, name}}
(Haven't tried POSTing myself, let me know if you succeeded, i structured this out of the url when using graphiql)
You have to explicitly label your mutation as such, i.e.
mutation {
removeUser(id: "1"){
id,
name
}
}
In GraphQL, if you leave out the mutation keyword, it's just a shorthand for sending a query, i.e. the execution engine will interpret it as
query {
removeUser(id: "1"){
id,
name
}
}
cf. Section 2.3 of the GraphQL Specification
const client = require("../common/gqlClient")();
const {
createContestParticipants,
} = require("../common/queriesAndMutations");
const gql = require("graphql-tag");
const createPartpantGql = async (predictObj) => {
try {
let resp = await client.mutate({
mutation: gql(createContestParticipants),
variables: {
input: {
...predictObj,
},
},
});
let contestParticipantResp = resp.data.createContestParticipants;
return {
success: true,
data: contestParticipantResp,
};
} catch (err) {
console.log(err.message)
console.error(`Error creating the contest`);
return {
success: false,
message: JSON.stringify(err.message),
};
}
};

Resources