mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 09:56:44 +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
	 soupette
						soupette