fix(cli): handle escaped single-quoted strings in schema change (#7321)

This commit is contained in:
Adam Benhassen 2025-11-27 11:04:57 +02:00 committed by GitHub
parent 594b176b74
commit 316859ebcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 109 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
'@graphql-hive/cli': patch
---
handle escaped single-quoted strings in schema changes

View file

@ -0,0 +1,10 @@
type Query {
status: Status
}
enum Status {
ACTIVE
INACTIVE @deprecated(reason: "Use 'DISABLED' instead, it's clearer")
PENDING
DISABLED
}

View file

@ -0,0 +1,9 @@
type Query {
status: Status
}
enum Status {
ACTIVE
INACTIVE
PENDING
}

View file

@ -1,5 +1,6 @@
/* eslint-disable no-process-env */
import { createHash } from 'node:crypto';
import stripAnsi from 'strip-ansi';
import { ProjectType, RuleInstanceSeverityLevel } from 'testkit/gql/graphql';
import * as GraphQLSchema from 'testkit/gql/graphql';
import type { CompositeSchema } from '@hive/api/__generated__/types';
@ -974,3 +975,33 @@ test.concurrent(
).rejects.toThrow('Failed to auto-approve: Schema check has schema policy errors');
},
);
test.concurrent(
'schema:check displays enum deprecation reason with single quotes correctly',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject(ProjectType.Single);
const { secret } = await createTargetAccessToken({});
await schemaPublish([
'--registry.accessToken',
secret,
'--author',
'Test',
'--commit',
'init',
'fixtures/enum-with-deprecation-init.graphql',
]);
const result = await schemaCheck([
'--registry.accessToken',
secret,
'fixtures/enum-with-deprecation-change.graphql',
]);
expect(stripAnsi(result)).toContain(
"Enum value Status.INACTIVE was deprecated with reason Use 'DISABLED' instead, it's clearer",
);
},
);

View file

@ -0,0 +1,46 @@
import colors from 'colors';
import { boldQuotedWords } from '../../../packages/libraries/cli/src/helpers/texture/texture';
describe('boldQuotedWords', () => {
test('handles simple single-quoted strings', () => {
const input = "Changed value for 'foo'";
const expected = `Changed value for ${colors.bold('foo')}`;
expect(boldQuotedWords(input)).toBe(expected);
});
test('handles simple double-quoted strings', () => {
const input = 'Changed value for "foo"';
const expected = `Changed value for ${colors.bold('foo')}`;
expect(boldQuotedWords(input)).toBe(expected);
});
test('handles multiple quoted strings', () => {
const input = "Field 'name' on type 'User' was changed";
const expected = `Field ${colors.bold('name')} on type ${colors.bold('User')} was changed`;
expect(boldQuotedWords(input)).toBe(expected);
});
test('handles string with no quotes', () => {
const input = 'No quotes here';
expect(boldQuotedWords(input)).toBe(input);
});
test('handles escaped single quotes within single-quoted strings', () => {
const input =
"Enum value 'Status.INACTIVE' has deprecation reason 'Use \\'DISABLED\\' instead'";
const expected = `Enum value ${colors.bold('Status.INACTIVE')} has deprecation reason ${colors.bold("Use 'DISABLED' instead")}`;
expect(boldQuotedWords(input)).toBe(expected);
});
test('handles escaped double quotes within double-quoted strings', () => {
const input = 'Default value changed from "\\"test\\"" to "other"';
const expected = `Default value changed from ${colors.bold('"test"')} to ${colors.bold('other')}`;
expect(boldQuotedWords(input)).toBe(expected);
});
test('handles apostrophes when quotes are escaped', () => {
const input = "Reason 'Use \\'DISABLED\\' instead, it\\'s clearer'";
const expected = `Reason ${colors.bold("Use 'DISABLED' instead, it's clearer")}`;
expect(boldQuotedWords(input)).toBe(expected);
});
});

View file

@ -21,11 +21,15 @@ export const trimEnd = (value: string) => value.replace(/\s+$/g, '');
* Convert quoted text to bolded text. Quotes are stripped.
*/
export const boldQuotedWords = (value: string) => {
const singleQuotedTextRegex = /'([^']+)'/gim;
const doubleQuotedTextRegex = /"([^"]+)"/gim;
const singleQuotedTextRegex = /'((?:[^'\\]|\\.)+?)'/g;
const doubleQuotedTextRegex = /"((?:[^"\\]|\\.)+?)"/g;
return value
.replace(singleQuotedTextRegex, (_, capturedValue: string) => colors.bold(capturedValue))
.replace(doubleQuotedTextRegex, (_, capturedValue: string) => colors.bold(capturedValue));
.replace(singleQuotedTextRegex, (_, capturedValue: string) =>
colors.bold(capturedValue.replace(/\\'/g, "'")),
)
.replace(doubleQuotedTextRegex, (_, capturedValue: string) =>
colors.bold(capturedValue.replace(/\\"/g, '"')),
);
};
export const prefixedInspect =