diff --git a/README.md b/README.md index a010c57..2d496a2 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Use the `inherits` directive to define a type or input object that inherits from // directive @inherits(type: String!) on OBJECT | INPUT_OBJECT + // -------------------------------------------------------- + // type Pet { id: ID @@ -51,6 +53,8 @@ Use the `inherits` directive to define a type or input object that inherits from breed: String } + // -------------------------------------------------------- + // input PetInput { name: String @@ -60,4 +64,36 @@ Use the `inherits` directive to define a type or input object that inherits from input DogInput @inherits(type: "PetInput") { breed: String } + + // -------------------------------------------------------- + + // + // For example from Object to Input type: + + type UserBase { + name: String + age: Int + } + input UserInput @inherits(type: "UserBase") { + email: String! + } + +``` + +#### Note! + +> Circular inheritances are not supported and will cause an `RangeError: Maximum call stack size exceeded`. + +### [License](./LICENSE) + +``` + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ``` diff --git a/example/graphql.schema.json b/example/graphql.schema.json index 07f1098..53180fd 100644 --- a/example/graphql.schema.json +++ b/example/graphql.schema.json @@ -122,6 +122,16 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "ID", @@ -144,8 +154,91 @@ }, { "kind": "OBJECT", - "name": "Pet", + "name": "MyAwesomePoodle", "description": null, + "fields": [ + { + "name": "age", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "awesomeName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "breed", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hairLength", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Pet", + "description": "---------------------------------------------------------------\nType definitions.\n---------------------------------------------------------------", "fields": [ { "name": "age", @@ -192,6 +285,112 @@ { "kind": "INPUT_OBJECT", "name": "PetInput", + "description": "---------------------------------------------------------------\nInput definitions.\n---------------------------------------------------------------", + "fields": null, + "inputFields": [ + { + "name": "age", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Poodle", + "description": null, + "fields": [ + { + "name": "age", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "breed", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hairLength", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PoodleInput", "description": null, "fields": null, "inputFields": [ @@ -207,6 +406,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "breed", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hairLength", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "name", "description": null, @@ -250,7 +473,7 @@ { "kind": "OBJECT", "name": "Status", - "description": null, + "description": "---------------------------------------------------------------", "fields": [ { "name": "status", @@ -280,6 +503,92 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "UserBase", + "description": "---------------------------------------------------------------\nCross type inherits\n---------------------------------------------------------------", + "fields": [ + { + "name": "age", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UserInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "age", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "__Directive", diff --git a/example/src/__generated__/graphql.ts b/example/src/__generated__/graphql.ts index de868ea..8a9f056 100644 --- a/example/src/__generated__/graphql.ts +++ b/example/src/__generated__/graphql.ts @@ -27,6 +27,21 @@ export type DogInput = { name?: InputMaybe; }; +export type MyAwesomePoodle = { + __typename?: 'MyAwesomePoodle'; + age?: Maybe; + awesomeName?: Maybe; + breed?: Maybe; + hairLength?: Maybe; + id?: Maybe; + name?: Maybe; +}; + +/** + * --------------------------------------------------------------- + * Type definitions. + * --------------------------------------------------------------- + */ export type Pet = { __typename?: 'Pet'; age?: Maybe; @@ -34,21 +49,60 @@ export type Pet = { name?: Maybe; }; +/** + * --------------------------------------------------------------- + * Input definitions. + * --------------------------------------------------------------- + */ export type PetInput = { age?: InputMaybe; name?: InputMaybe; }; +export type Poodle = { + __typename?: 'Poodle'; + age?: Maybe; + breed?: Maybe; + hairLength?: Maybe; + id?: Maybe; + name?: Maybe; +}; + +export type PoodleInput = { + age?: InputMaybe; + breed?: InputMaybe; + hairLength?: InputMaybe; + name?: InputMaybe; +}; + export type Query = { __typename?: 'Query'; test?: Maybe; }; +/** --------------------------------------------------------------- */ export type Status = { __typename?: 'Status'; status?: Maybe; }; +/** + * --------------------------------------------------------------- + * Cross type inherits + * --------------------------------------------------------------- + */ +export type UserBase = { + __typename?: 'UserBase'; + age?: Maybe; + name?: Maybe; +}; + +export type UserInput = { + age?: InputMaybe; + email: Scalars['String']; + name?: InputMaybe; +}; + export type ResolverTypeWrapper = Promise | T; @@ -123,13 +177,19 @@ export type ResolversTypes = { Boolean: ResolverTypeWrapper; Dog: ResolverTypeWrapper; DogInput: DogInput; + Float: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; + MyAwesomePoodle: ResolverTypeWrapper; Pet: ResolverTypeWrapper; PetInput: PetInput; + Poodle: ResolverTypeWrapper; + PoodleInput: PoodleInput; Query: ResolverTypeWrapper<{}>; Status: ResolverTypeWrapper; String: ResolverTypeWrapper; + UserBase: ResolverTypeWrapper; + UserInput: UserInput; }; /** Mapping between all available schema types and the resolvers parents */ @@ -137,13 +197,19 @@ export type ResolversParentTypes = { Boolean: Scalars['Boolean']; Dog: Dog; DogInput: DogInput; + Float: Scalars['Float']; ID: Scalars['ID']; Int: Scalars['Int']; + MyAwesomePoodle: MyAwesomePoodle; Pet: Pet; PetInput: PetInput; + Poodle: Poodle; + PoodleInput: PoodleInput; Query: {}; Status: Status; String: Scalars['String']; + UserBase: UserBase; + UserInput: UserInput; }; export type InheritsDirectiveArgs = { @@ -160,6 +226,16 @@ export type DogResolvers; }; +export type MyAwesomePoodleResolvers = { + age?: Resolver, ParentType, ContextType>; + awesomeName?: Resolver, ParentType, ContextType>; + breed?: Resolver, ParentType, ContextType>; + hairLength?: Resolver, ParentType, ContextType>; + id?: Resolver, ParentType, ContextType>; + name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type PetResolvers = { age?: Resolver, ParentType, ContextType>; id?: Resolver, ParentType, ContextType>; @@ -167,6 +243,15 @@ export type PetResolvers; }; +export type PoodleResolvers = { + age?: Resolver, ParentType, ContextType>; + breed?: Resolver, ParentType, ContextType>; + hairLength?: Resolver, ParentType, ContextType>; + id?: Resolver, ParentType, ContextType>; + name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type QueryResolvers = { test?: Resolver, ParentType, ContextType>; }; @@ -176,11 +261,20 @@ export type StatusResolvers; }; +export type UserBaseResolvers = { + age?: Resolver, ParentType, ContextType>; + name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { Dog?: DogResolvers; + MyAwesomePoodle?: MyAwesomePoodleResolvers; Pet?: PetResolvers; + Poodle?: PoodleResolvers; Query?: QueryResolvers; Status?: StatusResolvers; + UserBase?: UserBaseResolvers; }; export type DirectiveResolvers = { diff --git a/example/src/schema/schema.ts b/example/src/schema/schema.ts index 1ae7094..1a270fa 100644 --- a/example/src/schema/schema.ts +++ b/example/src/schema/schema.ts @@ -7,15 +7,35 @@ export default makeExecutableSchema({ gql` directive @inherits(type: String!) on OBJECT | INPUT_OBJECT + """ + --------------------------------------------------------------- + Type definitions. + --------------------------------------------------------------- + """ type Pet { id: ID name: String age: Int } + type Dog @inherits(type: "Pet") { breed: String } + type Poodle @inherits(type: "Dog") { + hairLength: Int + age: Float + } + + type MyAwesomePoodle @inherits(type: "Poodle") { + awesomeName: String + } + + """ + --------------------------------------------------------------- + Input definitions. + --------------------------------------------------------------- + """ input PetInput { name: String age: Int @@ -25,6 +45,26 @@ export default makeExecutableSchema({ breed: String } + input PoodleInput @inherits(type: "DogInput") { + hairLength: Int + } + + """ + --------------------------------------------------------------- + Cross type inherits + --------------------------------------------------------------- + """ + type UserBase { + name: String + age: Int + } + input UserInput @inherits(type: "UserBase") { + email: String! + } + + """ + --------------------------------------------------------------- + """ type Status { status: String } diff --git a/src/index.ts b/src/index.ts index 46ca740..cf2ef39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { mapSchema, getDirective, MapperKind } from "@graphql-tools/utils"; +import { mapSchema, MapperKind } from "@graphql-tools/utils"; import { GraphQLSchema, GraphQLObjectType, diff --git a/src/utils.ts b/src/utils.ts index dfe2a7d..77b5a2b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,12 @@ -import { mapSchema, getDirective, MapperKind } from "@graphql-tools/utils"; +import { getDirective } from "@graphql-tools/utils"; import { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType, + InputObjectTypeDefinitionNode, + ObjectTypeDefinitionNode, + ConstDirectiveNode, + StringValueNode, } from "graphql"; type BaseType = GraphQLObjectType | GraphQLInputObjectType; @@ -16,14 +20,63 @@ export function getFieldConfig( const directive = getDirective(schema, fieldConfig, directiveName)?.[0]; if (directive) { const { type } = directive; + // current type files const fields = fieldConfig.getFields(); - const baseType = schema.getTypeMap()[type] as T; - Object.entries(baseType.getFields()).forEach(([name, field]) => { - if (fields[name] === undefined) { - fields[name] = { ...field }; - } + const nestedASTs = collectNestedASTs(schema, type, directiveName); + const types = nestedASTs.map((t) => schema.getTypeMap()[t] as T); + types.forEach((objType) => { + Object.entries(objType.getFields()).forEach(([name, field]) => { + if (fields[name] === undefined) { + fields[name] = { ...field }; + } + }); }); - return fieldConfig as T; } } + +/** + * Method to get the nested Types in the syntax tree. + * @param schema + * @param type Graphql type to lookup + * @param directiveName + * @returns Array of strings with the chained types. + */ +function collectNestedASTs( + schema: GraphQLSchema, + type: string, + directiveName: string +): string[] { + const baseType = schema.getTypeMap()[type] as T; + // check the Abstract Syntax Tree for inheritances. + const directive = filterInterestedDirectives(directiveName, baseType.astNode); + const nestedType = directive?.arguments?.map( + (arg) => (arg.value as StringValueNode)?.value + ); + + // end recursive lookup + if (!nestedType || nestedType.length === 0) return [type]; + + return [type].concat( + ...nestedType.map((t) => collectNestedASTs(schema, t, directiveName)) + ); +} + +/** + * Filter the Abstract Syntax Tree (AST) for inheritance directives. + * @param directiveName Specified directive name + * @param astNode Current type Abstract Syntax Tree + * @returns inheritance Directive + */ +function filterInterestedDirectives( + directiveName: string, + astNode?: + | InputObjectTypeDefinitionNode + | ObjectTypeDefinitionNode + | null + | undefined +): ConstDirectiveNode | undefined { + return astNode?.directives?.find( + (directive) => directive.name.value === directiveName + ); +}