mirror of
https://github.com/strapi/strapi.git
synced 2025-09-08 08:08:18 +00:00
Merge branch 'develop' of github.com:strapi/strapi into ctm/tests
This commit is contained in:
commit
88a0241e38
@ -4,7 +4,6 @@
|
|||||||
- [.admin](#strapi-admin)
|
- [.admin](#strapi-admin)
|
||||||
- [.api](#strapi-api)
|
- [.api](#strapi-api)
|
||||||
- [.app](#strapiapp)
|
- [.app](#strapiapp)
|
||||||
- [.bootstrap()](#strapi-bootstrap)
|
|
||||||
- [.config](#strapi-config)
|
- [.config](#strapi-config)
|
||||||
- [.controllers](#strapi-controllers)
|
- [.controllers](#strapi-controllers)
|
||||||
- [.hook](#strapi-hook)
|
- [.hook](#strapi-hook)
|
||||||
@ -24,7 +23,7 @@
|
|||||||
|
|
||||||
## strapi.admin
|
## 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
|
## strapi.api
|
||||||
|
|
||||||
@ -35,14 +34,6 @@ And by using `strapi.api[:name]` you can access the controllers, services, the m
|
|||||||
|
|
||||||
Returns the Koa instance.
|
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
|
## 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.
|
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.
|
||||||
|
@ -80,7 +80,13 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
create(ctx) {
|
create(ctx) {
|
||||||
return strapi.services.product.create(ctx.request.body);
|
if (ctx.is('multipart')) {
|
||||||
|
// Parses strapi's formData format
|
||||||
|
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) {
|
update(ctx) {
|
||||||
return strapi.services.product.update(ctx.params, ctx.request.body);
|
if (ctx.is('multipart')) {
|
||||||
|
// Parses strapi's formData format
|
||||||
|
const { data, files } = this.parseMultipartData(ctx);
|
||||||
|
return service.update(ctx.params, data, { files });
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.update(ctx.params, ctx.request.body);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
@ -1,406 +1,287 @@
|
|||||||
# Queries
|
# 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.
|
||||||
|
|
||||||
In Strapi's [core services](./services.md#core-services) you can see we call a `strapi.query` function.
|
These queries handle for you specific Strapi features like `groups`, `filters` and `search`.
|
||||||
|
|
||||||
When customizing your model services you might want to implement some custom database queries.
|
## API Reference
|
||||||
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.
|
### `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', date: '2019-01-01T00:00:00Z' });
|
||||||
|
```
|
||||||
|
|
||||||
|
**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:00Z', _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 their `id` with the rest of the fields:
|
||||||
|
|
||||||
|
**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 its 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`
|
||||||
|
|
||||||
|
Returns the count of entries matching Strapi filters.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
**Count by lang**
|
||||||
|
|
||||||
|
```js
|
||||||
|
strapi.query('post').count({ lang: 'en' });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Count by title contains**
|
||||||
|
|
||||||
|
```js
|
||||||
|
strapi.query('post').count({ title_contains: 'food' });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Count by date less than**
|
||||||
|
|
||||||
|
```js
|
||||||
|
strapi.query('post').count({ date_lt: '2019-08-01T00:00:00Z' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `search`
|
||||||
|
|
||||||
|
Returns entries based on a search on all fields allowing it. (this feature will return all entries on sqlite).
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
**Search first ten starting at 20**
|
||||||
|
|
||||||
|
```js
|
||||||
|
strapi.query('post').search({ _q: 'my search query', _limit: 10, _start: 20 });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Search and sort**
|
||||||
|
|
||||||
|
```js
|
||||||
|
strapi
|
||||||
|
.query('post')
|
||||||
|
.search({ _q: 'my search query', _limit: 100, _sort: 'date:desc' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `countSearch`
|
||||||
|
|
||||||
|
Returns the total count of entries based on a search. (this feature will return all entries on sqlite).
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
strapi.query('post').countSearch({ _q: 'my search query' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Queries
|
||||||
|
|
||||||
|
When you want to customize your services or create new ones you will have to build your queries with the underlying ORM models.
|
||||||
|
|
||||||
|
To access the underlying model:
|
||||||
|
|
||||||
|
```js
|
||||||
|
strapi.query(modelName, plugin).model;
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can run any queries available on the model. You should refer to the specific ORM documentation for more details:
|
||||||
|
|
||||||
### Bookshelf
|
### Bookshelf
|
||||||
|
|
||||||
|
Documentation: [https://bookshelfjs.org/](https://bookshelfjs.org/)
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const _ = require('lodash');
|
strapi
|
||||||
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
|
.query('post')
|
||||||
|
.model.query(qb => {
|
||||||
module.exports = ({ model }) => {
|
qb.where('id', 1);
|
||||||
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({
|
.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
|
### Mongoose
|
||||||
|
|
||||||
|
Documentation: [https://mongoosejs.com/](https://mongoosejs.com/)
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const _ = require('lodash');
|
strapi
|
||||||
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
|
.query('post')
|
||||||
|
.model.find({
|
||||||
module.exports = ({ model, strapi }) => {
|
{ date: { $gte: '2019-01-01T00.00.00Z }
|
||||||
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();
|
|
||||||
```
|
```
|
||||||
|
@ -74,8 +74,16 @@ module.exports = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
create(values) {
|
async create(data, { files } = {}) {
|
||||||
return strapi.query(Product).create(values);
|
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}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
update(params, values) {
|
async update(params, data, { files } = {}) {
|
||||||
return strapi.query(Product).update(params, values);
|
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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
@ -35,10 +35,10 @@
|
|||||||
"app.components.HomePage.createBlock.content.second": " — плагин, который поможет вам определить структуру ваших данных. Если вы новичок, мы настоятельно рекомендуем вам изучить наше ",
|
"app.components.HomePage.createBlock.content.second": " — плагин, который поможет вам определить структуру ваших данных. Если вы новичок, мы настоятельно рекомендуем вам изучить наше ",
|
||||||
"app.components.HomePage.createBlock.content.tutorial": " руководство.",
|
"app.components.HomePage.createBlock.content.tutorial": " руководство.",
|
||||||
"app.components.HomePage.cta": "ПОДПИСАТЬСЯ",
|
"app.components.HomePage.cta": "ПОДПИСАТЬСЯ",
|
||||||
"app.components.HomePage.newsLetter": "Подпишитесь на нашу рассылку, чтобы быть в курсе новостей Strapi",
|
"app.components.HomePage.newsLetter": "Подпишитесь на рассылку, чтобы быть в курсе новостей Strapi",
|
||||||
"app.components.HomePage.support": "ПОДДЕРЖИТЕ НАС",
|
"app.components.HomePage.support": "ПОДДЕРЖИТЕ НАС",
|
||||||
"app.components.HomePage.support.content": "Покупая футболку, вы помогаете нам продолжать работу над проектом и предоставлять вам наилучшее из возможных решений!",
|
"app.components.HomePage.support.content": "Покупая футболку, вы помогаете нам продолжать работу над проектом и предоставлять вам наилучшее из возможных решений!",
|
||||||
"app.components.HomePage.support.link": "ЗАКАЗАТЬ НАШУ ФУТБОЛКУ СЕЙЧАС",
|
"app.components.HomePage.support.link": "ЗАКАЗАТЬ ФУТБОЛКУ СЕЙЧАС",
|
||||||
"app.components.HomePage.welcome": "Добро пожаловать!",
|
"app.components.HomePage.welcome": "Добро пожаловать!",
|
||||||
"app.components.HomePage.welcome.again": "Добро пожаловать, ",
|
"app.components.HomePage.welcome.again": "Добро пожаловать, ",
|
||||||
"app.components.HomePage.welcomeBlock.content": "Мы рады, что вы присоединились к сообществу. Нам необходима обратная связь для развития проекта, поэтому не стесняйтесь писать нам в ",
|
"app.components.HomePage.welcomeBlock.content": "Мы рады, что вы присоединились к сообществу. Нам необходима обратная связь для развития проекта, поэтому не стесняйтесь писать нам в ",
|
||||||
@ -52,6 +52,8 @@
|
|||||||
"app.components.InputFileDetails.originalName": "Первоначальное название:",
|
"app.components.InputFileDetails.originalName": "Первоначальное название:",
|
||||||
"app.components.InputFileDetails.remove": "Удалить этот файл",
|
"app.components.InputFileDetails.remove": "Удалить этот файл",
|
||||||
"app.components.InputFileDetails.size": "Размер:",
|
"app.components.InputFileDetails.size": "Размер:",
|
||||||
|
"app.components.InstallPluginPage.Download.title": "Загрузка...",
|
||||||
|
"app.components.InstallPluginPage.Download.description": "Для загрузки и установки плагина может потребоваться несколько секунд.",
|
||||||
"app.components.InstallPluginPage.InputSearch.label": " ",
|
"app.components.InstallPluginPage.InputSearch.label": " ",
|
||||||
"app.components.InstallPluginPage.InputSearch.placeholder": "Искать плагин... (ex: authentication)",
|
"app.components.InstallPluginPage.InputSearch.placeholder": "Искать плагин... (ex: authentication)",
|
||||||
"app.components.InstallPluginPage.description": "Расширяйте ваше приложение без усилий.",
|
"app.components.InstallPluginPage.description": "Расширяйте ваше приложение без усилий.",
|
||||||
@ -65,7 +67,9 @@
|
|||||||
"app.components.InstallPluginPopup.navLink.faq": "faq",
|
"app.components.InstallPluginPopup.navLink.faq": "faq",
|
||||||
"app.components.InstallPluginPopup.navLink.screenshots": "Скриншоты",
|
"app.components.InstallPluginPopup.navLink.screenshots": "Скриншоты",
|
||||||
"app.components.InstallPluginPopup.noDescription": "Нет описания",
|
"app.components.InstallPluginPopup.noDescription": "Нет описания",
|
||||||
"app.components.LeftMenuFooter.poweredBy": "С гордостью предоставлено ",
|
"app.components.LeftMenuFooter.documentation": "Документация",
|
||||||
|
"app.components.LeftMenuFooter.help": "Помощь",
|
||||||
|
"app.components.LeftMenuFooter.poweredBy": "Работает на ",
|
||||||
"app.components.LeftMenuLinkContainer.configuration": "Настройки",
|
"app.components.LeftMenuLinkContainer.configuration": "Настройки",
|
||||||
"app.components.LeftMenuLinkContainer.general": "Общие",
|
"app.components.LeftMenuLinkContainer.general": "Общие",
|
||||||
"app.components.LeftMenuLinkContainer.installNewPlugin": "Магазин",
|
"app.components.LeftMenuLinkContainer.installNewPlugin": "Магазин",
|
||||||
@ -75,26 +79,35 @@
|
|||||||
"app.components.ListPluginsPage.description": "Список установленных плагинов.",
|
"app.components.ListPluginsPage.description": "Список установленных плагинов.",
|
||||||
"app.components.ListPluginsPage.helmet.title": "Список плагинов",
|
"app.components.ListPluginsPage.helmet.title": "Список плагинов",
|
||||||
"app.components.ListPluginsPage.title": "Плагины",
|
"app.components.ListPluginsPage.title": "Плагины",
|
||||||
|
"app.components.Logout.admin": "Управлять администраторами",
|
||||||
"app.components.Logout.profile": "Профиль",
|
"app.components.Logout.profile": "Профиль",
|
||||||
"app.components.Logout.logout": "Выйти",
|
"app.components.Logout.logout": "Выйти",
|
||||||
"app.components.NotFoundPage.back": "Вернуться на главную",
|
"app.components.NotFoundPage.back": "Вернуться на главную",
|
||||||
"app.components.NotFoundPage.description": "Не найдено",
|
"app.components.NotFoundPage.description": "Не найдено",
|
||||||
"app.components.Official": "Официальный",
|
"app.components.Official": "Официальный",
|
||||||
|
"app.components.Onboarding.label.completed": "% завершено",
|
||||||
|
"app.components.Onboarding.title": "Смотреть вводные видео",
|
||||||
"app.components.PluginCard.Button.label.download": "Скачать",
|
"app.components.PluginCard.Button.label.download": "Скачать",
|
||||||
"app.components.PluginCard.Button.label.install": "Уже установленно",
|
"app.components.PluginCard.Button.label.install": "Уже установлено",
|
||||||
"app.components.PluginCard.Button.label.support": "Поддержать нас",
|
"app.components.PluginCard.Button.label.support": "Поддержать нас",
|
||||||
"app.components.PluginCard.compatible": "Совместимо с вашим приложением",
|
"app.components.PluginCard.compatible": "Совместимо с вашим приложением",
|
||||||
"app.components.PluginCard.compatibleCommunity": "Совместимо с сообществом",
|
"app.components.PluginCard.compatibleCommunity": "Совместимо с сообществом",
|
||||||
"app.components.PluginCard.more-details": "Больше деталей",
|
"app.components.PluginCard.more-details": "Больше деталей",
|
||||||
|
"app.components.PluginCard.PopUpWarning.install.impossible.autoReload.needed": "Функция автоматической перезагрузки должна быть отключена. Пожалуйста, запустите ваше приложение с помощью `yarn develop`.",
|
||||||
|
"app.components.PluginCard.PopUpWarning.install.impossible.environment": "В целях безопасности плагин может быть загружен только в develop-окружении.",
|
||||||
|
"app.components.PluginCard.PopUpWarning.install.impossible.confirm": "Я понимаю!",
|
||||||
|
"app.components.PluginCard.PopUpWarning.install.impossible.title": "Загрузка невозможна",
|
||||||
"app.components.PluginCard.price.free": "Бесплатно",
|
"app.components.PluginCard.price.free": "Бесплатно",
|
||||||
|
"app.components.PluginCard.settings": "Настройки",
|
||||||
"app.components.listPlugins.button": "Добавить новый плагин",
|
"app.components.listPlugins.button": "Добавить новый плагин",
|
||||||
"app.components.listPlugins.title.none": "Нет установленных плагинов",
|
"app.components.listPlugins.title.none": "Нет установленных плагинов",
|
||||||
"app.components.listPlugins.title.plural": "{number} плагинов установленно",
|
"app.components.listPlugins.title.plural": "{number} плагинов установлено",
|
||||||
"app.components.listPlugins.title.singular": "{number} плагин установлен",
|
"app.components.listPlugins.title.singular": "{number} плагин установлен",
|
||||||
"app.components.listPluginsPage.deletePlugin.error": "Возникла ошибка при установке плагина",
|
"app.components.listPluginsPage.deletePlugin.error": "Возникла ошибка при установке плагина",
|
||||||
"app.utils.SelectOption.defaultMessage": " ",
|
"app.utils.SelectOption.defaultMessage": " ",
|
||||||
"app.utils.defaultMessage": " ",
|
"app.utils.defaultMessage": " ",
|
||||||
"app.utils.placeholder.defaultMessage": " ",
|
"app.utils.placeholder.defaultMessage": " ",
|
||||||
|
"components.AutoReloadBlocker.description": "Запустите Strapi с помощью одной из следующих команд:",
|
||||||
"components.AutoReloadBlocker.header": "Функционал перезапуска необходим для этого плагина.",
|
"components.AutoReloadBlocker.header": "Функционал перезапуска необходим для этого плагина.",
|
||||||
"components.ErrorBoundary.title": "Что-то пошло не так...",
|
"components.ErrorBoundary.title": "Что-то пошло не так...",
|
||||||
"components.Input.error.attribute.key.taken": "Это значение уже существует",
|
"components.Input.error.attribute.key.taken": "Это значение уже существует",
|
||||||
@ -113,7 +126,9 @@
|
|||||||
"components.Input.error.validation.required": "Необходимое поле для заполнения.",
|
"components.Input.error.validation.required": "Необходимое поле для заполнения.",
|
||||||
"components.ListRow.empty": "Нет данных для отображения.",
|
"components.ListRow.empty": "Нет данных для отображения.",
|
||||||
"components.OverlayBlocker.description": "Вы воспользовались функционалом, который требует перезапуска сервера. Пожалуйста, подождите.",
|
"components.OverlayBlocker.description": "Вы воспользовались функционалом, который требует перезапуска сервера. Пожалуйста, подождите.",
|
||||||
|
"components.OverlayBlocker.description.serverError": "Сервер должен был перезагрузиться, пожалуйста, проверьте ваши логи в терминале.",
|
||||||
"components.OverlayBlocker.title": "Ожидание перезапуска...",
|
"components.OverlayBlocker.title": "Ожидание перезапуска...",
|
||||||
|
"components.OverlayBlocker.title.serverError": "Перезапуск занимает больше времени, чем ожидалось",
|
||||||
"components.PageFooter.select": "записей на странице",
|
"components.PageFooter.select": "записей на странице",
|
||||||
"components.ProductionBlocker.description": "Для безопасности мы должны заблокировать его для других вариантов.",
|
"components.ProductionBlocker.description": "Для безопасности мы должны заблокировать его для других вариантов.",
|
||||||
"components.ProductionBlocker.header": "Этот плагин доступен только на стадии разработки.",
|
"components.ProductionBlocker.header": "Этот плагин доступен только на стадии разработки.",
|
||||||
@ -137,5 +152,6 @@
|
|||||||
"components.popUpWarning.title": "Пожалуйста, подтвердите",
|
"components.popUpWarning.title": "Пожалуйста, подтвердите",
|
||||||
"notification.error": "Произошла ошибка",
|
"notification.error": "Произошла ошибка",
|
||||||
"notification.error.layout": "Не удалось получить макет",
|
"notification.error.layout": "Не удалось получить макет",
|
||||||
"request.error.model.unknown": "Модель данных не существует"
|
"request.error.model.unknown": "Модель данных не существует",
|
||||||
|
"app.utils.delete": "Удалить"
|
||||||
}
|
}
|
@ -154,6 +154,7 @@ function syncLayouts(configuration, schema) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appendToEditLayout = (layout = [], keysToAppend, schema) => {
|
const appendToEditLayout = (layout = [], keysToAppend, schema) => {
|
||||||
|
if (keysToAppend.length === 0) return layout;
|
||||||
let currentRowIndex = Math.max(layout.length - 1, 0);
|
let currentRowIndex = Math.max(layout.length - 1, 0);
|
||||||
|
|
||||||
// init currentRow if necessary
|
// init currentRow if necessary
|
||||||
|
@ -195,27 +195,49 @@ module.exports = {
|
|||||||
|
|
||||||
// Resolver can be a function. Be also a native resolver or a controller's action.
|
// Resolver can be a function. Be also a native resolver or a controller's action.
|
||||||
if (_.isFunction(resolver)) {
|
if (_.isFunction(resolver)) {
|
||||||
|
const normalizedName = _.toLower(name);
|
||||||
|
|
||||||
|
let primaryKey;
|
||||||
|
|
||||||
|
if (plugin) {
|
||||||
|
primaryKey = strapi.plugins[plugin].models[normalizedName].primaryKey;
|
||||||
|
} else {
|
||||||
|
primaryKey = strapi.models[normalizedName].primaryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.input && options.input.where) {
|
||||||
context.params = Query.convertToParams(
|
context.params = Query.convertToParams(
|
||||||
options.input.where || {},
|
options.input.where || {},
|
||||||
(plugin ? strapi.plugins[plugin].models[name] : strapi.models[name])
|
primaryKey
|
||||||
.primaryKey
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
context.params = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.input && options.input.data) {
|
||||||
context.request.body = options.input.data || {};
|
context.request.body = options.input.data || {};
|
||||||
|
} else {
|
||||||
|
context.request.body = options;
|
||||||
|
}
|
||||||
|
|
||||||
if (isController) {
|
if (isController) {
|
||||||
const values = await resolver.call(null, context);
|
const values = await resolver.call(null, context);
|
||||||
|
|
||||||
if (ctx.body) {
|
if (ctx.body) {
|
||||||
return {
|
return options.input
|
||||||
[pluralize.singular(name)]: ctx.body,
|
? {
|
||||||
};
|
[pluralize.singular(normalizedName)]: ctx.body,
|
||||||
|
}
|
||||||
|
: ctx.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = values && values.toJSON ? values.toJSON() : values;
|
const body = values && values.toJSON ? values.toJSON() : values;
|
||||||
|
|
||||||
return {
|
return options.input
|
||||||
[pluralize.singular(name)]: body,
|
? {
|
||||||
};
|
[pluralize.singular(normalizedName)]: body,
|
||||||
|
}
|
||||||
|
: body;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolver.call(null, obj, options, context);
|
return resolver.call(null, obj, options, context);
|
||||||
|
@ -223,14 +223,19 @@ const schemaBuilder = {
|
|||||||
: {};
|
: {};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'Mutation':
|
case 'Mutation': {
|
||||||
// TODO: Verify this...
|
// TODO: Verify this...
|
||||||
|
const [name, action] = acc[type][resolver].split('.');
|
||||||
|
const normalizedName = _.toLower(name);
|
||||||
|
|
||||||
acc[type][resolver] = Mutation.composeMutationResolver(
|
acc[type][resolver] = Mutation.composeMutationResolver(
|
||||||
strapi.plugins.graphql.config._schema.graphql,
|
strapi.plugins.graphql.config._schema.graphql,
|
||||||
plugin,
|
plugin,
|
||||||
resolver
|
normalizedName,
|
||||||
|
action
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'Query':
|
case 'Query':
|
||||||
default:
|
default:
|
||||||
acc[type][resolver] = Query.composeQueryResolver(
|
acc[type][resolver] = Query.composeQueryResolver(
|
||||||
|
@ -472,6 +472,9 @@ class Strapi extends EventEmitter {
|
|||||||
|
|
||||||
// custom queries made easy
|
// custom queries made easy
|
||||||
Object.assign(query, {
|
Object.assign(query, {
|
||||||
|
get model() {
|
||||||
|
return model;
|
||||||
|
},
|
||||||
custom(mapping) {
|
custom(mapping) {
|
||||||
if (typeof mapping === 'function') {
|
if (typeof mapping === 'function') {
|
||||||
return mapping.bind(query, { model, modelKey });
|
return mapping.bind(query, { model, modelKey });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user