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
161 changes: 144 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.ownConfig;
}

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

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

public create(childContainerProvider: ChildContainerProvider) {
super.create(childContainerProvider);
// Configure own config keys
["host", "port", "graphiql"].forEach((key) => {
this.ownConfigKeys.add(key);
});
}

this.graphqlServer = this.container.resolve("GraphqlServer");
public setContext(newContext: {}) {
this.context = newContext;
}

public async start(): Promise<void> {
assert(this.graphqlServer !== undefined);
public registerResolvers(resolvers: NonEmptyArray<Function>) {
if (this.resolvers === undefined) {
this.resolvers = resolvers;
} else {
this.resolvers = [...this.resolvers, ...resolvers];
}
}

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

public async start(): Promise<void> {
// eslint-disable-next-line guard-for-in
for (const moduleName in this.definition) {
const moduleClass = this.definition[moduleName];
Expand All @@ -65,9 +117,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 +132,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();
});
});
}
}
}
13 changes: 8 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,20 @@ 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(),
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
59 changes: 48 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,
OwnConfig = NoConfig,
> = OwnConfig & ModulesConfig<Modules>;

/**
* 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,16 @@ export interface ModuleContainerLike {
/**
* Reusable module container facilitating registration, resolution
* configuration, decoration and validation of modules
*
* @typeParam Modules - The record of child module classes.
* @typeParam OwnConfig - 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,
OwnConfig = NoConfig,
>
extends ConfigurableModule<CombinedModuleContainerConfig<Modules, OwnConfig>>
implements ModuleContainerLike
{
/**
Expand All @@ -155,10 +167,28 @@ export class ModuleContainer<Modules extends ModulesRecord>

private eventEmitterProxy: EventEmitterProxy<Modules> | undefined = undefined;

/**
* Set of config key names that belong to the container
*/
protected readonly ownConfigKeys: Set<string> = new Set();

public constructor(public definition: Modules) {
super();
}

/**
* Returns an object containing the keys listed in ownConfigKeys.
*/
public get ownConfig(): OwnConfig {
Copy link
Member

Choose a reason for hiding this comment

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

In general, I think this is good, a few notes:
Let's replace the ownConfigKeys mechanism by maing the OwnConfig type transform to { ownConfig: OwnConfig } when passing it to ConfigurableModule (in the intersection type CombinedModuleContainerConfig) and then just accessing this.config.ownConfig

Copy link
Member

Choose a reason for hiding this comment

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

Also let's rename ownConfig to containerConfig maybe? Or is that too misleading?

const fullConfig = this.config;
const result: Record<string, unknown> = {};
for (const key of this.ownConfigKeys) {
result[key] = fullConfig[key];
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return result as OwnConfig;
}

/**
* @returns list of module names
*/
Expand Down Expand Up @@ -296,25 +326,28 @@ export class ModuleContainer<Modules extends ModulesRecord>
* before the first resolution.
* @param config
*/
public configure(config: ModulesConfig<Modules>) {
public configure(config: CombinedModuleContainerConfig<Modules, OwnConfig>) {
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, OwnConfig>>
) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.config = merge(
this.currentConfig ?? {},
config
) as CombinedModuleContainerConfig<Modules, OwnConfig>;
}

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

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

Expand Down Expand Up @@ -365,6 +398,10 @@ export class ModuleContainer<Modules extends ModulesRecord>
moduleName: StringKeyOf<Modules>,
containedModule: InstanceType<Modules[StringKeyOf<Modules>]>
) {
if (this.ownConfigKeys.has(moduleName)) {
return;
}

const config = super.config?.[moduleName];
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!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
Loading
Loading