Skip to content

Commit

Permalink
Refactoring and fixed issues with double resolving modules
Browse files Browse the repository at this point in the history
  • Loading branch information
rpanic committed Oct 25, 2023
1 parent 50f64ef commit bd9f9a9
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 40 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
},
"dependencies": {
"@graphql-tools/stitch": "^9.0.3",
"lodash": "^4.17.21",
"reflect-metadata": "^0.1.13",
"@types/humanize-duration": "^3.27.2",
"class-validator": "^0.14.0",
"graphql": "16.6.0",
"graphql-scalars": "^1.22.4",
"humanize-duration": "^3.30.0",
"lodash": "^4.17.21",
"reflect-metadata": "^0.1.13",
"type-graphql": "2.0.0-beta.1"
},
"peerDependencies": {
Expand Down
39 changes: 36 additions & 3 deletions packages/api/src/graphql/GraphqlModule.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
import type { UnTypedClass } from "@proto-kit/protocol";
import { ConfigurableModule } from "@proto-kit/common";
import {
ConfigurableModule,
TypedClass,
} from "@proto-kit/common";
import { GraphQLSchema } from "graphql/type";
import { injectable, Lifecycle, scoped } from "tsyringe";
import { Resolver } from "type-graphql";

const graphqlModuleMetadataKey = "graphqlModule";

export abstract class GraphqlModule<Config> extends ConfigurableModule<Config> {
public abstract resolverType: UnTypedClass;
public constructor() {
super();

const isDecoratedProperly =
Reflect.getMetadata(graphqlModuleMetadataKey, this.constructor) === true;
if (!isDecoratedProperly) {
throw new Error(
`Module ${this.constructor.name} not decorated property. Make sure to use @graphqlModule() on all GraphqlModules`
);
}
}
}

export abstract class SchemaGeneratingGraphqlModule<
Config
> extends GraphqlModule<Config> {
public abstract generateSchema(): GraphQLSchema;
}

export function graphqlModule() {
return (
/**
* Check if the target class extends RuntimeModule, while
* also providing static config presets
*/
target: TypedClass<GraphqlModule<unknown>>
) => {
injectable()(target);
scoped(Lifecycle.ContainerScoped)(target);
// eslint-disable-next-line new-cap
Resolver()(target);

Reflect.defineMetadata(graphqlModuleMetadataKey, true, target);
};
}
15 changes: 11 additions & 4 deletions packages/api/src/graphql/GraphqlSequencerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@proto-kit/common";

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

export type GraphqlModulesRecord = ModulesRecord<any>;

Expand Down Expand Up @@ -52,11 +52,18 @@ export class GraphqlSequencerModule<GraphQLModules extends GraphqlModulesRecord>

// eslint-disable-next-line guard-for-in
for (const moduleName in this.definition.modules) {
const module: GraphqlModule<unknown> = this.resolve(moduleName);
this.graphqlServer.registerModule(module);
const moduleClass = this.definition.modules[moduleName];
this.graphqlServer.registerModule(moduleClass);

if (module instanceof SchemaGeneratingGraphqlModule) {
if (
Object.prototype.isPrototypeOf.call(
SchemaGeneratingGraphqlModule,
moduleClass
)
) {
log.debug(`Registering manual schema for ${moduleName}`);
const module: SchemaGeneratingGraphqlModule<unknown> =
this.resolve(moduleName);
this.graphqlServer.registerSchema(module.generateSchema());
}
}
Expand Down
27 changes: 19 additions & 8 deletions packages/api/src/graphql/GraphqlServer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildSchemaSync } from "type-graphql";
import { buildSchemaSync, NonEmptyArray } from "type-graphql";
import { DependencyContainer, injectable } from "tsyringe";
import { SequencerModule } from "@proto-kit/sequencer";
import { log, noop } from "@proto-kit/common";
import { log, noop, TypedClass } from "@proto-kit/common";
import { GraphQLSchema } from "graphql/type";
import { stitchSchemas } from "@graphql-tools/stitch";
import { createYoga } from "graphql-yoga";
Expand All @@ -14,9 +14,18 @@ interface GraphqlServerOptions {
port: number;
}

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

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

private readonly schemas: GraphQLSchema[] = [];

Expand All @@ -34,7 +43,7 @@ export class GraphqlServer extends SequencerModule<GraphqlServerOptions> {
}
}

public registerModule(module: GraphqlModule<unknown>) {
public registerModule(module: TypedClass<GraphqlModule<unknown>>) {
this.modules.push(module);
}

Expand All @@ -50,12 +59,14 @@ export class GraphqlServer extends SequencerModule<GraphqlServerOptions> {
const { dependencyContainer, modules } = this;
this.assertDependencyContainerSet(dependencyContainer);

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

// Building schema
const resolverSchema = buildSchemaSync({
resolvers: [
modules[0].resolverType,
...modules.slice(1).map((x) => x.resolverType),
],
resolvers: modules,

// resolvers: [MempoolResolver as Function],
// eslint-disable-next-line max-len
Expand Down
7 changes: 2 additions & 5 deletions packages/api/src/graphql/modules/BlockStorageResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ComputedBlockTransaction,
} from "@proto-kit/sequencer";

import { GraphqlModule } from "../GraphqlModule";
import { graphqlModule, GraphqlModule } from "../GraphqlModule";

import { TransactionObject } from "./MempoolResolver";

Expand Down Expand Up @@ -72,11 +72,8 @@ export class ComputedBlockModel {
}
}

@injectable()
@Resolver(ComputedBlockModel)
@graphqlModule()
export class BlockStorageResolver extends GraphqlModule<object> {
public resolverType = BlockStorageResolver;

// TODO seperate these two block interfaces
public constructor(
@inject("BlockStorage")
Expand Down
7 changes: 2 additions & 5 deletions packages/api/src/graphql/modules/MempoolResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { inject, injectable } from "tsyringe";
import { IsNumberString } from "class-validator";
import { Mempool, PendingTransaction } from "@proto-kit/sequencer";

import { GraphqlModule } from "../GraphqlModule.js";
import { graphqlModule, GraphqlModule } from "../GraphqlModule.js";

@ObjectType()
@InputType("SignatureInput")
Expand Down Expand Up @@ -71,11 +71,8 @@ export class TransactionObject {
}
}

@injectable()
@Resolver(TransactionObject)
@graphqlModule()
export class MempoolResolver extends GraphqlModule<object> {
public resolverType = MempoolResolver;

private readonly mempool: Mempool;

public constructor(@inject("Mempool") mempool: Mempool) {
Expand Down
55 changes: 55 additions & 0 deletions packages/api/src/graphql/modules/NodeStatusResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Field, ObjectType, Query } from "type-graphql";
import { ChildContainerProvider } from "@proto-kit/common";

import { graphqlModule, GraphqlModule } from "../GraphqlModule";
import { NodeStatus, NodeStatusService } from "../services/NodeStatusService";

@ObjectType()
export class NodeStatusObject {
public static fromServiceLayerModel(status: NodeStatus) {
return new NodeStatusObject(
status.uptime,
status.uptimeHumanReadable,
status.height
);
}

@Field()
public uptime: number;

@Field()
public height: number;

@Field()
public uptimeHumanReadable: string;

public constructor(
uptime: number,
uptimeHumanReadable: string,
height: number
) {
this.uptime = uptime;
this.uptimeHumanReadable = uptimeHumanReadable;
this.height = height;
}
}

@graphqlModule()
export class NodeStatusResolver extends GraphqlModule<object> {
public constructor(private readonly nodeStatusService: NodeStatusService) {
super();
}

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

// Workaround to initialize uptime
void this.nodeStatusService.getNodeStatus();
}

@Query(() => NodeStatusObject)
public async nodeStatus(): Promise<NodeStatusObject> {
const status = await this.nodeStatusService.getNodeStatus();
return NodeStatusObject.fromServiceLayerModel(status);
}
}
9 changes: 2 additions & 7 deletions packages/api/src/graphql/modules/QueryGraphqlModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
NetworkStateQuery,
BlockStorage
} from "@proto-kit/sequencer";
import { SchemaGeneratingGraphqlModule } from "../GraphqlModule";
import { graphqlModule, SchemaGeneratingGraphqlModule } from "../GraphqlModule";
import {
BaseModuleType,
log,
Expand All @@ -62,13 +62,10 @@ interface AnyJson {
[key: string]: any;
}

@injectable()
@Resolver()
@graphqlModule()
export class QueryGraphqlModule<
RuntimeModules extends RuntimeModulesRecord
> extends SchemaGeneratingGraphqlModule<object> {
public resolverType = QueryGraphqlModule;

public constructor(
@inject("QueryTransportModule")
private readonly queryTransportModule: QueryTransportModule,
Expand Down Expand Up @@ -265,8 +262,6 @@ export class QueryGraphqlModule<

if (stateProperty instanceof StateMap) {
// StateMap
console.log("StateMap");

moduleTypes[fieldKey] = this.generateStateMapResolver(
`${namePrefix}${fieldKey}`,
query[fieldKey],
Expand Down
31 changes: 31 additions & 0 deletions packages/api/src/graphql/services/NodeStatusService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { inject, injectable } from "tsyringe";
import { BlockStorage } from "@proto-kit/sequencer";
import humanizeDuration from "humanize-duration";

export interface NodeStatus {
uptime: number;
uptimeHumanReadable: string;
height: number;
}

@injectable()
export class NodeStatusService {
private readonly startupTime = Date.now();

public constructor(
@inject("BlockStorage") private readonly blockStorage: BlockStorage
) {
}

public async getNodeStatus(): Promise<NodeStatus> {
const uptime = Date.now() - this.startupTime;
const uptimeHumanReadable = humanizeDuration(uptime);
const height = await this.blockStorage.getCurrentBlockHeight();

return {
uptime,
uptimeHumanReadable,
height,
};
}
}
2 changes: 2 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export * from "./graphql/modules/BlockStorageResolver";
export * from "./graphql/GraphqlModule";
export * from "./graphql/GraphqlServer";
export * from "./graphql/GraphqlSequencerModule";
export * from "./graphql/modules/NodeStatusResolver";
export * from "./graphql/services/NodeStatusService";
2 changes: 2 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// allows to reference interfaces as 'classes' rather than instances
export type TypedClass<Class> = new (...args: any[]) => Class;

export type UnTypedClass = new (...args: any[]) => any;

/**
* Using simple `keyof Target` would result into the key
* being `string | number | symbol`, but we want just a `string`
Expand Down
2 changes: 0 additions & 2 deletions packages/protocol/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ export type ReturnType<FunctionType extends Function> = FunctionType extends (
? Return
: any;

export type UnTypedClass = new (...args: any[]) => any;

export type TypedClass<Class> = new (...args: any[]) => Class;

export type Subclass<Class extends new (...args: any) => any> = (new (
Expand Down
Loading

0 comments on commit bd9f9a9

Please sign in to comment.