From 70c189fb4eced2908ac4b59777c7bce19fcfe467 Mon Sep 17 00:00:00 2001 From: Rob Cameron Date: Thu, 23 Feb 2023 15:26:26 -0800 Subject: [PATCH 1/5] Adds new validateWithSync function --- packages/api/src/validations/validations.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/api/src/validations/validations.ts b/packages/api/src/validations/validations.ts index 7e1a4c6963f3..316406ae3b77 100644 --- a/packages/api/src/validations/validations.ts +++ b/packages/api/src/validations/validations.ts @@ -614,7 +614,7 @@ export function validate( // just send "Something went wrong" back to the client. This captures any custom // error you throw and turns it into a ServiceValidationError which will show // the actual error message. -export const validateWith = (func: () => void) => { +export const validateWithSync = (func: () => void) => { try { func() } catch (e) { @@ -623,6 +623,16 @@ export const validateWith = (func: () => void) => { } } +// Async version is the default +export const validateWith = async (func: () => Promise) => { + try { + await func() + } catch (e) { + const message = (e as Error).message || (e as string) + throw new ValidationErrors.ServiceValidationError(message) + } +} + // Wraps `callback` in a transaction to guarantee that `field` is not found in // the database and that the `callback` is executed before someone else gets a // chance to create the same value. From 8f5d85eb73479d89e694c356ea5c0a133860d278 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 21 Mar 2023 17:02:01 -0700 Subject: [PATCH 2/5] add codemod --- .../v5.x.x/renameValidateWith/README.md | 4 +++ .../__testfixtures__/default.input.js | 11 +++++++ .../__testfixtures__/default.output.js | 11 +++++++ .../__tests__/renameValidateWith.test.ts | 5 ++++ .../renameValidateWith/renameValidateWith.ts | 15 ++++++++++ .../renameValidateWith.yargs.ts | 29 +++++++++++++++++++ 6 files changed, 75 insertions(+) create mode 100644 packages/codemods/src/codemods/v5.x.x/renameValidateWith/README.md create mode 100644 packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.input.js create mode 100644 packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.output.js create mode 100644 packages/codemods/src/codemods/v5.x.x/renameValidateWith/__tests__/renameValidateWith.test.ts create mode 100644 packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.ts create mode 100644 packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.yargs.ts diff --git a/packages/codemods/src/codemods/v5.x.x/renameValidateWith/README.md b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/README.md new file mode 100644 index 000000000000..449980c6ad49 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/README.md @@ -0,0 +1,4 @@ +# Rename `validateWith` + +For (https://github.com/redwoodjs/redwood/pull/7681)[https://github.com/redwoodjs/redwood/pull/7681]. +This codemod renames any instance of `validateWith` to `validateWithSync`. diff --git a/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.input.js b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.input.js new file mode 100644 index 000000000000..e520d9fd869d --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.input.js @@ -0,0 +1,11 @@ +validateWith(() => { + if (input.name === 'Name') { + throw "You'll have to be more creative than that" + } +}) + +validateWith(() => { + if (input.name === 'Name') { + throw new Error("You'll have to be more creative than that") + } +}) diff --git a/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.output.js b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.output.js new file mode 100644 index 000000000000..7932f0929575 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__testfixtures__/default.output.js @@ -0,0 +1,11 @@ +validateWithSync(() => { + if (input.name === 'Name') { + throw "You'll have to be more creative than that" + } +}) + +validateWithSync(() => { + if (input.name === 'Name') { + throw new Error("You'll have to be more creative than that") + } +}) diff --git a/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__tests__/renameValidateWith.test.ts b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__tests__/renameValidateWith.test.ts new file mode 100644 index 000000000000..5841531ab340 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/__tests__/renameValidateWith.test.ts @@ -0,0 +1,5 @@ +describe('renameValidateWith', () => { + it('Converts validateWith to validateWithSync', async () => { + await matchTransformSnapshot('renameValidateWith', 'default') + }) +}) diff --git a/packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.ts b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.ts new file mode 100644 index 000000000000..24f680786b90 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.ts @@ -0,0 +1,15 @@ +import type { FileInfo, API } from 'jscodeshift' + +export default function transform(file: FileInfo, api: API) { + const j = api.jscodeshift + const root = j(file.source) + + root + .find(j.Identifier, { + type: 'Identifier', + name: 'validateWith', + }) + .replaceWith({ type: 'Identifier', name: 'validateWithSync' }) + + return root.toSource() +} diff --git a/packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.yargs.ts b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.yargs.ts new file mode 100644 index 000000000000..33daba57493f --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/renameValidateWith/renameValidateWith.yargs.ts @@ -0,0 +1,29 @@ +import path from 'path' + +import task, { TaskInnerAPI } from 'tasuku' + +import getFilesWithPattern from '../../../lib/getFilesWithPattern' +import getRWPaths from '../../../lib/getRWPaths' +import runTransform from '../../../lib/runTransform' + +export const command = 'rename-validate-with' +export const description = + '(v4.x.x->v5.x.x) Converts validateWith to validateWithSync' + +export const handler = () => { + task('Rename Validate With', async ({ setOutput }: TaskInnerAPI) => { + const rwPaths = getRWPaths() + + const files = getFilesWithPattern({ + pattern: 'validateWith', + filesToSearch: [rwPaths.api.src], + }) + + await runTransform({ + transformPath: path.join(__dirname, 'renameValidateWith.js'), + targetPaths: files, + }) + + setOutput('All done! Run `yarn rw lint --fix` to prettify your code') + }) +} From 3c3c4eddab93ca53b5b91fe9f08e9bb94920257d Mon Sep 17 00:00:00 2001 From: Rob Cameron Date: Thu, 23 Mar 2023 11:03:35 -0700 Subject: [PATCH 3/5] Adds tests --- .../validations/__tests__/validations.test.js | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/api/src/validations/__tests__/validations.test.js b/packages/api/src/validations/__tests__/validations.test.js index d450d26a0037..79ffdc2e819f 100644 --- a/packages/api/src/validations/__tests__/validations.test.js +++ b/packages/api/src/validations/__tests__/validations.test.js @@ -1,5 +1,10 @@ import * as ValidationErrors from '../errors' -import { validate, validateUniqueness, validateWith } from '../validations' +import { + validate, + validateUniqueness, + validateWith, + validateWithSync, +} from '../validations' describe('validate absence', () => { it('checks if value is null or undefined', () => { @@ -1146,10 +1151,10 @@ describe('validate', () => { }) }) -describe('validateWith', () => { +describe('validateWithSync', () => { it('runs a custom function as a validation', () => { const validateFunction = jest.fn() - validateWith(validateFunction) + validateWithSync(validateFunction) expect(validateFunction).toBeCalledWith() }) @@ -1157,7 +1162,7 @@ describe('validateWith', () => { it('catches errors and raises ServiceValidationError', () => { // Error instance try { - validateWith(() => { + validateWithSync(() => { throw new Error('Invalid value') }) } catch (e) { @@ -1167,7 +1172,7 @@ describe('validateWith', () => { // Error string try { - validateWith(() => { + validateWithSync(() => { throw 'Bad input' }) } catch (e) { @@ -1179,6 +1184,37 @@ describe('validateWith', () => { }) }) +describe('validateWith', () => { + it('runs a custom function as a validation', () => { + const validateFunction = jest.fn() + validateWith(validateFunction) + + expect(validateFunction).toBeCalledWith() + }) + + it('catches errors and raises ServiceValidationError', async () => { + // Error instance + try { + await validateWith(() => { + throw new Error('Invalid value') + }) + } catch (e) { + expect(e instanceof ValidationErrors.ServiceValidationError).toEqual(true) + expect(e.message).toEqual('Invalid value') + } + // Error string + try { + await validateWith(() => { + throw 'Bad input' + }) + } catch (e) { + expect(e instanceof ValidationErrors.ServiceValidationError).toEqual(true) + expect(e.message).toEqual('Bad input') + } + expect.assertions(4) + }) +}) + // Mock just enough of PrismaClient that we can test a transaction is running. // Prisma.PrismaClient is a class so we need to return a function that returns // the actual methods of an instance of the class From c19fe53487ecaabd56edc314ce45f98024146e70 Mon Sep 17 00:00:00 2001 From: Rob Cameron Date: Thu, 23 Mar 2023 11:11:03 -0700 Subject: [PATCH 4/5] Adds docs for validateWith() --- docs/docs/services.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/docs/services.md b/docs/docs/services.md index 4637c540c691..8e843896aa54 100644 --- a/docs/docs/services.md +++ b/docs/docs/services.md @@ -44,10 +44,10 @@ Finally, Services can also be called from [serverless functions](serverless-func ## Service Validations -Starting with `v0.38`, Redwood includes a feature we call Service Validations. These simplify an extremely common task: making sure that incoming data is formatted properly before continuing. These validations are meant to be included at the start of your Service function and will throw an error if conditions are not met: +Redwood includes a feature we call Service Validations. These simplify an extremely common task: making sure that incoming data is formatted properly before continuing. These validations are meant to be included at the start of your Service function and will throw an error if conditions are not met: ```jsx -import { validate, validateWith, validateUniqueness } from '@redwoodjs/api' +import { validate, validateWith, validateWithSync, validateUniqueness } from '@redwoodjs/api' export const createUser = async ({ input }) => { validate(input.firstName, 'First name', { @@ -55,11 +55,17 @@ export const createUser = async ({ input }) => { exclusion: { in: ['Admin', 'Owner'], message: 'That name is reserved, sorry!' }, length: { min: 2, max: 255 } }) - validateWith(() => { + validateWithSync(() => { if (input.role === 'Manager' && !context.currentUser.roles.includes('admin')) { throw 'Only Admins can create new Managers' } }) + validateWith(async () => { + const inviteCount = await db.invites.count({ where: { userId: currentUser.id } }) + if (inviteCount >= 10) { + throw 'You have already invited your max of 10 users' + } + }) return validateUniqueness('user', { username: input.username }, (db) => { return db.user.create({ data: input }) @@ -610,19 +616,18 @@ validate(input.value, 'Value', { } }) ``` - -### validateWith() +### validateWithSync() `validateWith()` is simply given a function to execute. This function should throw with a message if there is a problem, otherwise do nothing. ```jsx -validateWith(() => { +validateWithSync(() => { if (input.name === 'Name') { throw "You'll have to be more creative than that" } }) -validateWith(() => { +validateWithSync(() => { if (input.name === 'Name') { throw new Error("You'll have to be more creative than that") } @@ -633,6 +638,18 @@ Either of these errors will be caught and re-thrown as a `ServiceValidationError You could just write your own function and throw whatever you like, without using `validateWith()`. But, when accessing your Service function through GraphQL, that error would be swallowed and the user would simply see "Something went wrong" for security reasons: error messages could reveal source code or other sensitive information so most are hidden. Errors thrown by Service Validations are considered "safe" and allowed to be shown to the client. +### validateWithSync() + +The same behavior as `validateWithSync()` but works with Promises. + +```jsx +validateWithSync(async () => { + if (await db.products.count() >= 100) { + throw "There can only be a maximum of 100 products in your store" + } +}) +``` + ### validateUniqueness() This validation guarantees that the field(s) given in the first argument are unique in the database before executing the callback given in the last argument. If a record is found with the given fields then an error is thrown and the callback is not invoked. From 0cc622e9df1197b137a616b0b793daee8a12ec07 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Thu, 23 Mar 2023 12:36:56 -0700 Subject: [PATCH 5/5] fix yarn check --- package.json | 2 +- yarn.lock | 17 +---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 23193cf20e90..452d437e07da 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "prompts": "2.4.2", "rimraf": "3.0.2", "typescript": "4.9.5", - "yargs": "17.6.2", + "yargs": "17.7.1", "zx": "7.2.1" }, "packageManager": "yarn@3.5.0", diff --git a/yarn.lock b/yarn.lock index 282444f6f9d2..c71dacf86fa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27583,7 +27583,7 @@ __metadata: prompts: 2.4.2 rimraf: 3.0.2 typescript: 4.9.5 - yargs: 17.6.2 + yargs: 17.7.1 zx: 7.2.1 languageName: unknown linkType: soft @@ -32115,21 +32115,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.6.2": - version: 17.6.2 - resolution: "yargs@npm:17.6.2" - dependencies: - cliui: ^8.0.1 - escalade: ^3.1.1 - get-caller-file: ^2.0.5 - require-directory: ^2.1.1 - string-width: ^4.2.3 - y18n: ^5.0.5 - yargs-parser: ^21.1.1 - checksum: dd5c89aa8186d2a18625b26b68beb635df648617089135e9661107a92561056427bbd41dbfa228db5a7d968ea1043d96c036c2eb978acf7b61a0ae48bf3be206 - languageName: node - linkType: hard - "yargs@npm:17.7.1, yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.2": version: 17.7.1 resolution: "yargs@npm:17.7.1"