diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts old mode 100644 new mode 100755 diff --git a/packages/core/__tests__/diff/directive-usage-argument.test.ts b/packages/core/__tests__/diff/directive-usage-argument.test.ts new file mode 100644 index 0000000000..fd5b854c35 --- /dev/null +++ b/packages/core/__tests__/diff/directive-usage-argument.test.ts @@ -0,0 +1,108 @@ +import { buildSchema } from 'graphql'; +import { CriticalityLevel, diff } from '@graphql-inspector/core'; +import { findFirstChangeByPath } from '../../utils/testing'; +import { expect } from 'vitest'; + +describe('directive-usage-argument', () => { + describe('enum-value-level directives', () => { + test('directive argument unchanged', async () => { + const a = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "") + B + } + `); + + const b = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "") + B + } + `); + + const changes = await diff(a, b); + + expect(changes.length).toEqual(0); + }); + + test('added directive argument', async () => { + const a = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "") + B + } + `); + + const b = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "", argumentTwo: "") + B + } + `); + + const changes = await diff(a, b); + const change = findFirstChangeByPath(changes, 'enumA.A.external'); + + expect(changes.length).toEqual(1); + expect(change.criticality.level).toEqual(CriticalityLevel.Breaking); + expect(change.criticality.reason).toBeDefined(); + expect(change.message).toEqual(`Argument 'argumentTwo' was added to directive 'external' used on enum value 'enumA.A'`) + }); + + test('removed directive argument', async () => { + const a = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "", argumentTwo: "") + B + } + `); + + const b = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "") + B + } + `); + + const changes = await diff(a, b); + const change = findFirstChangeByPath(changes, 'enumA.A.external'); + + expect(changes.length).toEqual(1); + expect(change.criticality.level).toEqual(CriticalityLevel.Breaking); + expect(change.criticality.reason).toBeDefined(); + expect(change.message).toEqual(`Argument 'argumentTwo' was removed from directive 'external' used on enum value 'enumA.A'`) + }); + + test('changed directive argument', async () => { + const a = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "originalValue") + B + } + `); + + const b = buildSchema(/* GraphQL */ ` + directive @external(argumentOne: String, argumentTwo: String) on ENUM_VALUE + enum enumA { + A @external(argumentOne: "changedValue") + B + } + `); + + const changes = await diff(a, b); + const change = findFirstChangeByPath(changes, 'enumA.A.external.argumentOne'); + + expect(changes.length).toEqual(1); + expect(change.criticality.level).toEqual(CriticalityLevel.Breaking); + expect(change.criticality.reason).toBeDefined(); + expect(change.message).toEqual(`Argument 'argumentOne' was changed from 'originalValue' (StringValue) to 'changedValue' (StringValue) in directive 'external' used on enum value 'enumA.A'`) + }); + }) +}) diff --git a/packages/core/src/diff/argument.ts b/packages/core/src/diff/argument.ts index a93652c860..b46ade486b 100644 --- a/packages/core/src/diff/argument.ts +++ b/packages/core/src/diff/argument.ts @@ -40,6 +40,7 @@ export function changesInArgument( addChange(fieldArgumentTypeChanged(type, field, oldArg, newArg)); } + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage if (oldArg.astNode?.directives && newArg.astNode?.directives) { compareLists(oldArg.astNode.directives || [], newArg.astNode.directives || [], { onAdded(directive) { diff --git a/packages/core/src/diff/changes/change.ts b/packages/core/src/diff/changes/change.ts index 8ef6ea7181..791711d275 100644 --- a/packages/core/src/diff/changes/change.ts +++ b/packages/core/src/diff/changes/change.ts @@ -86,28 +86,42 @@ export const ChangeType = { // Directive Usage DirectiveUsageUnionMemberAdded: 'DIRECTIVE_USAGE_UNION_MEMBER_ADDED', DirectiveUsageUnionMemberRemoved: 'DIRECTIVE_USAGE_UNION_MEMBER_REMOVED', + DirectiveUsageUnionMemberChanged: 'DIRECTIVE_USAGE_UNION_MEMBER_CHANGED', DirectiveUsageEnumAdded: 'DIRECTIVE_USAGE_ENUM_ADDED', DirectiveUsageEnumRemoved: 'DIRECTIVE_USAGE_ENUM_REMOVED', + DirectiveUsageEnumChanged: 'DIRECTIVE_USAGE_ENUM_CHANGED', DirectiveUsageEnumValueAdded: 'DIRECTIVE_USAGE_ENUM_VALUE_ADDED', DirectiveUsageEnumValueRemoved: 'DIRECTIVE_USAGE_ENUM_VALUE_REMOVED', + DirectiveArgumentUsageEnumValueAdded: 'DIRECTIVE_USAGE_ARGUMENT_ENUM_VALUE_ADDED', + DirectiveArgumentUsageEnumValueRemoved: 'DIRECTIVE_USAGE_ARGUMENT_ENUM_VALUE_REMOVED', + DirectiveArgumentUsageEnumValueChanged: 'DIRECTIVE_USAGE_ARGUMENT_ENUM_VALUE_CHANGED', DirectiveUsageInputObjectAdded: 'DIRECTIVE_USAGE_INPUT_OBJECT_ADDED', DirectiveUsageInputObjectRemoved: 'DIRECTIVE_USAGE_INPUT_OBJECT_REMOVED', + DirectiveUsageInputObjectChanged: 'DIRECTIVE_USAGE_INPUT_OBJECT_CHANGED', DirectiveUsageFieldAdded: 'DIRECTIVE_USAGE_FIELD_ADDED', DirectiveUsageFieldRemoved: 'DIRECTIVE_USAGE_FIELD_REMOVED', + DirectiveUsageFieldChanged: 'DIRECTIVE_USAGE_FIELD_CHANGED', DirectiveUsageScalarAdded: 'DIRECTIVE_USAGE_SCALAR_ADDED', DirectiveUsageScalarRemoved: 'DIRECTIVE_USAGE_SCALAR_REMOVED', + DirectiveUsageScalarChanged: 'DIRECTIVE_USAGE_SCALAR_CHANGED', DirectiveUsageObjectAdded: 'DIRECTIVE_USAGE_OBJECT_ADDED', DirectiveUsageObjectRemoved: 'DIRECTIVE_USAGE_OBJECT_REMOVED', + DirectiveUsageObjectChanged: 'DIRECTIVE_USAGE_OBJECT_CHANGED', DirectiveUsageInterfaceAdded: 'DIRECTIVE_USAGE_INTERFACE_ADDED', DirectiveUsageInterfaceRemoved: 'DIRECTIVE_USAGE_INTERFACE_REMOVED', + DirectiveUsageInterfaceChanged: 'DIRECTIVE_USAGE_INTERFACE_CHANGED', DirectiveUsageArgumentDefinitionAdded: 'DIRECTIVE_USAGE_ARGUMENT_DEFINITION_ADDED', DirectiveUsageArgumentDefinitionRemoved: 'DIRECTIVE_USAGE_ARGUMENT_DEFINITION_REMOVED', + DirectiveUsageArgumentDefinitionChanged: 'DIRECTIVE_USAGE_ARGUMENT_DEFINITION_CHANGED', DirectiveUsageSchemaAdded: 'DIRECTIVE_USAGE_SCHEMA_ADDED', DirectiveUsageSchemaRemoved: 'DIRECTIVE_USAGE_SCHEMA_REMOVED', + DirectiveUsageSchemaChanged: 'DIRECTIVE_USAGE_SCHEMA_CHANGED', DirectiveUsageFieldDefinitionAdded: 'DIRECTIVE_USAGE_FIELD_DEFINITION_ADDED', DirectiveUsageFieldDefinitionRemoved: 'DIRECTIVE_USAGE_FIELD_DEFINITION_REMOVED', + DirectiveUsageFieldDefinitionChanged: 'DIRECTIVE_USAGE_FIELD_DEFINITION_CHANGED', DirectiveUsageInputFieldDefinitionAdded: 'DIRECTIVE_USAGE_INPUT_FIELD_DEFINITION_ADDED', DirectiveUsageInputFieldDefinitionRemoved: 'DIRECTIVE_USAGE_INPUT_FIELD_DEFINITION_REMOVED', + DirectiveUsageInputFieldDefinitionChanged: 'DIRECTIVE_USAGE_INPUT_FIELD_DEFINITION_CHANGED', } as const; export type TypeOfChangeType = (typeof ChangeType)[keyof typeof ChangeType]; @@ -413,6 +427,15 @@ export type DirectiveUsageUnionMemberRemovedChange = { }; }; +export type DirectiveUsageUnionMemberChangedChange = { + type: typeof ChangeType.DirectiveUsageUnionMemberChanged; + meta: { + unionName: string; + unionMemberTypeName: string; + directiveName: string; + }; +}; + export type FieldArgumentAddedChange = { type: typeof ChangeType.FieldArgumentAdded; meta: { @@ -635,6 +658,14 @@ export type DirectiveUsageEnumRemovedChange = { }; }; +export type DirectiveUsageEnumChangedChange = { + type: typeof ChangeType.DirectiveUsageEnumChanged; + meta: { + enumName: string; + directiveName: string; + }; +}; + export type DirectiveUsageEnumValueAddedChange = { type: typeof ChangeType.DirectiveUsageEnumValueAdded; meta: { @@ -653,6 +684,44 @@ export type DirectiveUsageEnumValueRemovedChange = { }; }; +export type DirectiveArgumentUsageEnumValueAddedChange = { + type: typeof ChangeType.DirectiveArgumentUsageEnumValueAdded; + meta: { + enumName: string; + enumValueName: string; + directiveName: string; + addedArgumentName: string; + addedArgumentValue: string; + addedArgumentType: string; + }; +}; + +export type DirectiveArgumentUsageEnumValueRemovedChange = { + type: typeof ChangeType.DirectiveArgumentUsageEnumValueRemoved; + meta: { + enumName: string; + enumValueName: string; + directiveName: string; + removedArgumentName: string; + removedArgumentValue: string; + removedArgumentType: string; + }; +}; + +export type DirectiveArgumentUsageEnumValueChangedChange = { + type: typeof ChangeType.DirectiveArgumentUsageEnumValueChanged; + meta: { + enumName: string; + enumValueName: string; + directiveName: string; + argumentName: string; + oldArgumentValue: string; + oldArgumentType: string; + newArgumentValue: string; + newArgumentType: string; + }; +}; + export type DirectiveUsageInputObjectRemovedChange = { type: typeof ChangeType.DirectiveUsageInputObjectRemoved; meta: { @@ -675,6 +744,17 @@ export type DirectiveUsageInputObjectAddedChange = { }; }; +export type DirectiveUsageInputObjectChangedChange = { + type: typeof ChangeType.DirectiveUsageInputObjectChanged; + meta: { + inputObjectName: string; + inputFieldName: string; + isInputFieldTypeNullable: boolean; + inputFieldType: string; + directiveName: string; + }; +}; + export type DirectiveUsageInputFieldDefinitionAddedChange = { type: typeof ChangeType.DirectiveUsageInputFieldDefinitionAdded; meta: { @@ -693,6 +773,15 @@ export type DirectiveUsageInputFieldDefinitionRemovedChange = { }; }; +export type DirectiveUsageInputFieldDefinitionChangedChange = { + type: typeof ChangeType.DirectiveUsageInputFieldDefinitionChanged; + meta: { + inputObjectName: string; + inputFieldName: string; + directiveName: string; + }; +}; + export type DirectiveUsageFieldAddedChange = { type: typeof ChangeType.DirectiveUsageFieldAdded; meta: { @@ -711,6 +800,15 @@ export type DirectiveUsageFieldRemovedChange = { }; }; +export type DirectiveUsageFieldChangedChange = { + type: typeof ChangeType.DirectiveUsageFieldChanged; + meta: { + typeName: string; + fieldName: string; + directiveName: string; + }; +}; + export type DirectiveUsageScalarAddedChange = { type: typeof ChangeType.DirectiveUsageScalarAdded; meta: { @@ -727,6 +825,14 @@ export type DirectiveUsageScalarRemovedChange = { }; }; +export type DirectiveUsageScalarChangedChange = { + type: typeof ChangeType.DirectiveUsageScalarChanged; + meta: { + scalarName: string; + directiveName: string; + }; +}; + export type DirectiveUsageObjectAddedChange = { type: typeof ChangeType.DirectiveUsageObjectAdded; meta: { @@ -743,6 +849,14 @@ export type DirectiveUsageObjectRemovedChange = { }; }; +export type DirectiveUsageObjectChangedChange = { + type: typeof ChangeType.DirectiveUsageObjectChanged; + meta: { + objectName: string; + directiveName: string; + }; +}; + export type DirectiveUsageInterfaceAddedChange = { type: typeof ChangeType.DirectiveUsageInterfaceAdded; meta: { @@ -767,6 +881,14 @@ export type DirectiveUsageSchemaRemovedChange = { }; }; +export type DirectiveUsageSchemaChangedChange = { + type: typeof ChangeType.DirectiveUsageSchemaChanged; + meta: { + directiveName: string; + schemaTypeName: string; + }; +}; + export type DirectiveUsageFieldDefinitionAddedChange = { type: typeof ChangeType.DirectiveUsageFieldDefinitionAdded; meta: { @@ -785,6 +907,15 @@ export type DirectiveUsageFieldDefinitionRemovedChange = { }; }; +export type DirectiveUsageFieldDefinitionChangedChange = { + type: typeof ChangeType.DirectiveUsageFieldDefinitionChanged; + meta: { + typeName: string; + fieldName: string; + directiveName: string; + }; +}; + export type DirectiveUsageArgumentDefinitionChange = { type: typeof ChangeType.DirectiveUsageArgumentDefinitionAdded; meta: { @@ -805,6 +936,16 @@ export type DirectiveUsageArgumentDefinitionRemovedChange = { }; }; +export type DirectiveUsageArgumentDefinitionChangedChange = { + type: typeof ChangeType.DirectiveUsageArgumentDefinitionChanged; + meta: { + typeName: string; + fieldName: string; + argumentName: string; + directiveName: string; + }; +}; + export type DirectiveUsageInterfaceRemovedChange = { type: typeof ChangeType.DirectiveUsageInterfaceRemoved; meta: { @@ -813,6 +954,14 @@ export type DirectiveUsageInterfaceRemovedChange = { }; }; +export type DirectiveUsageInterfaceChangedChange = { + type: typeof ChangeType.DirectiveUsageInterfaceChanged; + meta: { + interfaceName: string; + directiveName: string; + }; +}; + export type DirectiveUsageArgumentDefinitionAddedChange = { type: typeof ChangeType.DirectiveUsageArgumentDefinitionAdded; meta: { @@ -898,28 +1047,42 @@ type Changes = { [ChangeType.FieldTypeChanged]: FieldTypeChangedChange; [ChangeType.DirectiveUsageUnionMemberAdded]: DirectiveUsageUnionMemberAddedChange; [ChangeType.DirectiveUsageUnionMemberRemoved]: DirectiveUsageUnionMemberRemovedChange; + [ChangeType.DirectiveUsageUnionMemberChanged]: DirectiveUsageUnionMemberChangedChange; [ChangeType.DirectiveUsageEnumAdded]: DirectiveUsageEnumAddedChange; [ChangeType.DirectiveUsageEnumRemoved]: DirectiveUsageEnumRemovedChange; + [ChangeType.DirectiveUsageEnumChanged]: DirectiveUsageEnumChangedChange; [ChangeType.DirectiveUsageEnumValueAdded]: DirectiveUsageEnumValueAddedChange; [ChangeType.DirectiveUsageEnumValueRemoved]: DirectiveUsageEnumValueRemovedChange; + [ChangeType.DirectiveArgumentUsageEnumValueAdded]: DirectiveArgumentUsageEnumValueAddedChange; + [ChangeType.DirectiveArgumentUsageEnumValueRemoved]: DirectiveArgumentUsageEnumValueRemovedChange; + [ChangeType.DirectiveArgumentUsageEnumValueChanged]: DirectiveArgumentUsageEnumValueChangedChange; [ChangeType.DirectiveUsageInputObjectAdded]: DirectiveUsageInputObjectAddedChange; [ChangeType.DirectiveUsageInputObjectRemoved]: DirectiveUsageInputObjectRemovedChange; + [ChangeType.DirectiveUsageInputObjectChanged]: DirectiveUsageInputObjectChangedChange; [ChangeType.DirectiveUsageFieldAdded]: DirectiveUsageFieldAddedChange; [ChangeType.DirectiveUsageFieldRemoved]: DirectiveUsageFieldRemovedChange; + [ChangeType.DirectiveUsageFieldChanged]: DirectiveUsageFieldChangedChange; [ChangeType.DirectiveUsageScalarAdded]: DirectiveUsageScalarAddedChange; [ChangeType.DirectiveUsageScalarRemoved]: DirectiveUsageScalarRemovedChange; + [ChangeType.DirectiveUsageScalarChanged]: DirectiveUsageScalarChangedChange; [ChangeType.DirectiveUsageObjectAdded]: DirectiveUsageObjectAddedChange; [ChangeType.DirectiveUsageObjectRemoved]: DirectiveUsageObjectRemovedChange; + [ChangeType.DirectiveUsageObjectChanged]: DirectiveUsageObjectChangedChange; [ChangeType.DirectiveUsageInterfaceAdded]: DirectiveUsageInterfaceAddedChange; [ChangeType.DirectiveUsageInterfaceRemoved]: DirectiveUsageInterfaceRemovedChange; + [ChangeType.DirectiveUsageInterfaceChanged]: DirectiveUsageInterfaceChangedChange; [ChangeType.DirectiveUsageArgumentDefinitionAdded]: DirectiveUsageArgumentDefinitionAddedChange; [ChangeType.DirectiveUsageArgumentDefinitionRemoved]: DirectiveUsageArgumentDefinitionRemovedChange; + [ChangeType.DirectiveUsageArgumentDefinitionChanged]: DirectiveUsageArgumentDefinitionChangedChange; [ChangeType.DirectiveUsageSchemaAdded]: DirectiveUsageSchemaAddedChange; [ChangeType.DirectiveUsageSchemaRemoved]: DirectiveUsageSchemaRemovedChange; + [ChangeType.DirectiveUsageSchemaChanged]: DirectiveUsageSchemaChangedChange; [ChangeType.DirectiveUsageFieldDefinitionAdded]: DirectiveUsageFieldDefinitionAddedChange; [ChangeType.DirectiveUsageFieldDefinitionRemoved]: DirectiveUsageFieldDefinitionRemovedChange; + [ChangeType.DirectiveUsageFieldDefinitionChanged]: DirectiveUsageFieldDefinitionChangedChange; [ChangeType.DirectiveUsageInputFieldDefinitionAdded]: DirectiveUsageInputFieldDefinitionAddedChange; [ChangeType.DirectiveUsageInputFieldDefinitionRemoved]: DirectiveUsageInputFieldDefinitionRemovedChange; + [ChangeType.DirectiveUsageInputFieldDefinitionChanged]: DirectiveUsageInputFieldDefinitionChangedChange; }; export type SerializableChange = Changes[keyof Changes]; diff --git a/packages/core/src/diff/changes/directive-usage-arugment.ts b/packages/core/src/diff/changes/directive-usage-arugment.ts new file mode 100644 index 0000000000..5dc8891266 --- /dev/null +++ b/packages/core/src/diff/changes/directive-usage-arugment.ts @@ -0,0 +1,177 @@ +import { ConstDirectiveNode, ConstValueNode, Kind } from 'graphql'; +import { ConstArgumentNode } from 'graphql/language'; +import { + Change, + ChangeType, + CriticalityLevel, + DirectiveArgumentUsageEnumValueAddedChange, + DirectiveArgumentUsageEnumValueChangedChange, + DirectiveArgumentUsageEnumValueRemovedChange, +} from './change.js'; +import { isOfKind, KindToPayload } from './directive-usage.js'; + +function buildDirectiveArgumentUsageEnumValueAddedMessage( + args: DirectiveArgumentUsageEnumValueAddedChange['meta'], +): string { + return `Argument '${args.addedArgumentName}' was added to directive '${args.directiveName}' used on enum value '${args.enumName}.${args.enumValueName}'`; +} + +export function directiveArgumentUsageEnumValueAddedFromMeta( + args: DirectiveArgumentUsageEnumValueAddedChange, +) { + return { + criticality: { + level: CriticalityLevel.Breaking, + reason: `Argument '${args.meta.addedArgumentName}' was added to directive '${args.meta.directiveName}' used on enum value '${args.meta.enumName}.${args.meta.enumValueName}'`, + }, + type: ChangeType.DirectiveArgumentUsageEnumValueAdded, + message: buildDirectiveArgumentUsageEnumValueAddedMessage(args.meta), + path: [args.meta.enumName, args.meta.enumValueName, args.meta.directiveName].join('.'), + meta: args.meta, + } as const; +} + +function buildDirectiveArgumentUsageEnumValueRemovedMessage( + args: DirectiveArgumentUsageEnumValueRemovedChange['meta'], +): string { + return `Argument '${args.removedArgumentName}' was removed from directive '${args.directiveName}' used on enum value '${args.enumName}.${args.enumValueName}'`; +} + +export function directiveArgumentUsageEnumValueRemovedFromMeta( + args: DirectiveArgumentUsageEnumValueRemovedChange, +) { + return { + criticality: { + level: CriticalityLevel.Breaking, + reason: `Argument '${args.meta.removedArgumentName}' was removed from directive '${args.meta.directiveName}' used on enum value '${args.meta.enumName}.${args.meta.enumValueName}'`, + }, + type: ChangeType.DirectiveArgumentUsageEnumValueRemoved, + message: buildDirectiveArgumentUsageEnumValueRemovedMessage(args.meta), + path: [args.meta.enumName, args.meta.enumValueName, args.meta.directiveName].join('.'), + meta: args.meta, + } as const; +} + +function buildDirectiveArgumentUsageEnumValueChangedMessage( + args: DirectiveArgumentUsageEnumValueChangedChange['meta'], +): string { + return `Argument '${args.argumentName}' was changed from '${args.oldArgumentValue}' (${args.oldArgumentType}) to '${args.newArgumentValue}' (${args.newArgumentType}) in directive '${args.directiveName}' used on enum value '${args.enumName}.${args.enumValueName}'`; +} + +export function directiveArgumentUsageEnumValueChangedFromMeta( + args: DirectiveArgumentUsageEnumValueChangedChange, +) { + return { + criticality: { + level: CriticalityLevel.Breaking, + reason: `Argument '${args.meta.argumentName}' was changed in directive '${args.meta.directiveName}' used on enum value '${args.meta.enumName}.${args.meta.enumValueName}'`, + }, + type: ChangeType.DirectiveArgumentUsageEnumValueChanged, + message: buildDirectiveArgumentUsageEnumValueChangedMessage(args.meta), + path: [ + args.meta.enumName, + args.meta.enumValueName, + args.meta.directiveName, + args.meta.argumentName, + ].join('.'), + meta: args.meta, + } as const; +} + +export function directiveUsageArgumentAdded( + kind: K, + payload: KindToPayload[K]['input'], + directive: ConstDirectiveNode, + argument: ConstArgumentNode, +): Change { + if (isOfKind(kind, Kind.ENUM_VALUE_DEFINITION, payload)) { + return directiveArgumentUsageEnumValueAddedFromMeta({ + type: ChangeType.DirectiveArgumentUsageEnumValueAdded, + meta: { + enumName: payload.type.name, + enumValueName: payload.value.name, + directiveName: directive.name.value, + addedArgumentName: argument.name.value, + addedArgumentType: stringOfValueNode(argument.value), + addedArgumentValue: argument.value.kind, + }, + }); + } + return {} as any; +} + +export function directiveUsageArgumentRemoved( + kind: K, + payload: KindToPayload[K]['input'], + directive: ConstDirectiveNode, + argument: ConstArgumentNode, +): Change { + if (isOfKind(kind, Kind.ENUM_VALUE_DEFINITION, payload)) { + return directiveArgumentUsageEnumValueRemovedFromMeta({ + type: ChangeType.DirectiveArgumentUsageEnumValueRemoved, + meta: { + enumName: payload.type.name, + enumValueName: payload.value.name, + directiveName: directive.name.value, + removedArgumentName: argument.name.value, + removedArgumentValue: stringOfValueNode(argument.value), + removedArgumentType: argument.value.kind, + }, + }); + } + return {} as any; +} + +export function directiveUsageArgumentChanged( + kind: K, + payload: KindToPayload[K]['input'], + directive: ConstDirectiveNode, + oldVersion: ConstArgumentNode, + newVersion: ConstArgumentNode, +): Change { + if (isOfKind(kind, Kind.ENUM_VALUE_DEFINITION, payload)) { + return directiveArgumentUsageEnumValueChangedFromMeta({ + type: ChangeType.DirectiveArgumentUsageEnumValueChanged, + meta: { + enumName: payload.type.name, + enumValueName: payload.value.name, + directiveName: directive.name.value, + argumentName: oldVersion.name.value, + oldArgumentValue: stringOfValueNode(oldVersion.value), + oldArgumentType: oldVersion.value.kind, + newArgumentValue: stringOfValueNode(newVersion.value), + newArgumentType: newVersion.value.kind, + }, + }); + } + return {} as any; +} + +export function hasArgumentChanged(newArgument: ConstArgumentNode, oldArgument: ConstArgumentNode) { + if (newArgument.kind !== oldArgument.kind) { + return true; + } + const newValue = stringOfValueNode(newArgument.value); + const oldValue = stringOfValueNode(oldArgument.value); + return newValue !== oldValue; +} + +function stringOfValueNode(valueNode: ConstValueNode): string { + if (valueNode.kind === Kind.NULL) return ''; + if ([Kind.INT, Kind.FLOAT, Kind.STRING, Kind.BOOLEAN, Kind.ENUM].includes(valueNode.kind)) { + return 'value' in valueNode ? String(valueNode.value) : ''; + } + if (valueNode.kind === Kind.LIST) { + const values = 'values' in valueNode ? valueNode.values : []; + return values.map(stringOfValueNode).join(', '); + } + if (valueNode.kind === Kind.OBJECT) { + const fields = 'fields' in valueNode ? valueNode.fields : []; + return fields + .map((field): string => { + return `${field.name.value} : ${stringOfValueNode(field.value)}`; + }) + .join(', '); + } + return ''; +} diff --git a/packages/core/src/diff/changes/directive-usage.ts b/packages/core/src/diff/changes/directive-usage.ts index 00e0b165d4..9aed52b69e 100644 --- a/packages/core/src/diff/changes/directive-usage.ts +++ b/packages/core/src/diff/changes/directive-usage.ts @@ -13,6 +13,8 @@ import { GraphQLUnionType, Kind, } from 'graphql'; +import { compareLists } from '../../utils/compare.js'; +import { AddChange } from '../schema.js'; import { Change, ChangeType, @@ -40,6 +42,11 @@ import { DirectiveUsageUnionMemberAddedChange, DirectiveUsageUnionMemberRemovedChange, } from './change.js'; +import { + directiveUsageArgumentAdded, + directiveUsageArgumentChanged, + directiveUsageArgumentRemoved, hasArgumentChanged, +} from './directive-usage-arugment.js'; function addedSpecialDirective( directiveName: string, @@ -67,7 +74,7 @@ function removedSpecialDirective( return forceReturn; } -type KindToPayload = { +export type KindToPayload = { [Kind.ENUM_TYPE_DEFINITION]: { input: GraphQLEnumType; change: DirectiveUsageEnumAddedChange | DirectiveUsageEnumRemovedChange; @@ -809,7 +816,32 @@ export function directiveUsageRemoved( return {} as any; } -function isOfKind( +export function directiveUsageChanged( + kind: K, + oldDirective: ConstDirectiveNode, + newDirective: ConstDirectiveNode, + payload: KindToPayload[K]['input'], + addChange: AddChange, +) { + const oldArguments = oldDirective.arguments || []; + const newArguments = newDirective.arguments || []; + + compareLists(oldArguments, newArguments, { + onAdded(argument) { + addChange(directiveUsageArgumentAdded(kind, payload, newDirective, argument)); + }, + onRemoved(argument) { + addChange(directiveUsageArgumentRemoved(kind, payload, newDirective, argument)); + }, + onMutual({ newVersion, oldVersion }) { + if (hasArgumentChanged(newVersion, oldVersion)) { + addChange(directiveUsageArgumentChanged(kind, payload, newDirective, oldVersion, newVersion)); + } + }, + }); +} + +export function isOfKind( kind: keyof KindToPayload, expectedKind: K, _value: any, diff --git a/packages/core/src/diff/directive.ts b/packages/core/src/diff/directive.ts index 035325da99..36e3861802 100644 --- a/packages/core/src/diff/directive.ts +++ b/packages/core/src/diff/directive.ts @@ -65,3 +65,11 @@ function changesInDirectiveArgument( addChange(directiveArgumentTypeChanged(directive, oldArg, newArg)); } } + +// function changesInDirectiveUsage( +// oldDirective: GraphQLDirective, +// newDirective: GraphQLDirective, +// addChange: AddChange +// ) { +// +// } diff --git a/packages/core/src/diff/enum.ts b/packages/core/src/diff/enum.ts index 1be7b0dacf..e8630778c0 100644 --- a/packages/core/src/diff/enum.ts +++ b/packages/core/src/diff/enum.ts @@ -1,16 +1,14 @@ import { GraphQLEnumType, Kind } from 'graphql'; import { compareLists, isNotEqual, isVoid } from '../utils/compare.js'; -import { directiveUsageAdded, directiveUsageRemoved } from './changes/directive-usage.js'; import { - enumValueAdded, - enumValueDeprecationReasonAdded, - enumValueDeprecationReasonChanged, - enumValueDeprecationReasonRemoved, - enumValueDescriptionChanged, - enumValueRemoved, -} from './changes/enum.js'; + directiveUsageAdded, + directiveUsageChanged, + directiveUsageRemoved, +} from './changes/directive-usage.js'; +import { enumValueAdded, enumValueDeprecationReasonAdded, enumValueDeprecationReasonChanged, enumValueDeprecationReasonRemoved, enumValueDescriptionChanged, enumValueRemoved } from './changes/enum.js'; import { AddChange } from './schema.js'; + export function changesInEnum( oldEnum: GraphQLEnumType, newEnum: GraphQLEnumType, @@ -41,6 +39,7 @@ export function changesInEnum( } } + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage compareLists(oldValue.astNode?.directives || [], newValue.astNode?.directives || [], { onAdded(directive) { addChange( @@ -58,6 +57,12 @@ export function changesInEnum( }), ); }, + onMutual(value) { + directiveUsageChanged(Kind.ENUM_VALUE_DEFINITION, value.oldVersion, value.newVersion, { + type: newEnum, + value: newValue + }, addChange) + }, }); }, }); diff --git a/packages/core/src/diff/field.ts b/packages/core/src/diff/field.ts index ff9bf07c55..019d37d4c1 100644 --- a/packages/core/src/diff/field.ts +++ b/packages/core/src/diff/field.ts @@ -68,6 +68,7 @@ export function changesInField( }, }); + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage compareLists(oldField.astNode?.directives || [], newField.astNode?.directives || [], { onAdded(directive) { addChange( diff --git a/packages/core/src/diff/input.ts b/packages/core/src/diff/input.ts index 7b45de560d..a2e620a66b 100644 --- a/packages/core/src/diff/input.ts +++ b/packages/core/src/diff/input.ts @@ -72,6 +72,7 @@ function changesInInputField( addChange(inputFieldTypeChanged(input, oldField, newField)); } + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage if (oldField.astNode?.directives && newField.astNode?.directives) { compareLists(oldField.astNode.directives || [], newField.astNode.directives || [], { onAdded(directive) { diff --git a/packages/core/src/diff/interface.ts b/packages/core/src/diff/interface.ts index ac34f74b8a..6709ecdbda 100644 --- a/packages/core/src/diff/interface.ts +++ b/packages/core/src/diff/interface.ts @@ -21,6 +21,7 @@ export function changesInInterface( changesInField(oldInterface, field.oldVersion, field.newVersion, addChange); }, }); + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage compareLists(oldInterface.astNode?.directives || [], newInterface.astNode?.directives || [], { onAdded(directive) { addChange(directiveUsageAdded(Kind.INTERFACE_TYPE_DEFINITION, directive, newInterface)); diff --git a/packages/core/src/diff/object.ts b/packages/core/src/diff/object.ts index 12817e1f0f..c4e6f4af62 100644 --- a/packages/core/src/diff/object.ts +++ b/packages/core/src/diff/object.ts @@ -38,6 +38,7 @@ export function changesInObject( }, }); + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage compareLists(oldType.astNode?.directives || [], newType.astNode?.directives || [], { onAdded(directive) { addChange(directiveUsageAdded(Kind.OBJECT, directive, newType)); diff --git a/packages/core/src/diff/scalar.ts b/packages/core/src/diff/scalar.ts index 020752b322..8554c26a48 100644 --- a/packages/core/src/diff/scalar.ts +++ b/packages/core/src/diff/scalar.ts @@ -8,6 +8,7 @@ export function changesInScalar( newScalar: GraphQLScalarType, addChange: AddChange, ) { + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage compareLists(oldScalar.astNode?.directives || [], newScalar.astNode?.directives || [], { onAdded(directive) { addChange(directiveUsageAdded(Kind.SCALAR_TYPE_DEFINITION, directive, newScalar)); diff --git a/packages/core/src/diff/schema.ts b/packages/core/src/diff/schema.ts index 0badd64085..0cfadf8658 100644 --- a/packages/core/src/diff/schema.ts +++ b/packages/core/src/diff/schema.ts @@ -1,33 +1,11 @@ -import { - GraphQLNamedType, - GraphQLObjectType, - GraphQLSchema, - isEnumType, - isInputObjectType, - isInterfaceType, - isObjectType, - isScalarType, - isUnionType, - Kind, -} from 'graphql'; +import { GraphQLNamedType, GraphQLObjectType, GraphQLSchema, isEnumType, isInputObjectType, isInterfaceType, isObjectType, isScalarType, isUnionType, Kind } from 'graphql'; import { compareLists, isNotEqual, isVoid } from '../utils/compare.js'; import { isPrimitive } from '../utils/graphql.js'; import { Change } from './changes/change.js'; import { directiveUsageAdded, directiveUsageRemoved } from './changes/directive-usage.js'; import { directiveAdded, directiveRemoved } from './changes/directive.js'; -import { - schemaMutationTypeChanged, - schemaQueryTypeChanged, - schemaSubscriptionTypeChanged, -} from './changes/schema.js'; -import { - typeAdded, - typeDescriptionAdded, - typeDescriptionChanged, - typeDescriptionRemoved, - typeKindChanged, - typeRemoved, -} from './changes/type.js'; +import { schemaMutationTypeChanged, schemaQueryTypeChanged, schemaSubscriptionTypeChanged } from './changes/schema.js'; +import { typeAdded, typeDescriptionAdded, typeDescriptionChanged, typeDescriptionRemoved, typeKindChanged, typeRemoved } from './changes/type.js'; import { changesInDirective } from './directive.js'; import { changesInEnum } from './enum.js'; import { changesInInputObject } from './input.js'; @@ -75,6 +53,7 @@ export function diffSchema(oldSchema: GraphQLSchema, newSchema: GraphQLSchema): }, }); + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage compareLists(oldSchema.astNode?.directives || [], newSchema.astNode?.directives || [], { onAdded(directive) { addChange(directiveUsageAdded(Kind.SCHEMA_DEFINITION, directive, newSchema)); @@ -82,6 +61,9 @@ export function diffSchema(oldSchema: GraphQLSchema, newSchema: GraphQLSchema): onRemoved(directive) { addChange(directiveUsageRemoved(Kind.SCHEMA_DEFINITION, directive, oldSchema)); }, + // onMutual(directive) { + // changesInDirectiveUsage(directive.oldVersion, directive.newVersion, addChange) + // }, }); return changes; diff --git a/packages/core/src/diff/union.ts b/packages/core/src/diff/union.ts index 030539b675..3460e19184 100644 --- a/packages/core/src/diff/union.ts +++ b/packages/core/src/diff/union.ts @@ -21,6 +21,7 @@ export function changesInUnion( }, }); + // TODO: Heshan -> Introduce a mutual option to this function to detect changes in directive usage compareLists(oldUnion.astNode?.directives || [], newUnion.astNode?.directives || [], { onAdded(directive) { addChange(directiveUsageAdded(Kind.UNION_TYPE_DEFINITION, directive, newUnion)); diff --git a/vite.config.ts b/vite.config.ts index 9f79d95167..8b599e645c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,29 +1,32 @@ import { join } from 'path'; -import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; -export default defineConfig({ - test: { - globals: true, - alias: { - '@graphql-inspector/commands': 'packages/commands/commands/src/index.ts', - '@graphql-inspector/loaders': 'packages/loaders/loaders/src/index.ts', - '@graphql-inspector/logger': 'packages/logger/src/index.ts', - '@graphql-inspector/url-loader': 'packages/loaders/url/src/index.ts', - '@graphql-inspector/testing': 'packages/testing/src/index.ts', - '@graphql-inspector/core': 'packages/core/src/index.ts', - 'graphql/language/parser.js': 'graphql/language/parser.js', - graphql: 'graphql/index.js', - }, - deps: { - // fixes `graphql` Duplicate "graphql" modules cannot be used at the same time since different - fallbackCJS: true, +export default defineConfig(async () => { + const tsconfigPaths = await import('vite-tsconfig-paths'); + + return { + test: { + globals: true, + alias: { + '@graphql-inspector/commands': 'packages/commands/commands/src/index.ts', + '@graphql-inspector/loaders': 'packages/loaders/loaders/src/index.ts', + '@graphql-inspector/logger': 'packages/logger/src/index.ts', + '@graphql-inspector/url-loader': 'packages/loaders/url/src/index.ts', + '@graphql-inspector/testing': 'packages/testing/src/index.ts', + '@graphql-inspector/core': 'packages/core/src/index.ts', + 'graphql/language/parser.js': 'graphql/language/parser.js', + graphql: 'graphql/index.js', + }, + deps: { + // fixes `graphql` Duplicate "graphql" modules cannot be used at the same time since different + fallbackCJS: true, + }, + setupFiles: ['./packages/testing/src/setup-file.ts'], }, - setupFiles: ['./packages/testing/src/setup-file.ts'], - }, - plugins: [ - tsconfigPaths({ - projects: [join(__dirname, 'tsconfig.test.json')], - }), - ], + plugins: [ + tsconfigPaths.default({ + projects: [join(__dirname, 'tsconfig.test.json')], + }), + ], + } });