Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
159 changes: 31 additions & 128 deletions packages/api/src/graphql/GraphqlSequencerModule.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,39 @@
import { buildSchemaSync, NonEmptyArray } from "type-graphql";
import assert from "node:assert";

import { Closeable, closeable, SequencerModule } from "@proto-kit/sequencer";
import {
ChildContainerProvider,
Configurable,
CombinedModuleContainerConfig,
dependencyFactory,
log,
ModuleContainer,
ModulesRecord,
TypedClass,
} from "@proto-kit/common";
import { GraphQLSchema } from "graphql/type";
import { stitchSchemas } from "@graphql-tools/stitch";
import { createYoga } from "graphql-yoga";
import Koa from "koa";

import {
GraphqlModule,
ResolverFactoryGraphqlModule,
SchemaGeneratingGraphqlModule,
} from "./GraphqlModule";
import { GraphqlServer, GraphqlServerOptions } from "./GraphqlServer";

export type GraphqlModulesRecord = ModulesRecord<
TypedClass<GraphqlModule<unknown>>
>;

export interface GraphqlServerConfig {
host: string;
port: number;
graphiql: boolean;
}

export type GraphqlSequencerModuleConfig<
GraphQLModules extends GraphqlModulesRecord,
> = CombinedModuleContainerConfig<GraphQLModules, GraphqlServerConfig>;

type Server = ReturnType<Koa["listen"]>;

function assertArrayIsNotEmpty<T>(
array: readonly T[],
errorMessage: string
): asserts array is NonEmptyArray<T> {
if (array.length === 0) {
throw new Error(errorMessage);
}
}
> = CombinedModuleContainerConfig<GraphQLModules, GraphqlServerOptions>;

@closeable()
@dependencyFactory()
export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
extends ModuleContainer<GraphQLModules, GraphqlServerConfig>
extends ModuleContainer<GraphQLModules, GraphqlServerOptions>
implements Configurable<unknown>, SequencerModule<unknown>, Closeable
{
private readonly modules: TypedClass<GraphqlModule<unknown>>[] = [];

private readonly schemas: GraphQLSchema[] = [];

private resolvers: NonEmptyArray<Function> | undefined;

private server?: Server;

private context: {} = {};

public get serverConfig(): GraphqlServerConfig {
return this.containerConfig;
}
private graphqlServer?: GraphqlServer;

public static from<GraphQLModules extends GraphqlModulesRecord>(
definition: GraphQLModules
Expand All @@ -74,28 +45,35 @@ export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
};
}

public constructor(definition: GraphQLModules) {
super(definition);
public static dependencies() {
return {
graphqlServer: {
useClass: GraphqlServer,
},
};
}

public setContext(newContext: {}) {
this.context = newContext;
public constructor(definition: GraphQLModules) {
super(definition);
}

public registerResolvers(resolvers: NonEmptyArray<Function>) {
if (this.resolvers === undefined) {
this.resolvers = resolvers;
} else {
this.resolvers = [...this.resolvers, ...resolvers];
private getGraphqlConfig(graphqlServer: GraphqlServer): GraphqlServerOptions {
try {
return graphqlServer.config;
} catch {
return this.containerConfig;
}
}

public create(childContainerProvider: ChildContainerProvider) {
super.create(childContainerProvider);
this.container.register("GraphqlServer", { useValue: this });
this.graphqlServer = this.container.resolve("GraphqlServer");
}

public async start(): Promise<void> {
assert(this.graphqlServer !== undefined);
this.graphqlServer.setContainer(this.container);
this.graphqlServer.config = this.getGraphqlConfig(this.graphqlServer);
// eslint-disable-next-line guard-for-in
for (const moduleName in this.definition) {
const moduleClass = this.definition[moduleName];
Expand All @@ -112,9 +90,9 @@ export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
moduleName
) as ResolverFactoryGraphqlModule<unknown>;
// eslint-disable-next-line no-await-in-loop
this.registerResolvers(await module.resolvers());
this.graphqlServer.registerResolvers(await module.resolvers());
} else {
this.modules.push(moduleClass);
this.graphqlServer.registerModule(moduleClass);

if (
Object.prototype.isPrototypeOf.call(
Expand All @@ -127,91 +105,16 @@ export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
const module = this.resolve(
moduleName
) as SchemaGeneratingGraphqlModule<unknown>;
this.schemas.push(module.generateSchema());
this.graphqlServer.registerSchema(module.generateSchema());
}
}
}
await this.startServer();
}

// Server logic

private async startServer() {
const { modules, container: dependencyContainer } = this;

const resolvers = [...modules, ...(this.resolvers || [])];

assertArrayIsNotEmpty(
resolvers,
"At least one module has to be provided to GraphqlServer"
);

// Building schema
const resolverSchema = buildSchemaSync({
resolvers,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
container: { get: (cls) => dependencyContainer.resolve(cls) },
validate: {
enableDebugMessages: true,
},
});

// Instantiate all modules at startup
modules.forEach((module) => {
dependencyContainer.resolve(module);
});

const schema = [resolverSchema, ...this.schemas].reduce(
(schema1, schema2) =>
stitchSchemas({
subschemas: [{ schema: schema1 }, { schema: schema2 }],
})
);

const app = new Koa();

const { graphiql, port, host } = this.serverConfig;

const yoga = createYoga<Koa.ParameterizedContext>({
schema,
graphiql,
context: this.context,
});

// Bind GraphQL Yoga to `/graphql` endpoint
app.use(async (ctx) => {
// Second parameter adds Koa's context into GraphQL Context
const response = await yoga.handleNodeRequest(ctx.req, ctx);

// Set status code
ctx.status = response.status;

// Set headers
response.headers.forEach((value, key) => {
ctx.append(key, value);
});

// Converts ReadableStream to a NodeJS Stream
ctx.body = response.body;
});

this.server = app.listen({ port, host }, () => {
log.info(`GraphQL Server listening on ${host}:${port}`);
});
await this.graphqlServer.startServer();
}

public async close() {
if (this.server !== undefined) {
const { server } = this;

await new Promise<void>((res) => {
server.close((error) => {
if (error !== undefined) {
log.error(error);
}
res();
});
});
if (this.graphqlServer !== undefined) {
await this.graphqlServer.close();
}
}
}
5 changes: 3 additions & 2 deletions packages/api/src/graphql/GraphqlServer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { buildSchemaSync, NonEmptyArray } from "type-graphql";
import { DependencyContainer, injectable } from "tsyringe";
import { SequencerModule } from "@proto-kit/sequencer";
import { closeable, SequencerModule } from "@proto-kit/sequencer";
import { log, noop, TypedClass } from "@proto-kit/common";
import { GraphQLSchema } from "graphql/type";
import { stitchSchemas } from "@graphql-tools/stitch";
Expand All @@ -11,7 +11,7 @@ import type { GraphqlModule } from "./GraphqlModule";

type Server = ReturnType<Koa["listen"]>;

interface GraphqlServerOptions {
export interface GraphqlServerOptions {
host: string;
port: number;
graphiql: boolean;
Expand All @@ -27,6 +27,7 @@ function assertArrayIsNotEmpty<T>(
}

@injectable()
@closeable()
export class GraphqlServer extends SequencerModule<GraphqlServerOptions> {
private readonly modules: TypedClass<GraphqlModule<unknown>>[] = [];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
GraphqlModulesRecord,
GraphqlSequencerModule,
GraphqlServer,
ResolverFactoryGraphqlModule,
graphqlModule,
} from "@proto-kit/api";
Expand Down Expand Up @@ -99,7 +98,7 @@ export function ValidateTakeArg() {
export class GeneratedResolverFactoryGraphqlModule extends ResolverFactoryGraphqlModule {
public constructor(
@inject("GraphqlServer")
public graphqlServer: GraphqlSequencerModule<GraphqlModulesRecord>
public graphqlServer: GraphqlServer
) {
super();
}
Expand Down
7 changes: 2 additions & 5 deletions packages/processor/src/api/ResolverFactoryGraphqlModule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
GraphqlModulesRecord,
GraphqlSequencerModule,
GraphqlServer,
ResolverFactoryGraphqlModule as BaseResolverFactoryGraphqlModule,
graphqlModule,
} from "@proto-kit/api";
Expand Down Expand Up @@ -62,9 +61,7 @@ export class ResolverFactoryGraphqlModule<

public database: PrismaDatabaseConnection<PrismaClient> | undefined;

public constructor(
public graphqlServer: GraphqlSequencerModule<GraphqlModulesRecord>
) {
public constructor(public graphqlServer: GraphqlServer) {
super();
}

Expand Down
Loading