diff --git a/lib/query-validation-visitor.js b/lib/query-validation-visitor.js index 1a7e9c0..1460aaa 100644 --- a/lib/query-validation-visitor.js +++ b/lib/query-validation-visitor.js @@ -227,6 +227,11 @@ function validateArrayTypeValue (context, valueTypeDef, typeDefWithDirective, va // Validate array content if (value) { value.forEach((element, index) => { + // Skip null elements - GraphQL's built-in validation handles null checks for non-null types + if (element === null || element === undefined) { + return + } + const iFieldNameFullIndexed = iFieldNameFull ? `${iFieldNameFull}[${index++}]` : `[${index++}]` if (isInputObjectType(valueTypeDefArray)) { diff --git a/package-lock.json b/package-lock.json index 99558fc..ed26793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -587,6 +587,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -893,6 +894,7 @@ "resolved": "https://registry.npmjs.org/@envelop/core/-/core-2.6.0.tgz", "integrity": "sha512-yTptKinJN//i6m1kXUbnLBl/FobzddI4ehURAMS08eRUOQwAuXqJU9r8VdTav8nIZLb4t6cuDWFb3n331LiwLw==", "dev": true, + "peer": true, "dependencies": { "@envelop/types": "2.4.0", "tslib": "2.4.0" @@ -1856,6 +1858,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2422,6 +2425,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -3237,6 +3241,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz", "integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "@eslint/eslintrc": "^0.3.0", @@ -3413,6 +3418,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz", "integrity": "sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.3", "array.prototype.flat": "^1.2.4", @@ -3454,6 +3460,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "peer": true, "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -3492,6 +3499,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.1.tgz", "integrity": "sha512-XgdcdyNzHfmlQyweOPTxmc7pIsS6dE4MvwhXWMQ2Dxs1XAL2GJDilUsjWen6TWik0aSI+zD/PqocZBblcm9rdA==", "dev": true, + "peer": true, "engines": { "node": "^10.12.0 || >=12.0.0" }, @@ -3504,6 +3512,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.3.tgz", "integrity": "sha512-ZMbFvZ1WAYSZKY662MBVEWR45VaBT6KSJCiupjrNlcdakB90juaZeDCbJq19e73JZQubqFtgETohwgAt8u5P6w==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.3", "array.prototype.flatmap": "^1.2.4", @@ -3794,6 +3803,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4394,6 +4404,7 @@ "version": "16.6.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -8450,6 +8461,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -8685,6 +8697,7 @@ "resolved": "https://registry.npmjs.org/@envelop/core/-/core-2.6.0.tgz", "integrity": "sha512-yTptKinJN//i6m1kXUbnLBl/FobzddI4ehURAMS08eRUOQwAuXqJU9r8VdTav8nIZLb4t6cuDWFb3n331LiwLw==", "dev": true, + "peer": true, "requires": { "@envelop/types": "2.4.0", "tslib": "2.4.0" @@ -9522,7 +9535,8 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -9956,6 +9970,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -10578,6 +10593,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz", "integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "^7.0.0", "@eslint/eslintrc": "^0.3.0", @@ -10755,6 +10771,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz", "integrity": "sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==", "dev": true, + "peer": true, "requires": { "array-includes": "^3.1.3", "array.prototype.flat": "^1.2.4", @@ -10789,6 +10806,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "peer": true, "requires": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -10817,6 +10835,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.1.tgz", "integrity": "sha512-XgdcdyNzHfmlQyweOPTxmc7pIsS6dE4MvwhXWMQ2Dxs1XAL2GJDilUsjWen6TWik0aSI+zD/PqocZBblcm9rdA==", "dev": true, + "peer": true, "requires": {} }, "eslint-plugin-react": { @@ -10824,6 +10843,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.3.tgz", "integrity": "sha512-ZMbFvZ1WAYSZKY662MBVEWR45VaBT6KSJCiupjrNlcdakB90juaZeDCbJq19e73JZQubqFtgETohwgAt8u5P6w==", "dev": true, + "peer": true, "requires": { "array-includes": "^3.1.3", "array.prototype.flatmap": "^1.2.4", @@ -10980,6 +11000,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, + "peer": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -11422,7 +11443,8 @@ "graphql": { "version": "16.6.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", - "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==" + "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "peer": true }, "graphql-tag": { "version": "2.12.6", diff --git a/test/input-object.test.js b/test/input-object.test.js index 987b9f1..22f7f0c 100644 --- a/test/input-object.test.js +++ b/test/input-object.test.js @@ -194,5 +194,96 @@ module.exports.test = function (setup, implType) { }) } }) + + describe('Arrays with null values', function () { + const queryVariables = /* GraphQL */` + mutation createBook($input: BookInput) { + createBook(input: $input) { + title + } + } + ` + + before(async function () { + this.typeDefs = /* GraphQL */` + type Query { + books: [Book] + } + type Book { + title: String + } + type Mutation { + createBook(input: BookInput): Book + } + input BookInput { + title: Int! @constraint(min: 3) + options: [OptionInput] + } + input OptionInput { + id: ID + name: String @constraint(minLength: 2) + } + ` + + this.request = await setup({ typeDefs: this.typeDefs }) + }) + + it('should handle null values in array of input objects', async function () { + const { body, statusCode } = await this.request + .post('/graphql') + .set('Accept', 'application/json') + .send({ + query: queryVariables, + variables: { + input: { + title: 3, + options: [null, { id: '1', name: 'test' }, null] + } + } + }) + + strictEqual(statusCode, 200) + deepStrictEqual(body, { data: { createBook: null } }) + }) + + it('should handle array with only null values', async function () { + const { body, statusCode } = await this.request + .post('/graphql') + .set('Accept', 'application/json') + .send({ + query: queryVariables, + variables: { + input: { + title: 3, + options: [null, null] + } + } + }) + + strictEqual(statusCode, 200) + deepStrictEqual(body, { data: { createBook: null } }) + }) + + it('should still validate non-null elements when array contains nulls', async function () { + const { body, statusCode } = await this.request + .post('/graphql') + .set('Accept', 'application/json') + .send({ + query: queryVariables, + variables: { + input: { + title: 3, + options: [null, { id: '1', name: 'a' }] + } + } + }) + + isStatusCodeError(statusCode, implType) + strictEqual(body.errors[0].message, + 'Variable "$input" got invalid value "a" at "input.options[1].name"' + + valueByImplType(implType, '; Expected type "name_String_minLength_2"') + + '. Must be at least 2 characters in length') + }) + }) }) }