diff --git a/docs/3.0.0-beta.x/guides/filters.md b/docs/3.0.0-beta.x/guides/filters.md index 5b1d1780d2..e3e5c457aa 100644 --- a/docs/3.0.0-beta.x/guides/filters.md +++ b/docs/3.0.0-beta.x/guides/filters.md @@ -31,6 +31,7 @@ Filters are used as a suffix of a field name: - `ncontains`: Doesn't contain - `containss`: Contains case sensitive - `ncontainss`: Doesn't contain case sensitive +- `null`: Is null/Is not null #### Examples diff --git a/docs/3.0.0-beta.x/guides/graphql.md b/docs/3.0.0-beta.x/guides/graphql.md index 1d8bf3614a..643a575052 100644 --- a/docs/3.0.0-beta.x/guides/graphql.md +++ b/docs/3.0.0-beta.x/guides/graphql.md @@ -199,6 +199,7 @@ You can also apply different parameters to the query to make more complex querie - `_containss`: Contains sensitive. - `_in`: Matches any value in the array of values. - `_nin`: Doesn't match any value in the array of values. + - `_null`: Equals null/Not equals null Return the second decade of users which have an email that contains `@strapi.io` ordered by username. @@ -695,7 +696,6 @@ module.exports = { In this example, the policy `isAuthenticated` located in the `users-permissions` plugin will be executed first. Then, the `isOwner` policy located in the `Post` API `./api/post/config/policies/isOwner.js`. Next, it will execute the `logging` policy located in `./config/policies/logging.js`. Finally, the resolver will be executed. - ::: note There is no custom resolver in that case, so it will execute the default resolver (Post.find) provided by the Shadow CRUD feature. ::: diff --git a/docs/3.0.0-beta.x/guides/restapi.md b/docs/3.0.0-beta.x/guides/restapi.md index 5b9efaeea1..74bd579eff 100644 --- a/docs/3.0.0-beta.x/guides/restapi.md +++ b/docs/3.0.0-beta.x/guides/restapi.md @@ -28,6 +28,7 @@ Easily filter results according to fields values. - `_containss`: Contains case sensitive - `_in`: Matches any value in the array of values - `_nin`: Doesn't match any value in the array of values + - `_null`: Equals null/Not equals null #### Examples diff --git a/packages/strapi-hook-bookshelf/lib/buildQuery.js b/packages/strapi-hook-bookshelf/lib/buildQuery.js index 3dbe562f09..01e00bd0ff 100644 --- a/packages/strapi-hook-bookshelf/lib/buildQuery.js +++ b/packages/strapi-hook-bookshelf/lib/buildQuery.js @@ -245,6 +245,9 @@ const buildWhereClause = ({ qb, field, operator, value }) => { return qb.where(field, 'like', `%${value}%`); case 'ncontainss': return qb.whereNot(field, 'like', `%${value}%`); + case 'null': { + return value ? qb.whereNull(field) : qb.whereNotNull(field); + } default: throw new Error(`Unhandled whereClause : ${field} ${operator} ${value}`); diff --git a/packages/strapi-hook-mongoose/lib/buildQuery.js b/packages/strapi-hook-mongoose/lib/buildQuery.js index 3677e0d194..7373d473c0 100644 --- a/packages/strapi-hook-mongoose/lib/buildQuery.js +++ b/packages/strapi-hook-mongoose/lib/buildQuery.js @@ -438,6 +438,9 @@ const buildWhereClause = ({ field, operator, value }) => { $not: new RegExp(val), }, }; + case 'null': { + return value ? { [field]: { $eq: null } } : { [field]: { $ne: null } }; + } default: throw new Error(`Unhandled whereClause : ${field} ${operator} ${value}`); diff --git a/packages/strapi-plugin-graphql/test/graphqlCrud.test.e2e.js b/packages/strapi-plugin-graphql/test/graphqlCrud.test.e2e.js index f310611e7c..1eefa6e17a 100644 --- a/packages/strapi-plugin-graphql/test/graphqlCrud.test.e2e.js +++ b/packages/strapi-plugin-graphql/test/graphqlCrud.test.e2e.js @@ -25,6 +25,12 @@ const postModel = { type: 'biginteger', }, }, + { + name: 'nullable', + params: { + type: 'string', + }, + }, ], connection: 'default', name: 'post', @@ -54,8 +60,8 @@ describe('Test Graphql API End to End', () => { describe('Test CRUD', () => { const postsPayload = [ - { name: 'post 1', bigint: 1316130638171 }, - { name: 'post 2', bigint: 1416130639261 }, + { name: 'post 1', bigint: 1316130638171, nullable: 'value' }, + { name: 'post 2', bigint: 1416130639261, nullable: null }, ]; let data = { posts: [], @@ -69,6 +75,7 @@ describe('Test Graphql API End to End', () => { post { name bigint + nullable } } } @@ -100,6 +107,7 @@ describe('Test Graphql API End to End', () => { id name bigint + nullable } } `, @@ -126,6 +134,7 @@ describe('Test Graphql API End to End', () => { id name bigint + nullable } } `, @@ -147,6 +156,7 @@ describe('Test Graphql API End to End', () => { id name bigint + nullable } } `, @@ -168,6 +178,7 @@ describe('Test Graphql API End to End', () => { id name bigint + nullable } } `, @@ -229,7 +240,7 @@ describe('Test Graphql API End to End', () => { ], [ { - name_in: ['post 1', 'post 2'], + name_in: ['post 1', 'post 2', 'post 3'], }, postsPayload, ], @@ -239,6 +250,18 @@ describe('Test Graphql API End to End', () => { }, [postsPayload[0]], ], + [ + { + nullable_null: true, + }, + [postsPayload[1]], + ], + [ + { + nullable_null: false, + }, + [postsPayload[0]], + ], ])('List posts with where clause %o', async (where, expected) => { const res = await graphqlQuery({ query: /* GraphQL */ ` @@ -246,6 +269,7 @@ describe('Test Graphql API End to End', () => { posts(where: $where) { name bigint + nullable } } `, @@ -278,6 +302,7 @@ describe('Test Graphql API End to End', () => { id name bigint + nullable } } `, @@ -360,4 +385,4 @@ describe('Test Graphql API End to End', () => { } }); }); -}); +}); \ No newline at end of file diff --git a/packages/strapi-provider-upload-aws-s3/lib/index.js b/packages/strapi-provider-upload-aws-s3/lib/index.js index d03c0b27e1..f5f5bb1943 100644 --- a/packages/strapi-provider-upload-aws-s3/lib/index.js +++ b/packages/strapi-provider-upload-aws-s3/lib/index.js @@ -10,6 +10,8 @@ const _ = require('lodash'); const AWS = require('aws-sdk'); +const trimParam = str => typeof str === "string" ? str.trim() : undefined + module.exports = { provider: 'aws-s3', name: 'Amazon Web Service S3', @@ -55,15 +57,15 @@ module.exports = { init: (config) => { // configure AWS S3 bucket connection AWS.config.update({ - accessKeyId: config.public, - secretAccessKey: config.private, + accessKeyId: trimParam(config.public), + secretAccessKey: trimParam(config.private), region: config.region }); const S3 = new AWS.S3({ apiVersion: '2006-03-01', params: { - Bucket: config.bucket + Bucket: trimParam(config.bucket) } }); diff --git a/packages/strapi-utils/lib/__tests__/convertRestQueryParams.test.js b/packages/strapi-utils/lib/__tests__/convertRestQueryParams.test.js index 49ad72c674..af8bdc7d10 100644 --- a/packages/strapi-utils/lib/__tests__/convertRestQueryParams.test.js +++ b/packages/strapi-utils/lib/__tests__/convertRestQueryParams.test.js @@ -365,5 +365,33 @@ describe('convertRestQueryParams', () => { ], }); }); + + test('Null', () => { + expect( + convertRestQueryParams({ 'content.text_null': true }) + ).toMatchObject({ + where: [ + { + field: 'content.text', + operator: 'null', + value: true, + }, + ], + }); + }); + + test('Not Null', () => { + expect( + convertRestQueryParams({ 'content.text_null': false }) + ).toMatchObject({ + where: [ + { + field: 'content.text', + operator: 'null', + value: false, + }, + ], + }); + }); }); }); diff --git a/packages/strapi-utils/lib/buildQuery.js b/packages/strapi-utils/lib/buildQuery.js index b66b43b55d..f17cca6bdb 100644 --- a/packages/strapi-utils/lib/buildQuery.js +++ b/packages/strapi-utils/lib/buildQuery.js @@ -66,7 +66,7 @@ const castValueToType = ({ type, value }) => { return false; } - return value; + return Boolean(value); } case 'integer': case 'biginteger': @@ -79,6 +79,17 @@ const castValueToType = ({ type, value }) => { } }; +/** + * Cast basic values based on attribute type + * @param {Object} options - Options + * @param {string} options.type - type of the atribute + * @param {*} options.value - value tu cast + * @param {string} options.operator - name of operator + */ +const castValue = ({ type, value, operator}) => { + if (operator === 'null') return castValueToType({ type: 'boolean', value }) + return castValueToType({ type, value}) +} /** * * @param {Object} options - Options @@ -109,8 +120,8 @@ const buildQuery = ({ model, filters = {}, ...rest }) => { // cast value or array of values const castedValue = Array.isArray(value) - ? value.map(val => castValueToType({ type, value: val })) - : castValueToType({ type, value: value }); + ? value.map(val => castValue({ type, operator, value: val })) + : castValue({ type, operator, value: value }); return { field, operator, value: castedValue }; }); diff --git a/packages/strapi-utils/lib/convertRestQueryParams.js b/packages/strapi-utils/lib/convertRestQueryParams.js index 2592bd5e03..40a7c4d535 100644 --- a/packages/strapi-utils/lib/convertRestQueryParams.js +++ b/packages/strapi-utils/lib/convertRestQueryParams.js @@ -132,6 +132,7 @@ const VALID_OPERATORS = [ 'lte', 'gt', 'gte', + 'null', ]; /** diff --git a/packages/strapi/__tests__/filtering.test.e2e.js b/packages/strapi/__tests__/filtering.test.e2e.js index ec0e3d82d7..fb85bf24a3 100644 --- a/packages/strapi/__tests__/filtering.test.e2e.js +++ b/packages/strapi/__tests__/filtering.test.e2e.js @@ -81,6 +81,14 @@ const productFixtures = [ rank: 91, big_rank: 926372323421, }, + { + name: 'Product 4', + description: 'Product description 4', + price: null, + decimal_field: 12.22, + rank: 99, + big_rank: 999999999999, + }, ]; async function createFixtures() { @@ -194,6 +202,35 @@ describe('Filtering API', () => { }); }); + describe('Filter null', () => { + test('Should return only one match', async () => { + const res = await rq({ + method: 'GET', + url: '/products', + qs: { + price_null: true, + }, + }); + + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(1); + expect(res.body[0]).toMatchObject(data.products[3]); + }); + + test('Should return three matches', async () => { + const res = await rq({ + method: 'GET', + url: '/products', + qs: { + price_null: false, + }, + }); + + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(3); + }); + }); + describe('Filter contains insensitive', () => { test('Should match with insensitive case', async () => { const res1 = await rq({ @@ -985,11 +1022,14 @@ describe('Filtering API', () => { }, }); - expect(res.body).toEqual([ + [ + data.products[3], data.products[0], data.products[2], data.products[1], - ]); + ].forEach(expectedPost => { + expect(res.body).toEqual(expect.arrayContaining([expectedPost])); + }); }); });