Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/compare-changes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ export const requiredMatchMetaMap: {
FIELD_ARGUMENT_DESCRIPTION_CHANGED: ['argumentName', 'fieldName', 'newDescription', 'typeName'],
FIELD_ARGUMENT_REMOVED: ['fieldName', 'removedFieldArgumentName', 'typeName'],
FIELD_ARGUMENT_TYPE_CHANGED: ['argumentName', 'fieldName', 'newArgumentType', 'typeName'],
FIELD_ARGUMENT_DEPRECATION_ADDED: ['argumentName', 'fieldName', 'typeName'],
FIELD_ARGUMENT_DEPRECATION_REMOVED: ['argumentName', 'fieldName', 'typeName'],
FIELD_ARGUMENT_DEPRECATION_REASON_CHANGED: [
'argumentName',
'fieldName',
'newDeprecationReason',
'typeName',
],
FIELD_ARGUMENT_DEPRECATION_REASON_ADDED: [
'addedDeprecationReason',
'argumentName',
'fieldName',
'typeName',
],
FIELD_ARGUMENT_DEPRECATION_REASON_REMOVED: ['argumentName', 'fieldName', 'typeName'],
FIELD_DEPRECATION_ADDED: ['fieldName', 'typeName'],
FIELD_DEPRECATION_REASON_ADDED: ['addedDeprecationReason', 'fieldName', 'typeName'],
FIELD_DEPRECATION_REASON_CHANGED: ['fieldName', 'newDeprecationReason', 'typeName'],
Expand All @@ -148,6 +163,19 @@ export const requiredMatchMetaMap: {
INPUT_FIELD_DESCRIPTION_REMOVED: ['inputFieldName', 'inputName'],
INPUT_FIELD_REMOVED: ['inputName', 'removedFieldName'],
INPUT_FIELD_TYPE_CHANGED: ['inputFieldName', 'inputName', 'newInputFieldType'],
INPUT_FIELD_DEPRECATION_ADDED: ['inputFieldName', 'inputName'],
INPUT_FIELD_DEPRECATION_REMOVED: ['inputFieldName', 'inputName'],
INPUT_FIELD_DEPRECATION_REASON_CHANGED: [
'inputFieldName',
'inputName',
'newDeprecationReason',
],
INPUT_FIELD_DEPRECATION_REASON_ADDED: [
'addedDeprecationReason',
'inputFieldName',
'inputName',
],
INPUT_FIELD_DEPRECATION_REASON_REMOVED: ['inputFieldName', 'inputName'],
OBJECT_TYPE_INTERFACE_ADDED: ['addedInterfaceName', 'objectTypeName'],
OBJECT_TYPE_INTERFACE_REMOVED: ['objectTypeName', 'removedInterfaceName'],
SCHEMA_MUTATION_TYPE_CHANGED: ['newMutationTypeName'],
Expand Down
87 changes: 86 additions & 1 deletion packages/core/__tests__/diff/argument.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,93 @@
import { buildSchema } from 'graphql';
import { CriticalityLevel, diff } from '../../src/index.js';
import { findFirstChangeByPath } from '../../utils/testing.js';
import { findChangesByPath, findFirstChangeByPath } from '../../utils/testing.js';

describe('argument', () => {
test('argument deprecation reason changed / added / removed', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
f(
a: String! @deprecated(reason: "OLD"),
b: String! @deprecated(reason: "BBB"),
c: String!
): String
}
`);
const b = buildSchema(/* GraphQL */ `
type Query {
f(
a: String! @deprecated(reason: "NEW"),
b: String!,
c: String! @deprecated(reason: "CCC")
): String
}
`);

const changes = await diff(a, b);
const change = {
a: findFirstChangeByPath(changes, 'Query.f.a.@deprecated'),
b: findFirstChangeByPath(changes, 'Query.f.b.@deprecated'),
c: findFirstChangeByPath(changes, 'Query.f.c.@deprecated'),
};

expect(change.a.type).toEqual('FIELD_ARGUMENT_DEPRECATION_REASON_CHANGED');
expect(change.a.message).toEqual(
"Deprecation reason on argument 'a' on field 'Query.f' has changed from 'OLD' to 'NEW'",
);
expect(change.b.type).toEqual('FIELD_ARGUMENT_DEPRECATION_REMOVED');
expect(change.b.message).toEqual("Argument 'b' on field 'Query.f' is no longer deprecated");
expect(change.c.type).toEqual('FIELD_ARGUMENT_DEPRECATION_ADDED');
expect(change.c.message).toEqual("Argument 'c' on field 'Query.f' is deprecated");
});

test('argument deprecation added / removed', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
f(a: String! @deprecated, b: String!): String
}
`);
const b = buildSchema(/* GraphQL */ `
type Query {
f(a: String!, b: String! @deprecated): String
}
`);

const changes = await diff(a, b);
const change = {
a: findFirstChangeByPath(changes, 'Query.f.a.@deprecated'),
b: findFirstChangeByPath(changes, 'Query.f.b.@deprecated'),
};

expect(change.a.type).toEqual('FIELD_ARGUMENT_DEPRECATION_REMOVED');
expect(change.a.message).toEqual("Argument 'a' on field 'Query.f' is no longer deprecated");
expect(change.b.type).toEqual('FIELD_ARGUMENT_DEPRECATION_ADDED');
expect(change.b.message).toEqual("Argument 'b' on field 'Query.f' is deprecated");
});

test('argument deprecation added w/reason', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
f(a: String!): String
}
`);
const b = buildSchema(/* GraphQL */ `
type Query {
f(a: String! @deprecated(reason: "unused")): String
}
`);

const changes = await diff(a, b);
expect(findChangesByPath(changes, 'Query.f.a.@deprecated')).toHaveLength(2);
const change = findFirstChangeByPath(changes, 'Query.f.a.@deprecated');
expect(change.type).toEqual('FIELD_ARGUMENT_DEPRECATION_ADDED');
expect(change.meta).toMatchObject({
argumentName: 'a',
deprecationReason: 'unused',
fieldName: 'f',
typeName: 'Query',
});
});

test('added non-nullable with default value', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
Expand Down
91 changes: 91 additions & 0 deletions packages/core/__tests__/diff/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,97 @@ import { findChangesByPath, findFirstChangeByPath } from '../../utils/testing.js

describe('input', () => {
describe('fields', () => {
test('deprecation reason changed / added / removed', async () => {
const a = buildSchema(/* GraphQL */ `
input Foo {
a: String! @deprecated(reason: "OLD")
b: String! @deprecated(reason: "BBB")
c: String!
}
`);
const b = buildSchema(/* GraphQL */ `
input Foo {
a: String! @deprecated(reason: "NEW")
b: String!
c: String! @deprecated(reason: "CCC")
}
`);

const changes = await diff(a, b);
const change = {
a: findFirstChangeByPath(changes, 'Foo.a.@deprecated'),
b: findFirstChangeByPath(changes, 'Foo.b.@deprecated'),
c: findFirstChangeByPath(changes, 'Foo.c.@deprecated'),
};

expect(change.a.type).toEqual('INPUT_FIELD_DEPRECATION_REASON_CHANGED');
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a.message).toEqual(
"Deprecation reason on input field 'Foo.a' has changed from 'OLD' to 'NEW'",
);
expect(change.b.type).toEqual('INPUT_FIELD_DEPRECATION_REMOVED');
expect(change.b.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.b.message).toEqual("Input field 'Foo.b' is no longer deprecated");
expect(change.c.type).toEqual('INPUT_FIELD_DEPRECATION_ADDED');
expect(change.c.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.c.message).toEqual("Input field 'Foo.c' is deprecated");
});

test('deprecation added / removed', async () => {
const a = buildSchema(/* GraphQL */ `
input Foo {
a: String! @deprecated
b: String!
}
`);
const b = buildSchema(/* GraphQL */ `
input Foo {
a: String!
b: String! @deprecated
}
`);

const changes = await diff(a, b);
const change = {
a: findFirstChangeByPath(changes, 'Foo.a.@deprecated'),
b: findFirstChangeByPath(changes, 'Foo.b.@deprecated'),
};

expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a.type).toEqual('INPUT_FIELD_DEPRECATION_REMOVED');
expect(change.a.message).toEqual("Input field 'Foo.a' is no longer deprecated");
expect(change.b.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.b.type).toEqual('INPUT_FIELD_DEPRECATION_ADDED');
expect(change.b.message).toEqual("Input field 'Foo.b' is deprecated");
});

test('deprecation added w/reason', async () => {
const a = buildSchema(/* GraphQL */ `
input Foo {
a: String!
}
`);
const b = buildSchema(/* GraphQL */ `
input Foo {
a: String! @deprecated(reason: "Use b instead.")
}
`);

const changes = await diff(a, b);

expect(findChangesByPath(changes, 'Foo.a.@deprecated')).toHaveLength(2);
const change = findFirstChangeByPath(changes, 'Foo.a.@deprecated');

expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.type).toEqual('INPUT_FIELD_DEPRECATION_ADDED');
expect(change.message).toEqual("Input field 'Foo.a' is deprecated");
expect(change.meta).toMatchObject({
deprecationReason: 'Use b instead.',
inputFieldName: 'a',
inputName: 'Foo',
});
});

test('added', async () => {
const a = buildSchema(/* GraphQL */ `
input Foo {
Expand Down
100 changes: 65 additions & 35 deletions packages/core/src/diff/argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import {
GraphQLObjectType,
Kind,
} from 'graphql';
import { compareDirectiveLists, diffArrays, isNotEqual } from '../utils/compare.js';
import { compareDirectiveLists, diffArrays, isNotEqual, isVoid } from '../utils/compare.js';
import { isDeprecated } from '../utils/is-deprecated.js';
import {
fieldArgumentDefaultChanged,
fieldArgumentDeprecationAdded,
fieldArgumentDeprecationReasonAdded,
fieldArgumentDeprecationReasonChanged,
fieldArgumentDeprecationReasonRemoved,
fieldArgumentDeprecationRemoved,
fieldArgumentDescriptionChanged,
fieldArgumentTypeChanged,
} from './changes/argument.js';
Expand All @@ -18,6 +24,8 @@ import {
} from './changes/directive-usage.js';
import { AddChange } from './schema.js';

const DEPRECATION_REASON_DEFAULT = 'No longer supported';

export function changesInArgument(
type: GraphQLObjectType | GraphQLInterfaceType,
field: GraphQLField<any, any, any>,
Expand All @@ -29,6 +37,30 @@ export function changesInArgument(
addChange(fieldArgumentDescriptionChanged(type, field, oldArg, newArg));
}

if (isVoid(oldArg) || !isDeprecated(oldArg)) {
if (isDeprecated(newArg)) {
addChange(fieldArgumentDeprecationAdded(type, field, newArg));
}
} else if (!isDeprecated(newArg)) {
if (isDeprecated(oldArg)) {
addChange(fieldArgumentDeprecationRemoved(type, field, oldArg));
}
} else if (isNotEqual(oldArg.deprecationReason, newArg.deprecationReason)) {
if (
isVoid(oldArg.deprecationReason) ||
oldArg.deprecationReason === DEPRECATION_REASON_DEFAULT
) {
addChange(fieldArgumentDeprecationReasonAdded(type, field, newArg));
} else if (
isVoid(newArg.deprecationReason) ||
newArg.deprecationReason === DEPRECATION_REASON_DEFAULT
) {
addChange(fieldArgumentDeprecationReasonRemoved(type, field, oldArg));
} else {
addChange(fieldArgumentDeprecationReasonChanged(type, field, oldArg, newArg));
}
}

if (isNotEqual(oldArg?.defaultValue, newArg.defaultValue)) {
if (Array.isArray(oldArg?.defaultValue) && Array.isArray(newArg.defaultValue)) {
const diff = diffArrays(oldArg.defaultValue, newArg.defaultValue);
Expand All @@ -44,40 +76,38 @@ export function changesInArgument(
addChange(fieldArgumentTypeChanged(type, field, oldArg, newArg));
}

if (newArg.astNode?.directives) {
compareDirectiveLists(oldArg?.astNode?.directives || [], newArg.astNode.directives || [], {
onAdded(directive) {
addChange(
directiveUsageAdded(
Kind.ARGUMENT,
directive,
{
argument: newArg,
field,
type,
},
oldArg === null,
),
);
directiveUsageChanged(null, directive, addChange, type, field, newArg);
},
compareDirectiveLists(oldArg?.astNode?.directives || [], newArg.astNode?.directives || [], {
onAdded(directive) {
addChange(
directiveUsageAdded(
Kind.ARGUMENT,
directive,
{
argument: newArg,
field,
type,
},
oldArg === null,
),
);
directiveUsageChanged(null, directive, addChange, type, field, newArg);
},

onMutual(directive) {
directiveUsageChanged(
directive.oldVersion,
directive.newVersion,
addChange,
type,
field,
newArg,
);
},
onMutual(directive) {
directiveUsageChanged(
directive.oldVersion,
directive.newVersion,
addChange,
type,
field,
newArg,
);
},

onRemoved(directive) {
addChange(
directiveUsageRemoved(Kind.ARGUMENT, directive, { argument: oldArg!, field, type }),
);
},
});
}
onRemoved(directive) {
addChange(
directiveUsageRemoved(Kind.ARGUMENT, directive, { argument: oldArg!, field, type }),
);
},
});
}
Loading