diff --git a/packages/web/app/src/components/target/proposals/schema-diff/compare-lists.ts b/packages/web/app/src/components/target/proposals/schema-diff/compare-lists.ts index 18a459eba..51c4a5027 100644 --- a/packages/web/app/src/components/target/proposals/schema-diff/compare-lists.ts +++ b/packages/web/app/src/components/target/proposals/schema-diff/compare-lists.ts @@ -176,7 +176,7 @@ export function compareDirectiveLists( const newItems = newMap[itemName]; const oldItems = oldMap[itemName]; for (let i = (oldItems ?? []).length; i < (newItems ?? []).length; i++) { - const oldItem = oldItems[i]; + const oldItem = oldItems?.[i]; const newItem = newItems[i]; if (oldItem === undefined) { added.push(newItem); diff --git a/packages/web/app/src/components/target/proposals/schema-diff/components.tsx b/packages/web/app/src/components/target/proposals/schema-diff/components.tsx index 6429e492a..5d52e9c99 100644 --- a/packages/web/app/src/components/target/proposals/schema-diff/components.tsx +++ b/packages/web/app/src/components/target/proposals/schema-diff/components.tsx @@ -108,14 +108,14 @@ export function ChangeRow(props: { 'bg-neutral-2 px-2', props.className, props.type === 'removal' && 'bg-[#561c1d]', - props.type === 'addition' && 'bg-[#11362b]', + props.type === 'addition' && 'bg-green-600 dark:bg-[#11362b]', )} > {!!props.indent && @@ -178,7 +178,14 @@ function Addition(props: { children: ReactNode; className?: string }): ReactNode } }, [change.addition]); return ( - {props.children} + + {props.children} + ); } diff --git a/packages/web/app/src/pages/target-proposal.tsx b/packages/web/app/src/pages/target-proposal.tsx index 896836b18..0e346ea9b 100644 --- a/packages/web/app/src/pages/target-proposal.tsx +++ b/packages/web/app/src/pages/target-proposal.tsx @@ -1,5 +1,16 @@ import { useMemo } from 'react'; -import { buildSchema, GraphQLSchema } from 'graphql'; +import { + buildASTSchema, + buildSchema, + DefinitionNode, + DocumentNode, + GraphQLSchema, + isTypeDefinitionNode, + isTypeExtensionNode, + Kind, + parse, + visit, +} from 'graphql'; import { useMutation, useQuery } from 'urql'; import { Page, TargetLayout } from '@/components/layouts/target'; import { CompositionErrorsSection_SchemaErrorConnection } from '@/components/target/history/errors-and-changes'; @@ -179,6 +190,68 @@ export function TargetProposalsSinglePage(props: { ); } +const extensionToDefinitionKindMap = { + [Kind.OBJECT_TYPE_EXTENSION]: Kind.OBJECT_TYPE_DEFINITION, + [Kind.INPUT_OBJECT_TYPE_EXTENSION]: Kind.INPUT_OBJECT_TYPE_DEFINITION, + [Kind.INTERFACE_TYPE_EXTENSION]: Kind.INTERFACE_TYPE_DEFINITION, + [Kind.UNION_TYPE_EXTENSION]: Kind.UNION_TYPE_DEFINITION, + [Kind.ENUM_TYPE_EXTENSION]: Kind.ENUM_TYPE_DEFINITION, + [Kind.SCALAR_TYPE_EXTENSION]: Kind.SCALAR_TYPE_DEFINITION, +} as const; + +function addTypeForExtensions(ast: DocumentNode) { + const trackTypeDefs = new Map< + string, + | { + state: 'TYPE_ONLY'; + } + | { + state: 'EXTENSION_ONLY' | 'VALID_EXTENSION'; + kind: + | Kind.OBJECT_TYPE_EXTENSION + | Kind.ENUM_TYPE_EXTENSION + | Kind.UNION_TYPE_EXTENSION + | Kind.SCALAR_TYPE_EXTENSION + | Kind.INTERFACE_TYPE_EXTENSION + | Kind.INPUT_OBJECT_TYPE_EXTENSION; + } + >(); + for (const node of ast.definitions) { + if ('name' in node && node.name) { + const name = node.name.value; + const entry = trackTypeDefs.get(name); + if (isTypeExtensionNode(node)) { + if (!entry) { + trackTypeDefs.set(name, { state: 'EXTENSION_ONLY', kind: node.kind }); + } else if (entry.state === 'TYPE_ONLY') { + trackTypeDefs.set(name, { kind: node.kind, state: 'VALID_EXTENSION' }); + } + } else if (isTypeDefinitionNode(node)) { + if (!entry) { + trackTypeDefs.set(name, { state: 'TYPE_ONLY' }); + } else if (entry.state === 'EXTENSION_ONLY') { + trackTypeDefs.set(name, { ...entry, state: 'VALID_EXTENSION' }); + } + } + } + } + + const astCopy = visit(ast, {}); + for (const [name, entry] of trackTypeDefs) { + if (entry.state === 'EXTENSION_ONLY') { + console.log('FOUND EXTENSION: ', name, entry.kind); + (astCopy.definitions as DefinitionNode[]).push({ + kind: extensionToDefinitionKindMap[entry.kind], + name: { + kind: Kind.NAME, + value: name, + }, + }); + } + } + return astCopy; +} + const ProposalsContent = (props: Parameters[0]) => { // fetch main page details const [query, refreshProposal] = useQuery({ @@ -252,9 +325,12 @@ const ProposalsContent = (props: Parameters[0] (proposalVersion.serviceName == null || proposalVersion.serviceName === '') */, )?.node.source; - const beforeSchema = existingSchema?.length - ? buildSchema(existingSchema, { assumeValid: true, assumeValidSDL: true }) - : null; + let beforeSchema: GraphQLSchema | null = null; + if (existingSchema?.length) { + const ast = addTypeForExtensions(parse(existingSchema)); + beforeSchema = buildASTSchema(ast, { assumeValid: true, assumeValidSDL: true }); + } + // @todo better handle pagination const allChanges = proposalVersion.schemaChanges?.edges