@tinkerstack/graphql-annotation 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { Query, Resolver } from "@nestjs/graphql";
3
+ import { InjectableArg, ProtectedResolver } from "../consumer/consumer.example";
4
+
5
+ @Resolver()
6
+ export class ExampleProducerResolver {
7
+ @Query(() => [String])
8
+ @ProtectedResolver(["admin"])
9
+ testProtectedQuery() {
10
+ return ["This resolver should only be available to if guard check passed"];
11
+ }
12
+ @Query(() => [String])
13
+ testQueryWithParam(
14
+ @InjectableArg("username", "session:username")
15
+ username: string
16
+ ) {
17
+ return [`Received params: ${username}`];
18
+ }
19
+ @Query(() => [String])
20
+ testPublicQuery() {
21
+ return ["This resolver should be available to everyone"];
22
+ }
23
+ }
24
+
25
+ @Module({
26
+ providers: [ExampleProducerResolver],
27
+ })
28
+ export class ExampleProducerModule {}
@@ -0,0 +1,20 @@
1
+ # ------------------------------------------------------
2
+ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
3
+ # ------------------------------------------------------
4
+
5
+ type GraphQLAnnotationMeta {
6
+ name: String!
7
+ resolvers: JSONObject!
8
+ }
9
+
10
+ """
11
+ The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
12
+ """
13
+ scalar JSONObject @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")
14
+
15
+ type Query {
16
+ _annotations: GraphQLAnnotationMeta!
17
+ testProtectedQuery: [String!]!
18
+ testQueryWithParam(username: String!): [String!]!
19
+ testPublicQuery: [String!]!
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./modules";
@@ -0,0 +1,37 @@
1
+ export const ANNOTATION_METADATA = "graphql-annotation"; // Should not conflict
2
+ export interface MethodInfo {
3
+ type: "method";
4
+ name: string;
5
+ }
6
+ export interface ParameterInfo {
7
+ type: "parameter";
8
+ paramIndex: number;
9
+ paramName: string;
10
+ }
11
+ export type AnnotationTarget = MethodInfo | ParameterInfo;
12
+ export interface AnnotationInfo<T = unknown> {
13
+ annotation: string;
14
+ target: AnnotationTarget;
15
+ data: T;
16
+ }
17
+
18
+ export const ANNOTATION_RESOLVER_METADATA = "graphql-annotation-resolver";
19
+ export interface AnnotationResolverMetadata {
20
+ annotation: string;
21
+ }
22
+
23
+ type SourceName = string;
24
+ type RemoteURL = string;
25
+ export type AnnotationSchemaSources = Record<SourceName, RemoteURL>;
26
+
27
+ export type ResolverName = string;
28
+ export type GraphQLResolversAnnotations = Record<
29
+ ResolverName,
30
+ AnnotationInfo[]
31
+ >;
32
+
33
+ export const GRAPHQL_ANNOTATION_RESOLVER_METADATA =
34
+ "graphql-annotation-provider";
35
+ export type GraphQLAnnotationResolverMetadata = {
36
+ name: string;
37
+ };
@@ -0,0 +1,147 @@
1
+ import { SetMetadata } from "@nestjs/common";
2
+ import { Args } from "@nestjs/graphql";
3
+ import "reflect-metadata";
4
+ import {
5
+ ANNOTATION_METADATA,
6
+ ANNOTATION_RESOLVER_METADATA,
7
+ AnnotationInfo,
8
+ AnnotationResolverMetadata,
9
+ } from "./annotation.constants";
10
+
11
+ /**
12
+ * Applies an annotation to a Query or Mutation handler or parameter for
13
+ * external stitching party to interpret.
14
+ *
15
+ * For parameter decorator, you must specify `parameter` option for external providers.
16
+ *
17
+ * @publicApi
18
+ */
19
+ export function Annotate(
20
+ name: string,
21
+ opts?: { data?: Record<string, any>; parameter?: undefined },
22
+ ): MethodDecorator;
23
+ /**
24
+ * Applies an annotation to a Query or Mutation handler or parameter for
25
+ * external stitching party to interpret.
26
+ *
27
+ * For parameter decorator, you must specify `parameter` option for external providers.
28
+ *
29
+ * @publicApi
30
+ */
31
+ export function Annotate(
32
+ name: string,
33
+ opts: { data?: Record<string, any>; parameter: string },
34
+ ): ParameterDecorator & MethodDecorator;
35
+ /**
36
+ * Applies an annotation to a Query or Mutation handler or parameter for
37
+ * external stitching party to interpret.
38
+ *
39
+ * For parameter decorator, you must specify `parameter` option for external providers.
40
+ *
41
+ * @publicApi
42
+ */
43
+ export function Annotate(
44
+ name: string,
45
+ opts?: { data?: Record<string, any>; parameter?: string },
46
+ ): MethodDecorator & ParameterDecorator {
47
+ const annotationName = name;
48
+ const annotationData = opts?.data;
49
+
50
+ return (
51
+ target: Object,
52
+ propertyKey: string,
53
+ descriptorOrParameterIndex: number | TypedPropertyDescriptor<unknown>,
54
+ ) => {
55
+ switch (typeof descriptorOrParameterIndex) {
56
+ // Method
57
+ case "object": {
58
+ const descriptor = descriptorOrParameterIndex;
59
+ _updateAnnotation(descriptor.value ?? target, (meta) => ({
60
+ ...meta,
61
+ [annotationName]: {
62
+ annotation: annotationName,
63
+ data: annotationData,
64
+ target: {
65
+ type: "method",
66
+ name: propertyKey,
67
+ },
68
+ },
69
+ }));
70
+ break;
71
+ }
72
+ // Parameter
73
+ case "number": {
74
+ const paramIndex = descriptorOrParameterIndex;
75
+ const paramName = opts?.parameter;
76
+
77
+ if (!paramName)
78
+ throw new Error(
79
+ `Unable to apply annotation "${annotationName}". Parameter name not specified.`,
80
+ );
81
+
82
+ Args(paramName)(target, propertyKey, paramIndex);
83
+ _updateAnnotation(
84
+ target.constructor,
85
+ (meta) => ({
86
+ ...meta,
87
+ [`${annotationName}@${paramIndex}`]: {
88
+ annotation: annotationName,
89
+ data: annotationData,
90
+ target: {
91
+ type: "parameter",
92
+ paramIndex,
93
+ paramName,
94
+ },
95
+ },
96
+ }),
97
+ propertyKey,
98
+ );
99
+
100
+ break;
101
+ }
102
+ default:
103
+ throw new Error(
104
+ `Unable to attach annotation "${name}". Unsupported target.`,
105
+ );
106
+ }
107
+ };
108
+ }
109
+ type AnnotationList = Record<string, AnnotationInfo>;
110
+ function _updateAnnotation(
111
+ target: any,
112
+ updateFn: (old: AnnotationList) => AnnotationList,
113
+ propertyKey?: string | symbol,
114
+ ) {
115
+ const meta = Reflect.getMetadata(ANNOTATION_METADATA, target) ?? {};
116
+
117
+ if (propertyKey)
118
+ Reflect.defineMetadata(
119
+ ANNOTATION_METADATA,
120
+ updateFn(meta),
121
+ target,
122
+ propertyKey,
123
+ );
124
+ else Reflect.defineMetadata(ANNOTATION_METADATA, updateFn(meta), target);
125
+ }
126
+
127
+ /**
128
+ * Declare `Resolver` method for resolving annotations
129
+ * @publicApi
130
+ */
131
+ export function ResolveAnnotation(): MethodDecorator;
132
+ /**
133
+ * Declare `Resolver` method for resolving annotations
134
+ * @publicApi
135
+ */
136
+ export function ResolveAnnotation(annotation: string): MethodDecorator;
137
+ /**
138
+ * Declare `Resolver` method for resolving annotations
139
+ * @publicApi
140
+ */
141
+ export function ResolveAnnotation(name?: string): MethodDecorator {
142
+ return (target, property, descriptor) => {
143
+ SetMetadata(ANNOTATION_RESOLVER_METADATA, {
144
+ annotation: name ?? (property as string),
145
+ } satisfies AnnotationResolverMetadata)(target, property, descriptor);
146
+ };
147
+ }
@@ -0,0 +1,303 @@
1
+ import { stitchSchemas } from "@graphql-tools/stitch";
2
+ import { MapperKind, mapSchema } from "@graphql-tools/utils";
3
+ import { FilterRootFields, wrapSchema } from "@graphql-tools/wrap";
4
+ import { Injectable, Logger, ParamData } from "@nestjs/common";
5
+ import {
6
+ ExternalContextCreator,
7
+ MetadataScanner,
8
+ ParamsFactory,
9
+ } from "@nestjs/core";
10
+ import { ParamMetadata } from "@nestjs/core/helpers/interfaces/params-metadata.interface";
11
+ import {
12
+ GqlContextType,
13
+ GraphQLExecutionContext,
14
+ PARAM_ARGS_METADATA,
15
+ } from "@nestjs/graphql";
16
+ import { GqlParamtype } from "@nestjs/graphql/dist/enums/gql-paramtype.enum";
17
+ import { GraphQLResolveInfo, GraphQLSchema } from "graphql";
18
+ import { GraphQLClient } from "graphql-request";
19
+ import {
20
+ ANNOTATION_RESOLVER_METADATA,
21
+ AnnotationResolverMetadata,
22
+ AnnotationSchemaSources,
23
+ ResolverName,
24
+ } from "./annotation.constants";
25
+ import {
26
+ GraphQLAnnotationMeta,
27
+ GraphQLAnnotationResolver,
28
+ } from "./annotation.resolver";
29
+ import {
30
+ isPlainObject,
31
+ loadGraphQLSchema,
32
+ shallowMerge,
33
+ split,
34
+ } from "./annotation.utils";
35
+ import { ResolverDiscoveryService } from "./resolver.explorer";
36
+ import { instanceToPlain } from "class-transformer";
37
+
38
+ @Injectable()
39
+ export class GraphQLAnnotatedSchemaLoader {
40
+ private _logger = new Logger("GraphQLAnnotationModule");
41
+
42
+ constructor(
43
+ private readonly resolverDiscoveryService: ResolverDiscoveryService,
44
+ private readonly metadataScanner: MetadataScanner,
45
+ private readonly externalContextCreator: ExternalContextCreator,
46
+ ) {}
47
+
48
+ public async load(sources: AnnotationSchemaSources) {
49
+ const loadedSources = await this.batchLoadSources(sources);
50
+
51
+ const schema = stitchSchemas({
52
+ subschemas: loadedSources.map((s) => s.subschema),
53
+ });
54
+ const annotations = shallowMerge(
55
+ loadedSources.map((s) => s.annotation),
56
+ ) as GraphQLAnnotationMeta;
57
+ const annotationResolvers = await this.discoverAnnotationResolvers();
58
+
59
+ return this.embedAnnotationResolversToSchema(
60
+ schema,
61
+ annotations,
62
+ annotationResolvers,
63
+ );
64
+ }
65
+
66
+ private async discoverAnnotationResolvers() {
67
+ type AnnotationName = string;
68
+ type AnnotationCallback = (...args: any[]) => any;
69
+ const annotationResolvers = new Map<AnnotationName, AnnotationCallback>();
70
+
71
+ const paramFactory = new GraphQLAnnotationResolverParamsFactory();
72
+ const instances = this.resolverDiscoveryService.explore();
73
+
74
+ for (const { instance } of instances)
75
+ for (const methodName of this.metadataScanner.getAllMethodNames(
76
+ Object.getPrototypeOf(instance),
77
+ )) {
78
+ const resolverMeta = Reflect.getMetadata(
79
+ ANNOTATION_RESOLVER_METADATA,
80
+ instance[methodName],
81
+ ) as AnnotationResolverMetadata;
82
+
83
+ if (!resolverMeta) continue; // Skip non-annotated
84
+
85
+ this._logger.log(
86
+ `Discovered resolver for annotation "${resolverMeta.annotation}"`,
87
+ );
88
+ annotationResolvers.set(
89
+ resolverMeta.annotation,
90
+ this.externalContextCreator.create<
91
+ Record<number, ParamMetadata>,
92
+ GqlContextType
93
+ >(
94
+ instance,
95
+ instance[methodName],
96
+ methodName,
97
+ PARAM_ARGS_METADATA,
98
+ paramFactory,
99
+ undefined,
100
+ undefined,
101
+ undefined,
102
+ "graphql",
103
+ ),
104
+ );
105
+ }
106
+
107
+ return annotationResolvers;
108
+ }
109
+
110
+ private async batchLoadSources(sources: AnnotationSchemaSources) {
111
+ const pending = Object.entries(sources).map(async ([name, url]) => {
112
+ const loadId = `${name}@${url}`;
113
+ try {
114
+ const loaded = {
115
+ id: loadId,
116
+ url: url,
117
+ name: name,
118
+ subschema: await loadGraphQLSchema(url),
119
+ annotation: await this.loadAnnotations(url),
120
+ };
121
+
122
+ // Cleanup
123
+ // Remove `_annotations` from being merged to main schema
124
+ loaded.subschema.transforms = [
125
+ new FilterRootFields(
126
+ (op, fieldName) =>
127
+ !fieldName.endsWith(GraphQLAnnotationResolver.QUERY_NAME),
128
+ ),
129
+ ];
130
+
131
+ this._logger.log(`Loaded schema for ${loadId}`);
132
+ return loaded;
133
+ } catch (err) {
134
+ this._logger.error(`Unable to load schema for ${loadId}`);
135
+ throw err;
136
+ }
137
+ });
138
+
139
+ const [loaded, failed] = split(
140
+ await Promise.allSettled(pending),
141
+ (q) => q.status === "fulfilled",
142
+ );
143
+ if (failed.length > 0)
144
+ throw new Error(
145
+ `Failed to load ${failed.length} schema(s) from sources.`,
146
+ );
147
+
148
+ return loaded.map((l) => l.value);
149
+ }
150
+
151
+ private async loadAnnotations(url: string) {
152
+ const client = new GraphQLClient(url);
153
+
154
+ try {
155
+ type ExpectedResult = {
156
+ [GraphQLAnnotationResolver.QUERY_NAME]: GraphQLAnnotationMeta;
157
+ };
158
+ const res = await client.rawRequest<ExpectedResult>(`
159
+ query IntrospectAnnotations {
160
+ ${GraphQLAnnotationResolver.QUERY_NAME} {
161
+ name
162
+ resolvers
163
+ }
164
+ }
165
+ `);
166
+
167
+ this._logger.log(`Loaded annotations for ${url}`);
168
+ return res.data[GraphQLAnnotationResolver.QUERY_NAME];
169
+ } catch (err) {
170
+ this._logger.warn(`Failed to load annotations at ${url}, assuming empty`);
171
+ return {} as GraphQLAnnotationMeta;
172
+ }
173
+ }
174
+
175
+ private async embedAnnotationResolversToSchema(
176
+ schema: GraphQLSchema,
177
+ annotations: GraphQLAnnotationMeta,
178
+ annotationResolvers: Map<ResolverName, (...args: any[]) => any>,
179
+ ) {
180
+ return mapSchema(schema, {
181
+ [MapperKind.ROOT_FIELD]: (fieldSchema, name, type) => {
182
+ const fieldAnnotations = (annotations.resolvers[name] ?? [])
183
+ .map((info) => ({
184
+ ...info,
185
+ resolver: annotationResolvers.get(info.annotation)!, // NOTE: should be validated in filter
186
+ }))
187
+ .filter((info) => {
188
+ const annotationHasResolver = info.resolver !== undefined;
189
+ if (!annotationHasResolver)
190
+ this._logger.warn(
191
+ `Found unhandled annotation "${info.annotation}", skipping linking process`,
192
+ );
193
+
194
+ return annotationHasResolver;
195
+ });
196
+
197
+ if (fieldAnnotations.length <= 0) return fieldSchema; // No transform if there's no annotation
198
+
199
+ // If there exist a resolver for an annotate parameter, remove it from schema to prevent external injection
200
+ for (const annotation of fieldAnnotations)
201
+ if (annotation.target.type === "parameter" && fieldSchema.args)
202
+ delete fieldSchema.args[annotation.target.paramName];
203
+
204
+ const defaultResolver = fieldSchema.resolve!;
205
+ fieldSchema.resolve = async function (
206
+ parent,
207
+ args: Record<string, any>,
208
+ context: GraphQLExecutionContext,
209
+ info,
210
+ ) {
211
+ const annotationCallbacks = await Promise.all(
212
+ fieldAnnotations.map(async (annotation) => ({
213
+ annotation,
214
+ return: await annotation.resolver(
215
+ ...([
216
+ info.rootValue,
217
+ annotation.data as Record<string, unknown>,
218
+ context,
219
+ info,
220
+ ] satisfies GraphQLAnnotationResolverArgs),
221
+ ),
222
+ })),
223
+ );
224
+
225
+ const resolvedParams = annotationCallbacks.reduce(
226
+ (params, call) => {
227
+ if (call.annotation.target.type !== "parameter") return params;
228
+
229
+ const returnedClassType =
230
+ typeof call.return === "object" && !isPlainObject(call.return);
231
+ const returnValue = returnedClassType
232
+ ? instanceToPlain(call.return)
233
+ : call.return;
234
+ params[call.annotation.target.paramName] = returnValue;
235
+
236
+ return params;
237
+ },
238
+ {} as Record<string, any>,
239
+ );
240
+
241
+ return defaultResolver(
242
+ parent,
243
+ { ...args, ...resolvedParams },
244
+ context,
245
+ info,
246
+ );
247
+ };
248
+
249
+ return fieldSchema;
250
+ },
251
+ });
252
+ }
253
+ }
254
+
255
+ type GraphQLAnnotationResolverArgs = [
256
+ // Root/parent (follows graphql context to match)
257
+ any,
258
+ // Data
259
+ Record<string, unknown>,
260
+ // GraphQL Context
261
+ GraphQLExecutionContext,
262
+ // GraphQL Resolver Info
263
+ GraphQLResolveInfo,
264
+ ];
265
+ class GraphQLAnnotationResolverParamsFactory implements ParamsFactory {
266
+ // This param factory extends graphql's own just with different inputs from annotations instead.
267
+ // https://github.com/nestjs/graphql/blob/master/packages/graphql/lib/factories/params.factory.ts#L9
268
+ exchangeKeyForValue(
269
+ type: number,
270
+ possibleKey: ParamData,
271
+ // NOTE: nestjs passes this in array, so we have to "get the first element"
272
+ argsContext: GraphQLAnnotationResolverArgs,
273
+ ) {
274
+ if (!argsContext) return null;
275
+ console.log(type, possibleKey, argsContext);
276
+
277
+ const args = {
278
+ parentValue: argsContext[0],
279
+ data: argsContext[1],
280
+ context: argsContext[2],
281
+ info: argsContext[3],
282
+ };
283
+
284
+ switch (type as GqlParamtype) {
285
+ case GqlParamtype.ROOT:
286
+ return args.parentValue;
287
+ case GqlParamtype.ARGS:
288
+ return possibleKey && args.data
289
+ ? args.data[possibleKey as string]
290
+ : args.data;
291
+ case GqlParamtype.CONTEXT:
292
+ return possibleKey && args.context
293
+ ? args.context[possibleKey as string]
294
+ : args.context;
295
+ case GqlParamtype.INFO:
296
+ return possibleKey && args.context
297
+ ? args.info[possibleKey as string]
298
+ : args.info;
299
+ default:
300
+ return null;
301
+ }
302
+ }
303
+ }
@@ -0,0 +1,47 @@
1
+ import { DynamicModule, Module } from "@nestjs/common";
2
+ import { DiscoveryModule } from "@nestjs/core";
3
+ import { GraphQLAnnotationResolver } from "./annotation.resolver";
4
+ import { GraphQLAnnotatedSchemaLoader } from "./annotation.loader";
5
+ import { ResolverDiscoveryService } from "./resolver.explorer";
6
+ import {
7
+ ANNOTATION_RESOLVER_METADATA,
8
+ GraphQLAnnotationResolverMetadata,
9
+ } from "./annotation.constants";
10
+
11
+ @Module({})
12
+ export class GraphQLAnnotationModule {
13
+ static forRoot(opts?: { serveAnnotations?: false }): DynamicModule;
14
+ static forRoot(opts: {
15
+ serveAnnotations: true;
16
+ serveConfig: GraphQLAnnotationResolverMetadata;
17
+ }): DynamicModule;
18
+ static forRoot(opts?: {
19
+ serveAnnotations?: boolean;
20
+ serveConfig?: GraphQLAnnotationResolverMetadata;
21
+ }): DynamicModule {
22
+ if (opts?.serveAnnotations)
23
+ return {
24
+ global: true,
25
+ module: GraphQLAnnotationModule,
26
+ imports: [DiscoveryModule],
27
+ providers: [
28
+ ResolverDiscoveryService,
29
+ GraphQLAnnotatedSchemaLoader,
30
+ {
31
+ provide: ANNOTATION_RESOLVER_METADATA,
32
+ useValue: opts.serveConfig!,
33
+ },
34
+ GraphQLAnnotationResolver,
35
+ ],
36
+ exports: [GraphQLAnnotatedSchemaLoader],
37
+ };
38
+
39
+ return {
40
+ global: true,
41
+ module: GraphQLAnnotationModule,
42
+ imports: [DiscoveryModule],
43
+ providers: [ResolverDiscoveryService, GraphQLAnnotatedSchemaLoader],
44
+ exports: [GraphQLAnnotatedSchemaLoader],
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,86 @@
1
+ import { Inject, Injectable, Logger, OnModuleInit } from "@nestjs/common";
2
+ import { MetadataScanner } from "@nestjs/core";
3
+ import { Field, ObjectType, Query, Resolver } from "@nestjs/graphql";
4
+ import { GraphQLJSONObject } from "graphql-scalars";
5
+ import "reflect-metadata";
6
+ import type {
7
+ AnnotationInfo,
8
+ GraphQLAnnotationResolverMetadata,
9
+ GraphQLResolversAnnotations,
10
+ } from "./annotation.constants";
11
+ import {
12
+ ANNOTATION_METADATA,
13
+ ANNOTATION_RESOLVER_METADATA,
14
+ } from "./annotation.constants";
15
+ import { ResolverDiscoveryService } from "./resolver.explorer";
16
+
17
+ @ObjectType()
18
+ export class GraphQLAnnotationMeta {
19
+ @Field(() => String)
20
+ name: string;
21
+
22
+ @Field(() => GraphQLJSONObject, { name: "resolvers" })
23
+ resolvers: GraphQLResolversAnnotations;
24
+ }
25
+
26
+ @Resolver()
27
+ @Injectable()
28
+ export class GraphQLAnnotationResolver implements OnModuleInit {
29
+ private _logger = new Logger("GraphQLAnnotationModule");
30
+
31
+ public static QUERY_NAME = "_annotations";
32
+ constructor(
33
+ private readonly metadataScanner: MetadataScanner,
34
+ private readonly resolverDiscoveryService: ResolverDiscoveryService,
35
+ @Inject(ANNOTATION_RESOLVER_METADATA)
36
+ private readonly meta: GraphQLAnnotationResolverMetadata,
37
+ ) {}
38
+
39
+ @Query(() => GraphQLAnnotationMeta, {
40
+ name: GraphQLAnnotationResolver.QUERY_NAME,
41
+ })
42
+ async getGraphQLAnnotations() {
43
+ return {
44
+ ...this.meta,
45
+ resolvers: this.resolverAnnotations,
46
+ } satisfies GraphQLAnnotationMeta;
47
+ }
48
+
49
+ private resolverAnnotations = {} as GraphQLResolversAnnotations;
50
+ async onModuleInit() {
51
+ this.resolverAnnotations = this.compileResolversAnnotations();
52
+ }
53
+
54
+ private compileResolversAnnotations() {
55
+ const instances = this.resolverDiscoveryService.explore();
56
+ const resolversAnnotations = {} as GraphQLResolversAnnotations;
57
+
58
+ for (const { instance } of instances)
59
+ for (const methodName of this.metadataScanner.getAllMethodNames(
60
+ Object.getPrototypeOf(instance),
61
+ )) {
62
+ const annotations = [
63
+ ...Object.values(
64
+ Reflect.getMetadata(ANNOTATION_METADATA, instance[methodName]) ??
65
+ {},
66
+ ),
67
+ ...Object.values(
68
+ Reflect.getMetadata(
69
+ ANNOTATION_METADATA,
70
+ instance.constructor,
71
+ methodName,
72
+ ) ?? {},
73
+ ),
74
+ ] as AnnotationInfo[];
75
+
76
+ if (annotations.length <= 0) continue;
77
+
78
+ resolversAnnotations[methodName] = annotations;
79
+ this._logger.log(
80
+ `Discovered annotated resolver "${instance.constructor.name}.${methodName}" with ${annotations.length} annotation(s)`,
81
+ );
82
+ }
83
+
84
+ return resolversAnnotations;
85
+ }
86
+ }