Merge branch 'develop' of github.com:strapi/strapi into ctm/tests

This commit is contained in:
soupette 2019-08-13 17:37:52 +02:00
commit 88a0241e38
9 changed files with 372 additions and 425 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')) {
// 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);
},
};
```

View File

@ -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 }
});
```

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;
},
};
```

View File

@ -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": "Удалить"
}

View File

@ -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

View File

@ -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);

View File

@ -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(

View File

@ -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 });