Skip to content

Commit

Permalink
feat: allow class fields resolvers and support inheritance from class…
Browse files Browse the repository at this point in the history
… prototype (#60)

Walk the prototype chain until we reach null or Object.prototype (which is ignored).

Closes: #59
  • Loading branch information
targos authored Aug 26, 2024
1 parent 129cd61 commit 1fdb109
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 15 deletions.
57 changes: 46 additions & 11 deletions src/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import path from 'node:path';

import { Ioc } from '@adonisjs/fold';
import { FakeLogger } from '@adonisjs/logger';
import { Kind } from 'graphql';
import type { IFieldResolver } from '@graphql-tools/utils';
import { type GraphQLResolveInfo, Kind } from 'graphql';

import { getTypeDefsAndResolvers, printWarnings } from '../schema';

Expand All @@ -13,43 +14,77 @@ describe('getTypeDefsAndResolvers', () => {
__dirname,
'../../test-utils/fixtures/schema/test1',
);
const result = getTypeDefsAndResolvers(
const { typeDefs, resolvers, warnings } = getTypeDefsAndResolvers(
[path.join(fixture, 'schemas')],
[path.join(fixture, 'resolvers')],
testIoC,
);
it('should merge schemas', () => {
// Query, Mutation
expect(
result.typeDefs.definitions.filter(
typeDefs.definitions.filter(
(def) => def.kind === Kind.OBJECT_TYPE_DEFINITION,
),
).toHaveLength(3);

// URL, Bad, OtherBad
expect(
result.typeDefs.definitions.filter(
typeDefs.definitions.filter(
(def) => def.kind === Kind.SCALAR_TYPE_DEFINITION,
),
).toHaveLength(3);
});

it('should merge resolvers', () => {
expect(Object.keys(result.resolvers)).toStrictEqual([
expect(Object.keys(resolvers)).toStrictEqual([
'Query',
'Mutation',
'D',
'URL',
]);
expect(Object.keys(resolvers.Query)).toStrictEqual(['queryA', 'queryD']);
expect(Object.keys(resolvers.Mutation)).toStrictEqual(['mutationA']);
});

it('should warn about missing resolvers', () => {
expect(result.warnings.missingQuery).toStrictEqual(['queryB', 'queryC']);
expect(result.warnings.missingMutation).toStrictEqual([
'mutationB',
'mutationC',
it('should walk the class prototype chain and support class fields', () => {
const DResolvers = resolvers.D as Record<
string,
IFieldResolver<unknown, unknown>
>;

expect(Object.keys(DResolvers)).toStrictEqual([
'value',
'parentOverride',
'grandParentValue',
'parentValue',
'valueField',
'parentOverrideField',
'grandParentValueField',
'parentValueField',
]);
expect(result.warnings.missingScalars).toStrictEqual(['Bad', 'OtherBad']);

function callResolver(resolver: string) {
return DResolvers[resolver](null, {}, null, {} as GraphQLResolveInfo);
}

for (const [resolver, expected] of [
['value', 'testGrandParent-testParent-test'],
['parentOverride', 'testParent'],
['grandParentValue', 'testGrandParent'],
['parentValue', 'testParent'],
['valueField', 'testGrandParent-testParent-test'],
['parentOverrideField', 'testParent'],
['grandParentValueField', 'testGrandParent'],
['parentValueField', 'testParent'],
]) {
expect(callResolver(resolver)).toBe(expected);
}
});

it('should warn about missing resolvers', () => {
expect(warnings.missingQuery).toStrictEqual(['queryB', 'queryC']);
expect(warnings.missingMutation).toStrictEqual(['mutationB', 'mutationC']);
expect(warnings.missingScalars).toStrictEqual(['Bad', 'OtherBad']);
});
});

Expand Down
19 changes: 17 additions & 2 deletions src/loadResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,28 @@ function mapResolverClass(
container: IocContract<ContainerBindings>,
) {
const instance = container.make(value);
const prototype = Object.getPrototypeOf(instance);
const ownProperties = Object.getOwnPropertyDescriptors(instance);
const prototypeChainProperties: Record<string, PropertyDescriptor> = {};
walkPrototypeChain(instance, prototypeChainProperties);
return Object.fromEntries(
Object.entries(Object.getOwnPropertyDescriptors(prototype))
Object.entries({ ...prototypeChainProperties, ...ownProperties })
.filter(
([name, desc]) =>
name !== 'constructor' && typeof desc.value === 'function',
)
.map(([name, desc]) => [name, desc.value.bind(instance)]),
);
}

function walkPrototypeChain(
instance: unknown,
properties: Record<string, PropertyDescriptor>,
) {
const proto = Object.getPrototypeOf(instance);
if (proto !== null && proto !== Object.prototype) {
// Use recursion so that the ancestor properties are added first and can
// be overridden by the descendant properties.
walkPrototypeChain(proto, properties);
Object.assign(properties, Object.getOwnPropertyDescriptors(proto));
}
}
43 changes: 41 additions & 2 deletions test-utils/fixtures/schema/test1/resolvers/D.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,47 @@ exports.Query = class Query {
}
};

exports.DResolvers = class D {
class DGrandParent {
value() {
return 'test';
return 'testGrandParent';
}
valueField = () => 'testGrandParent';

parentOverride() {
return 'testGrandParent';
}
parentOverrideField = () => 'testGrandParent';

grandParentValue() {
return 'testGrandParent';
}
grandParentValueField = () => 'testGrandParent';
}

class DParent extends DGrandParent {
value() {
return 'testParent';
}
valueField = () => 'testParent';

parentOverride() {
return 'testParent';
}
parentOverrideField = () => 'testParent';

parentValue() {
return 'testParent';
}
parentValueField = () => 'testParent';
}

exports.DResolvers = class D extends DParent {
#internalValue = 'test';

value() {
return this.valueField();
}
valueField = () => {
return `${super.grandParentValue()}-${this.parentValueField()}-${this.#internalValue}`;
};
};

0 comments on commit 1fdb109

Please sign in to comment.