Skip to content

Commit d9ebc84

Browse files
committed
feat(core): escape single quotes in all enum descriptions and deprecation reasons
1 parent 2d22d32 commit d9ebc84

File tree

5 files changed

+136
-9
lines changed

5 files changed

+136
-9
lines changed

.changeset/large-walls-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-inspector/core': minor
3+
---
4+
5+
Escape single quotes in all enum descriptions and deprecation reasons

packages/core/__tests__/diff/enum.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,105 @@ describe('enum', () => {
274274
expect(change.criticality.reason).toBeDefined();
275275
expect(change.message).toEqual(`Enum value 'C' was added to enum 'enumA'`);
276276
});
277+
278+
describe('string escaping', () => {
279+
test('deprecation reason changed with escaped single quotes', async () => {
280+
const a = buildSchema(/* GraphQL */ `
281+
type Query {
282+
fieldA: String
283+
}
284+
285+
enum enumA {
286+
A @deprecated(reason: "It's old")
287+
B
288+
}
289+
`);
290+
291+
const b = buildSchema(/* GraphQL */ `
292+
type Query {
293+
fieldA: String
294+
}
295+
296+
enum enumA {
297+
A @deprecated(reason: "It's new")
298+
B
299+
}
300+
`);
301+
302+
const changes = await diff(a, b);
303+
const change = findFirstChangeByPath(changes, 'enumA.A');
304+
305+
expect(changes.length).toEqual(1);
306+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
307+
expect(change.message).toEqual(
308+
`Enum value 'enumA.A' deprecation reason changed from 'It\\'s old' to 'It\\'s new'`,
309+
);
310+
});
311+
312+
test('deprecation reason added with escaped single quotes', async () => {
313+
const a = buildSchema(/* GraphQL */ `
314+
type Query {
315+
fieldA: String
316+
}
317+
318+
enum enumA {
319+
A
320+
B
321+
}
322+
`);
323+
324+
const b = buildSchema(/* GraphQL */ `
325+
type Query {
326+
fieldA: String
327+
}
328+
329+
enum enumA {
330+
A @deprecated(reason: "Don't use this")
331+
B
332+
}
333+
`);
334+
335+
const changes = await diff(a, b);
336+
const change = findFirstChangeByPath(changes, 'enumA.A');
337+
338+
expect(changes.length).toEqual(2);
339+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
340+
expect(change.message).toEqual(
341+
`Enum value 'enumA.A' was deprecated with reason 'Don\\'t use this'`,
342+
);
343+
});
344+
345+
test('deprecation reason without single quotes is unchanged', async () => {
346+
const a = buildSchema(/* GraphQL */ `
347+
type Query {
348+
fieldA: String
349+
}
350+
351+
enum enumA {
352+
A @deprecated(reason: "Old Reason")
353+
B
354+
}
355+
`);
356+
357+
const b = buildSchema(/* GraphQL */ `
358+
type Query {
359+
fieldA: String
360+
}
361+
362+
enum enumA {
363+
A @deprecated(reason: "New Reason")
364+
B
365+
}
366+
`);
367+
368+
const changes = await diff(a, b);
369+
const change = findFirstChangeByPath(changes, 'enumA.A');
370+
371+
expect(changes.length).toEqual(1);
372+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
373+
expect(change.message).toEqual(
374+
`Enum value 'enumA.A' deprecation reason changed from 'Old Reason' to 'New Reason'`,
375+
);
376+
});
377+
});
277378
});

packages/core/__tests__/utils/string.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { safeString } from '../../src/utils/string.js';
1+
import { fmt, safeString } from '../../src/utils/string.js';
22

33
test('scalars', () => {
44
expect(safeString(0)).toBe('0');
@@ -33,3 +33,18 @@ test('array', () => {
3333
'[ { foo: 42 } ]',
3434
);
3535
});
36+
37+
describe('fmt', () => {
38+
test('escapes single quotes in strings', () => {
39+
expect(fmt("It's a test")).toBe("It\\'s a test");
40+
expect(fmt("Don't do this")).toBe("Don\\'t do this");
41+
expect(fmt("'quoted'")).toBe("\\'quoted\\'");
42+
});
43+
44+
test('handles strings without single quotes', () => {
45+
expect(fmt('test')).toBe('test');
46+
expect(fmt('Old Reason')).toBe('Old Reason');
47+
expect(fmt('enumA.B')).toBe('enumA.B');
48+
expect(fmt('')).toBe('');
49+
});
50+
});

packages/core/src/diff/changes/enum.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { GraphQLEnumType, GraphQLEnumValue } from 'graphql';
22
import { isDeprecated } from '../../utils/is-deprecated.js';
3+
import { fmt } from '../../utils/string.js';
34
import {
45
Change,
56
ChangeType,
@@ -80,13 +81,11 @@ export function enumValueAdded(
8081
}
8182

8283
function buildEnumValueDescriptionChangedMessage(args: EnumValueDescriptionChangedChange['meta']) {
84+
const oldDesc = fmt(args.oldEnumValueDescription ?? 'undefined');
85+
const newDesc = fmt(args.newEnumValueDescription ?? 'undefined');
8386
return args.oldEnumValueDescription === null
84-
? `Description '${args.newEnumValueDescription ?? 'undefined'}' was added to enum value '${
85-
args.enumName
86-
}.${args.enumValueName}'`
87-
: `Description for enum value '${args.enumName}.${args.enumValueName}' changed from '${
88-
args.oldEnumValueDescription ?? 'undefined'
89-
}' to '${args.newEnumValueDescription ?? 'undefined'}'`;
87+
? `Description '${newDesc}' was added to enum value '${args.enumName}.${args.enumValueName}'`
88+
: `Description for enum value '${args.enumName}.${args.enumValueName}' changed from '${oldDesc}' to '${newDesc}'`;
9089
}
9190

9291
export function enumValueDescriptionChangedFromMeta(
@@ -122,7 +121,9 @@ export function enumValueDescriptionChanged(
122121
function buildEnumValueDeprecationChangedMessage(
123122
args: EnumValueDeprecationReasonChangedChange['meta'],
124123
) {
125-
return `Enum value '${args.enumName}.${args.enumValueName}' deprecation reason changed from '${args.oldEnumValueDeprecationReason}' to '${args.newEnumValueDeprecationReason}'`;
124+
const oldReason = fmt(args.oldEnumValueDeprecationReason);
125+
const newReason = fmt(args.newEnumValueDeprecationReason);
126+
return `Enum value '${args.enumName}.${args.enumValueName}' deprecation reason changed from '${oldReason}' to '${newReason}'`;
126127
}
127128

128129
export function enumValueDeprecationReasonChangedFromMeta(
@@ -158,7 +159,8 @@ export function enumValueDeprecationReasonChanged(
158159
function buildEnumValueDeprecationReasonAddedMessage(
159160
args: EnumValueDeprecationReasonAddedChange['meta'],
160161
) {
161-
return `Enum value '${args.enumName}.${args.enumValueName}' was deprecated with reason '${args.addedValueDeprecationReason}'`;
162+
const reason = fmt(args.addedValueDeprecationReason);
163+
return `Enum value '${args.enumName}.${args.enumValueName}' was deprecated with reason '${reason}'`;
162164
}
163165

164166
export function enumValueDeprecationReasonAddedFromMeta(

packages/core/src/utils/string.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,7 @@ export function safeString(obj: unknown) {
8080
.replace(/\[Object: null prototype\] /g, '')
8181
.replace(/(^')|('$)/g, '');
8282
}
83+
84+
export function fmt(reason: string): string {
85+
return reason.replace(/'/g, "\\'");
86+
}

0 commit comments

Comments
 (0)