Avoid re-building model functionality in a Strapi custom controller - strapi

Using Strapi, how can you maintain standard Strapi model features when using a custom controller and raw query?
The Strapi docs mention that when implementing a custom controller, you will lose model functionality. Given this, is there still a way to manually leverage the model features, to avoid having to re-build everything from scratch?
For example:
A model with many fields (e.g. 40), with some fields being a relationship to other models (each having many fields).
Strapi generates everything for you: API, routes, filtering, etc. This is the power of Strapi!
Sample requests:
Shows all
/myModel
Some basic filters
/myModel?Cost_gte=1&Cost_lte=50
Or filters dynamically generated (using qs)
/myModel?_where[_or][0][0][stars]=2&_where[_or][0][1][pricing_lt]=80&_where[_or][1][0][stars]=1&_where[_or][1][1][categories.name]=French
Ultimately, after processing, these requests simply become SQL queries (from the perspective of the database).
PROBLEM:
For special contexts, I need to introduce some raw SQL into the queries above. The recommended way to do this is making a custom controller and manually formulating a query using BookshelfJS and Knex (tech that Strapi uses).
However this means having to interpret and process all of the request parameters myself; essentially building an entire controller from scratch.
For example, a custom controller may look like:
find: async (ctx) => {
const knex = strapi.connections.default;
var query = knex('myModel')
.join('locations', 'myModel.Location', 'locations.id')
// A whole ton of select statements
.select('myModel.Cost as Cost')
// ... and 50 more lines, maybe...
// A whole ton of statements to process request parameters
if(ctx.query.Cost_gte) query.where('events.Cost', '>=', ctx.query.Cost_gte)
if(ctx.query.Cost_lte) query.where('events.Cost', '<=', ctx.query.Cost_lte)
// ... and 50 more lines, maybe...
// Here is where I use a raw query, performing distance calculation using db functionality
if(ctx.query.Longitude && ctx.query.Latitude) {
query.where(knex.raw(
round(st_distance_sphere(
st_geomfromtext(CONCAT(' POINT(',locations.Longitude, ' ', locations.Latitude,')' )),
st_geomfromtext(CONCAT(' POINT(` + ctx.query.Longitude + ` ` + ctx.query.Latitude + `) '))
)) <= 5000`
))
}
const result = await query
return result
}
How can one avoid re-building everything from scratch?
Ultimately, I'm just looking to get search results within 50km of a user. All signs point towards doing a custom query being the only way.

Same issue here, not a solution but i went down the route of trying to replicate the strapi model filtering with some customisations that i needed.
leaving this here incase someone needs to do the same, warning it's probably missing stuff
const result = await strapi
.query('articles')
.model.query(qb => {
// try do the strapi filter
var filters = ['eq', 'ne', 'lt', 'gt', 'lte', 'gte', 'in', 'nin', 'contains', 'ncontains', 'consatinss', 'ncontainss', 'null']
for(const param in ctx.query) {
var array = param.split('.')
var column = array.pop()
var table = array.join('.').length > 0 ? array.join('.') : 'articles'
var filter = column.match(/[^_]*$/)[0];
if(filter!=column) column = column.slice(0, column.lastIndexOf('_'));
if(filter==column) filter = 'eq';
// join relations if needed
switch (table) {
case 'guided_series':
qb.leftJoin('articles__guided_series', 'articles__guided_series.article_id', 'articles.id')
qb.leftJoin('guided_series', 'guided-sery_id', 'guided_series.id')
break;
case 'contributors':
qb.leftJoin('articles__contributors', 'articles__contributors.article_id', 'articles.id')
qb.leftJoin('contributors', 'contributor_id', 'contributors.id')
break;
case 'tags':
qb.leftJoin('articles__tags', 'articles__tags.article_id', 'articles.id')
qb.leftJoin('tags', 'tag_id', 'tags.id')
break;
default:
break;
}
if(filters.includes(filter)) {
// fix bool values
ctx.query[param] = ctx.query[param] === "true" ? true : ctx.query[param];
ctx.query[param] = ctx.query[param] === "false" ? false : ctx.query[param];
switch (filter) {
case 'eq':
qb.where(`${table}.${column}`, ctx.query[param])
break;
case 'ne':
qb.where(`${table}.${column}`, '!=', ctx.query[param])
break;
case 'lt':
qb.where(`${table}.${column}`, '<', ctx.query[param])
break;
case 'gt':
qb.where(`${table}.${column}`, '>', ctx.query[param])
break;
case 'lte':
qb.where(`${table}.${column}`, '<=', ctx.query[param])
break;
case 'gte':
qb.where(`${table}.${column}`, '>=', ctx.query[param])
break;
case 'in':
if(Array.isArray(ctx.query[param])) {
// for extra flexibility, handle null here
if(ctx.query[param].includes('null')) {
qb.where(`${table}.${column}`, 'in', ctx.query[param])
.orWhereNull(`${table}.${column}`);
} else {
qb.where(`${table}.${column}`, 'in', ctx.query[param])
}
} else {
qb.where(`${table}.${column}`, ctx.query[param])
}
break;
case 'nin':
if(Array.isArray(ctx.query[param])) {
qb.where(`${table}.${column}`, 'nin', ctx.query[param])
.orWhereNull(`${table}.${column}`);
} else {
qb.where(`${table}.${column}`, '!=', ctx.query[param])
.orWhereNull(`${table}.${column}`);
}
break;
case 'contains':
//todo
break;
case 'ncontains':
//todo
break;
case 'containss':
//todo
break;
case 'ncontainss':
//todo
break;
case 'null':
if(ctx.query[param]) {
qb.whereNull(`${table}.${column}`)
} else {
qb.whereNotNull(`${table}.${column}`)
}
break;
default:
break;
}
}
}
// todo handle where, where[or]
// default to 'en' locale
qb.where('articles.locale', ctx.query._locale ?? 'en')
// default to '10' limit
qb.limit(ctx.query._limit ?? 10)
// sorting
if(ctx.query._sort) {
var sorts = ctx.query._sort.split(',');
sorts.forEach(element => {
var bits = element .split(':');
qb.orderBy(`articles.${bits[0]}`, bits[1])
});
} else {
qb.orderBy('articles.published_at', 'DESC')
qb.orderBy('articles.created_at', 'DESC')
}
})
.fetchAll({
withRelated: [
'category', 'tags', 'contributors', 'localizations', {
'guided_series': function(qb) {
//qb.where('slug', 'depression');
}
}]
});
const fields = result.toJSON();
return fields;

Related

Is that possible to use IF statement to get data from database and save it into String?

I want to get some record from tbl_jadwal and save it into string in my laravel.
This is my code,
$get_time = if (now()->isMonday()) {
return DB::table('tbl_jadwal')->where('Hari', 'Senin')->value('Jadwal_masuk');
} else if(now()->isSaturday()) { {
return DB::table('tbl_jadwal')->where('Hari', 'Sabtu')->value('Jadwal_masuk');
};
$time = $get_time;
but i got message syntax error, unexpected token "if", is there any other way to solve this problem ?
Broadly speaking you can do:
if (now()->isMonday()) {
$get_time = DB::table('tbl_jadwal')->where('Hari', 'Senin')->value('Jadwal_masuk');
} else if(now()->isSaturday()) { {
$get_time = DB::table('tbl_jadwal')->where('Hari', 'Sabtu')->value('Jadwal_masuk');
}
// Caution: $get_time may not be defined here
return $get_time??null;
You can also use when:
if (!now()->isSaturday() && !now()->isMonday()) { return null; }
return DB::table('tbl_jadwal')
->when(now()->isSaturday(), function ($q) { $q->where('Hari', 'Sabtu'); )
->when(now()->isMonday(), function ($q) { $q->where('Hari', 'Senin'); )
->value('Jadwal_masuk');
However carbon also supports localisation so you may be able to get now()->dayName to actually return the day name according to your locale.

redux-toolkit -- Type error: "unit" is read-only

I am using react-redux and redux-toolkit for this project. I want to add item to the cart. If there is already a same item in cart, just increment the unit. The error occurs at the slice, so I will just show the slice here.
const CartListSlice = createSlice({
name: 'cartItem',
initialState,
reducers: {
addToCart: (state, action) => {
let alreadyExist = false;
// get a copy of it to avoid mutating the state
let copyState = current(state.cartItem).slice();
// loop throught the cart to check if that item is already exist in the cart
copyState.map(item => {
if (item.cartItem._id === action.payload._id) {
alreadyExist = true;
item.unit += 1 // <--- Error occurs here
}
})
// If the item is not exist in the cart, put it in the cart and along with its unit
if (alreadyExist === false) {
state.cartItem.push({
cartItem: action.payload,
unit: 1
});
}
},
}
});
I get a type error telling me that unit is read-only.
How can I update the "unit" variable so that it increments whenever it is supposed to.
In React Toolkit's createSlice, you can modify the state directly and even should do so. So don't create a copy, just modify it.
In fact, this error might in some way stem from making that copy with current.
See the "Writing Reducers with Immer" documentation page on this
Meanwhile, a suggestion:
const CartListSlice = createSlice({
name: 'cartItem',
initialState,
reducers: {
addToCart: (state, action) => {
const existingItem = state.find(item => item.cartItem._id === action.payload._id)
if (existingItem) {
item.unit += 1
} else {
state.push({
cartItem: action.payload,
unit: 1
});
}
},
}
});
You don't need line:
let copyState = current(state.cartItem).slice();
Instead of copyState, just use state.cartItem.map
As #phry said, you should mutate state directly, because redux-toolkit is using immerJS in the background which takes care of mutations.

updating an array of nested documents rethinkdb

I have a document schema like this:
{
"name":"",
"type":"",
"posts":[
{
"url":"",
"content":"",
...
},
{
"url":"",
"content":"",
...
}
...
]
}...
I forgot to create id's for each post on insertion in database. So i'm trying to create a query for that:
r.db('test').table('crawlerNovels').filter(function (x){
return x.keys().contains('chapters')
}).map(function (x){
return x('chapters')
}).map(
function(x){
return x.merge({id:r.uuid()})
}
)
instead this query return all posts with an id but doesn't actually update in the database. I tried using a forEach instead of a map function at the end this doesn't work
After lots of tweaking and frustration i figured it out:
r.db('test').table('crawlerNovels').filter(function (x){
return x.keys().contains('chapters')
}).update(function(novel){
return {"chapters":novel('chapters').map(
function(chapter){
return chapter.merge({"id":r.uuid()})
})}
},{nonAtomic:true})

Chaining queries in rethinkdb

Lets say I have a "category" table, each category has associated data in the "data" table and it has associated data in other tables "associated" and I want to remove a category with all it's associated data.
What I'm currently doing is something like this:
getAllDataIdsFromCategory()
.then(removeAllAssociated)
.then(handleChanges)
.then(removeDatas)
.then(handleChanges)
.then(removeCategory)
.then(handleChanges);
Is there a way to chain these queries on the db-side?
my functions currently look like this:
var getAllDataIdsFromCategory = () => {
return r
.table('data')
.getAll(categoryId, { index: 'categoryId' })
.pluck('id').map(r.row('id')).run();
}
var removeAllAssociated = (_dataIds: string[]) => {
dataIds = _dataIds;
return r
.table('associated')
.getAll(dataIds, { index: 'dataId' })
.delete()
.run()
}
var removeDatas = () => {
return r
.table('data')
.getAll(dataIds)
.delete()
.run()
}
notice that I cannot use r.expr() or r.do() since I want to do queries based on the result of the previous query.
The problem with my approach is that it won't work for large amounts of "data" since I have to bring all of the ids to the client side, and doing paging for it in the client side in a loop seems like a workaround.
You can use forEach for this:
r.table('data').getAll(categoryID, {index: 'categoryId'})('id').forEach(function(id) {
return r.table('associated').getAll(id, {index: 'dataId'}).delete().merge(
r.table('data').get(id).delete())
});

datatables yadcf custom function not triggered

I'm using datatables and the yadcf plugin which is working well. However I'm trying to add a custom filter and having no luck getting it to work. It appears it is being ignored.
Basically I want to filter whether a cell is empty or has data
I used this example as a starting point:
http://yadcf-showcase.appspot.com/DOM_source.html
My custom function is:
function filterGroupName(filterVal, columnVal) {
var found;
if (columnVal === '') {
return true;
}
switch (filterVal) {
case 'Groups':
found = columnVal.length > 0;
break;
case 'No Groups':
found = columnVal.length < 1 || columnVal === '';
break;
default:
found = 1;
break;
}
if (found !== -1) {
return true;
}
return false;
}
And here is the part of the script that sets up yadcf:
{
column_number: 3,
filter_container_id: "filter-group-name",
filter_type: "custom_func",
data: [{
value: 'Groups',
label: 'Groups'
}, {
value: 'No Groups',
label: 'No Groups'
}],
filter_default_label: "Select Groups",
custom_func: filterGroupName
}
I've set a breakpoint in the script to see what's happening but it never gets triggered
The page gets the correct select boxes created but selecting either option returns no entries - everything is being filtered out in the datatable.
So, what have I missed in getting the function to work?
Thank you
I'm managed to work out the problem.
During development I modified the data property of yadcf for the column to that above. However I had state save set on datatables so the original filter was being used - ignoring any changes I made to the strucuture of the new filters and the resulting code.
I removed state save and the filter came to life!

Resources