Update docs and init new Queries doc

This commit is contained in:
Alexandre Bodin 2019-08-12 16:58:18 +02:00
parent 5bd4b0d747
commit df822ddbe6
4 changed files with 237 additions and 410 deletions

View File

@ -4,7 +4,6 @@
- [.admin](#strapi-admin)
- [.api](#strapi-api)
- [.app](#strapiapp)
- [.bootstrap()](#strapi-bootstrap)
- [.config](#strapi-config)
- [.controllers](#strapi-controllers)
- [.hook](#strapi-hook)
@ -24,7 +23,7 @@
## strapi.admin
This object contains the controllers, models, services and configurations contained in the `./admin` folder.
This object contains the controllers, models, services and configurations contained in the `strapi-admin` package.
## strapi.api
@ -35,14 +34,6 @@ And by using `strapi.api[:name]` you can access the controllers, services, the m
Returns the Koa instance.
## strapi.bootstrap
Returns a `Promise`. When resolved, it means that the `./config/functions/bootstrap.js` has been executed. Otherwise, it throws an error.
::: note
You can also access to the bootstrap function through `strapi.config.functions.boostrap`.
:::
## strapi.config
Returns an object that represents the configurations of the project. Every JavaScript or JSON file located in the `./config` folder will be parsed into the `strapi.config` object.

View File

@ -80,7 +80,13 @@ module.exports = {
*/
create(ctx) {
return strapi.services.product.create(ctx.request.body);
if (ctx.is('multipart')) {
// parse the specific strapi's formData format and return the data and the files
const { data, files } = this.parseMultipartData(ctx);
return service.create(data, { files });
}
return service.create(ctx.request.body);
},
};
```
@ -96,7 +102,13 @@ module.exports = {
*/
update(ctx) {
return strapi.services.product.update(ctx.params, ctx.request.body);
if (ctx.is('multipart')) {
// parse the specific strapi's formData format and return the data and the files
const { data, files } = this.parseMultipartData(ctx);
return service.update(ctx.params, data, { files });
}
return service.update(ctx.params, ctx.request.body);
},
};
```

View File

@ -1,406 +1,214 @@
# Queries
Strapi provides a utility function `strapi.query` to make database queries ORM agnostic.
Strapi provides a utility function `strapi.query` to make database queries.
## Core Queries
You can just call `strapi.query(modelName, pluginName)` to access the query API for any model.
Those queries handle for you specific Strapi features like `groups` `filters` and `search`.
## API Reference
### `findOne`
This method returns the first entry matching some basic params.
You can also pass a populate option to specify which relations you want to be populated.
#### Examples
**Find one by id**:
```js
strapi.query('post').findOne({ id: 1 });
```
**Find one by title**:
```js
strapi.query('post').findOne({ title: 'my title' });
```
**Find one by title and creation_date**:
```js
strapi
.query('post')
.findOne({ title: 'my title', created_at: '2019-01-01T00:00:00.000Z' });
```
**Find one by id and populate a relation**
```js
strapi.query('post').findOne({ id: 1 }, ['tag', 'tag.picture']);
```
### `find`
This method returns a list of entries matching Strapi filters.
You can also pass a populate option to specify which relations you want to be populated.
#### Examples
**Find by id**:
```js
strapi.query('post').find({ id: 1 });
```
**Find by in IN, with a limit**:
```js
strapi.query('post').find({ _limit: 10, id_in: [1, 2] });
```
**Find by date orderBy title**:
```js
strapi
.query('post')
.find({ date_gt: '2019-01-01T00:00:00.000Z', _sort: 'title:desc' });
```
**Find by id not in and populate a relation. Skip the first ten results**
```js
strapi.query('post').find({ id_nin: [1], _start: 10 }, ['tag', 'tag.picture']);
```
### `create`
Creates an entry in the database and returns the entry.
#### Example
```js
strapi.query('post').create({
title: 'Mytitle',
// this is a group field. the order is persisted in db.
seo: [
{
metadata: 'description',
value: 'My description',
},
{
metadata: 'keywords',
value: 'someKeyword,otherKeyword',
},
],
// pass the id of a media to link it to the entry
picture: 1,
// automatically creates the relations when passing the ids in the field
tags: [1, 2, 3],
});
```
### `update`
Updates an entry in the database and returns the entry.
#### Examples
**Update by id**
```js
strapi.query('post').update(
{ id: 1 },
{
title: 'Mytitle',
seo: [
{
metadata: 'description',
value: 'My description',
},
{
metadata: 'keywords',
value: 'someKeyword,otherKeyword',
},
],
// pass the id of a media to link it to the entry
picture: 1,
// automatically creates the relations when passing the ids in the field
tags: [1, 2, 3],
}
);
```
When updating an entry with its groups beware that if you send the groups without any `id` the previous groups will be deleted and replaced. You can update the groups by sending there `id` :
**Update by id and update previous groups**
```js
strapi.query('post').update(
{ id: 1 },
{
title: 'Mytitle',
seo: [
{
id: 2
metadata: 'keywords',
value: 'someKeyword,otherKeyword',
},
{
id: 1
metadata: 'description',
value: 'My description',
}
],
// pass the id of a media to link it to the entry
picture: 1,
// automatically creates the relations when passing the ids in the field
tags: [1, 2, 3],
}
);
```
**Partial update by title**
```js
strapi.query('post').update(
{ title: 'specific title' },
{
title: 'Mytitle',
}
);
```
### `delete`
Deletes and entry and return it's value before deletion.
You can delete multiple entries at once with the passed params.
#### Examples
**Delete one by id**
```js
strapi.query('post').delete({ id: 1 });
```
**Delete multiple by field**
```js
strapi.query('post').delete({ lang: 'en' });
```
### `count`
### `search`
### `countSearch`
## Custom 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`.
When customizing your model services you might want to implement some custom database queries. directly with the underlying ORM (bookshelf or mongoose).
You can simply copy and paste the code in your custom services.
To achieve that you can take some inspiration from the current code in the ORM queries utils.
### Bookshelf
```js
const _ = require('lodash');
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
You can see the current implementation of bookshelf queries [here](https://github.com/strapi/strapi/tree/master/packages/strapi-hook-bookshelf/lib/queries.js)
module.exports = ({ model }) => {
return {
find(params, populate) {
const withRelated =
populate ||
model.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
### Mongoose`
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.start) {
qb.offset(_.toNumber(filters.start));
}
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();
```
You can see the current implementation of mongoose queries [here](https://github.com/strapi/strapi/tree/master/packages/strapi-hook-mongoose/lib/queries.js)

View File

@ -74,8 +74,16 @@ module.exports = {
* @return {Promise}
*/
create(values) {
return strapi.query(Product).create(values);
async create(data, { files } = {}) {
const entry = await strapi.query(model).create(data);
if (files) {
// automatically uploads the files based on the entry and the model
await this.uploadFiles(entry, files, { model });
return this.findOne({ id: entry.id });
}
return entry;
},
};
```
@ -90,8 +98,16 @@ module.exports = {
* @return {Promise}
*/
update(params, values) {
return strapi.query(Product).update(params, values);
async update(params, data, { files } = {}) {
const entry = await strapi.query(model).update(params, data);
if (files) {
// automatically uploads the files based on the entry and the model
await this.uploadFiles(entry, files, { model });
return this.findOne({ id: entry.id });
}
return entry;
},
};
```