Skip to content
156 changes: 139 additions & 17 deletions packages/api/src/graphql/GraphqlSequencerModule.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import assert from "node:assert";

import { buildSchemaSync, NonEmptyArray } from "type-graphql";
import { Closeable, closeable, SequencerModule } from "@proto-kit/sequencer";
import {
ChildContainerProvider,
Configurable,
CombinedModuleContainerConfig,
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 { GraphqlServer } from "./GraphqlServer";
import {
GraphqlModule,
ResolverFactoryGraphqlModule,
Expand All @@ -21,11 +24,46 @@ 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);
}
}

@closeable()
export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
extends ModuleContainer<GraphQLModules>
extends ModuleContainer<GraphQLModules, GraphqlServerConfig>
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;
}

public static from<GraphQLModules extends GraphqlModulesRecord>(
definition: GraphQLModules
): TypedClass<GraphqlSequencerModule<GraphQLModules>> {
Expand All @@ -36,19 +74,28 @@ export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
};
}

private graphqlServer?: GraphqlServer;
public constructor(definition: GraphQLModules) {
super(definition);
}

public setContext(newContext: {}) {
this.context = newContext;
}

public registerResolvers(resolvers: NonEmptyArray<Function>) {
if (this.resolvers === undefined) {
this.resolvers = resolvers;
} else {
this.resolvers = [...this.resolvers, ...resolvers];
}
}

public create(childContainerProvider: ChildContainerProvider) {
super.create(childContainerProvider);

this.graphqlServer = this.container.resolve("GraphqlServer");
this.container.register("GraphqlServer", { useValue: this });
}

public async start(): Promise<void> {
assert(this.graphqlServer !== undefined);

this.graphqlServer.setContainer(this.container);

// eslint-disable-next-line guard-for-in
for (const moduleName in this.definition) {
const moduleClass = this.definition[moduleName];
Expand All @@ -65,9 +112,9 @@ export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>
moduleName
) as ResolverFactoryGraphqlModule<unknown>;
// eslint-disable-next-line no-await-in-loop
this.graphqlServer.registerResolvers(await module.resolvers());
this.registerResolvers(await module.resolvers());
} else {
this.graphqlServer.registerModule(moduleClass);
this.modules.push(moduleClass);

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

// Server logic
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought - instead of flattening the Graphql implementation into here. Could we not just make this class a dependencyfactory creating the GraphqlServer? This way users could override it by passing a GraphqlServer module if they don't want to use Koa for some reason


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}`);
});
}

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

await new Promise<void>((res) => {
server.close((error) => {
if (error !== undefined) {
log.error(error);
}
res();
});
});
}
}
}
15 changes: 10 additions & 5 deletions packages/cli/src/scripts/graphqlDocs/generateGqlDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default async function (args: {
VanillaProtocolModules,
VanillaRuntimeModules,
} = await import("@proto-kit/library");
const { GraphqlSequencerModule, GraphqlServer, VanillaGraphqlModules } =
const { GraphqlSequencerModule, VanillaGraphqlModules } =
await import("@proto-kit/api");
const { Runtime } = await import("@proto-kit/module");
const { port } = args;
Expand All @@ -30,7 +30,6 @@ export default async function (args: {
Protocol: Protocol.from(VanillaProtocolModules.with({})),
Sequencer: Sequencer.from(
InMemorySequencerModules.with({
GraphqlServer: GraphqlServer,
Graphql: GraphqlSequencerModule.from(VanillaGraphqlModules.with({})),
})
),
Expand All @@ -45,16 +44,22 @@ export default async function (args: {
Sequencer: {
Database: {},
TaskQueue: {},
LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(),
WorkerModule: VanillaTaskWorkerModules.defaultConfig(),
Mempool: {},
BlockProducerModule: {},
SequencerStartupModule: {},
BlockTrigger: { blockInterval: 5000, produceEmptyBlocks: true },
FeeStrategy: {},
BaseLayer: {},
BatchProducerModule: {},
Graphql: VanillaGraphqlModules.defaultConfig(),
GraphqlServer: { port, host: "localhost", graphiql: true },
Graphql: {
...VanillaGraphqlModules.defaultConfig(),
containerConfig: {
port,
host: "localhost",
graphiql: true,
},
},
},
});

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/utils/create-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,10 @@ export function generateWorkerConfig(answers: WizardAnswers): string {
const presetEnv = PRESET_ENV_NAMES[answers.preset];
const taskWorkerImports = answers.settlementEnabled
? ""
: " LocalTaskWorkerModule, VanillaTaskWorkerModules";
: " WorkerModule, VanillaTaskWorkerModules";
const withoutSettlementTask = answers.settlementEnabled
? ""
: `LocalTaskWorkerModule: LocalTaskWorkerModule.from(
: `WorkerModule: WorkerModule.from(
VanillaTaskWorkerModules.withoutSettlement()
),
`;
Expand Down
53 changes: 42 additions & 11 deletions packages/common/src/config/ModuleContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export type ModulesConfig<Modules extends ModulesRecord> = {
: never;
};

export type CombinedModuleContainerConfig<
Modules extends ModulesRecord,
ContainerConfig = NoConfig,
> = ModulesConfig<Modules> & { containerConfig?: ContainerConfig };

/**
* This type make any config partial (i.e. optional) up to the first level
* So { Module: { a: { b: string } } }
Expand Down Expand Up @@ -139,9 +144,18 @@ export interface ModuleContainerLike {
/**
* Reusable module container facilitating registration, resolution
* configuration, decoration and validation of modules
*
* @typeParam Modules - The record of child module classes.
* @typeParam ContainerConfig - Optional config type for keys that belong to the
* container itself (not forwarded to child modules). Defaults to NoConfig.
*/
export class ModuleContainer<Modules extends ModulesRecord>
extends ConfigurableModule<ModulesConfig<Modules>>
export class ModuleContainer<
Modules extends ModulesRecord,
ContainerConfig = NoConfig,
>
extends ConfigurableModule<
CombinedModuleContainerConfig<Modules, ContainerConfig>
>
implements ModuleContainerLike
{
/**
Expand All @@ -159,6 +173,14 @@ export class ModuleContainer<Modules extends ModulesRecord>
super();
}

/**
* Returns the container's own configuration.
*/
public get containerConfig(): ContainerConfig {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return this.config?.containerConfig as ContainerConfig;
}

/**
* @returns list of module names
*/
Expand Down Expand Up @@ -296,25 +318,34 @@ export class ModuleContainer<Modules extends ModulesRecord>
* before the first resolution.
* @param config
*/
public configure(config: ModulesConfig<Modules>) {
public configure(
config: CombinedModuleContainerConfig<Modules, ContainerConfig>
) {
this.config = config;
}

public configurePartial(config: RecursivePartial<ModulesConfig<Modules>>) {
this.config = merge<
ModulesConfig<Modules> | NoConfig,
RecursivePartial<ModulesConfig<Modules>>
>(this.currentConfig ?? {}, config);
public configurePartial(
config: RecursivePartial<
CombinedModuleContainerConfig<Modules, ContainerConfig>
>
) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.config = merge(
this.currentConfig ?? {},
config
) as CombinedModuleContainerConfig<Modules, ContainerConfig>;
}

public get config() {
return super.config;
}

public set config(config: ModulesConfig<Modules>) {
public set config(
config: CombinedModuleContainerConfig<Modules, ContainerConfig>
) {
super.config = merge<
ModulesConfig<Modules> | NoConfig,
ModulesConfig<Modules>
CombinedModuleContainerConfig<Modules, ContainerConfig> | NoConfig,
CombinedModuleContainerConfig<Modules, ContainerConfig>
>(this.currentConfig ?? {}, config);
}

Expand Down
4 changes: 3 additions & 1 deletion packages/common/src/events/EventEmitterProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ export type FlattenedContainerEvents<Modules extends ModulesRecord> =
export class EventEmitterProxy<
Modules extends ModulesRecord,
> extends EventEmitter<CastToEventsRecord<FlattenedContainerEvents<Modules>>> {
public constructor(private readonly container: ModuleContainer<Modules>) {
public constructor(
private readonly container: ModuleContainer<Modules, any>
) {
super();
container.moduleNames.forEach((moduleName) => {
if (container.isValidModuleName(container.definition, moduleName)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/deployment/src/queue/BullQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,6 @@ export class BullQueue
public async close() {
await this.closeQueues();

// Closing of active workers is handled by the LocalTaskWorkerModule
// Closing of active workers is handled by the WorkerModule
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
GraphqlServer,
GraphqlModulesRecord,
GraphqlSequencerModule,
ResolverFactoryGraphqlModule,
graphqlModule,
} from "@proto-kit/api";
Expand Down Expand Up @@ -97,7 +98,8 @@ export function ValidateTakeArg() {
@graphqlModule()
export class GeneratedResolverFactoryGraphqlModule extends ResolverFactoryGraphqlModule {
public constructor(
@inject("GraphqlServer") public graphqlServer: GraphqlServer
@inject("GraphqlServer")
public graphqlServer: GraphqlSequencerModule<GraphqlModulesRecord>
) {
super();
}
Expand Down
Loading
Loading