Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(handler-graphql): add support for resolver decoration #4199

Merged
merged 4 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/api-headless-cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.0",
"@graphql-tools/schema": "^7.1.2",
"@graphql-tools/schema": "^10.0.4",
"@webiny/api": "0.0.0",
"@webiny/api-i18n": "0.0.0",
"@webiny/api-security": "0.0.0",
Expand Down
27 changes: 18 additions & 9 deletions packages/api-headless-cms/src/graphql/createExecutableSchema.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
import { makeExecutableSchema } from "@graphql-tools/schema";
import { ResolverDecoration } from "@webiny/handler-graphql";
import { ICmsGraphQLSchemaPlugin } from "~/plugins";
import { Resolvers, TypeDefs } from "@webiny/handler-graphql/types";

interface Params {
plugins: ICmsGraphQLSchemaPlugin[];
}

export const createExecutableSchema = (params: Params) => {
const { plugins } = params;
/**
* Really hard to type this to satisfy the makeExecutableSchema
*/
// TODO @ts-refactor
const typeDefs: any = [];
const resolvers: any = [];

const typeDefs: TypeDefs[] = [];
const resolvers: Resolvers<any>[] = [];

const resolverDecoration = new ResolverDecoration();

// Get schema definitions from plugins
for (const plugin of plugins) {
typeDefs.push(plugin.schema.typeDefs);
resolvers.push(plugin.schema.resolvers);
const schema = plugin.schema;
if (schema.typeDefs) {
typeDefs.push(schema.typeDefs);
}
if (schema.resolvers) {
resolvers.push(schema.resolvers);
}
if (schema.resolverDecorators) {
resolverDecoration.addDecorators(schema.resolverDecorators);
}
}

return makeExecutableSchema({
typeDefs,
resolvers
resolvers: resolverDecoration.decorateResolvers(resolvers)
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createCmsGraphQLSchemaPlugin, ICmsGraphQLSchemaPlugin } from "~/plugins
import { getEntryDescription } from "~/utils/getEntryDescription";
import { getEntryImage } from "~/utils/getEntryImage";
import { entryFieldFromStorageTransform } from "~/utils/entryStorage";
import { Resolvers } from "@webiny/handler-graphql/types";
import { GraphQLFieldResolver } from "@webiny/handler-graphql/types";
import { ENTRY_META_FIELDS, isDateTimeEntryMetaField } from "~/constants";

interface EntriesByModel {
Expand Down Expand Up @@ -278,7 +278,7 @@ const getContentEntry = async (
/**
* As we support description field, we need to transform the value from storage.
*/
const createResolveDescription = (): Resolvers<CmsContext> => {
const createResolveDescription = (): GraphQLFieldResolver<any, any, CmsContext> => {
return async (parent, _, context) => {
const models = await context.cms.listModels();
const model = models.find(({ modelId }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import set from "lodash/set";
import { ApiEndpoint, CmsContext, CmsFieldTypePlugins, CmsModel, CmsModelField } from "~/types";
import { entryFieldFromStorageTransform } from "~/utils/entryStorage";
import { Resolvers } from "@webiny/handler-graphql/types";
import WebinyError from "@webiny/error";
import { ApiEndpoint, CmsContext, CmsFieldTypePlugins, CmsModel, CmsModelField } from "~/types";
import { entryFieldFromStorageTransform } from "~/utils/entryStorage";
import { getBaseFieldType } from "~/utils/getBaseFieldType";

interface CreateFieldResolvers {
Expand Down Expand Up @@ -77,8 +77,6 @@ export const createFieldResolversFactory = (factoryParams: CreateFieldResolversF
}

const { fieldId } = field;
// TODO @ts-refactor figure out types for parameters
// @ts-expect-error
fieldResolvers[fieldId] = async (parent, args, context: CmsContext, info) => {
/**
* This is required because due to ref field can be requested without the populated data.
Expand Down
3 changes: 0 additions & 3 deletions packages/api-websockets/src/graphql/createResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const createResolvers = (): Resolvers<Context> => {
}
},
WebsocketsMutation: {
// @ts-expect-error
disconnect: async (_, args: IWebsocketsMutationDisconnectConnectionsArgs, context) => {
return resolve(async () => {
await checkPermissions(context);
Expand All @@ -46,7 +45,6 @@ export const createResolvers = (): Resolvers<Context> => {
});
});
},
// @ts-expect-error
disconnectIdentity: async (
_,
args: IWebsocketsMutationDisconnectIdentityArgs,
Expand All @@ -61,7 +59,6 @@ export const createResolvers = (): Resolvers<Context> => {
});
});
},
// @ts-expect-error
disconnectTenant: async (_, args: IWebsocketsMutationDisconnectTenantArgs, context) => {
return resolve<IWebsocketsConnectionRegistryData[]>(async () => {
await checkPermissions(context);
Expand Down
45 changes: 45 additions & 0 deletions packages/handler-graphql/__tests__/graphql.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import useGqlHandler from "./useGqlHandler";
import { booksSchema, booksCrudPlugin } from "~tests/mocks/booksSchema";
import { createGraphQLSchemaPlugin } from "~/plugins";
import { createResolverDecorator } from "~/index";
import { Context } from "./types";

describe("GraphQL Handler", () => {
test("should return errors if schema doesn't exist", async () => {
Expand Down Expand Up @@ -76,4 +79,46 @@ describe("GraphQL Handler", () => {
]
});
});

test("should compose resolvers", async () => {
const lowerCaseName = createResolverDecorator<any, any, Context>(
resolver => async (parent, args, context, info) => {
const name = await resolver(parent, args, context, info);

return name.toLowerCase();
}
);

const listBooks = createResolverDecorator(() => async () => {
return [{ name: "Article 1" }];
});

const decorator1 = createGraphQLSchemaPlugin({
resolverDecorators: {
"Query.books": [listBooks],
"Book.name": [lowerCaseName]
}
});

const addNameSuffix = createResolverDecorator(resolver => async (...args) => {
const name = await resolver(...args);

return `${name} (suffix)`;
});

const decorator2 = createGraphQLSchemaPlugin({
resolverDecorators: {
"Book.name": [addNameSuffix]
}
});

const { invoke } = useGqlHandler({
debug: true,
plugins: [booksCrudPlugin, booksSchema, decorator1, decorator2]
});
const [response] = await invoke({ body: { query: `{ books { name } }` } });
expect(response.errors).toBeFalsy();
expect(response.data.books.length).toBe(1);
expect(response.data.books[0].name).toBe("article 1 (suffix)");
});
});
5 changes: 5 additions & 0 deletions packages/handler-graphql/__tests__/mocks/booksSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export const booksSchema = createGraphQLSchemaPlugin<Context>({
return null;
}
},
Book: {
name: book => {
return book.name;
}
},
Mutation: {
async createBook() {
return true;
Expand Down
5 changes: 4 additions & 1 deletion packages/handler-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
],
"dependencies": {
"@babel/runtime": "^7.24.0",
"@graphql-tools/schema": "^7.0.0",
"@graphql-tools/merge": "^9.0.4",
"@graphql-tools/resolvers-composition": "^7.0.1",
"@graphql-tools/schema": "^10.0.4",
"@graphql-tools/utils": "^10.3.1",
"@webiny/api": "0.0.0",
"@webiny/error": "0.0.0",
"@webiny/handler": "0.0.0",
Expand Down
23 changes: 23 additions & 0 deletions packages/handler-graphql/src/ResolverDecoration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { mergeResolvers } from "@graphql-tools/merge";
import { composeResolvers } from "@graphql-tools/resolvers-composition";
import { ResolverDecorators } from "./types";

export class ResolverDecoration {
private decorators: ResolverDecorators = {};

addDecorators(resolverDecorators: ResolverDecorators) {
for (const key in resolverDecorators) {
const decorators = resolverDecorators[key];
if (!decorators) {
continue;
}

const existingDecorators = this.decorators[key] ?? [];
this.decorators[key] = [...existingDecorators, ...decorators];
}
}

decorateResolvers(resolvers: any) {
return composeResolvers(mergeResolvers(resolvers), this.decorators);
}
}
34 changes: 20 additions & 14 deletions packages/handler-graphql/src/createGraphQLSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { GraphQLScalarPlugin, GraphQLSchemaPlugin } from "./types";
import { GraphQLScalarType } from "graphql/type/definition";
import { GraphQLScalarPlugin, GraphQLSchemaPlugin, Resolvers, TypeDefs } from "./types";
import { Context } from "@webiny/api/types";
import {
RefInputScalar,
Expand All @@ -12,7 +13,7 @@ import {
TimeScalar,
LongScalar
} from "./builtInTypes";
import { GraphQLScalarType } from "graphql/type/definition";
import { ResolverDecoration } from "./ResolverDecoration";

export const getSchemaPlugins = (context: Context) => {
return context.plugins.byType<GraphQLSchemaPlugin>("graphql-schema");
Expand All @@ -23,9 +24,9 @@ export const createGraphQLSchema = (context: Context) => {
.byType<GraphQLScalarPlugin>("graphql-scalar")
.map(item => item.scalar);

// TODO: once the API packages more closed, we'll have the opportunity
// TODO: once the API packages are more closed, we'll have the opportunity
// TODO: to maybe import the @ps directive from `api-prerendering-service` package.
const typeDefs = [
const typeDefs: TypeDefs[] = [
gql`
type Query
type Mutation
Expand All @@ -46,7 +47,7 @@ export const createGraphQLSchema = (context: Context) => {
`
];

const resolvers = [
const resolvers: Resolvers<any>[] = [
{
...scalars.reduce<Record<string, GraphQLScalarType>>((acc, s) => {
acc[s.name] = s;
Expand All @@ -63,21 +64,26 @@ export const createGraphQLSchema = (context: Context) => {
}
];

const resolverDecoration = new ResolverDecoration();

const plugins = getSchemaPlugins(context);

for (const plugin of plugins) {
/**
* TODO @ts-refactor
* Figure out correct types on typeDefs and resolvers
*/
// @ts-expect-error
typeDefs.push(plugin.schema.typeDefs);
// @ts-expect-error
resolvers.push(plugin.schema.resolvers);
const schema = plugin.schema;
if (schema.typeDefs) {
typeDefs.push(schema.typeDefs);
}
if (schema.resolvers) {
resolvers.push(schema.resolvers);
}
if (schema.resolverDecorators) {
resolverDecoration.addDecorators(schema.resolverDecorators);
}
}

return makeExecutableSchema({
typeDefs,
resolvers,
resolvers: resolverDecoration.decorateResolvers(resolvers),
inheritResolversFromInterfaces: true
});
};
7 changes: 7 additions & 0 deletions packages/handler-graphql/src/createResolverDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ResolverDecorator } from "./types";

export const createResolverDecorator = <TSource = any, TContext = any, TArgs = any>(
decorator: ResolverDecorator<TSource, TContext, TArgs>
) => {
return decorator;
};
2 changes: 2 additions & 0 deletions packages/handler-graphql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ export * from "./errors";
export * from "./responses";
export * from "./plugins";
export * from "./processRequestBody";
export * from "./createResolverDecorator";
export * from "./ResolverDecoration";

export default (options: HandlerGraphQLOptions = {}) => [createGraphQLHandler(options)];
10 changes: 6 additions & 4 deletions packages/handler-graphql/src/plugins/GraphQLSchemaPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Plugin } from "@webiny/plugins";
import { GraphQLSchemaDefinition, Resolvers, Types } from "~/types";
import { Context } from "@webiny/api/types";
import { Plugin } from "@webiny/plugins";
import { GraphQLSchemaDefinition, ResolverDecorators, Resolvers, TypeDefs } from "~/types";

export interface IGraphQLSchemaPlugin<TContext = Context> extends Plugin {
schema: GraphQLSchemaDefinition<TContext>;
}

export interface GraphQLSchemaPluginConfig<TContext> {
typeDefs?: Types;
typeDefs?: TypeDefs;
resolvers?: Resolvers<TContext>;
resolverDecorators?: ResolverDecorators;
}

export class GraphQLSchemaPlugin<TContext = Context>
Expand All @@ -26,7 +27,8 @@ export class GraphQLSchemaPlugin<TContext = Context>
get schema(): GraphQLSchemaDefinition<TContext> {
return {
typeDefs: this.config.typeDefs || "",
resolvers: this.config.resolvers
resolvers: this.config.resolvers,
resolverDecorators: this.config.resolverDecorators
};
}
}
Expand Down
23 changes: 16 additions & 7 deletions packages/handler-graphql/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
GraphQLSchema
} from "graphql";
import { Plugin } from "@webiny/plugins/types";
import { Context } from "@webiny/api/types";
import { Context, GenericRecord } from "@webiny/api/types";
import { RouteMethodPath } from "@webiny/handler/types";
import { ResolversComposition } from "@graphql-tools/resolvers-composition";
import { IResolvers, TypeSource } from "@graphql-tools/utils";

export interface GraphQLScalarPlugin extends Plugin {
type: "graphql-scalar";
Expand All @@ -23,23 +25,30 @@ export type GraphQLFieldResolver<
TContext = Context
> = BaseGraphQLFieldResolver<TSource, TContext, TArgs>;

// `GraphQLSchemaPlugin` types.
export type Types = string | string[] | (() => string | string[] | Promise<string | string[]>);
/**
* @deprecated Use `TypeDefs` instead.
*/
export type Types = TypeDefs;
export type TypeDefs = TypeSource;

export interface GraphQLSchemaPluginTypeArgs {
context?: any;
args?: any;
source?: any;
}

export type Resolvers<TContext> =
| GraphQLScalarType
| GraphQLFieldResolver<any, Record<string, any>, TContext>
| { [property: string]: Resolvers<TContext> };
export type Resolvers<TContext> = IResolvers<any, TContext>;

export type ResolverDecorator<TSource = any, TContext = any, TArgs = any> = ResolversComposition<
GraphQLFieldResolver<TSource, TContext, TArgs>
>;

export type ResolverDecorators = GenericRecord<string, ResolverDecorator[]>;

export interface GraphQLSchemaDefinition<TContext> {
typeDefs: Types;
resolvers?: Resolvers<TContext>;
resolverDecorators?: GenericRecord<string, ResolverDecorator[]>;
}

export interface GraphQLSchemaPlugin<TContext extends Context = Context> extends Plugin {
Expand Down
Loading
Loading