407 lines
11 KiB
Markdown
Raw Normal View History

2019-05-16 21:37:45 +02:00
# Queries
Strapi provides a utility function `strapi.query` to make database queries ORM agnostic.
## Core Queries
In Strapi's [core services](./services.md#core-services) you can see we call a `strapi.query` function.
When customizing your model services you might want to implement some custom database queries.
To help you with that here is the current implementation of the queries for both `bookshelf` and `mongoose`.
You can simply copy and paste the code in your custom services.
### Bookshelf
```js
const _ = require('lodash');
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
module.exports = ({ model }) => {
return {
find(params, populate) {
const withRelated =
populate ||
model.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
const filters = convertRestQueryParams(params);
return model
.query(buildQuery({ model, filters }))
.fetchAll({ withRelated });
},
findOne(params, populate) {
const withRelated =
populate ||
model.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
return model
.forge({
[model.primaryKey]: params[model.primaryKey] || params.id,
})
.fetch({
withRelated,
});
},
count(params = {}) {
const { where } = convertRestQueryParams(params);
return model.query(buildQuery({ model, filters: { where } })).count();
},
async create(values) {
const relations = _.pick(
values,
model.associations.map(ast => ast.alias)
);
const data = _.omit(values, model.associations.map(ast => ast.alias));
// Create entry with no-relational data.
const entry = await model.forge(data).save();
// Create relational data and return the entry.
return model.updateRelations({ id: entry.id, values: relations });
},
async update(params, values) {
// Extract values related to relational data.
const relations = _.pick(
values,
model.associations.map(ast => ast.alias)
);
const data = _.omit(values, model.associations.map(ast => ast.alias));
// Create entry with no-relational data.
await model.forge(params).save(data);
// Create relational data and return the entry.
return model.updateRelations(
Object.assign(params, { values: relations })
);
},
async delete(params) {
params.values = {};
model.associations.map(association => {
switch (association.nature) {
case 'oneWay':
case 'oneToOne':
case 'manyToOne':
case 'oneToManyMorph':
params.values[association.alias] = null;
break;
case 'oneToMany':
case 'manyToMany':
case 'manyToManyMorph':
params.values[association.alias] = [];
break;
default:
}
});
await model.updateRelations(params);
return model.forge(params).destroy();
},
search(params, populate) {
// Convert `params` object to filters compatible with Bookshelf.
const filters = strapi.utils.models.convertParams(model.globalId, params);
// Select field to populate.
const withRelated =
populate ||
model.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
return model
.query(qb => {
buildSearchQuery(qb, model, params);
if (filters.sort) {
qb.orderBy(filters.sort.key, filters.sort.order);
}
if (filters.skip) {
qb.offset(_.toNumber(filters.skip));
}
if (filters.limit) {
qb.limit(_.toNumber(filters.limit));
}
})
.fetchAll({
withRelated,
});
},
countSearch(params) {
return model.query(qb => buildSearchQuery(qb, model, params)).count();
},
};
};
/**
* util to build search query
* @param {*} qb
* @param {*} model
* @param {*} params
*/
const buildSearchQuery = (qb, model, params) => {
const query = (params._q || '').replace(/[^a-zA-Z0-9.-\s]+/g, '');
const associations = model.associations.map(x => x.alias);
const searchText = Object.keys(model._attributes)
.filter(
attribute =>
attribute !== model.primaryKey && !associations.includes(attribute)
)
.filter(attribute =>
['string', 'text'].includes(model._attributes[attribute].type)
);
const searchInt = Object.keys(model._attributes)
.filter(
attribute =>
attribute !== model.primaryKey && !associations.includes(attribute)
)
.filter(attribute =>
['integer', 'decimal', 'float'].includes(
model._attributes[attribute].type
)
);
const searchBool = Object.keys(model._attributes)
.filter(
attribute =>
attribute !== model.primaryKey && !associations.includes(attribute)
)
.filter(attribute =>
['boolean'].includes(model._attributes[attribute].type)
);
if (!_.isNaN(_.toNumber(query))) {
searchInt.forEach(attribute => {
qb.orWhereRaw(`${attribute} = ${_.toNumber(query)}`);
});
}
if (query === 'true' || query === 'false') {
searchBool.forEach(attribute => {
qb.orWhereRaw(`${attribute} = ${_.toNumber(query === 'true')}`);
});
}
// Search in columns with text using index.
switch (model.client) {
case 'mysql':
qb.orWhereRaw(
`MATCH(${searchText.join(',')}) AGAINST(? IN BOOLEAN MODE)`,
`*${query}*`
);
break;
case 'pg': {
const searchQuery = searchText.map(attribute =>
_.toLower(attribute) === attribute
? `to_tsvector(${attribute})`
: `to_tsvector('${attribute}')`
);
qb.orWhereRaw(`${searchQuery.join(' || ')} @@ to_tsquery(?)`, query);
break;
}
}
};
```
### Mongoose
```js
const _ = require('lodash');
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
module.exports = ({ model, strapi }) => {
const assocs = model.associations.map(ast => ast.alias);
const defaultPopulate = model.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
return {
find(params, populate) {
const populateOpt = populate || defaultPopulate;
const filters = convertRestQueryParams(params);
return buildQuery({
model,
filters,
populate: populateOpt,
});
},
findOne(params, populate) {
const populateOpt = populate || defaultPopulate;
return model
.findOne({
[model.primaryKey]: params[model.primaryKey] || params.id,
})
.populate(populateOpt);
},
count(params) {
const filters = convertRestQueryParams(params);
return buildQuery({
model,
filters: { where: filters.where },
}).count();
},
async create(values) {
// Extract values related to relational data.
const relations = _.pick(values, assocs);
const data = _.omit(values, assocs);
// Create entry with no-relational data.
const entry = await model.create(data);
// Create relational data and return the entry.
return model.updateRelations({ _id: entry.id, values: relations });
},
async update(params, values) {
// Extract values related to relational data.
const relations = _.pick(values, assocs);
const data = _.omit(values, assocs);
// Update entry with no-relational data.
await model.updateOne(params, data, { multi: true });
// Update relational data and return the entry.
return model.updateRelations(
Object.assign(params, { values: relations })
);
},
async delete(params) {
const data = await model
.findOneAndRemove(params, {})
.populate(defaultPopulate);
if (!data) {
return data;
}
await Promise.all(
model.associations.map(async association => {
if (!association.via || !data._id || association.dominant) {
return true;
}
const search =
_.endsWith(association.nature, 'One') ||
association.nature === 'oneToMany'
? { [association.via]: data._id }
: { [association.via]: { $in: [data._id] } };
const update =
_.endsWith(association.nature, 'One') ||
association.nature === 'oneToMany'
? { [association.via]: null }
: { $pull: { [association.via]: data._id } };
// Retrieve model.
const model = association.plugin
? strapi.plugins[association.plugin].models[
association.model || association.collection
]
: strapi.models[association.model || association.collection];
return model.update(search, update, { multi: true });
})
);
return data;
},
search(params, populate) {
// Convert `params` object to filters compatible with Mongo.
const filters = strapi.utils.models.convertParams(model.globalId, params);
const $or = buildSearchOr(model, params._q);
return model
.find({ $or })
.sort(filters.sort)
.skip(filters.start)
.limit(filters.limit)
.populate(populate || defaultPopulate);
},
countSearch(params) {
const $or = buildSearchOr(model, params._q);
return model.find({ $or }).countDocuments();
},
};
};
const buildSearchOr = (model, query) => {
return Object.keys(model.attributes).reduce((acc, curr) => {
switch (model.attributes[curr].type) {
case 'integer':
case 'float':
case 'decimal':
if (!_.isNaN(_.toNumber(query))) {
return acc.concat({ [curr]: query });
}
return acc;
case 'string':
case 'text':
case 'password':
return acc.concat({ [curr]: { $regex: query, $options: 'i' } });
case 'boolean':
if (query === 'true' || query === 'false') {
return acc.concat({ [curr]: query === 'true' });
}
return acc;
default:
return acc;
}
}, []);
};
```
## Understanding queries
`strapi.query` will generate a queries object by passing a model to the factory function matching the model's ORM.
In this example the User model from the Users and Permissions plugin is used.
By default the model is passed to `strapi/lib/core-api/queries/bookshelf.js` or `strapi/lib/core-api/queries/mongoose.js` depending on your connection configuration.
```js
const queries = strapi.query('users', 'users-permissions');
// makes the bookshelf or mongoose queries available with a specific model binded to them
queries.find();
```
### Usage in plugins
To make plugins ORM agnostic, we create a queries function for every plugin that will either load the queries from the plugin's `config/queries` folder if it exists or use the default queries from the `core-api/queries` folder.
```js
// this will call the queries defined in the users-permissions plugin
// with the model user from the users-permissions plugin
strapi.plugins['users-permissions'].queries('user', 'users-permissions').find();
```