diff --git a/.changeset/wicked-starfishes-join.md b/.changeset/wicked-starfishes-join.md new file mode 100644 index 0000000..7f9a482 --- /dev/null +++ b/.changeset/wicked-starfishes-join.md @@ -0,0 +1,7 @@ +--- +"objection-graphql-resolver": minor +"orchid-graphql": minor +"graphql-orm": minor +--- + +Inline graphql-parse-resolve-info to prevent false peer dependency error in dependent packages. diff --git a/packages/graphql-orm/package.json b/packages/graphql-orm/package.json index 53861da..4e6c8bd 100644 --- a/packages/graphql-orm/package.json +++ b/packages/graphql-orm/package.json @@ -6,13 +6,11 @@ "author": "Ilya Semenov", "license": "MIT", "type": "module", - "dependencies": { - "graphql-parse-resolve-info": "^4.12.0" - }, "peerDependencies": { "graphql": "^16" }, "devDependencies": { + "@types/node": "^20.11.20", "tsconfig-vite-node": "^1.1.0" } } diff --git a/packages/graphql-orm/src/resolvers/graph.ts b/packages/graphql-orm/src/resolvers/graph.ts index 1726ab3..5688fdd 100644 --- a/packages/graphql-orm/src/resolvers/graph.ts +++ b/packages/graphql-orm/src/resolvers/graph.ts @@ -1,9 +1,12 @@ import type { GraphQLResolveInfo } from "graphql" -import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info" import { FiltersDef } from "../filters/filters" import { OrmAdapter } from "../orm/orm" import { Paginator } from "../paginators/base" +import { + parseResolveInfo, + type ResolveTree, +} from "../utils/graphql-parse-resolve-info" import type { TableResolver, TableResolverOptions } from "./table" export type GraphResolverOptions = Pick< diff --git a/packages/graphql-orm/src/utils/graphql-parse-resolve-info-debug.ts b/packages/graphql-orm/src/utils/graphql-parse-resolve-info-debug.ts new file mode 100644 index 0000000..271816a --- /dev/null +++ b/packages/graphql-orm/src/utils/graphql-parse-resolve-info-debug.ts @@ -0,0 +1,13 @@ +// Mimic npm debug (but don't actually have it as a dependency) + +interface DebugFn { + (message: string, ...args: any[]): void + enabled: boolean +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function debugFactory(name: string): DebugFn { + function debug() {} + debug.enabled = false + return debug +} diff --git a/packages/graphql-orm/src/utils/graphql-parse-resolve-info.ts b/packages/graphql-orm/src/utils/graphql-parse-resolve-info.ts new file mode 100644 index 0000000..cbb5ec8 --- /dev/null +++ b/packages/graphql-orm/src/utils/graphql-parse-resolve-info.ts @@ -0,0 +1,416 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// +// Copy of graphql-parse-resolve-info. +// +// Copy/paste from https://github.com/graphile/graphile-engine/blob/9d6c29e3505844ca64020fb6850093a7678a0fa4/packages/graphql-parse-resolve-info/src/index.ts +// with import fixes. +// +// The original graphql-parse-resolve-info lists graphql 14 as a peer dependency. +// Copy/pasting it here prevents unmet peer dependency errors in projects using graphql-orm. +// +// Uses fake debug instead of npm debug (via tsconfig.paths) + +import assert from "node:assert" + +import { + ASTNode, + FieldNode, + FragmentSpreadNode, + getNamedType, + GraphQLCompositeType, + GraphQLField, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLResolveInfo, + GraphQLType, + GraphQLUnionType, + InlineFragmentNode, + isCompositeType, + NamedTypeNode, + SelectionNode, +} from "graphql" +import { getArgumentValues } from "graphql" + +import debugFactory from "./graphql-parse-resolve-info-debug" + +export interface FieldsByTypeName { + [str: string]: { + [str: string]: ResolveTree + } +} + +export interface ResolveTree { + name: string + alias: string + args: { + [str: string]: unknown + } + fieldsByTypeName: FieldsByTypeName +} + +const debug = debugFactory("graphql-parse-resolve-info") + +const DEBUG_ENABLED = debug.enabled + +function getArgVal(resolveInfo: GraphQLResolveInfo, argument: any) { + if (argument.kind === "Variable") { + return resolveInfo.variableValues[argument.name.value] + } else if (argument.kind === "BooleanValue") { + return argument.value + } +} + +function argNameIsIf(arg: any): boolean { + return arg && arg.name ? arg.name.value === "if" : false +} + +function skipField( + resolveInfo: GraphQLResolveInfo, + { directives = [] }: SelectionNode, +) { + let skip = false + directives.forEach((directive) => { + const directiveName = directive.name.value + if (Array.isArray(directive.arguments)) { + const ifArgumentAst = directive.arguments.find(argNameIsIf) + if (ifArgumentAst) { + const argumentValueAst = ifArgumentAst.value + if (directiveName === "skip") { + skip = skip || getArgVal(resolveInfo, argumentValueAst) + } else if (directiveName === "include") { + skip = skip || !getArgVal(resolveInfo, argumentValueAst) + } + } + } + }) + return skip +} + +// Originally based on https://github.com/tjmehta/graphql-parse-fields + +export function getAliasFromResolveInfo( + resolveInfo: GraphQLResolveInfo, +): string { + const asts: ReadonlyArray = + // @ts-ignore Property 'fieldASTs' does not exist on type 'GraphQLResolveInfo'. + resolveInfo.fieldNodes || resolveInfo.fieldASTs + for (let i = 0, l = asts.length; i < l; i++) { + const val = asts[i] + if (val.kind === "Field") { + const alias = val.alias ? val.alias.value : val.name && val.name.value + if (alias) { + return alias + } + } + } + throw new Error("Could not determine alias?!") +} + +export interface ParseOptions { + keepRoot?: boolean + deep?: boolean + forceParse?: boolean +} + +export function parseResolveInfo( + resolveInfo: GraphQLResolveInfo, +): ResolveTree | null +export function parseResolveInfo( + resolveInfo: GraphQLResolveInfo, + forceParse: true, +): ResolveTree +export function parseResolveInfo( + resolveInfo: GraphQLResolveInfo, + options: ParseOptions, +): ResolveTree | FieldsByTypeName | null | void +export function parseResolveInfo( + resolveInfo: GraphQLResolveInfo, + inOptions: ParseOptions | true = {}, +): ResolveTree | FieldsByTypeName | null | void { + const fieldNodes: ReadonlyArray = + // @ts-ignore Property 'fieldASTs' does not exist on type 'GraphQLResolveInfo'. + resolveInfo.fieldNodes || resolveInfo.fieldASTs + const options = inOptions === true ? {} : inOptions + const forceParse = inOptions === true || inOptions.forceParse + + const { parentType } = resolveInfo + if (!fieldNodes) { + throw new Error("No fieldNodes provided!") + } + if (options.keepRoot == null) { + options.keepRoot = false + } + if (options.deep == null) { + options.deep = true + } + const tree = fieldTreeFromAST( + fieldNodes, + resolveInfo, + undefined, + options, + parentType, + ) + if (!options.keepRoot) { + const typeKey = firstKey(tree) + if (!typeKey) { + if (forceParse) { + throw new Error( + `GraphQL schema issue: simplified parseResolveInfo failed (tree had no keys); perhaps you need to use the keepRoot option?`, + ) + } + return null + } + const fields = tree[typeKey] + const fieldKey = firstKey(fields) + if (!fieldKey) { + if (forceParse) { + throw new Error( + `GraphQL schema issue: simplified parseResolveInfo failed (could not get key from fields); perhaps you need to use the keepRoot option?`, + ) + } + return null + } + return fields[fieldKey] + } + return tree +} + +function getFieldFromAST( + ast: ASTNode, + parentType: GraphQLCompositeType, +): GraphQLField | undefined { + if (ast.kind === "Field") { + const fieldNode: FieldNode = ast + const fieldName = fieldNode.name.value + if (!(parentType instanceof GraphQLUnionType)) { + const type: GraphQLObjectType | GraphQLInterfaceType = parentType + return type.getFields()[fieldName] + } else { + // XXX: TODO: Handle GraphQLUnionType + } + } + return undefined +} + +let iNum = 1 +function fieldTreeFromAST( + inASTs: ReadonlyArray | T, + resolveInfo: GraphQLResolveInfo, + initTree: FieldsByTypeName = {}, + options: ParseOptions = {}, + parentType: GraphQLCompositeType, + depth = "", +): FieldsByTypeName { + const instance = iNum++ + if (DEBUG_ENABLED) + debug( + "%s[%d] Entering fieldTreeFromAST with parent type '%s'", + depth, + instance, + parentType, + ) + const { variableValues } = resolveInfo + const fragments = resolveInfo.fragments || {} + const asts: ReadonlyArray = Array.isArray(inASTs) ? inASTs : [inASTs] + if (!initTree[parentType.name]) { + initTree[parentType.name] = {} + } + const outerDepth = depth + return asts.reduce((tree, selectionVal: SelectionNode, idx) => { + const depth = DEBUG_ENABLED ? `${outerDepth} ` : null + if (DEBUG_ENABLED) + debug( + "%s[%d] Processing AST %d of %d; kind = %s", + depth, + instance, + idx + 1, + asts.length, + selectionVal.kind, + ) + if (skipField(resolveInfo, selectionVal)) { + if (DEBUG_ENABLED) + debug("%s[%d] IGNORING due to directive", depth, instance) + } else if (selectionVal.kind === "Field") { + const val: FieldNode = selectionVal + const name = val.name.value + const isReserved = name[0] === "_" && name[1] === "_" && name !== "__id" + if (isReserved) { + if (DEBUG_ENABLED) + debug( + "%s[%d] IGNORING because field '%s' is reserved", + depth, + instance, + name, + ) + } else { + const alias: string = + val.alias && val.alias.value ? val.alias.value : name + if (DEBUG_ENABLED) + debug( + "%s[%d] Field '%s' (alias = '%s')", + depth, + instance, + name, + alias, + ) + const field = getFieldFromAST(val, parentType) + if (field == null) { + return tree + } + const fieldGqlTypeOrUndefined = getNamedType(field.type) + if (!fieldGqlTypeOrUndefined) { + return tree + } + const fieldGqlType: GraphQLNamedType = fieldGqlTypeOrUndefined + const args = + getArgumentValues( + field as GraphQLField, + val, + variableValues, + ) || {} + if (parentType.name && !tree[parentType.name][alias]) { + const newTreeRoot: ResolveTree = { + name, + alias, + args, + fieldsByTypeName: isCompositeType(fieldGqlType) + ? { + [fieldGqlType.name]: {}, + } + : {}, + } + tree[parentType.name][alias] = newTreeRoot + } + const selectionSet = val.selectionSet + if ( + selectionSet != null && + options.deep && + isCompositeType(fieldGqlType) + ) { + const newParentType: GraphQLCompositeType = fieldGqlType + if (DEBUG_ENABLED) + debug("%s[%d] Recursing into subfields", depth, instance) + fieldTreeFromAST( + selectionSet.selections, + resolveInfo, + tree[parentType.name][alias].fieldsByTypeName, + options, + newParentType, + `${depth} `, + ) + } else { + // No fields to add + if (DEBUG_ENABLED) + debug("%s[%d] Exiting (no fields to add)", depth, instance) + } + } + } else if (selectionVal.kind === "FragmentSpread" && options.deep) { + const val: FragmentSpreadNode = selectionVal + const name = val.name && val.name.value + if (DEBUG_ENABLED) + debug("%s[%d] Fragment spread '%s'", depth, instance, name) + const fragment = fragments[name] + assert(fragment, 'unknown fragment "' + name + '"') + let fragmentType: GraphQLNamedType | null | undefined = parentType + if (fragment.typeCondition) { + fragmentType = getType(resolveInfo, fragment.typeCondition) + } + if (fragmentType && isCompositeType(fragmentType)) { + const newParentType: GraphQLCompositeType = fragmentType + fieldTreeFromAST( + fragment.selectionSet.selections, + resolveInfo, + tree, + options, + newParentType, + `${depth} `, + ) + } + } else if (selectionVal.kind === "InlineFragment" && options.deep) { + const val: InlineFragmentNode = selectionVal + const fragment = val + let fragmentType: GraphQLNamedType | null | undefined = parentType + if (fragment.typeCondition) { + fragmentType = getType(resolveInfo, fragment.typeCondition) + } + if (DEBUG_ENABLED) + debug( + "%s[%d] Inline fragment (parent = '%s', type = '%s')", + depth, + instance, + parentType, + fragmentType, + ) + if (fragmentType && isCompositeType(fragmentType)) { + const newParentType: GraphQLCompositeType = fragmentType + fieldTreeFromAST( + fragment.selectionSet.selections, + resolveInfo, + tree, + options, + newParentType, + `${depth} `, + ) + } + } else { + if (DEBUG_ENABLED) + debug( + "%s[%d] IGNORING because kind '%s' not understood", + depth, + instance, + selectionVal.kind, + ) + } + // Ref: https://github.com/graphile/postgraphile/pull/342/files#diff-d6702ec9fed755c88b9d70b430fda4d8R148 + return tree + }, initTree) +} + +const hasOwnProperty = Object.prototype.hasOwnProperty +function firstKey(obj: object) { + for (const key in obj) { + if (hasOwnProperty.call(obj, key)) { + return key + } + } +} + +function getType( + resolveInfo: GraphQLResolveInfo, + typeCondition: NamedTypeNode, +) { + const { schema } = resolveInfo + const { kind, name } = typeCondition + if (kind === "NamedType") { + const typeName = name.value + return schema.getType(typeName) + } +} + +export function simplifyParsedResolveInfoFragmentWithType( + parsedResolveInfoFragment: ResolveTree, + type: GraphQLType, +) { + const { fieldsByTypeName } = parsedResolveInfoFragment + const fields = {} + const strippedType = getNamedType(type) + if (isCompositeType(strippedType)) { + Object.assign(fields, fieldsByTypeName[strippedType.name]) + if (strippedType instanceof GraphQLObjectType) { + const objectType: GraphQLObjectType = strippedType + // GraphQL ensures that the subfields cannot clash, so it's safe to simply overwrite them + for (const anInterface of objectType.getInterfaces()) { + Object.assign(fields, fieldsByTypeName[anInterface.name]) + } + } + } + return { + ...parsedResolveInfoFragment, + fields, + } +} + +export const parse = parseResolveInfo +export const simplify = simplifyParsedResolveInfoFragmentWithType +export const getAlias = getAliasFromResolveInfo diff --git a/packages/objection-graphql-resolver/package.json b/packages/objection-graphql-resolver/package.json index 4d7bf87..3b01051 100644 --- a/packages/objection-graphql-resolver/package.json +++ b/packages/objection-graphql-resolver/package.json @@ -26,9 +26,6 @@ "test": "vitest run", "prepublishOnly": "pnpm build" }, - "dependencies": { - "graphql-parse-resolve-info": "^4.12.0" - }, "peerDependencies": { "graphql": "^16", "objection": "^2 || ^3" diff --git a/packages/orchid-graphql/package.json b/packages/orchid-graphql/package.json index d5b52ab..50d475a 100644 --- a/packages/orchid-graphql/package.json +++ b/packages/orchid-graphql/package.json @@ -26,9 +26,6 @@ "test": "vitest run", "prepublishOnly": "pnpm build" }, - "dependencies": { - "graphql-parse-resolve-info": "^4.12.0" - }, "peerDependencies": { "graphql": "^16", "orchid-orm": "^1.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f18aa9..a7e8f5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,19 +44,15 @@ importers: graphql: specifier: ^16 version: 16.6.0 - graphql-parse-resolve-info: - specifier: ^4.12.0 - version: 4.12.0(graphql@16.6.0) devDependencies: + '@types/node': + specifier: ^20.11.20 + version: 20.11.20 tsconfig-vite-node: specifier: ^1.1.0 version: 1.1.0 packages/objection-graphql-resolver: - dependencies: - graphql-parse-resolve-info: - specifier: ^4.12.0 - version: 4.12.0(graphql@16.6.0) devDependencies: '@apollo/server': specifier: ~4.7.1 @@ -102,10 +98,6 @@ importers: version: 1.3.1 packages/orchid-graphql: - dependencies: - graphql-parse-resolve-info: - specifier: ^4.12.0 - version: 4.12.0(graphql@16.6.0) devDependencies: '@apollo/server': specifier: ~4.7.1 @@ -1146,13 +1138,13 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.2.5 + '@types/node': 20.11.20 dev: true /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.11.20 dev: true /@types/estree@1.0.5: @@ -1162,7 +1154,7 @@ packages: /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.11.20 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 @@ -1200,7 +1192,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.11.20 form-data: 3.0.1 dev: true @@ -1208,8 +1200,10 @@ packages: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true - /@types/node@20.2.5: - resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} + /@types/node@20.11.20: + resolution: {integrity: sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==} + dependencies: + undici-types: 5.26.5 dev: true /@types/normalize-package-data@2.4.1: @@ -1219,7 +1213,7 @@ packages: /@types/pg@8.10.1: resolution: {integrity: sha512-AmEHA/XxMxemQom5iDwP62FYNkv+gDDnetRG7v2N2dPtju7UKI7FknUimcZo7SodKTHtckYPzaTqUEvUKbVJEA==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.11.20 pg-protocol: 1.6.0 pg-types: 4.0.1 dev: true @@ -1240,14 +1234,14 @@ packages: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 20.2.5 + '@types/node': 20.11.20 dev: true /@types/serve-static@1.15.1: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.2.5 + '@types/node': 20.11.20 dev: true /@typescript-eslint/eslint-plugin@7.0.2(@typescript-eslint/parser@7.0.2)(eslint@8.56.0)(typescript@5.3.3): @@ -2087,6 +2081,7 @@ packages: optional: true dependencies: ms: 2.1.2 + dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -2922,19 +2917,6 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /graphql-parse-resolve-info@4.12.0(graphql@16.6.0): - resolution: {integrity: sha512-sQyJeWCzFQwLj8SdgrWeAQG46Nc+VLxof91/AtvEVdbvFCvb+S6OoA4OtIp5OpWBrFo+JzW6LIKifNHXtRKPpA==} - engines: {node: '>=8.6'} - peerDependencies: - graphql: '>=0.9 <0.14 || ^14.0.2 || ^15.4.0' - dependencies: - debug: 4.3.4 - graphql: 16.6.0 - tslib: 2.6.2 - transitivePeerDependencies: - - supports-color - dev: false - /graphql-request@6.0.0(graphql@16.6.0): resolution: {integrity: sha512-2BmHTuglonjZvmNVw6ZzCfFlW/qkIPds0f+Qdi/Lvjsl3whJg2uvHmSvHnLWhUTEw6zcxPYAHiZoPvSVKOZ7Jw==} peerDependencies: @@ -3868,6 +3850,7 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5303,6 +5286,7 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true /tsup@8.0.2(typescript@5.3.3): resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==} @@ -5447,6 +5431,10 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + /unique-filename@1.1.1: resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} requiresBuild: true