From fc3a1d88da9764c1e1bf979d0dc8d62f25f0fc00 Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Wed, 17 Dec 2025 21:15:16 +0100 Subject: [PATCH 1/2] feat(supermassive): isFragmentOf utility --- .../src/benchmarks/isFragmentOf.benchmark.ts | 1174 +++++++++++++++++ packages/supermassive/src/index.ts | 1 + .../utilities/__tests__/isFragmentOf.test.ts | 1173 ++++++++++++++++ .../src/utilities/isFragmentOf.ts | 280 ++++ 4 files changed, 2628 insertions(+) create mode 100644 packages/supermassive/src/benchmarks/isFragmentOf.benchmark.ts create mode 100644 packages/supermassive/src/utilities/__tests__/isFragmentOf.test.ts create mode 100644 packages/supermassive/src/utilities/isFragmentOf.ts diff --git a/packages/supermassive/src/benchmarks/isFragmentOf.benchmark.ts b/packages/supermassive/src/benchmarks/isFragmentOf.benchmark.ts new file mode 100644 index 000000000..640ea9a92 --- /dev/null +++ b/packages/supermassive/src/benchmarks/isFragmentOf.benchmark.ts @@ -0,0 +1,1174 @@ +/** + * Benchmark for isFragmentOf function + * + * Run with: npx ts-node -T ./src/benchmarks/isFragmentOf.benchmark.ts + */ + +import { buildASTSchema, parse } from "graphql"; +import NiceBenchmark from "./nice-benchmark"; +import { extractMinimalViableSchemaForRequestDocument } from "../utilities/extractMinimalViableSchemaForRequestDocument"; +import { encodeASTSchema } from "../utilities/encodeASTSchema"; +import { + SchemaDefinitions, + FieldDefinitionRecord, + InputValueDefinitionRecord, + TypeDefinitionTuple, + DirectiveDefinitionTuple, + getDirectiveDefinitionArgs, + getDirectiveName, + getDirectiveLocations, + getEnumValues, + getFieldTypeReference, + getFieldArgs, + getFields, + getInputObjectFields, + getInputValueTypeReference, + getUnionTypeMembers, + isEnumTypeDefinition, + isInputObjectTypeDefinition, + isInterfaceTypeDefinition, + isObjectTypeDefinition, + isScalarTypeDefinition, + isUnionTypeDefinition, + getObjectTypeInterfaces, + getInterfaceTypeInterfaces, +} from "../schema/definition"; + +// Real-world schema (similar to production size) +const schemaSDL = ` + type Query { + user(id: ID!): User + users(first: Int, after: String, filter: UserFilter): UserConnection! + post(id: ID!): Post + posts(first: Int, after: String): PostConnection! + node(id: ID!): Node + search(query: String!, type: SearchType): SearchResult + viewer: Viewer + } + + type Mutation { + createUser(input: CreateUserInput!): CreateUserPayload + updateUser(input: UpdateUserInput!): UpdateUserPayload + deleteUser(id: ID!): DeleteUserPayload + createPost(input: CreatePostInput!): CreatePostPayload + } + + interface Node { + id: ID! + } + + interface Connection { + edges: [Edge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + interface Edge { + node: Node! + cursor: String! + } + + type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + + type User implements Node { + id: ID! + email: String! + name: String + avatar(size: Int = 100): String + posts(first: Int, after: String): PostConnection! + followers(first: Int, after: String): UserConnection! + following(first: Int, after: String): UserConnection! + createdAt: DateTime! + updatedAt: DateTime! + role: UserRole! + settings: UserSettings + } + + type UserConnection implements Connection { + edges: [UserEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type UserEdge implements Edge { + node: User! + cursor: String! + } + + type Post implements Node { + id: ID! + title: String! + content: String! + author: User! + comments(first: Int, after: String): CommentConnection! + likes: Int! + tags: [String!]! + status: PostStatus! + createdAt: DateTime! + updatedAt: DateTime! + } + + type PostConnection implements Connection { + edges: [PostEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type PostEdge implements Edge { + node: Post! + cursor: String! + } + + type Comment implements Node { + id: ID! + content: String! + author: User! + post: Post! + createdAt: DateTime! + } + + type CommentConnection implements Connection { + edges: [CommentEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type CommentEdge implements Edge { + node: Comment! + cursor: String! + } + + type Viewer { + user: User + notifications(first: Int, unreadOnly: Boolean): NotificationConnection! + } + + type Notification implements Node { + id: ID! + message: String! + read: Boolean! + createdAt: DateTime! + } + + type NotificationConnection implements Connection { + edges: [NotificationEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type NotificationEdge implements Edge { + node: Notification! + cursor: String! + } + + type UserSettings { + theme: Theme! + emailNotifications: Boolean! + pushNotifications: Boolean! + } + + union SearchResult = User | Post | Comment + + enum UserRole { + ADMIN + MODERATOR + USER + GUEST + } + + enum PostStatus { + DRAFT + PUBLISHED + ARCHIVED + } + + enum Theme { + LIGHT + DARK + SYSTEM + } + + enum SearchType { + ALL + USERS + POSTS + COMMENTS + } + + input UserFilter { + role: UserRole + createdAfter: DateTime + createdBefore: DateTime + } + + input CreateUserInput { + email: String! + name: String + role: UserRole = USER + } + + input UpdateUserInput { + id: ID! + email: String + name: String + role: UserRole + } + + input CreatePostInput { + title: String! + content: String! + tags: [String!] + status: PostStatus = DRAFT + } + + type CreateUserPayload { + user: User + errors: [Error!] + } + + type UpdateUserPayload { + user: User + errors: [Error!] + } + + type DeleteUserPayload { + success: Boolean! + errors: [Error!] + } + + type CreatePostPayload { + post: Post + errors: [Error!] + } + + type Error { + field: String + message: String! + } + + scalar DateTime + + directive @auth(requires: UserRole = USER) on FIELD_DEFINITION + directive @deprecated(reason: String) on FIELD_DEFINITION + directive @cacheControl(maxAge: Int, scope: CacheScope) on FIELD_DEFINITION | OBJECT + + enum CacheScope { + PUBLIC + PRIVATE + } +`; + +// Sample operations (<1kb each, typical real-world queries) +const sampleOperations = [ + // Simple query + `query GetUser($id: ID!) { + user(id: $id) { + id + name + email + role + } + }`, + // Query with nested fields + `query GetUserWithPosts($id: ID!, $first: Int) { + user(id: $id) { + id + name + posts(first: $first) { + edges { + node { + id + title + status + } + cursor + } + pageInfo { + hasNextPage + } + } + } + }`, + // Query with fragments + `query GetViewer { + viewer { + user { + id + name + avatar(size: 200) + settings { + theme + emailNotifications + } + } + } + }`, + // Search query with union + `query Search($query: String!, $type: SearchType) { + search(query: $query, type: $type) { + ... on User { + id + name + email + } + ... on Post { + id + title + author { + name + } + } + } + }`, + // Mutation + `mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + post { + id + title + content + status + } + errors { + field + message + } + } + }`, + // Query with interface + `query GetNode($id: ID!) { + node(id: $id) { + id + ... on User { + name + email + } + ... on Post { + title + content + } + ... on Comment { + content + } + } + }`, +]; + +// Build schema and extract fragments +const schema = buildASTSchema(parse(schemaSDL)); +const fullSchemaDefinitions = encodeASTSchema(parse(schemaSDL))[0]; + +const fragments = sampleOperations.map((op) => { + const doc = parse(op); + return extractMinimalViableSchemaForRequestDocument(schema, doc).definitions; +}); + +// ============================================================================ +// Implementation 1: Original (current) +// ============================================================================ +function isFragmentOf_v1( + schema: SchemaDefinitions, + fragment: SchemaDefinitions, +): boolean { + for (const [typeName, fragmentType] of Object.entries(fragment.types)) { + const schemaType = schema.types[typeName]; + if (!schemaType) return false; + if (!isTypeFragmentOf_v1(schemaType, fragmentType)) return false; + } + + if (fragment.directives) { + if (!schema.directives) return false; + for (const fragmentDirective of fragment.directives) { + const fragmentName = getDirectiveName(fragmentDirective); + const schemaDirective = schema.directives.find( + (d) => getDirectiveName(d) === fragmentName, + ); + if (!schemaDirective) return false; + if (!isDirectiveFragmentOf_v1(schemaDirective, fragmentDirective)) + return false; + } + } + + return true; +} + +function isTypeFragmentOf_v1( + schemaType: TypeDefinitionTuple, + fragmentType: TypeDefinitionTuple, +): boolean { + if (schemaType[0] !== fragmentType[0]) return false; + + if ( + isObjectTypeDefinition(schemaType) && + isObjectTypeDefinition(fragmentType) + ) { + if (!isFieldsFragmentOf_v1(getFields(schemaType), getFields(fragmentType))) + return false; + const schemaInterfaces = getObjectTypeInterfaces(schemaType); + const fragmentInterfaces = getObjectTypeInterfaces(fragmentType); + return isArraySubset_v1(schemaInterfaces, fragmentInterfaces); + } + + if ( + isInterfaceTypeDefinition(schemaType) && + isInterfaceTypeDefinition(fragmentType) + ) { + if (!isFieldsFragmentOf_v1(getFields(schemaType), getFields(fragmentType))) + return false; + const schemaInterfaces = getInterfaceTypeInterfaces(schemaType); + const fragmentInterfaces = getInterfaceTypeInterfaces(fragmentType); + return isArraySubset_v1(schemaInterfaces, fragmentInterfaces); + } + + if ( + isInputObjectTypeDefinition(schemaType) && + isInputObjectTypeDefinition(fragmentType) + ) { + return isInputFieldsFragmentOf_v1( + getInputObjectFields(schemaType), + getInputObjectFields(fragmentType), + ); + } + + if ( + isUnionTypeDefinition(schemaType) && + isUnionTypeDefinition(fragmentType) + ) { + const schemaMembers = getUnionTypeMembers(schemaType); + const fragmentMembers = getUnionTypeMembers(fragmentType); + return arraysEqual_v1(schemaMembers, fragmentMembers); + } + + if (isEnumTypeDefinition(schemaType) && isEnumTypeDefinition(fragmentType)) { + const schemaValues = getEnumValues(schemaType); + const fragmentValues = getEnumValues(fragmentType); + return arraysEqual_v1(schemaValues, fragmentValues); + } + + if ( + isScalarTypeDefinition(schemaType) && + isScalarTypeDefinition(fragmentType) + ) { + return true; + } + + return false; +} + +function isFieldsFragmentOf_v1( + schemaFields: FieldDefinitionRecord, + fragmentFields: FieldDefinitionRecord, +): boolean { + for (const [fieldName, fragmentField] of Object.entries(fragmentFields)) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + if ( + getFieldTypeReference(schemaField) !== + getFieldTypeReference(fragmentField) + ) + return false; + if ( + !isInputValuesSubset_v1( + getFieldArgs(schemaField), + getFieldArgs(fragmentField), + ) + ) + return false; + } + return true; +} + +function isInputFieldsFragmentOf_v1( + schemaFields: InputValueDefinitionRecord, + fragmentFields: InputValueDefinitionRecord, +): boolean { + for (const [fieldName, fragmentField] of Object.entries(fragmentFields)) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + if ( + getInputValueTypeReference(schemaField) !== + getInputValueTypeReference(fragmentField) + ) + return false; + } + return true; +} + +function isDirectiveFragmentOf_v1( + schemaDirective: DirectiveDefinitionTuple, + fragmentDirective: DirectiveDefinitionTuple, +): boolean { + if ( + !isInputValuesSubset_v1( + getDirectiveDefinitionArgs(schemaDirective), + getDirectiveDefinitionArgs(fragmentDirective), + ) + ) { + return false; + } + return isArraySubset_v1( + getDirectiveLocations(schemaDirective), + getDirectiveLocations(fragmentDirective), + ); +} + +function isInputValuesSubset_v1( + schemaArgs: InputValueDefinitionRecord | undefined, + fragmentArgs: InputValueDefinitionRecord | undefined, +): boolean { + if (!fragmentArgs) return true; + if (!schemaArgs) return false; + for (const [argName, fragmentArg] of Object.entries(fragmentArgs)) { + const schemaArg = schemaArgs[argName]; + if (schemaArg === undefined) return false; + if ( + getInputValueTypeReference(schemaArg) !== + getInputValueTypeReference(fragmentArg) + ) + return false; + } + return true; +} + +function arraysEqual_v1(a: T[], b: T[]): boolean { + if (a.length !== b.length) return false; + const sortedA = [...a].sort(); + const sortedB = [...b].sort(); + for (let i = 0; i < sortedA.length; i++) { + if (sortedA[i] !== sortedB[i]) return false; + } + return true; +} + +function isArraySubset_v1(superset: T[], subset: T[]): boolean { + const supersetSet = new Set(superset); + return subset.every((item) => supersetSet.has(item)); +} + +// ============================================================================ +// Implementation 2: Optimized with for-in loops and direct property access +// ============================================================================ +function isFragmentOf_v2( + schema: SchemaDefinitions, + fragment: SchemaDefinitions, +): boolean { + const schemaTypes = schema.types; + const fragmentTypes = fragment.types; + + for (const typeName in fragmentTypes) { + const schemaType = schemaTypes[typeName]; + if (!schemaType) return false; + if (!isTypeFragmentOf_v2(schemaType, fragmentTypes[typeName])) return false; + } + + const fragmentDirectives = fragment.directives; + if (fragmentDirectives) { + const schemaDirectives = schema.directives; + if (!schemaDirectives) return false; + + const directiveMap = new Map(); + for (let i = 0; i < schemaDirectives.length; i++) { + directiveMap.set(schemaDirectives[i][0], schemaDirectives[i]); + } + + for (let i = 0; i < fragmentDirectives.length; i++) { + const fragDir = fragmentDirectives[i]; + const schemaDir = directiveMap.get(fragDir[0]); + if (!schemaDir) return false; + if (!isDirectiveFragmentOf_v2(schemaDir, fragDir)) return false; + } + } + + return true; +} + +function isTypeFragmentOf_v2( + schemaType: TypeDefinitionTuple, + fragmentType: TypeDefinitionTuple, +): boolean { + const kind = schemaType[0]; + if (kind !== fragmentType[0]) return false; + + // Use direct kind comparison instead of type guard functions + switch (kind) { + case 2: // OBJECT + case 3: // INTERFACE + if ( + !isFieldsFragmentOf_v2( + schemaType[1] as FieldDefinitionRecord, + fragmentType[1] as FieldDefinitionRecord, + ) + ) { + return false; + } + const schemaIfaces = schemaType[2] as string[] | undefined; + const fragIfaces = fragmentType[2] as string[] | undefined; + if (fragIfaces && fragIfaces.length > 0) { + if (!schemaIfaces) return false; + return isArraySubset_v2(schemaIfaces, fragIfaces); + } + return true; + + case 6: // INPUT + return isInputFieldsFragmentOf_v2( + schemaType[1] as InputValueDefinitionRecord, + fragmentType[1] as InputValueDefinitionRecord, + ); + + case 4: // UNION + return arraysEqual_v2( + schemaType[1] as string[], + fragmentType[1] as string[], + ); + + case 5: // ENUM + return arraysEqual_v2( + schemaType[1] as string[], + fragmentType[1] as string[], + ); + + case 1: // SCALAR + return true; + + default: + return false; + } +} + +function isFieldsFragmentOf_v2( + schemaFields: FieldDefinitionRecord, + fragmentFields: FieldDefinitionRecord, +): boolean { + for (const fieldName in fragmentFields) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + + const fragmentField = fragmentFields[fieldName]; + const schemaType = Array.isArray(schemaField) + ? schemaField[0] + : schemaField; + const fragmentType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + + if (schemaType !== fragmentType) return false; + + const fragmentArgs = Array.isArray(fragmentField) + ? fragmentField[1] + : undefined; + if (fragmentArgs) { + const schemaArgs = Array.isArray(schemaField) + ? schemaField[1] + : undefined; + if (!schemaArgs) return false; + if (!isInputValuesSubset_v2(schemaArgs, fragmentArgs)) return false; + } + } + return true; +} + +function isInputFieldsFragmentOf_v2( + schemaFields: InputValueDefinitionRecord, + fragmentFields: InputValueDefinitionRecord, +): boolean { + for (const fieldName in fragmentFields) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + + const fragmentField = fragmentFields[fieldName]; + const schemaType = Array.isArray(schemaField) + ? schemaField[0] + : schemaField; + const fragmentType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + + if (schemaType !== fragmentType) return false; + } + return true; +} + +function isDirectiveFragmentOf_v2( + schemaDirective: DirectiveDefinitionTuple, + fragmentDirective: DirectiveDefinitionTuple, +): boolean { + const fragmentArgs = fragmentDirective[2]; + if (fragmentArgs) { + const schemaArgs = schemaDirective[2]; + if (!schemaArgs) return false; + if (!isInputValuesSubset_v2(schemaArgs, fragmentArgs)) return false; + } + return isArraySubset_v2(schemaDirective[1], fragmentDirective[1]); +} + +function isInputValuesSubset_v2( + schemaArgs: InputValueDefinitionRecord, + fragmentArgs: InputValueDefinitionRecord, +): boolean { + for (const argName in fragmentArgs) { + const schemaArg = schemaArgs[argName]; + if (schemaArg === undefined) return false; + + const fragmentArg = fragmentArgs[argName]; + const schemaType = Array.isArray(schemaArg) ? schemaArg[0] : schemaArg; + const fragmentType = Array.isArray(fragmentArg) + ? fragmentArg[0] + : fragmentArg; + + if (schemaType !== fragmentType) return false; + } + return true; +} + +function arraysEqual_v2(a: T[], b: T[]): boolean { + const len = a.length; + if (len !== b.length) return false; + if (len === 0) return true; + if (len === 1) return a[0] === b[0]; + if (len === 2) + return (a[0] === b[0] && a[1] === b[1]) || (a[0] === b[1] && a[1] === b[0]); + + // For larger arrays, use Set-based comparison (no sorting) + const setA = new Set(a); + for (let i = 0; i < len; i++) { + if (!setA.has(b[i])) return false; + } + return true; +} + +function isArraySubset_v2(superset: T[], subset: T[]): boolean { + const len = subset.length; + if (len === 0) return true; + if (len === 1) return superset.includes(subset[0]); + + const supersetSet = new Set(superset); + for (let i = 0; i < len; i++) { + if (!supersetSet.has(subset[i])) return false; + } + return true; +} + +// ============================================================================ +// Implementation 3: Fully inlined with minimal function calls +// ============================================================================ +function isFragmentOf_v3( + schema: SchemaDefinitions, + fragment: SchemaDefinitions, +): boolean { + const schemaTypes = schema.types; + const fragmentTypes = fragment.types; + + // Types check + for (const typeName in fragmentTypes) { + const schemaType = schemaTypes[typeName]; + if (!schemaType) return false; + + const fragmentType = fragmentTypes[typeName]; + const kind = schemaType[0]; + if (kind !== fragmentType[0]) return false; + + if (kind === 2 || kind === 3) { + // OBJECT or INTERFACE + // Check fields + const schemaFields = schemaType[1] as FieldDefinitionRecord; + const fragmentFields = fragmentType[1] as FieldDefinitionRecord; + + for (const fieldName in fragmentFields) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + + const fragmentField = fragmentFields[fieldName]; + const schemaFieldType = Array.isArray(schemaField) + ? schemaField[0] + : schemaField; + const fragmentFieldType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + + if (schemaFieldType !== fragmentFieldType) return false; + + // Check args + const fragmentArgs = Array.isArray(fragmentField) + ? fragmentField[1] + : undefined; + if (fragmentArgs) { + const schemaArgs = Array.isArray(schemaField) + ? schemaField[1] + : undefined; + if (!schemaArgs) return false; + + for (const argName in fragmentArgs) { + const schemaArg = schemaArgs[argName]; + if (schemaArg === undefined) return false; + const sArgType = Array.isArray(schemaArg) + ? schemaArg[0] + : schemaArg; + const fArgType = Array.isArray(fragmentArgs[argName]) + ? fragmentArgs[argName][0] + : fragmentArgs[argName]; + if (sArgType !== fArgType) return false; + } + } + } + + // Check interfaces + const fragIfaces = fragmentType[2] as string[] | undefined; + if (fragIfaces && fragIfaces.length > 0) { + const schemaIfaces = schemaType[2] as string[] | undefined; + if (!schemaIfaces) return false; + const ifaceSet = new Set(schemaIfaces); + for (let i = 0; i < fragIfaces.length; i++) { + if (!ifaceSet.has(fragIfaces[i])) return false; + } + } + } else if (kind === 6) { + // INPUT + const schemaFields = schemaType[1] as InputValueDefinitionRecord; + const fragmentFields = fragmentType[1] as InputValueDefinitionRecord; + + for (const fieldName in fragmentFields) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + const fragmentField = fragmentFields[fieldName]; + const sType = Array.isArray(schemaField) ? schemaField[0] : schemaField; + const fType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + if (sType !== fType) return false; + } + } else if (kind === 4 || kind === 5) { + // UNION or ENUM + const schemaValues = schemaType[1] as string[]; + const fragmentValues = fragmentType[1] as string[]; + const len = schemaValues.length; + if (len !== fragmentValues.length) return false; + if (len > 0) { + const setA = new Set(schemaValues); + for (let i = 0; i < len; i++) { + if (!setA.has(fragmentValues[i])) return false; + } + } + } + // SCALAR (kind === 1) - always true if kind matches + } + + // Directives check + const fragmentDirectives = fragment.directives; + if (fragmentDirectives && fragmentDirectives.length > 0) { + const schemaDirectives = schema.directives; + if (!schemaDirectives) return false; + + // Build directive map + const directiveMap = new Map(); + for (let i = 0; i < schemaDirectives.length; i++) { + directiveMap.set(schemaDirectives[i][0], schemaDirectives[i]); + } + + for (let i = 0; i < fragmentDirectives.length; i++) { + const fragDir = fragmentDirectives[i]; + const schemaDir = directiveMap.get(fragDir[0]); + if (!schemaDir) return false; + + // Check args + const fragmentArgs = fragDir[2]; + if (fragmentArgs) { + const schemaArgs = schemaDir[2]; + if (!schemaArgs) return false; + for (const argName in fragmentArgs) { + const schemaArg = schemaArgs[argName]; + if (schemaArg === undefined) return false; + const sType = Array.isArray(schemaArg) ? schemaArg[0] : schemaArg; + const fType = Array.isArray(fragmentArgs[argName]) + ? fragmentArgs[argName][0] + : fragmentArgs[argName]; + if (sType !== fType) return false; + } + } + + // Check locations + const schemaLocs = schemaDir[1]; + const fragLocs = fragDir[1]; + if (fragLocs.length > 0) { + const locSet = new Set(schemaLocs); + for (let j = 0; j < fragLocs.length; j++) { + if (!locSet.has(fragLocs[j])) return false; + } + } + } + } + + return true; +} + +// ============================================================================ +// Verify all implementations produce same results +// ============================================================================ +console.log("Verifying implementations produce identical results..."); +let allMatch = true; +for (let i = 0; i < fragments.length; i++) { + const r1 = isFragmentOf_v1(fullSchemaDefinitions, fragments[i]); + const r2 = isFragmentOf_v2(fullSchemaDefinitions, fragments[i]); + const r3 = isFragmentOf_v3(fullSchemaDefinitions, fragments[i]); + + if (r1 !== r2 || r2 !== r3) { + console.error(`Mismatch for fragment ${i}: v1=${r1}, v2=${r2}, v3=${r3}`); + allMatch = false; + } +} + +if (!allMatch) { + console.error( + "IMPLEMENTATIONS PRODUCE DIFFERENT RESULTS! Aborting benchmark.", + ); + process.exit(1); +} +console.log("All implementations match!\n"); + +// ============================================================================ +// Run benchmark +// ============================================================================ +const ITERATIONS = 10000; + +const suite = new NiceBenchmark( + `isFragmentOf Benchmark (${ITERATIONS} iterations × ${fragments.length} fragments)`, +); + +suite.add("v1: Original implementation", () => { + for (let iter = 0; iter < ITERATIONS; iter++) { + for (let i = 0; i < fragments.length; i++) { + isFragmentOf_v1(fullSchemaDefinitions, fragments[i]); + } + } +}); + +suite.add("v2: Optimized (for-in, direct access, switch)", () => { + for (let iter = 0; iter < ITERATIONS; iter++) { + for (let i = 0; i < fragments.length; i++) { + isFragmentOf_v2(fullSchemaDefinitions, fragments[i]); + } + } +}); + +suite.add("v3: Fully inlined", () => { + for (let iter = 0; iter < ITERATIONS; iter++) { + for (let i = 0; i < fragments.length; i++) { + isFragmentOf_v3(fullSchemaDefinitions, fragments[i]); + } + } +}); + +// ============================================================================ +// Implementation 4: v2 with Object.keys optimization for small objects +// ============================================================================ +function isFragmentOf_v4( + schema: SchemaDefinitions, + fragment: SchemaDefinitions, +): boolean { + const schemaTypes = schema.types; + const fragmentTypes = fragment.types; + const fragmentTypeNames = Object.keys(fragmentTypes); + + for (let t = 0; t < fragmentTypeNames.length; t++) { + const typeName = fragmentTypeNames[t]; + const schemaType = schemaTypes[typeName]; + if (!schemaType) return false; + if (!isTypeFragmentOf_v4(schemaType, fragmentTypes[typeName])) return false; + } + + const fragmentDirectives = fragment.directives; + if (fragmentDirectives && fragmentDirectives.length > 0) { + const schemaDirectives = schema.directives; + if (!schemaDirectives) return false; + + // Build directive map only if needed + const directiveMap = new Map(); + for (let i = 0; i < schemaDirectives.length; i++) { + directiveMap.set(schemaDirectives[i][0], schemaDirectives[i]); + } + + for (let i = 0; i < fragmentDirectives.length; i++) { + const fragDir = fragmentDirectives[i]; + const schemaDir = directiveMap.get(fragDir[0]); + if (!schemaDir) return false; + if (!isDirectiveFragmentOf_v4(schemaDir, fragDir)) return false; + } + } + + return true; +} + +function isTypeFragmentOf_v4( + schemaType: TypeDefinitionTuple, + fragmentType: TypeDefinitionTuple, +): boolean { + const kind = schemaType[0]; + if (kind !== fragmentType[0]) return false; + + if (kind === 2 || kind === 3) { + // OBJECT or INTERFACE + const schemaFields = schemaType[1] as FieldDefinitionRecord; + const fragmentFields = fragmentType[1] as FieldDefinitionRecord; + const fieldNames = Object.keys(fragmentFields); + + for (let f = 0; f < fieldNames.length; f++) { + const fieldName = fieldNames[f]; + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + + const fragmentField = fragmentFields[fieldName]; + const schemaFieldType = Array.isArray(schemaField) + ? schemaField[0] + : schemaField; + const fragmentFieldType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + + if (schemaFieldType !== fragmentFieldType) return false; + + const fragmentArgs = Array.isArray(fragmentField) + ? fragmentField[1] + : undefined; + if (fragmentArgs) { + const schemaArgs = Array.isArray(schemaField) + ? schemaField[1] + : undefined; + if (!schemaArgs) return false; + + const argNames = Object.keys(fragmentArgs); + for (let a = 0; a < argNames.length; a++) { + const argName = argNames[a]; + const schemaArg = schemaArgs[argName]; + if (schemaArg === undefined) return false; + const sArgType = Array.isArray(schemaArg) ? schemaArg[0] : schemaArg; + const fArgType = Array.isArray(fragmentArgs[argName]) + ? fragmentArgs[argName][0] + : fragmentArgs[argName]; + if (sArgType !== fArgType) return false; + } + } + } + + const fragIfaces = fragmentType[2] as string[] | undefined; + if (fragIfaces && fragIfaces.length > 0) { + const schemaIfaces = schemaType[2] as string[] | undefined; + if (!schemaIfaces) return false; + if (fragIfaces.length === 1) { + if (!schemaIfaces.includes(fragIfaces[0])) return false; + } else { + const ifaceSet = new Set(schemaIfaces); + for (let i = 0; i < fragIfaces.length; i++) { + if (!ifaceSet.has(fragIfaces[i])) return false; + } + } + } + return true; + } + + if (kind === 6) { + // INPUT + const schemaFields = schemaType[1] as InputValueDefinitionRecord; + const fragmentFields = fragmentType[1] as InputValueDefinitionRecord; + const fieldNames = Object.keys(fragmentFields); + + for (let f = 0; f < fieldNames.length; f++) { + const fieldName = fieldNames[f]; + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + const fragmentField = fragmentFields[fieldName]; + const sType = Array.isArray(schemaField) ? schemaField[0] : schemaField; + const fType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + if (sType !== fType) return false; + } + return true; + } + + if (kind === 4 || kind === 5) { + // UNION or ENUM + const schemaValues = schemaType[1] as string[]; + const fragmentValues = fragmentType[1] as string[]; + const len = schemaValues.length; + if (len !== fragmentValues.length) return false; + if (len <= 3) { + // For small arrays, linear search is faster + for (let i = 0; i < len; i++) { + if (!schemaValues.includes(fragmentValues[i])) return false; + } + } else { + const setA = new Set(schemaValues); + for (let i = 0; i < len; i++) { + if (!setA.has(fragmentValues[i])) return false; + } + } + return true; + } + + // SCALAR (kind === 1) + return true; +} + +function isDirectiveFragmentOf_v4( + schemaDirective: DirectiveDefinitionTuple, + fragmentDirective: DirectiveDefinitionTuple, +): boolean { + const fragmentArgs = fragmentDirective[2]; + if (fragmentArgs) { + const schemaArgs = schemaDirective[2]; + if (!schemaArgs) return false; + + const argNames = Object.keys(fragmentArgs); + for (let a = 0; a < argNames.length; a++) { + const argName = argNames[a]; + const schemaArg = schemaArgs[argName]; + if (schemaArg === undefined) return false; + const sType = Array.isArray(schemaArg) ? schemaArg[0] : schemaArg; + const fType = Array.isArray(fragmentArgs[argName]) + ? fragmentArgs[argName][0] + : fragmentArgs[argName]; + if (sType !== fType) return false; + } + } + + const schemaLocs = schemaDirective[1]; + const fragLocs = fragmentDirective[1]; + const locLen = fragLocs.length; + if (locLen === 0) return true; + if (locLen === 1) return schemaLocs.includes(fragLocs[0]); + + const locSet = new Set(schemaLocs); + for (let i = 0; i < locLen; i++) { + if (!locSet.has(fragLocs[i])) return false; + } + return true; +} + +// Verify v4 matches +const v4Results = fragments.map((f) => + isFragmentOf_v4(fullSchemaDefinitions, f), +); +const v1Results = fragments.map((f) => + isFragmentOf_v1(fullSchemaDefinitions, f), +); +if (v4Results.some((r, i) => r !== v1Results[i])) { + console.error("v4 produces different results!"); + process.exit(1); +} +console.log("v4 verified!\n"); + +suite.add("v4: Object.keys + small array optimizations", () => { + for (let iter = 0; iter < ITERATIONS; iter++) { + for (let i = 0; i < fragments.length; i++) { + isFragmentOf_v4(fullSchemaDefinitions, fragments[i]); + } + } +}); + +suite.run().then(() => { + console.log("\nDone!"); +}); diff --git a/packages/supermassive/src/index.ts b/packages/supermassive/src/index.ts index 457cb95fc..8c8e4c953 100644 --- a/packages/supermassive/src/index.ts +++ b/packages/supermassive/src/index.ts @@ -35,6 +35,7 @@ export { mergeSchemaDefinitions, } from "./utilities/mergeSchemaDefinitions"; export { subtractSchemaDefinitions } from "./utilities/subtractSchemaDefinitions"; +export { isFragmentOf } from "./utilities/isFragmentOf"; export { schemaFragmentFromMinimalViableSchemaDocument } from "./utilities/schemaFragmentFromMinimalViableSchemaDocument"; export { pathToArray } from "./jsutils/Path"; export { isPromise } from "./jsutils/isPromise"; diff --git a/packages/supermassive/src/utilities/__tests__/isFragmentOf.test.ts b/packages/supermassive/src/utilities/__tests__/isFragmentOf.test.ts new file mode 100644 index 000000000..4cca4153b --- /dev/null +++ b/packages/supermassive/src/utilities/__tests__/isFragmentOf.test.ts @@ -0,0 +1,1173 @@ +import { isFragmentOf } from "../isFragmentOf"; +import { + SchemaDefinitions, + createObjectTypeDefinition, + createInterfaceTypeDefinition, + createInputObjectTypeDefinition, + createUnionTypeDefinition, + createEnumTypeDefinition, + createScalarTypeDefinition, +} from "../../schema/definition"; + +describe("isFragmentOf", () => { + describe("empty schemas", () => { + it("should return true for two empty schemas", () => { + const schema: SchemaDefinitions = { + types: {}, + }; + + const fragment: SchemaDefinitions = { + types: {}, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return true when fragment is empty and schema is not", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: {}, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when schema is empty and fragment is not", () => { + const schema: SchemaDefinitions = { + types: {}, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("object types", () => { + it("should return true when fragment type is identical to schema type", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return true when fragment has subset of fields", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + email: "String", + age: "Int", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment has field not in schema", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + email: "String", // Not in schema + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment field has different type", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + count: "Int", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + count: "String", // Different type + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return true when fragment has subset of types", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + }), + Post: createObjectTypeDefinition({ + title: "String", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment has type not in schema", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Post: createObjectTypeDefinition({ + title: "String", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("field arguments", () => { + it("should return true when field arguments match exactly", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + offset: "Int", + }, + ], + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + offset: "Int", + }, + ], + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return true when fragment has fewer arguments (subset is valid)", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + offset: "Int", + }, + ], + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + // offset omitted - valid subset + }, + ], + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment has argument not in schema", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + }, + ], + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + offset: "Int", // Not in schema + }, + ], + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment field has arguments but schema field does not", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: "User", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + }, + ], + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return true when schema field has arguments but fragment field does not (empty subset is valid)", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + }, + ], + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: "User", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment argument has different type", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "Int", + }, + ], + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + users: [ + "User", + { + limit: "String", // Different type + }, + ], + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("interface types", () => { + it("should return true when fragment interface is subset of schema interface", () => { + const schema: SchemaDefinitions = { + types: { + Node: createInterfaceTypeDefinition({ + id: "ID", + createdAt: "String", + updatedAt: "String", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Node: createInterfaceTypeDefinition({ + id: "ID", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment interface has field not in schema", () => { + const schema: SchemaDefinitions = { + types: { + Node: createInterfaceTypeDefinition({ + id: "ID", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Node: createInterfaceTypeDefinition({ + id: "ID", + createdAt: "String", // Not in schema + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("interface implementations", () => { + it("should return true when fragment implements subset of interfaces", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition( + { + id: "ID", + name: "String", + }, + ["Node", "Timestamped", "Auditable"], + ), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition( + { + id: "ID", + }, + ["Node", "Timestamped"], + ), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment implements interface not in schema", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition( + { + id: "ID", + }, + ["Node"], + ), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition( + { + id: "ID", + }, + ["Node", "Timestamped"], // Timestamped not in schema + ), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return true when fragment has no interfaces and schema does", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition( + { + id: "ID", + }, + ["Node"], + ), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return true for interface implementing other interfaces", () => { + const schema: SchemaDefinitions = { + types: { + Node: createInterfaceTypeDefinition( + { + id: "ID", + }, + ["Identifiable", "Timestamped"], + ), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Node: createInterfaceTypeDefinition( + { + id: "ID", + }, + ["Identifiable"], + ), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + }); + + describe("input object types", () => { + it("should return true when fragment input has subset of fields", () => { + const schema: SchemaDefinitions = { + types: { + UserInput: createInputObjectTypeDefinition({ + name: "String", + email: "String", + age: "Int", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + UserInput: createInputObjectTypeDefinition({ + name: "String", + email: "String", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment input has field not in schema", () => { + const schema: SchemaDefinitions = { + types: { + UserInput: createInputObjectTypeDefinition({ + name: "String", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + UserInput: createInputObjectTypeDefinition({ + name: "String", + email: "String", // Not in schema + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment input field has different type", () => { + const schema: SchemaDefinitions = { + types: { + UserInput: createInputObjectTypeDefinition({ + age: "Int", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + UserInput: createInputObjectTypeDefinition({ + age: "String", // Different type + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("union types (atomic)", () => { + it("should return true when union types match exactly", () => { + const schema: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post", "Comment"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post", "Comment"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return true when union members are in different order", () => { + const schema: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post", "Comment"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["Comment", "User", "Post"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment union has fewer members", () => { + const schema: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post", "Comment"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment union has more members", () => { + const schema: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post", "Comment"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment union has different members", () => { + const schema: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Post"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + SearchResult: createUnionTypeDefinition(["User", "Comment"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("enum types (atomic)", () => { + it("should return true when enum types match exactly", () => { + const schema: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER", "GUEST"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER", "GUEST"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return true when enum values are in different order", () => { + const schema: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER", "GUEST"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["GUEST", "ADMIN", "USER"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment enum has fewer values", () => { + const schema: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER", "GUEST"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment enum has more values", () => { + const schema: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER", "GUEST"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment enum has different values", () => { + const schema: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "USER"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Role: createEnumTypeDefinition(["ADMIN", "GUEST"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("scalar types", () => { + it("should return true when scalar exists in both", () => { + const schema: SchemaDefinitions = { + types: { + DateTime: createScalarTypeDefinition(), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + DateTime: createScalarTypeDefinition(), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment scalar does not exist in schema", () => { + const schema: SchemaDefinitions = { + types: { + DateTime: createScalarTypeDefinition(), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + UUID: createScalarTypeDefinition(), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("directives", () => { + it("should return true when fragment has no directives and schema does", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [ + ["auth", [4, 12]], // FIELD, FIELD_DEFINITION + ], + }; + + const fragment: SchemaDefinitions = { + types: {}, + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return true when directive exists in both with matching arguments", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "Role", + }, + ], + ], + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "Role", + }, + ], + ], + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment directive does not exist in schema", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [["auth", [4, 12]]], + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [["deprecated", [12]]], + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when fragment has directives but schema does not", () => { + const schema: SchemaDefinitions = { + types: {}, + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [["auth", [4, 12]]], + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return true when fragment directive has fewer arguments (subset is valid)", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "Role", + scopes: "String", + }, + ], + ], + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "Role", + // scopes omitted - valid subset + }, + ], + ], + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment directive has argument not in schema", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "Role", + }, + ], + ], + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "Role", + scopes: "String", // Not in schema + }, + ], + ], + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when directive argument types don't match", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "Role", + }, + ], + ], + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [ + [ + "auth", + [4, 12], + { + requires: "String", // Different type + }, + ], + ], + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return true when fragment directive locations are subset of schema locations", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [["auth", [4, 12, 14]]], // FIELD, FIELD_DEFINITION, INTERFACE + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [["auth", [4, 12]]], // FIELD, FIELD_DEFINITION (subset) + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false when fragment directive has location not in schema", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [["auth", [4, 12]]], // FIELD, FIELD_DEFINITION + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [["auth", [4, 12, 14]]], // FIELD, FIELD_DEFINITION, INTERFACE (14 not in schema) + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return true for directives without arguments", () => { + const schema: SchemaDefinitions = { + types: {}, + directives: [["skip", [4]]], + }; + + const fragment: SchemaDefinitions = { + types: {}, + directives: [["skip", [4]]], + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + }); + + describe("type kind mismatches", () => { + it("should return false when same type name has different kinds (object vs interface)", () => { + const schema: SchemaDefinitions = { + types: { + User: createObjectTypeDefinition({ + id: "ID", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + User: createInterfaceTypeDefinition({ + id: "ID", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when same type name has different kinds (scalar vs object)", () => { + const schema: SchemaDefinitions = { + types: { + DateTime: createScalarTypeDefinition(), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + DateTime: createObjectTypeDefinition({ + value: "String", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should return false when same type name has different kinds (enum vs union)", () => { + const schema: SchemaDefinitions = { + types: { + Result: createEnumTypeDefinition(["SUCCESS", "FAILURE"]), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Result: createUnionTypeDefinition(["Success", "Failure"]), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + }); + + describe("complex scenarios", () => { + it("should validate a realistic schema fragment", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + user: ["User", { id: "ID" }], + users: ["User", { limit: "Int", offset: "Int" }], + post: ["Post", { id: "ID" }], + posts: "Post", + }), + User: createObjectTypeDefinition( + { + id: "ID", + name: "String", + email: "String", + posts: "Post", + }, + ["Node"], + ), + Post: createObjectTypeDefinition( + { + id: "ID", + title: "String", + content: "String", + author: "User", + }, + ["Node"], + ), + Node: createInterfaceTypeDefinition({ + id: "ID", + }), + Role: createEnumTypeDefinition(["ADMIN", "USER", "GUEST"]), + SearchResult: createUnionTypeDefinition(["User", "Post"]), + }, + directives: [ + ["auth", [4, 12], { requires: "Role" }], + ["deprecated", [12]], + ], + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + user: ["User", { id: "ID" }], + }), + User: createObjectTypeDefinition( + { + id: "ID", + name: "String", + }, + ["Node"], + ), + Node: createInterfaceTypeDefinition({ + id: "ID", + }), + Role: createEnumTypeDefinition(["ADMIN", "USER", "GUEST"]), + }, + directives: [["auth", [4], { requires: "Role" }]], + }; + + expect(isFragmentOf(schema, fragment)).toBe(true); + }); + + it("should return false for invalid fragment with missing type", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + user: "User", + }), + User: createObjectTypeDefinition({ + id: "ID", + }), + }, + }; + + const fragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + user: "User", + }), + User: createObjectTypeDefinition({ + id: "ID", + }), + Post: createObjectTypeDefinition({ + // Not in schema + title: "String", + }), + }, + }; + + expect(isFragmentOf(schema, fragment)).toBe(false); + }); + + it("should handle multiple issues in fragment", () => { + const schema: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + user: "User", + }), + User: createObjectTypeDefinition({ + id: "ID", + name: "String", + }), + }, + }; + + // Fragment with field not in schema + const invalidFragment: SchemaDefinitions = { + types: { + Query: createObjectTypeDefinition({ + user: "User", + }), + User: createObjectTypeDefinition({ + id: "ID", + email: "String", // Not in schema + }), + }, + }; + + expect(isFragmentOf(schema, invalidFragment)).toBe(false); + }); + }); +}); diff --git a/packages/supermassive/src/utilities/isFragmentOf.ts b/packages/supermassive/src/utilities/isFragmentOf.ts new file mode 100644 index 000000000..bd3629dc0 --- /dev/null +++ b/packages/supermassive/src/utilities/isFragmentOf.ts @@ -0,0 +1,280 @@ +import { + SchemaDefinitions, + DirectiveDefinitionTuple, + FieldDefinitionRecord, + InputValueDefinitionRecord, + TypeDefinitionTuple, +} from "../schema/definition"; + +// Type kind constants (matching the internal enum values in definition.ts) +const TYPE_KIND_SCALAR = 1; +const TYPE_KIND_OBJECT = 2; +const TYPE_KIND_INTERFACE = 3; +const TYPE_KIND_UNION = 4; +const TYPE_KIND_ENUM = 5; +const TYPE_KIND_INPUT = 6; + +/** + * Checks if `fragment` is a valid fragment of `schema`. + * + * A fragment is valid if any GraphQL operation that could be executed using + * the fragment could also be executed with the full schema. + * + * Rules: + * - Every type in fragment must exist in schema with the same kind + * - Object/Interface types: every field in fragment must exist in schema with matching type; + * field arguments in fragment must be a subset of schema arguments (fragment may omit optional args) + * - Union types: must match exactly (atomic) + * - Enum types: must match exactly (atomic) + * - Input object types: every field in fragment must exist in schema with matching type + * - Scalar types: if fragment has it, schema must have it + * - Directives: fragment's directive must exist in schema, fragment arguments must be a subset + * of schema arguments, and fragment's locations must be a subset of schema's locations + * + * @param schema - The full schema definitions + * @param fragment - The schema fragment to validate + * @returns true if fragment is a valid subset of schema + */ +export function isFragmentOf( + schema: SchemaDefinitions, + fragment: SchemaDefinitions, +): boolean { + const schemaTypes = schema.types; + const fragmentTypes = fragment.types; + + // Check all types in fragment exist in schema + for (const typeName in fragmentTypes) { + const schemaType = schemaTypes[typeName]; + if (!schemaType) return false; + if (!isTypeFragmentOf(schemaType, fragmentTypes[typeName])) return false; + } + + // Check all directives in fragment exist in schema + const fragmentDirectives = fragment.directives; + if (fragmentDirectives && fragmentDirectives.length > 0) { + const schemaDirectives = schema.directives; + if (!schemaDirectives) return false; + + // Build directive lookup map + const directiveMap = new Map(); + for (let i = 0; i < schemaDirectives.length; i++) { + directiveMap.set(schemaDirectives[i][0], schemaDirectives[i]); + } + + for (let i = 0; i < fragmentDirectives.length; i++) { + const fragDir = fragmentDirectives[i]; + const schemaDir = directiveMap.get(fragDir[0]); + if (!schemaDir) return false; + if (!isDirectiveFragmentOf(schemaDir, fragDir)) return false; + } + } + + return true; +} + +/** + * Checks if fragmentType is a valid fragment of schemaType. + */ +function isTypeFragmentOf( + schemaType: TypeDefinitionTuple, + fragmentType: TypeDefinitionTuple, +): boolean { + const kind = schemaType[0]; + + // Types must have the same kind + if (kind !== fragmentType[0]) return false; + + // Use direct kind comparison with switch for better performance + switch (kind) { + case TYPE_KIND_OBJECT: + case TYPE_KIND_INTERFACE: { + // Check fields are a subset + if ( + !isFieldsFragmentOf( + schemaType[1] as FieldDefinitionRecord, + fragmentType[1] as FieldDefinitionRecord, + ) + ) { + return false; + } + // Check interfaces in fragment exist in schema + const schemaIfaces = schemaType[2] as string[] | undefined; + const fragIfaces = fragmentType[2] as string[] | undefined; + if (fragIfaces && fragIfaces.length > 0) { + if (!schemaIfaces) return false; + return isArraySubset(schemaIfaces, fragIfaces); + } + return true; + } + + case TYPE_KIND_INPUT: + return isInputFieldsFragmentOf( + schemaType[1] as InputValueDefinitionRecord, + fragmentType[1] as InputValueDefinitionRecord, + ); + + case TYPE_KIND_UNION: + case TYPE_KIND_ENUM: + // Unions and enums are atomic - must match exactly + return arraysEqual( + schemaType[1] as string[], + fragmentType[1] as string[], + ); + + case TYPE_KIND_SCALAR: + // Scalars match if both exist (already checked kind) + return true; + + default: + return false; + } +} + +/** + * Checks if fragmentFields is a valid fragment of schemaFields. + * Every field in fragment must exist in schema with matching type. + * Fragment arguments must be a subset of schema arguments. + */ +function isFieldsFragmentOf( + schemaFields: FieldDefinitionRecord, + fragmentFields: FieldDefinitionRecord, +): boolean { + for (const fieldName in fragmentFields) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + + const fragmentField = fragmentFields[fieldName]; + + // Check field types match (direct array access for performance) + const schemaType = Array.isArray(schemaField) + ? schemaField[0] + : schemaField; + const fragmentType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + + if (schemaType !== fragmentType) return false; + + // Check fragment arguments are a subset of schema arguments + const fragmentArgs = Array.isArray(fragmentField) + ? fragmentField[1] + : undefined; + if (fragmentArgs) { + const schemaArgs = Array.isArray(schemaField) + ? schemaField[1] + : undefined; + if (!schemaArgs) return false; + if (!isInputValuesSubset(schemaArgs, fragmentArgs)) return false; + } + } + return true; +} + +/** + * Checks if fragmentFields is a valid fragment of schemaFields for input objects. + * Every field in fragment must exist in schema with matching type. + */ +function isInputFieldsFragmentOf( + schemaFields: InputValueDefinitionRecord, + fragmentFields: InputValueDefinitionRecord, +): boolean { + for (const fieldName in fragmentFields) { + const schemaField = schemaFields[fieldName]; + if (schemaField === undefined) return false; + + const fragmentField = fragmentFields[fieldName]; + + // Check field types match (direct array access for performance) + const schemaType = Array.isArray(schemaField) + ? schemaField[0] + : schemaField; + const fragmentType = Array.isArray(fragmentField) + ? fragmentField[0] + : fragmentField; + + if (schemaType !== fragmentType) return false; + } + return true; +} + +/** + * Checks if fragmentDirective is a valid fragment of schemaDirective. + * Fragment arguments must be a subset of schema arguments, locations must be a subset. + */ +function isDirectiveFragmentOf( + schemaDirective: DirectiveDefinitionTuple, + fragmentDirective: DirectiveDefinitionTuple, +): boolean { + // Check fragment arguments are a subset of schema arguments + const fragmentArgs = fragmentDirective[2]; + if (fragmentArgs) { + const schemaArgs = schemaDirective[2]; + if (!schemaArgs) return false; + if (!isInputValuesSubset(schemaArgs, fragmentArgs)) return false; + } + + // Check fragment locations are a subset of schema locations + return isArraySubset(schemaDirective[1], fragmentDirective[1]); +} + +/** + * Checks if fragment arguments are a subset of schema arguments. + * Every argument in fragment must exist in schema with matching type. + */ +function isInputValuesSubset( + schemaArgs: InputValueDefinitionRecord, + fragmentArgs: InputValueDefinitionRecord, +): boolean { + for (const argName in fragmentArgs) { + const schemaArg = schemaArgs[argName]; + if (schemaArg === undefined) return false; + + const fragmentArg = fragmentArgs[argName]; + + // Check argument types match (direct array access for performance) + const schemaType = Array.isArray(schemaArg) ? schemaArg[0] : schemaArg; + const fragmentType = Array.isArray(fragmentArg) + ? fragmentArg[0] + : fragmentArg; + + if (schemaType !== fragmentType) return false; + } + return true; +} + +/** + * Checks if two arrays contain the same elements (order-independent). + * Optimized for small arrays typical in GraphQL schemas. + */ +function arraysEqual(a: T[], b: T[]): boolean { + const len = a.length; + if (len !== b.length) return false; + if (len === 0) return true; + if (len === 1) return a[0] === b[0]; + if (len === 2) { + return (a[0] === b[0] && a[1] === b[1]) || (a[0] === b[1] && a[1] === b[0]); + } + + // For larger arrays, use Set-based comparison (no sorting allocation) + const setA = new Set(a); + for (let i = 0; i < len; i++) { + if (!setA.has(b[i])) return false; + } + return true; +} + +/** + * Checks if all elements in `subset` exist in `superset`. + * Optimized for small arrays typical in GraphQL schemas. + */ +function isArraySubset(superset: T[], subset: T[]): boolean { + const len = subset.length; + if (len === 0) return true; + if (len === 1) return superset.includes(subset[0]); + + const supersetSet = new Set(superset); + for (let i = 0; i < len; i++) { + if (!supersetSet.has(subset[i])) return false; + } + return true; +} From b3f080105381ef6ffa0552888e912c822b5756ef Mon Sep 17 00:00:00 2001 From: vrazuvaev Date: Mon, 5 Jan 2026 10:59:41 +0100 Subject: [PATCH 2/2] Change files --- ...-supermassive-2aeb66d6-4462-4140-a223-81e4bc279a5e.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-supermassive-2aeb66d6-4462-4140-a223-81e4bc279a5e.json diff --git a/change/@graphitation-supermassive-2aeb66d6-4462-4140-a223-81e4bc279a5e.json b/change/@graphitation-supermassive-2aeb66d6-4462-4140-a223-81e4bc279a5e.json new file mode 100644 index 000000000..b2dae1ed3 --- /dev/null +++ b/change/@graphitation-supermassive-2aeb66d6-4462-4140-a223-81e4bc279a5e.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat(supermassive): isFragmentOf utility", + "packageName": "@graphitation/supermassive", + "email": "vrazuvaev@microsoft.com_msteamsmdb", + "dependentChangeType": "patch" +}