diff --git a/CHANGELOG.md b/CHANGELOG.md index 674c6db72..6676f6526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- Optional form fields are now truly optional, can be omitted from the payload. + ## [20.0.4] - 2024-08-30 - Improves thirdParty debug logging to help with debugging issues with JSON parsing. diff --git a/lib/build/recipe/emailpassword/api/utils.js b/lib/build/recipe/emailpassword/api/utils.js index ead402cfb..8ba082c73 100644 --- a/lib/build/recipe/emailpassword/api/utils.js +++ b/lib/build/recipe/emailpassword/api/utils.js @@ -50,37 +50,50 @@ function newBadRequestError(message) { message, }); } -// We check that the number of fields in input and config form field is the same. -// We check that each item in the config form field is also present in the input form field +// We check to make sure we are validating each required form field +// and also validate optional form fields only when present async function validateFormOrThrowError(inputs, configFormFields, tenantId, userContext) { let validationErrors = []; - if (configFormFields.length !== inputs.length) { + let requiredFormFields = new Set(); + configFormFields.forEach((formField) => { + if (!formField.optional) { + requiredFormFields.add(formField.id); + } + }); + if (inputs.length < requiredFormFields.size || inputs.length > configFormFields.length) { throw newBadRequestError("Are you sending too many / too few formFields?"); } - // Loop through all form fields. - for (let i = 0; i < configFormFields.length; i++) { - const field = configFormFields[i]; - // Find corresponding input value. - const input = inputs.find((i) => i.id === field.id); - // Absent or not optional empty field - if (input === undefined || (input.value === "" && !field.optional)) { - validationErrors.push({ - error: "Field is not optional", - id: field.id, - }); + for (const formField of configFormFields) { + const input = inputs.find((input) => input.id === formField.id); + if (formField.optional) { + // Validate optional inputs only when they are present + if (input && input.value.length > 0) { + const error = await formField.validate(input.value, tenantId, userContext); + if (error) { + validationErrors.push({ + error, + id: formField.id, + }); + } + } } else { - // Otherwise, use validate function. - const error = await field.validate(input.value, tenantId, userContext); - // If error, add it. - if (error !== undefined) { + if (input && input.value.length > 0) { + const error = await formField.validate(input.value, tenantId, userContext); + if (error) { + validationErrors.push({ + error, + id: formField.id, + }); + } + } else { validationErrors.push({ - error, - id: field.id, + error: "Field is not optional", + id: formField.id, }); } } } - if (validationErrors.length !== 0) { + if (validationErrors.length > 0) { throw new error_1.default({ type: error_1.default.FIELD_ERROR, payload: validationErrors, diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts index 34ebb2116..93718a872 100644 --- a/lib/build/version.d.ts +++ b/lib/build/version.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck -export declare const version = "20.0.4"; +export declare const version = "20.0.5"; export declare const cdiSupported: string[]; export declare const dashboardVersion = "0.13"; diff --git a/lib/build/version.js b/lib/build/version.js index ccd6143d0..e256659d6 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -15,7 +15,7 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * License for the specific language governing permissions and limitations * under the License. */ -exports.version = "20.0.4"; +exports.version = "20.0.5"; exports.cdiSupported = ["5.1"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} exports.dashboardVersion = "0.13"; diff --git a/lib/ts/recipe/emailpassword/api/utils.ts b/lib/ts/recipe/emailpassword/api/utils.ts index ae944e1f9..1f45333ca 100644 --- a/lib/ts/recipe/emailpassword/api/utils.ts +++ b/lib/ts/recipe/emailpassword/api/utils.ts @@ -82,8 +82,8 @@ function newBadRequestError(message: string) { }); } -// We check that the number of fields in input and config form field is the same. -// We check that each item in the config form field is also present in the input form field +// We check to make sure we are validating each required form field +// and also validate optional form fields only when present async function validateFormOrThrowError( inputs: { id: string; @@ -94,38 +94,51 @@ async function validateFormOrThrowError( userContext: UserContext ) { let validationErrors: { id: string; error: string }[] = []; + let requiredFormFields = new Set(); - if (configFormFields.length !== inputs.length) { + configFormFields.forEach((formField) => { + if (!formField.optional) { + requiredFormFields.add(formField.id); + } + }); + + if (inputs.length < requiredFormFields.size || inputs.length > configFormFields.length) { throw newBadRequestError("Are you sending too many / too few formFields?"); } - // Loop through all form fields. - for (let i = 0; i < configFormFields.length; i++) { - const field = configFormFields[i]; + for (const formField of configFormFields) { + const input = inputs.find((input) => input.id === formField.id); - // Find corresponding input value. - const input = inputs.find((i) => i.id === field.id); - - // Absent or not optional empty field - if (input === undefined || (input.value === "" && !field.optional)) { - validationErrors.push({ - error: "Field is not optional", - id: field.id, - }); + if (formField.optional) { + // Validate optional inputs only when they are present + if (input && input.value.length > 0) { + const error = await formField.validate(input.value, tenantId, userContext); + if (error) { + validationErrors.push({ + error, + id: formField.id, + }); + } + } } else { - // Otherwise, use validate function. - const error = await field.validate(input.value, tenantId, userContext); - // If error, add it. - if (error !== undefined) { + if (input && input.value.length > 0) { + const error = await formField.validate(input.value, tenantId, userContext); + if (error) { + validationErrors.push({ + error, + id: formField.id, + }); + } + } else { validationErrors.push({ - error, - id: field.id, + error: "Field is not optional", + id: formField.id, }); } } } - if (validationErrors.length !== 0) { + if (validationErrors.length > 0) { throw new STError({ type: STError.FIELD_ERROR, payload: validationErrors, diff --git a/lib/ts/version.ts b/lib/ts/version.ts index deab8f43b..9f9d7d051 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -12,7 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -export const version = "20.0.4"; +export const version = "20.0.5"; export const cdiSupported = ["5.1"]; diff --git a/package.json b/package.json index 4951f4d13..b32c64b53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supertokens-node", - "version": "20.0.4", + "version": "20.0.5", "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { diff --git a/test/emailpassword/signupFeature.test.js b/test/emailpassword/signupFeature.test.js index 7c19a062f..9d9bab9bf 100644 --- a/test/emailpassword/signupFeature.test.js +++ b/test/emailpassword/signupFeature.test.js @@ -1185,6 +1185,72 @@ describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js] assert(response.formFields[0].id === "testField2"); }); + // Custom optional field missing in the payload should not throw an error + it("Custom optional field missing in the payload should not throw an error", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: "testField", + optional: true, + }, + { + id: "testField1", + optional: true, + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => "cookie" }), + ], + }); + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + let response = await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "password", + value: "validpass123", + }, + { + id: "email", + value: "random@gmail.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(JSON.parse(res.text)); + } + }) + ); + + assert(response.status === "OK"); + assert(response.user.id !== undefined); + assert(response.user.emails[0] === "random@gmail.com"); + }); + // Test custom field validation error (one and two custom fields) it("test custom field validation error", async function () { const connectionURI = await startST();