mirror of
https://github.com/strapi/strapi.git
synced 2025-12-24 13:43:41 +00:00
Create a generic Builder and Query APIs
This commit is contained in:
parent
e8c92fc3d7
commit
bd1930a75c
@ -43,6 +43,9 @@ Find products having a price equal or greater than `3`.
|
||||
Find multiple product with id 3, 6, 8
|
||||
`GET /products?id_in=3&id_in=6&id_in=8`
|
||||
|
||||
Find posts written by a user belongs to the strapi company.
|
||||
`GET /posts?author.company.name=strapi`
|
||||
|
||||
::: note
|
||||
You can't use filter to have specific results inside relation, like "Find users and only their posts older than yesterday" as example. If you need it, you can modify or create your own service or use [GraphQL](./graphql.md#query-api).
|
||||
:::
|
||||
@ -85,10 +88,11 @@ Get the second page of results.
|
||||
## Programmatic usage
|
||||
|
||||
Requests system can be implemented in custom code sections.
|
||||
In the newest version of strapi, we introduced the Builder API which is an easy/new way to manipulate filters.
|
||||
|
||||
### Extracting requests filters
|
||||
|
||||
To extract the filters from a JavaScript object or a request, you need to call the [`strapi.utils.models.convertParams` helper](../api-reference/reference.md#strapiutils).
|
||||
To extract the filters from a JavaScript object or a request, you need to call the [`Builder` API](../api-reference/reference.md#strapiutils).
|
||||
|
||||
::: note
|
||||
The returned objects are formatted according to the ORM used by the model.
|
||||
@ -96,39 +100,75 @@ The returned objects are formatted according to the ORM used by the model.
|
||||
|
||||
#### Example
|
||||
|
||||
**Path —** `./api/user/controllers/User.js`.
|
||||
**Path —** `./api/product/services/Product.js`.
|
||||
|
||||
```JS
|
||||
const { Builder } = require('strapi-utils');
|
||||
|
||||
module.exports = {
|
||||
// Find products having a price equal or greater than `3`.
|
||||
fetchExpensiveProducts: (params, populate) => {
|
||||
const filter = new Builder(Product)
|
||||
.gte('price', 3)
|
||||
.convert(); // the Convert method will convert your filter according to the Model's ORM (Mongoose/Bookshelf).
|
||||
|
||||
return new Query(Product)
|
||||
.find(filter)
|
||||
.populate(populate)
|
||||
.execute();
|
||||
},
|
||||
|
||||
// Find products that belongs to strapi company ordered by creation date.
|
||||
fetchLastestStrapiProducts: (params, populate) => {
|
||||
const filter = new Builder(Product)
|
||||
.eq('company.name', 'strapi')
|
||||
.contains('company.country', 'fr')
|
||||
.sort('createdAt:DESC') // createdAt:desc will also work here
|
||||
.convert();
|
||||
|
||||
return new Query(Product)
|
||||
.find(filter)
|
||||
.populate(populate)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Path —** `./api/product/controllers/Product.js`.
|
||||
|
||||
```js
|
||||
const { Builder } = require('strapi-utils');
|
||||
|
||||
// Define a list of params.
|
||||
const params = {
|
||||
'_limit': 20,
|
||||
'_sort': 'email'
|
||||
'_sort': 'email:desc'
|
||||
};
|
||||
|
||||
// Convert params.
|
||||
const formattedParams = strapi.utils.models.convertParams('user', params); // { limit: 20, sort: 'email' }
|
||||
const formattedParams = new Builder(Product, params).build(); // { limit: 20, sort: { order: 'desc', key: 'email' } }
|
||||
const convertedParams = new Builder(Product, params).convert(); // { limit: 20, sort: '-email' }
|
||||
```
|
||||
|
||||
### Query usage
|
||||
|
||||
#### Example
|
||||
|
||||
**Path —** `./api/user/controllers/User.js`.
|
||||
**Path —** `./api/product/controllers/Product.js`.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
const { Builder, Query } = require('strapi-utils');
|
||||
|
||||
module.exports = {
|
||||
find: async (ctx) => {
|
||||
// Convert params.
|
||||
const formattedParams = strapi.utils.models.convertParams('user', ctx.request.query);
|
||||
const filter = new Builder(Product, ctx.request.query).convert();
|
||||
|
||||
// Get the list of users according to the request query.
|
||||
const filteredUsers = await User
|
||||
.find()
|
||||
.where(formattedParams.where)
|
||||
.sort(formattedParams.sort)
|
||||
.skip(formattedParams.start)
|
||||
.limit(formattedParams.limit);
|
||||
const filteredUsers = await new Query(Product)
|
||||
.find(filter)
|
||||
.execute();
|
||||
|
||||
// Finally, send the results to the client.
|
||||
ctx.body = filteredUsers;
|
||||
|
||||
@ -56,6 +56,7 @@ module.exports = scope => {
|
||||
'dependencies': Object.assign({}, {
|
||||
'lodash': '^4.17.5',
|
||||
'strapi': getDependencyVersion(cliPkg, 'strapi'),
|
||||
'strapi-utils': getDependencyVersion(cliPkg, 'strapi'),
|
||||
[scope.client.connector]: getDependencyVersion(cliPkg, 'strapi'),
|
||||
}, additionalsDependencies, {
|
||||
[scope.client.module]: scope.client.version
|
||||
|
||||
@ -134,7 +134,7 @@ module.exports = {
|
||||
fetchAll: params => {
|
||||
return strapi
|
||||
.query('file', 'upload')
|
||||
.find(strapi.utils.models.convertParams('file', params));
|
||||
.find(params);
|
||||
},
|
||||
|
||||
count: async () => {
|
||||
|
||||
197
packages/strapi-utils/lib/builder.js
Normal file
197
packages/strapi-utils/lib/builder.js
Normal file
@ -0,0 +1,197 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const getFilterKey = (key) => {
|
||||
const matched = key.match(/^_?(sort|limit|start)$/);
|
||||
if (matched) {
|
||||
return matched[1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getOperatorKey = (key) => {
|
||||
const matched = key.match(/(.*)_(neq?|lte?|gte?|containss?|n?in|exists)$/);
|
||||
if (matched) {
|
||||
return matched.slice(1);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
class Builder {
|
||||
constructor(model, filter) {
|
||||
// Initialize Model
|
||||
this.model = model;
|
||||
// Initialize the default filter options
|
||||
this.filter = {
|
||||
limit: 100,
|
||||
start: 0,
|
||||
where: {}
|
||||
};
|
||||
|
||||
if (!_.isEmpty(filter)) {
|
||||
this.parse(filter);
|
||||
}
|
||||
}
|
||||
|
||||
parse(filter) {
|
||||
for (const key of _.keys(filter)) {
|
||||
const value = filter[key];
|
||||
|
||||
// Check if the key is a filter key
|
||||
const filterKey = getFilterKey(key);
|
||||
if (filterKey) {
|
||||
this[filterKey].apply(this, [value]);
|
||||
} else {
|
||||
const matched = getOperatorKey(key);
|
||||
let field = key;
|
||||
let operation = 'eq';
|
||||
if (matched) {
|
||||
[field, operation] = matched;
|
||||
}
|
||||
this[operation].apply(this, [field, value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort(sort) {
|
||||
const [key, order = 'ASC'] = _.isString(sort) ? sort.split(':') : sort;
|
||||
|
||||
this.filter.sort = {
|
||||
order: order.toLowerCase(),
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
limit(limit) {
|
||||
this.filter.limit = _.toNumber(limit);
|
||||
}
|
||||
|
||||
start(start) {
|
||||
this.filter.start = _.toNumber(start);
|
||||
}
|
||||
|
||||
add(w) {
|
||||
for (const k of _.keys(w)) {
|
||||
if (k in this.filter.where) {
|
||||
// Found conflicting keys, create an `and` operator to join the existing
|
||||
// conditions with the new one
|
||||
const where = {};
|
||||
where.and = [this.filter.where, w];
|
||||
this.filter.where = where;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
// Merge the where items
|
||||
this.filter.where = {
|
||||
...this.filter.where,
|
||||
...w
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
eq(key, value) {
|
||||
const w = {};
|
||||
w[key] = value;
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
neq(key, value) {
|
||||
const w = {};
|
||||
w[key] = { ne: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
ne(key, value) {
|
||||
// This method needs to be deprecated in favor of neq
|
||||
return this.neq(key, value);
|
||||
}
|
||||
|
||||
exists(key, value) {
|
||||
const w = {};
|
||||
w[key] = { exists: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
in(key, value) {
|
||||
const w = {};
|
||||
w[key] = { in: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
nin(key, value) {
|
||||
const w = {};
|
||||
w[key] = { nin: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
contains(key, value) {
|
||||
const w = {};
|
||||
w[key] = {
|
||||
contains: value,
|
||||
};
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
containss(key, value) {
|
||||
const w = {};
|
||||
w[key] = {
|
||||
containss: value,
|
||||
};
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
startsWith(key, value) {
|
||||
const w = {};
|
||||
w[key] = { startsWith: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
endsWith(key, value) {
|
||||
const w = {};
|
||||
w[key] = { endsWith: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
gt(key, value) {
|
||||
const w = {};
|
||||
w[key] = { gt: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
gte(key, value) {
|
||||
const w = {};
|
||||
w[key] = { gte: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
lt(key, value) {
|
||||
const w = {};
|
||||
w[key] = { lt: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements <= operation
|
||||
* @param {string} key field id
|
||||
* @param {number} value its value
|
||||
*/
|
||||
lte(key, value) {
|
||||
const w = {};
|
||||
w[key] = { lte: value };
|
||||
return this.add(w);
|
||||
}
|
||||
|
||||
convert() {
|
||||
const hook = strapi.hook[this.model.orm];
|
||||
const { Converter } = hook.load();
|
||||
|
||||
return new Converter(this.model, this.filter).convert();
|
||||
}
|
||||
|
||||
build() {
|
||||
return this.filter;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Builder
|
||||
};
|
||||
@ -16,5 +16,7 @@ module.exports = {
|
||||
packageManager: require('./packageManager'),
|
||||
policy: require('./policy'),
|
||||
regex: require('./regex'),
|
||||
templateConfiguration: require('./templateConfiguration')
|
||||
templateConfiguration: require('./templateConfiguration'),
|
||||
Query: require('./query').Query,
|
||||
Builder: require('./builder').Builder
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ const path = require('path');
|
||||
|
||||
// Public node modules.
|
||||
const _ = require('lodash');
|
||||
const pluralize = require('pluralize');
|
||||
|
||||
// Following this discussion https://stackoverflow.com/questions/18082/validate-decimal-numbers-in-javascript-isnumeric this function is the best implem to determine if a value is a valid number candidate
|
||||
const isNumeric = (value) => {
|
||||
@ -309,6 +310,16 @@ module.exports = {
|
||||
return _.get(strapi.models, collectionIdentity.toLowerCase() + '.orm');
|
||||
},
|
||||
|
||||
/**
|
||||
* Return table name for a collection many-to-many
|
||||
*/
|
||||
getCollectionName: (associationA, associationB) => {
|
||||
return [associationA, associationB]
|
||||
.sort((a, b) => a.collection < b.collection ? -1 : 1)
|
||||
.map(table => _.snakeCase(`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`))
|
||||
.join('__');
|
||||
},
|
||||
|
||||
/**
|
||||
* Define associations key to models
|
||||
*/
|
||||
@ -338,7 +349,7 @@ module.exports = {
|
||||
|
||||
// Build associations object
|
||||
if (association.hasOwnProperty('collection') && association.collection !== '*') {
|
||||
definition.associations.push({
|
||||
const ast = {
|
||||
alias: key,
|
||||
type: 'collection',
|
||||
collection: association.collection,
|
||||
@ -348,7 +359,13 @@ module.exports = {
|
||||
dominant: details.dominant !== true,
|
||||
plugin: association.plugin || undefined,
|
||||
filter: details.filter,
|
||||
});
|
||||
};
|
||||
|
||||
if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') {
|
||||
ast.tableCollectionName = this.getCollectionName(association, details);
|
||||
}
|
||||
|
||||
definition.associations.push(ast);
|
||||
} else if (association.hasOwnProperty('model') && association.model !== '*') {
|
||||
definition.associations.push({
|
||||
alias: key,
|
||||
|
||||
29
packages/strapi-utils/lib/query.js
Normal file
29
packages/strapi-utils/lib/query.js
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
class Query {
|
||||
constructor(model) {
|
||||
this.model = model;
|
||||
const hook = strapi.hook[this.model.orm];
|
||||
const ORMQuery = hook.load().Query;
|
||||
this.instance = new ORMQuery(this.model);
|
||||
}
|
||||
|
||||
find(...args) {
|
||||
return this.instance.find(...args);
|
||||
}
|
||||
|
||||
count(filter) {
|
||||
return this.instance.count(filter);
|
||||
}
|
||||
|
||||
populate(populate) {
|
||||
return this.instance.populate(populate);
|
||||
}
|
||||
|
||||
execute() {
|
||||
return this.instance.execute();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Query
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user