diff --git a/docs/3.0.0-beta.x/api-reference/reference.md b/docs/3.0.0-beta.x/api-reference/reference.md index a06178bb08..5a0b64a272 100644 --- a/docs/3.0.0-beta.x/api-reference/reference.md +++ b/docs/3.0.0-beta.x/api-reference/reference.md @@ -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. diff --git a/docs/3.0.0-beta.x/guides/controllers.md b/docs/3.0.0-beta.x/guides/controllers.md index 25d8cff934..19e974407e 100644 --- a/docs/3.0.0-beta.x/guides/controllers.md +++ b/docs/3.0.0-beta.x/guides/controllers.md @@ -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); }, }; ``` diff --git a/docs/3.0.0-beta.x/guides/queries.md b/docs/3.0.0-beta.x/guides/queries.md index 8083649a6c..c9eed07632 100644 --- a/docs/3.0.0-beta.x/guides/queries.md +++ b/docs/3.0.0-beta.x/guides/queries.md @@ -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 } + }); ``` diff --git a/docs/3.0.0-beta.x/guides/services.md b/docs/3.0.0-beta.x/guides/services.md index d7900a0ce5..71955ac678 100644 --- a/docs/3.0.0-beta.x/guides/services.md +++ b/docs/3.0.0-beta.x/guides/services.md @@ -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; }, }; ``` diff --git a/packages/strapi-admin/admin/src/translations/ru.json b/packages/strapi-admin/admin/src/translations/ru.json index b7dfd29155..77f01dc508 100644 --- a/packages/strapi-admin/admin/src/translations/ru.json +++ b/packages/strapi-admin/admin/src/translations/ru.json @@ -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": "Модель данных не существует" -} \ No newline at end of file + "request.error.model.unknown": "Модель данных не существует", + "app.utils.delete": "Удалить" +} diff --git a/packages/strapi-plugin-content-manager/services/utils/configuration/layouts.js b/packages/strapi-plugin-content-manager/services/utils/configuration/layouts.js index c0fbadc2fa..da01b72d99 100644 --- a/packages/strapi-plugin-content-manager/services/utils/configuration/layouts.js +++ b/packages/strapi-plugin-content-manager/services/utils/configuration/layouts.js @@ -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 diff --git a/packages/strapi-plugin-graphql/services/Mutation.js b/packages/strapi-plugin-graphql/services/Mutation.js index fec5c97b82..a9ca8c5990 100644 --- a/packages/strapi-plugin-graphql/services/Mutation.js +++ b/packages/strapi-plugin-graphql/services/Mutation.js @@ -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); diff --git a/packages/strapi-plugin-graphql/services/Schema.js b/packages/strapi-plugin-graphql/services/Schema.js index 7fef48b61e..c1217ca39d 100644 --- a/packages/strapi-plugin-graphql/services/Schema.js +++ b/packages/strapi-plugin-graphql/services/Schema.js @@ -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( diff --git a/packages/strapi/lib/Strapi.js b/packages/strapi/lib/Strapi.js index 97d6734c98..fda2f74a51 100644 --- a/packages/strapi/lib/Strapi.js +++ b/packages/strapi/lib/Strapi.js @@ -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 });