Skip to content

Commit

Permalink
chore: save
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-sa committed Nov 20, 2023
1 parent eb68eb8 commit 3d1fd59
Show file tree
Hide file tree
Showing 13 changed files with 447 additions and 124 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@
"@deepkit/type": "1.0.1-alpha.105",
"@deepkit/type-compiler": "1.0.1-alpha.105",
"@deepkit/workflow": "1.0.1-alpha.105",
"@graphql-tools/utils": "^10.0.8",
"graphql": "^16.8.0",
"graphql-request": "^6.1.0",
"graphql-scalars": "^1.22.2",
"graphql-yoga": "^5.0.0",
"radash": "^11.0.0",
"rxjs": "^7.8.1",
"tslib": "2.6.2"
},
"packageManager": "[email protected]",
Expand Down
96 changes: 96 additions & 0 deletions packages/core/src/lib/decorators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import { buildSchema } from './schema-builder';
import { Resolvers } from './resolvers';
import { Context, GraphQLMiddleware, Parent } from './types';
import { InjectorContext, InjectorModule } from '@deepkit/injector';
import {
Broker,
BrokerBus,
BrokerBusChannel,
BrokerMemoryAdapter,
} from '@deepkit/broker';

/*test('invalid return type for mutation', () => {
expect(() => {
Expand Down Expand Up @@ -211,6 +217,96 @@ test('mutation middleware is invoked', async () => {
expect(testResolverSpy).toHaveBeenCalled();
});

test('subscription middleware is invoked', async () => {
class TestMiddleware implements GraphQLMiddleware {
execute(context: Context<unknown>, next: () => void): void {
next();
}
}

@graphql.resolver()
class TestResolver {
@graphql.subscription().middleware(TestMiddleware)
async *create(): AsyncIterable<boolean> {
yield true;
}
}

const injectorContext = new InjectorContext(
new InjectorModule([TestResolver, TestMiddleware]),
);

const testResolver = injectorContext.get(TestResolver);

const testResolverSpy = jest.spyOn(testResolver, 'create');

const testMiddleware = injectorContext.get(TestMiddleware);

const testMiddlewareExecuteSpy = jest.spyOn(testMiddleware, 'execute');

const resolvers = new Resolvers([testResolver]);

const schema = buildSchema(resolvers, injectorContext);

await executeGraphQL({
schema,
contextValue: {},
source: `subscription { create }`,
});

expect(testMiddlewareExecuteSpy).toHaveBeenCalled();

expect(testResolverSpy).toHaveBeenCalled();
});

test.todo(
'subscription return type AsyncIterable' /*, async () => {
@graphql.resolver()
class TestResolver {
async *subscribe(): AsyncGenerator<boolean> {
yield true;
}
}
}*/,
);

test(
'subscription return type BrokerBus' /*, async () => {
const broker = new Broker(new BrokerMemoryAdapter());
type UserEvents = { type: 'user-created', id: number } | { type: 'user-deleted', id: number };
type UserEventChannel = BrokerBusChannel<UserEvents, 'user-events'>;
const channel = broker.busChannel<UserEventChannel>();
@graphql.resolver()
class UserResolver {
@graphql.subscription()
userEvents(): BrokerBus<UserEvents> {
return channel;
}
}
const userResolver = new UserResolver();
const userEventsSpy = jest.spyOn(userResolver, 'userEvents');
const resolvers = new Resolvers([userResolver]);
const schema = buildSchema(resolvers);
await executeGraphQL({
schema,
contextValue: {},
source: `subscription { userEvents }`,
});
await channel.publish({ type: 'user-created', id: 1 });
expect(userEventsSpy).toHaveBeenCalled();
}*/,
);

test('mutation', async () => {
interface User {
readonly id: integer & PositiveNoZero;
Expand Down
49 changes: 34 additions & 15 deletions packages/core/src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class GraphQLClassMetadata {
readonly mutations = new Map<string, GraphQLPropertyMetadata>();
readonly queries = new Map<string, GraphQLPropertyMetadata>();
readonly resolveFields = new Map<string, GraphQLPropertyMetadata>();
readonly subscriptions = new Map<string, GraphQLPropertyMetadata>();
readonly checks = new Set<(decorator: GraphQLClassDecorator) => void>();
}

Expand Down Expand Up @@ -70,16 +71,20 @@ export class GraphQLClassDecorator {
this.t.checks.forEach(check => check(this));
}

addMutation(property: string, mutation: GraphQLPropertyMetadata) {
this.t.mutations.set(property, mutation);
addMutation(property: string, metadata: GraphQLPropertyMetadata) {
this.t.mutations.set(property, metadata);
}

addQuery(property: string, query: GraphQLPropertyMetadata) {
this.t.queries.set(property, query);
addQuery(property: string, metadata: GraphQLPropertyMetadata) {
this.t.queries.set(property, metadata);
}

addResolveField(property: string, field: GraphQLPropertyMetadata) {
this.t.resolveFields.set(property, field);
addSubscription(property: string, metadata: GraphQLPropertyMetadata) {
this.t.subscriptions.set(property, metadata);
}

addResolveField(property: string, metadata: GraphQLPropertyMetadata) {
this.t.resolveFields.set(property, metadata);
}

addCheck(check: (decorator: GraphQLClassDecorator) => void) {
Expand All @@ -98,6 +103,12 @@ interface GraphQLQueryOptions {
deprecationReason?: string;
}

interface GraphQLSubscriptionOptions {
name?: string;
description?: string;
deprecationReason?: string;
}

interface GraphQLMutationOptions {
name?: string;
description?: string;
Expand Down Expand Up @@ -145,7 +156,8 @@ class GraphQLPropertyDecorator {
break;

case 'subscription':
throw new Error('Not yet implemented');
gqlClassDecorator.addSubscription(property, this.t)(classType);
break;

default:
throw new Error('Invalid type');
Expand Down Expand Up @@ -175,6 +187,15 @@ class GraphQLPropertyDecorator {
this.t.deprecationReason = options?.deprecationReason;
}

subscription(options?: GraphQLSubscriptionOptions) {
if (options?.name) {
this.t.name = options.name;
}
this.t.type = 'subscription';
this.t.description = options?.description;
this.t.deprecationReason = options?.deprecationReason;
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
resolveField(options?: GraphQLResolveFieldOptions) {
if (options?.name) {
Expand Down Expand Up @@ -203,13 +224,6 @@ class GraphQLPropertyDecorator {
});
}

// TODO: subscriptions
// subscription(options?: GraphQLSubscriptionOptions) {
// this.t.type = 'subscription';
// this.t.description = options?.description;
// this.t.deprecationReason = options?.deprecationReason;
// }

// eslint-disable-next-line functional/prefer-readonly-type
middleware(...middleware: InternalMiddleware[]) {
this.t.middleware = new Set(middleware);
Expand Down Expand Up @@ -258,7 +272,12 @@ type MergedGraphQL<T extends any[]> = GraphQLMerge<

export type MergedGraphQLDecorator = Omit<
MergedGraphQL<[typeof gqlClassDecorator, typeof gqlPropertyDecorator]>,
'addMutation' | 'addQuery' | 'addResolveField' | 'onDecorator' | 'addCheck'
| 'addMutation'
| 'addQuery'
| 'addResolveField'
| 'addSubscription'
| 'onDecorator'
| 'addCheck'
>;

export const graphql: MergedGraphQLDecorator = mergeDecorator(
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/lib/directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
GraphQLDirective,
GraphQLSkipDirective,
GraphQLIncludeDirective,
} from 'graphql';
import {
GraphQLDeferDirective,
GraphQLStreamDirective,
} from '@graphql-tools/utils';

export const DIRECTIVE_META_NAME = 'directive';

export type Directive<
D extends GraphQLDirective | string,
A extends any[] = [],
> = { __meta?: [typeof DIRECTIVE_META_NAME, D, A] };

export type Defer = Directive<typeof GraphQLDeferDirective>;

export type Stream = Directive<typeof GraphQLStreamDirective>;

export type Skip = Directive<typeof GraphQLSkipDirective>;

export type Include = Directive<typeof GraphQLIncludeDirective>;

// TODO: use jsdoc @deprecated instead
// export type Deprecated = Directive<'deprecated', { reason: string }>;
8 changes: 6 additions & 2 deletions packages/core/src/lib/driver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { GraphQLSchema } from 'graphql';
import { ApplicationServer, WebWorker, WebWorkerFactory } from '@deepkit/framework';
import {
ApplicationServer,
WebWorker,
WebWorkerFactory,
} from '@deepkit/framework';
import * as https from 'node:https';
import * as http from 'node:http';

Expand Down Expand Up @@ -29,6 +33,6 @@ export abstract class Driver {
(this.appServer as any).httpWorker.start();
httpWorker = this.appServer.getHttpWorker() as any;
}
return httpWorker.servers || httpWorker.server;
return httpWorker.servers || httpWorker.server;
}
}
72 changes: 46 additions & 26 deletions packages/core/src/lib/schema-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ export class SchemaBuilder {
);
}

generateSubscriptionResolverFields(): GraphQLFieldConfigMap<
unknown,
unknown
> {
return [...this.resolvers.instances].reduce<
GraphQLFieldConfigMap<unknown, unknown>
>(
(fields, instance) => ({
// TODO: validate that fields don't override each other
...fields,
...this.typesBuilder.generateSubscriptionResolverFields(instance),
}),
{},
);
}

generateQueryResolverFields(): GraphQLFieldConfigMap<unknown, unknown> {
return [...this.resolvers.instances].reduce<
GraphQLFieldConfigMap<unknown, unknown>
Expand All @@ -84,6 +100,26 @@ export class SchemaBuilder {
);
}

hasSubscriptionResolvers(classType: ClassType): boolean {
const resolver = gqlClassDecorator._fetch(classType);
return !!resolver?.subscriptions.size;
}

hasQueryResolvers(classType: ClassType): boolean {
const resolver = gqlClassDecorator._fetch(classType);
return !!resolver?.queries.size;
}

hasMutationResolvers(classType: ClassType): boolean {
const resolver = gqlClassDecorator._fetch(classType);
return !!resolver?.mutations.size;
}

hasFieldResolvers(classType: ClassType): boolean {
const resolver = gqlClassDecorator._fetch(classType);
return !!resolver?.resolveFields.size;
}

private buildRootMutationType(): GraphQLObjectType | undefined {
const classTypes = [...this.resolvers.classTypes];

Expand All @@ -98,36 +134,18 @@ export class SchemaBuilder {
});
}

/*private buildRootSubscriptionType(
resolvers: Function[],
): GraphQLObjectType | undefined {
const subscriptionsHandlers = this.filterHandlersByResolvers(
getMetadataStorage().subscriptions,
resolvers,
private buildRootSubscriptionType(): GraphQLObjectType | undefined {
const classTypes = [...this.resolvers.classTypes];

const someSubscriptions = classTypes.some(classType =>
this.hasSubscriptionResolvers(classType),
);
if (subscriptionsHandlers.length === 0) {
return undefined;
}
if (!someSubscriptions) return;

return new GraphQLObjectType({
name: 'Subscription',
fields: this.generateSubscriptionsFields(subscriptionsHandlers),
fields: () => this.generateSubscriptionResolverFields(),
});
}*/

hasQueryResolvers(classType: ClassType): boolean {
const resolver = gqlClassDecorator._fetch(classType);
return !!resolver?.queries.size;
}

hasMutationResolvers(classType: ClassType): boolean {
const resolver = gqlClassDecorator._fetch(classType);
return !!resolver?.mutations.size;
}

hasFieldResolvers(classType: ClassType): boolean {
const resolver = gqlClassDecorator._fetch(classType);
return !!resolver?.resolveFields.size;
}

private buildRootQueryType(): GraphQLObjectType | undefined {
Expand Down Expand Up @@ -168,13 +186,15 @@ export class SchemaBuilder {
}

build(): GraphQLSchema {
const mutation = this.buildRootMutationType();
const query = this.buildRootQueryType();
const mutation = this.buildRootMutationType();
const subscription = this.buildRootSubscriptionType();
const types = [...this.buildInputTypes(), ...this.buildOutputTypes()];

return new GraphQLSchema({
query,
mutation,
subscription,
types,
});
}
Expand Down
Loading

0 comments on commit 3d1fd59

Please sign in to comment.