Skip to content

Commit

Permalink
Switch from tsyringe singletons to injectables
Browse files Browse the repository at this point in the history
  • Loading branch information
aldahick committed May 25, 2024
1 parent e8a5063 commit 8d595c3
Show file tree
Hide file tree
Showing 19 changed files with 388 additions and 44 deletions.
12 changes: 12 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@
}
}
}
},
{
"include": [
"graphql*sdk.ts"
],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
]
}
9 changes: 5 additions & 4 deletions packages/core/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { registry, singleton } from "tsyringe";
export { singleton as injectable };
export { container, inject, injectAll } from "tsyringe";
import { injectable, registry } from "tsyringe";
// export { singleton as injectable };
export { container, inject, injectAll, injectable } from "tsyringe";

export const makeRegistryDecorator =
(token: symbol) => (): ClassDecorator => (target) => {
const targetConstructor = target as unknown as new () => unknown;
singleton()(targetConstructor);
injectable()(targetConstructor);
// singleton()(targetConstructor);
registry([{ token, useClass: targetConstructor }])(targetConstructor);
};
2 changes: 1 addition & 1 deletion packages/core/src/graphql/graphql-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("graphql-decorators", () => {
it("should register a class in the container", () => {
@resolver()
class HelloResolver {}
expect(container.isRegistered(HelloResolver)).toEqual(true);
expect(container.resolve(HelloResolver)).toBeDefined();
});

it("should register a class as a resolver in the container", () => {
Expand Down
18 changes: 11 additions & 7 deletions packages/core/src/graphql/graphql-server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { ApolloServer, ApolloServerOptions, BaseContext } from "@apollo/server";
import fastifyApollo, {
fastifyApolloDrainPlugin,
} from "@as-integrations/fastify";
import { assign, recursiveReaddir } from "@athenajs/utils";
import { assignDeep } from "@athenajs/utils";
import { FastifyInstance } from "fastify";
import { GraphQLFieldResolver } from "graphql";
import { createBatchResolver } from "graphql-resolve-batch";
Expand Down Expand Up @@ -66,20 +67,23 @@ export class GraphQLServer<Context extends BaseContext = BaseContext> {
async getTypeDefs(): Promise<TypeDefs> {
const dirs = this.config.graphqlSchemaDirs.join(", ");
this.logger.debug(`loading graphql type definitions from ${dirs}`);
const schemaPaths = await Promise.all(
this.config.graphqlSchemaDirs.map((d) => recursiveReaddir(d)),
);
return Promise.all(
schemaPaths.flat().map((path) => fs.readFile(path, "utf-8")),
const schemaFiles = await Promise.all(
this.config.graphqlSchemaDirs.map(async (d) =>
fs.readdir(d, { recursive: true, withFileTypes: true }),
),
);
const schemaPaths = schemaFiles
.flat()
.map((file) => path.resolve(file.parentPath, file.name));
return Promise.all(schemaPaths.map((path) => fs.readFile(path, "utf-8")));
}

getResolvers(): Resolvers {
const resolvers: Resolvers = {};
for (const instance of getResolverInstances()) {
for (const info of getResolverInfos(instance)) {
const resolver = this.makeResolver(instance, info);
assign(resolvers, info.typeName, resolver);
assignDeep(resolvers, info.typeName, resolver);
}
}
return resolvers;
Expand Down
12 changes: 12 additions & 0 deletions packages/demo/codegen.graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ const config: CodegenConfig = {
"src/graphql.ts": {
plugins: ["typescript", "typescript-operations"],
},
"src/graphql-sdk.ts": {
documents: "src/**/*.sdk.gql",
plugins: [
"typescript",
"typescript-operations",
"typescript-graphql-request",
],
config: {
noExport: true,
gqlImport: "graphql-request#gql",
},
},
},
};
export default config;
2 changes: 2 additions & 0 deletions packages/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
"dependencies": {
"@athenajs/core": "workspace:*",
"@athenajs/utils": "workspace:*",
"graphql-request": "^6.1.0",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/typescript": "^4.0.6",
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@graphql-codegen/typescript-operations": "^4.2.0"
}
}
6 changes: 4 additions & 2 deletions packages/demo/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { recursiveReaddir } from "@athenajs/utils";
import fs from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { Config } from "./config.js";

Expand All @@ -9,7 +9,9 @@ describe("Config", () => {
it("should point to a dir containing graphql files", async () => {
const files = (
await Promise.all(
config.graphqlSchemaDirs.map((dir) => recursiveReaddir(dir)),
config.graphqlSchemaDirs.map((dir) =>
fs.readdir(dir, { recursive: true }),
),
)
).flat();
expect(files.length > 0).toEqual(true);
Expand Down
118 changes: 118 additions & 0 deletions packages/demo/src/graphql-sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { GraphQLClient, RequestOptions } from "graphql-request";
import { gql } from "graphql-request";
type Maybe<T> = T | undefined;
type InputMaybe<T> = T | undefined;
type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
[SubKey in K]?: Maybe<T[SubKey]>;
};
type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
[SubKey in K]: Maybe<T[SubKey]>;
};
type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = {
[_ in K]?: never;
};
type Incremental<T> =
| T
| {
[P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never;
};
type GraphQLClientRequestHeaders = RequestOptions["requestHeaders"];
/** All built-in and custom scalars, mapped to their actual values */
type Scalars = {
ID: { input: string; output: string };
String: { input: string; output: string };
Boolean: { input: boolean; output: boolean };
Int: { input: number; output: number };
Float: { input: number; output: number };
};

type IQuery = {
__typename?: "Query";
hello: Scalars["String"]["output"];
users: IUser[];
};

type IUser = {
__typename?: "User";
id: Scalars["ID"]["output"];
username: Scalars["String"]["output"];
};

type IHelloQueryVariables = Exact<{ [key: string]: never }>;

type IHelloQuery = { __typename?: "Query"; hello: string };

type IGetUsersQueryVariables = Exact<{ [key: string]: never }>;

type IGetUsersQuery = {
__typename?: "Query";
users: Array<{ __typename?: "User"; id: string; username: string }>;
};

const HelloDocument = gql`
query hello {
hello
}
`;
const GetUsersDocument = gql`
query getUsers {
users {
id
username
}
}
`;

export type SdkFunctionWrapper = <T>(
action: (requestHeaders?: Record<string, string>) => Promise<T>,
operationName: string,
operationType?: string,
variables?: any,
) => Promise<T>;

const defaultWrapper: SdkFunctionWrapper = (
action,
_operationName,
_operationType,
_variables,
) => action();

export function getSdk(
client: GraphQLClient,
withWrapper: SdkFunctionWrapper = defaultWrapper,
) {
return {
hello(
variables?: IHelloQueryVariables,
requestHeaders?: GraphQLClientRequestHeaders,
): Promise<IHelloQuery> {
return withWrapper(
(wrappedRequestHeaders) =>
client.request<IHelloQuery>(HelloDocument, variables, {
...requestHeaders,
...wrappedRequestHeaders,
}),
"hello",
"query",
variables,
);
},
getUsers(
variables?: IGetUsersQueryVariables,
requestHeaders?: GraphQLClientRequestHeaders,
): Promise<IGetUsersQuery> {
return withWrapper(
(wrappedRequestHeaders) =>
client.request<IGetUsersQuery>(GetUsersDocument, variables, {
...requestHeaders,
...wrappedRequestHeaders,
}),
"getUsers",
"query",
variables,
);
},
};
}
export type Sdk = ReturnType<typeof getSdk>;
4 changes: 2 additions & 2 deletions packages/demo/src/hello/hello-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe("HelloController", () => {
describe("hello()", () => {
it("should return a polite, understated greeting", async () => {
const expected = { hello: "Hello, world!" };
await withTestApp(async (url) => {
await withTestApp(async (sdk, url) => {
const res = await fetch(`${url}/hello`);
expect(res.status).toEqual(200);
expect(await res.json()).toEqual(expected);
Expand All @@ -17,7 +17,7 @@ describe("HelloController", () => {
it("should take a file and respond briskly", async () => {
const body = new FormData();
body.set("file", new Blob(["Hello, world!"]));
await withTestApp(async (url) => {
await withTestApp(async (sdk, url) => {
const res = await fetch(`${url}/hello`, {
method: "POST",
body,
Expand Down
3 changes: 3 additions & 0 deletions packages/demo/src/hello/hello.sdk.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query hello {
hello
}
12 changes: 3 additions & 9 deletions packages/demo/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ import { withTestApp } from "./test-util.js";

describe("main", () => {
it("should start a working GraphQL server", async () => {
await withTestApp(async (url) => {
const res = await fetch(`${url}/graphql`, {
method: "POST",
body: JSON.stringify({ query: "query { hello }" }),
headers: {
"Content-Type": "application/json",
},
}).then((r) => r.json());
expect(res).toEqual({ data: { hello: "hello, world!" } });
await withTestApp(async (sdk) => {
const { hello } = await sdk.hello();
expect(hello).toEqual("hello, world!");
});
});
});
9 changes: 6 additions & 3 deletions packages/demo/src/test-util.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { randomInt } from "node:crypto";
import { Application, container } from "@athenajs/core";
import { container } from "@athenajs/core";
import { GraphQLClient } from "graphql-request";
import { Sdk, getSdk } from "./graphql-sdk.js";
import { main } from "./main.js";

export const withTestApp = async (
callback: (url: string, app: Application) => Promise<void>,
callback: (sdk: Sdk, url: string) => Promise<void>,
) => {
const port = randomInt(16384, 65536).toString();
process.env.HTTP_PORT = port;
const app = await main();
const url = `http://localhost:${port}`;
try {
await callback(`http://localhost:${port}`, app);
await callback(getSdk(new GraphQLClient(`${url}/graphql`)), url);
} finally {
await app.stop();
container.clearInstances();
Expand Down
44 changes: 32 additions & 12 deletions packages/demo/src/user/user-resolver.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import { createHash } from "node:crypto";
import { container } from "@athenajs/core";
import { describe, expect, it } from "vitest";
import { withTestApp } from "../test-util.js";
import { UserResolver } from "./user-resolver.js";
import { UserService } from "./user-service.js";

const ids = {
foo: createHash("md5").update("foo").digest("hex"),
bar: createHash("md5").update("bar").digest("hex"),
};

describe("UserResolver", () => {
const userResolver = new UserResolver();
describe("users()", () => {
it("should return some users with usernames", async () => {
const expected = [{ username: "foo" }, { username: "bar" }];
const actual = await userResolver.users();
expect(actual).toEqual(expected);
it("should resolve IDs", async () => {
await withTestApp(async (sdk) => {
const { users } = await sdk.getUsers();
expect(users).toEqual([
{
id: ids.foo,
username: "foo",
},
{
id: ids.bar,
username: "bar",
},
]);
});
});
});

describe("#id", () => {
it("should return md5 hashes of usernames", async () => {
// md5 of my name. this is a demo
const expected = ["534b44a19bf18d20b71ecc4eb77c572f"];
const actual = await userResolver.id([{ username: "alex" }]);
expect(actual).toEqual(expected);
it("should return mocked data", async () => {
container.register(UserService, {
useValue: {
getMany: () => Promise.resolve([{ username: "mocked" }]),
},
});
const resolver = container.resolve(UserResolver);
const users = await resolver.users();
expect(users).toEqual([{ username: "mocked" }]);
});
});
});
9 changes: 6 additions & 3 deletions packages/demo/src/user/user-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import crypto from "node:crypto";
import { resolveField, resolveQuery, resolver } from "@athenajs/core";
import { IUser } from "../graphql.js";
import { UserService } from "./user-service.js";

@resolver()
export class UserResolver {
constructor(private readonly userService: UserService) {}

@resolveQuery()
users() {
return Promise.resolve([{ username: "foo" }, { username: "bar" }]);
async users() {
return await this.userService.getMany();
}

@resolveField("User.id", true)
async id(users: Omit<IUser, "id">[]): Promise<IUser["id"][]> {
async id(users: IUser[]): Promise<IUser["id"][]> {
return users.map((u) =>
crypto.createHash("md5").update(u.username).digest("hex"),
);
Expand Down
Loading

0 comments on commit 8d595c3

Please sign in to comment.