mirror of
https://github.com/strapi/strapi.git
synced 2025-09-06 15:22:59 +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)
|
||||
- [.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.
|
||||
|
@ -80,7 +80,13 @@ module.exports = {
|
||||
*/
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
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.
|
||||
To help you with that here is the current implementation of the queries for both `bookshelf` and `mongoose`.
|
||||
## API Reference
|
||||
|
||||
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
|
||||
|
||||
Documentation: [https://bookshelfjs.org/](https://bookshelfjs.org/)
|
||||
|
||||
**Example**
|
||||
|
||||
```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.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;
|
||||
}
|
||||
}
|
||||
};
|
||||
strapi
|
||||
.query('post')
|
||||
.model.query(qb => {
|
||||
qb.where('id', 1);
|
||||
})
|
||||
.fetch();
|
||||
```
|
||||
|
||||
### Mongoose
|
||||
|
||||
```js
|
||||
const _ = require('lodash');
|
||||
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
|
||||
Documentation: [https://mongoosejs.com/](https://mongoosejs.com/)
|
||||
|
||||
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.
|
||||
**Example**
|
||||
|
||||
```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();
|
||||
strapi
|
||||
.query('post')
|
||||
.model.find({
|
||||
{ date: { $gte: '2019-01-01T00.00.00Z }
|
||||
});
|
||||
```
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
@ -35,10 +35,10 @@
|
||||
"app.components.HomePage.createBlock.content.second": " — плагин, который поможет вам определить структуру ваших данных. Если вы новичок, мы настоятельно рекомендуем вам изучить наше ",
|
||||
"app.components.HomePage.createBlock.content.tutorial": " руководство.",
|
||||
"app.components.HomePage.cta": "ПОДПИСАТЬСЯ",
|
||||
"app.components.HomePage.newsLetter": "Подпишитесь на нашу рассылку, чтобы быть в курсе новостей Strapi",
|
||||
"app.components.HomePage.newsLetter": "Подпишитесь на рассылку, чтобы быть в курсе новостей Strapi",
|
||||
"app.components.HomePage.support": "ПОДДЕРЖИТЕ НАС",
|
||||
"app.components.HomePage.support.content": "Покупая футболку, вы помогаете нам продолжать работу над проектом и предоставлять вам наилучшее из возможных решений!",
|
||||
"app.components.HomePage.support.link": "ЗАКАЗАТЬ НАШУ ФУТБОЛКУ СЕЙЧАС",
|
||||
"app.components.HomePage.support.link": "ЗАКАЗАТЬ ФУТБОЛКУ СЕЙЧАС",
|
||||
"app.components.HomePage.welcome": "Добро пожаловать!",
|
||||
"app.components.HomePage.welcome.again": "Добро пожаловать, ",
|
||||
"app.components.HomePage.welcomeBlock.content": "Мы рады, что вы присоединились к сообществу. Нам необходима обратная связь для развития проекта, поэтому не стесняйтесь писать нам в ",
|
||||
@ -52,6 +52,8 @@
|
||||
"app.components.InputFileDetails.originalName": "Первоначальное название:",
|
||||
"app.components.InputFileDetails.remove": "Удалить этот файл",
|
||||
"app.components.InputFileDetails.size": "Размер:",
|
||||
"app.components.InstallPluginPage.Download.title": "Загрузка...",
|
||||
"app.components.InstallPluginPage.Download.description": "Для загрузки и установки плагина может потребоваться несколько секунд.",
|
||||
"app.components.InstallPluginPage.InputSearch.label": " ",
|
||||
"app.components.InstallPluginPage.InputSearch.placeholder": "Искать плагин... (ex: authentication)",
|
||||
"app.components.InstallPluginPage.description": "Расширяйте ваше приложение без усилий.",
|
||||
@ -65,7 +67,9 @@
|
||||
"app.components.InstallPluginPopup.navLink.faq": "faq",
|
||||
"app.components.InstallPluginPopup.navLink.screenshots": "Скриншоты",
|
||||
"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.general": "Общие",
|
||||
"app.components.LeftMenuLinkContainer.installNewPlugin": "Магазин",
|
||||
@ -75,26 +79,35 @@
|
||||
"app.components.ListPluginsPage.description": "Список установленных плагинов.",
|
||||
"app.components.ListPluginsPage.helmet.title": "Список плагинов",
|
||||
"app.components.ListPluginsPage.title": "Плагины",
|
||||
"app.components.Logout.admin": "Управлять администраторами",
|
||||
"app.components.Logout.profile": "Профиль",
|
||||
"app.components.Logout.logout": "Выйти",
|
||||
"app.components.NotFoundPage.back": "Вернуться на главную",
|
||||
"app.components.NotFoundPage.description": "Не найдено",
|
||||
"app.components.Official": "Официальный",
|
||||
"app.components.Onboarding.label.completed": "% завершено",
|
||||
"app.components.Onboarding.title": "Смотреть вводные видео",
|
||||
"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.compatible": "Совместимо с вашим приложением",
|
||||
"app.components.PluginCard.compatibleCommunity": "Совместимо с сообществом",
|
||||
"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.settings": "Настройки",
|
||||
"app.components.listPlugins.button": "Добавить новый плагин",
|
||||
"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.listPluginsPage.deletePlugin.error": "Возникла ошибка при установке плагина",
|
||||
"app.utils.SelectOption.defaultMessage": " ",
|
||||
"app.utils.defaultMessage": " ",
|
||||
"app.utils.placeholder.defaultMessage": " ",
|
||||
"components.AutoReloadBlocker.description": "Запустите Strapi с помощью одной из следующих команд:",
|
||||
"components.AutoReloadBlocker.header": "Функционал перезапуска необходим для этого плагина.",
|
||||
"components.ErrorBoundary.title": "Что-то пошло не так...",
|
||||
"components.Input.error.attribute.key.taken": "Это значение уже существует",
|
||||
@ -113,7 +126,9 @@
|
||||
"components.Input.error.validation.required": "Необходимое поле для заполнения.",
|
||||
"components.ListRow.empty": "Нет данных для отображения.",
|
||||
"components.OverlayBlocker.description": "Вы воспользовались функционалом, который требует перезапуска сервера. Пожалуйста, подождите.",
|
||||
"components.OverlayBlocker.description.serverError": "Сервер должен был перезагрузиться, пожалуйста, проверьте ваши логи в терминале.",
|
||||
"components.OverlayBlocker.title": "Ожидание перезапуска...",
|
||||
"components.OverlayBlocker.title.serverError": "Перезапуск занимает больше времени, чем ожидалось",
|
||||
"components.PageFooter.select": "записей на странице",
|
||||
"components.ProductionBlocker.description": "Для безопасности мы должны заблокировать его для других вариантов.",
|
||||
"components.ProductionBlocker.header": "Этот плагин доступен только на стадии разработки.",
|
||||
@ -137,5 +152,6 @@
|
||||
"components.popUpWarning.title": "Пожалуйста, подтвердите",
|
||||
"notification.error": "Произошла ошибка",
|
||||
"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) => {
|
||||
if (keysToAppend.length === 0) return layout;
|
||||
let currentRowIndex = Math.max(layout.length - 1, 0);
|
||||
|
||||
// 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.
|
||||
if (_.isFunction(resolver)) {
|
||||
context.params = Query.convertToParams(
|
||||
options.input.where || {},
|
||||
(plugin ? strapi.plugins[plugin].models[name] : strapi.models[name])
|
||||
.primaryKey
|
||||
);
|
||||
context.request.body = options.input.data || {};
|
||||
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(
|
||||
options.input.where || {},
|
||||
primaryKey
|
||||
);
|
||||
} else {
|
||||
context.params = {};
|
||||
}
|
||||
|
||||
if (options.input && options.input.data) {
|
||||
context.request.body = options.input.data || {};
|
||||
} else {
|
||||
context.request.body = options;
|
||||
}
|
||||
|
||||
if (isController) {
|
||||
const values = await resolver.call(null, context);
|
||||
|
||||
if (ctx.body) {
|
||||
return {
|
||||
[pluralize.singular(name)]: ctx.body,
|
||||
};
|
||||
return options.input
|
||||
? {
|
||||
[pluralize.singular(normalizedName)]: ctx.body,
|
||||
}
|
||||
: ctx.body;
|
||||
}
|
||||
|
||||
const body = values && values.toJSON ? values.toJSON() : values;
|
||||
|
||||
return {
|
||||
[pluralize.singular(name)]: body,
|
||||
};
|
||||
return options.input
|
||||
? {
|
||||
[pluralize.singular(normalizedName)]: body,
|
||||
}
|
||||
: body;
|
||||
}
|
||||
|
||||
return resolver.call(null, obj, options, context);
|
||||
|
@ -223,14 +223,19 @@ const schemaBuilder = {
|
||||
: {};
|
||||
|
||||
switch (type) {
|
||||
case 'Mutation':
|
||||
case 'Mutation': {
|
||||
// TODO: Verify this...
|
||||
const [name, action] = acc[type][resolver].split('.');
|
||||
const normalizedName = _.toLower(name);
|
||||
|
||||
acc[type][resolver] = Mutation.composeMutationResolver(
|
||||
strapi.plugins.graphql.config._schema.graphql,
|
||||
plugin,
|
||||
resolver
|
||||
normalizedName,
|
||||
action
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'Query':
|
||||
default:
|
||||
acc[type][resolver] = Query.composeQueryResolver(
|
||||
|
@ -472,6 +472,9 @@ class Strapi extends EventEmitter {
|
||||
|
||||
// custom queries made easy
|
||||
Object.assign(query, {
|
||||
get model() {
|
||||
return model;
|
||||
},
|
||||
custom(mapping) {
|
||||
if (typeof mapping === 'function') {
|
||||
return mapping.bind(query, { model, modelKey });
|
||||
|
Loading…
x
Reference in New Issue
Block a user