diff --git a/src/graphql.ts b/src/graphql.ts index 60c847fb..60005294 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -185,3 +185,20 @@ export function isGeneratedByIntrospection(schema: GraphQLSchema): boolean { .filter(([name, type]) => !name.startsWith('__') && !isSpecifiedScalarType(type)) .every(([, type]) => type.astNode === undefined) } + +// https://spec.graphql.org/October2021/#EscapedCharacter +const escapeMap: { [key: string]: string } = { + '\"': '\\\"', + '\\': '\\\\', + '\/': '\\/', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +}; + +export function escapeGraphQLCharacters(input: string): string { + // eslint-disable-next-line regexp/no-escape-backspace + return input.replace(/["\\/\f\n\r\t\b]/g, match => escapeMap[match]); +} diff --git a/src/myzod/index.ts b/src/myzod/index.ts index ca44c10f..0cfbdf15 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -22,6 +22,7 @@ import type { Visitor } from '../visitor'; import { InterfaceTypeDefinitionBuilder, ObjectTypeDefinitionBuilder, + escapeGraphQLCharacters, isInput, isListType, isNamedType, @@ -282,7 +283,7 @@ function generateFieldTypeMyZodSchema(config: ValidationSchemaPluginConfig, visi appliedDirectivesGen = `${appliedDirectivesGen}.default(${defaultValue.value})`; if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) - appliedDirectivesGen = `${appliedDirectivesGen}.default("${defaultValue.value}")`; + appliedDirectivesGen = `${appliedDirectivesGen}.default("${escapeGraphQLCharacters(defaultValue.value)}")`; } if (isNonNullType(parentType)) { diff --git a/src/yup/index.ts b/src/yup/index.ts index 4d654fb4..e5763dae 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -22,6 +22,7 @@ import type { Visitor } from '../visitor'; import { InterfaceTypeDefinitionBuilder, ObjectTypeDefinitionBuilder, + escapeGraphQLCharacters, isInput, isListType, isNamedType, @@ -284,7 +285,7 @@ function shapeFields(fields: readonly (FieldDefinitionNode | InputValueDefinitio } if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) - fieldSchema = `${fieldSchema}.default("${defaultValue.value}")`; + fieldSchema = `${fieldSchema}.default("${escapeGraphQLCharacters(defaultValue.value)}")`; } if (isNonNullType(field.type)) diff --git a/src/zod/index.ts b/src/zod/index.ts index e5bd802f..739c892b 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -22,6 +22,7 @@ import type { Visitor } from '../visitor'; import { InterfaceTypeDefinitionBuilder, ObjectTypeDefinitionBuilder, + escapeGraphQLCharacters, isInput, isListType, isNamedType, @@ -295,7 +296,7 @@ function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visito appliedDirectivesGen = `${appliedDirectivesGen}.default(${defaultValue.value})`; if (defaultValue?.kind === Kind.STRING || defaultValue?.kind === Kind.ENUM) - appliedDirectivesGen = `${appliedDirectivesGen}.default("${defaultValue.value}")`; + appliedDirectivesGen = `${appliedDirectivesGen}.default("${escapeGraphQLCharacters(defaultValue.value)}")`; } if (isNonNullType(parentType)) { diff --git a/tests/graphql.spec.ts b/tests/graphql.spec.ts index 5f65ff39..1ba0376c 100644 --- a/tests/graphql.spec.ts +++ b/tests/graphql.spec.ts @@ -12,7 +12,7 @@ import { } from 'graphql'; import dedent from 'ts-dedent'; -import { ObjectTypeDefinitionBuilder, isGeneratedByIntrospection, topologicalSortAST, topsort } from '../src/graphql'; +import { ObjectTypeDefinitionBuilder, escapeGraphQLCharacters, isGeneratedByIntrospection, topologicalSortAST, topsort } from '../src/graphql'; describe('graphql', () => { describe('objectTypeDefinitionBuilder', () => { @@ -297,3 +297,65 @@ describe('isGeneratedByIntrospection function', () => { expect(isGeneratedByIntrospection(clientSchema)).toBe(true); }); }); + +describe('escapeGraphQLCharacters', () => { + it('should escape double quotes', () => { + const input = 'This is a "test" string.'; + const expected = 'This is a \\\"test\\\" string.'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape backslashes', () => { + const input = 'This is a backslash: \\'; + const expected = 'This is a backslash: \\\\'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape forward slashes', () => { + const input = 'This is a forward slash: /'; + const expected = 'This is a forward slash: \\/'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape backspaces', () => { + const input = 'This is a backspace: \b'; + const expected = 'This is a backspace: \\b'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape form feeds', () => { + const input = 'This is a form feed: \f'; + const expected = 'This is a form feed: \\f'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape new lines', () => { + const input = 'This is a new line: \n'; + const expected = 'This is a new line: \\n'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape carriage returns', () => { + const input = 'This is a carriage return: \r'; + const expected = 'This is a carriage return: \\r'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape horizontal tabs', () => { + const input = 'This is a tab: \t'; + const expected = 'This is a tab: \\t'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should escape multiple special characters', () => { + const input = 'This is a "test" string with \n new line and \t tab.'; + const expected = 'This is a \\\"test\\\" string with \\n new line and \\t tab.'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); + + it('should not escape non-special characters', () => { + const input = 'Normal string with no special characters.'; + const expected = 'Normal string with no special characters.'; + expect(escapeGraphQLCharacters(input)).toBe(expected); + }); +}); diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts index cd9bc95a..f5753b10 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -1376,6 +1376,7 @@ describe('myzod', () => { input PageInput { pageType: PageType! = PUBLIC greeting: String = "Hello" + newline: String = "Hello\\nWorld" score: Int = 100 ratio: Float = 0.5 isMember: Boolean = true @@ -1399,6 +1400,7 @@ describe('myzod', () => { return myzod.object({ pageType: PageTypeSchema.default("PUBLIC"), greeting: myzod.string().default("Hello").optional().nullable(), + newline: myzod.string().default("Hello\\nWorld").optional().nullable(), score: myzod.number().default(100).optional().nullable(), ratio: myzod.number().default(0.5).optional().nullable(), isMember: myzod.boolean().default(true).optional().nullable() diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index bd927891..c23d71b9 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -1400,6 +1400,7 @@ describe('yup', () => { input PageInput { pageType: PageType! = PUBLIC greeting: String = "Hello" + newline: String = "Hello\\nWorld" score: Int = 100 ratio: Float = 0.5 isMember: Boolean = true @@ -1423,6 +1424,7 @@ describe('yup', () => { return yup.object({ pageType: PageTypeSchema.nonNullable().default("PUBLIC"), greeting: yup.string().defined().nullable().default("Hello").optional(), + newline: yup.string().defined().nullable().default("Hello\\nWorld").optional(), score: yup.number().defined().nullable().default(100).optional(), ratio: yup.number().defined().nullable().default(0.5).optional(), isMember: yup.boolean().defined().nullable().default(true).optional() diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 75b93d1a..0fb1a324 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -539,6 +539,7 @@ describe('zod', () => { input PageInput { pageType: PageType! = PUBLIC greeting: String = "Hello" + newline: String = "Hello\\nWorld" score: Int = 100 ratio: Float = 0.5 isMember: Boolean = true @@ -562,6 +563,7 @@ describe('zod', () => { return z.object({ pageType: PageTypeSchema.default("PUBLIC"), greeting: z.string().default("Hello").nullish(), + newline: z.string().default("Hello\\nWorld").nullish(), score: z.number().default(100).nullish(), ratio: z.number().default(0.5).nullish(), isMember: z.boolean().default(true).nullish()