Skip to content

Commit

Permalink
🔧
Browse files Browse the repository at this point in the history
  • Loading branch information
divmgl committed Oct 29, 2023
1 parent 935ce27 commit 39eff48
Show file tree
Hide file tree
Showing 24 changed files with 980 additions and 442 deletions.
14 changes: 5 additions & 9 deletions packages/example-fastify/src/SQLiteTaskStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@ import { Service } from "./Service"

export class SQLiteTaskStore extends Service implements TaskStore {
async get(id: number): Promise<Task | null> {
return (
(await this.context.db.get(`SELECT * FROM tasks WHERE id = ?`, [id])) ??
null
)
return (await this.db.get(`SELECT * FROM tasks WHERE id = ?`, [id])) ?? null
}

async save(title: string): Promise<Task> {
const insert = await this.context.db.run(
`INSERT INTO tasks (title) VALUES (?);`,
[title]
)
const insert = await this.db.run(`INSERT INTO tasks (title) VALUES (?);`, [
title,
])

if (!insert.lastID) throw new Error("unable to save task")

return (await this.get(insert.lastID))!
}

async delete(id: number): Promise<void> {
await this.context.db.run(`DELETE FROM tasks WHERE id = ?`, [id])
await this.db.run(`DELETE FROM tasks WHERE id = ?`, [id])
}
}
10 changes: 9 additions & 1 deletion packages/example-fastify/src/Service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Injected } from "nwire"
import { AppContext } from "./AppContext"

export class Service extends Injected<AppContext> {}
export class Service extends Injected<AppContext> {
get db() {
return this._context.db
}

get tasks() {
return this._context.tasks
}
}
4 changes: 2 additions & 2 deletions packages/example-fastify/src/TasksCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Service } from "./Service"

export class TasksCreator extends Service {
async createBasicTasks() {
await this.context.tasks.save("My first test")
await this.context.tasks.save("My second test")
await this.tasks.save("My first test")
await this.tasks.save("My second test")
}
}
3 changes: 2 additions & 1 deletion packages/example-fastify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ start()
// Can use top-level `await` in ESM.
async function start() {
try {
const server = createServer(await createContext())
const context = await createContext()
const server = createServer(context)

server.listen({ port: 3000 }, (err, address) => {
if (err) {
Expand Down
2 changes: 1 addition & 1 deletion packages/example-hello-world/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type MyTypedContext = {

export class MyService extends Injected<MyTypedContext> {
helloWorld() {
return this.context.banner
return this._context.banner
}
}

Expand Down
39 changes: 21 additions & 18 deletions packages/nwire/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ type MyTypedContext = {

export class MyService extends Injected<MyTypedContext> {
helloWorld() {
return this.context.banner
return this._context.banner
}
}

const context = Container.register("banner", () => "Hello world!")
const context = Container.new()
.register("banner", () => "Hello world!")
.instance("my", MyService)
.context()

Expand Down Expand Up @@ -52,7 +53,7 @@ const context = container.context()
In a majority of cases you'll be creating a single container, registering a bunch of dependencies, and then grabbing the generated `Context`. For this reason we've included static methods that return a new container and are chainable, so you can write your code like this instead:

```tsx
const context = Container
const context = Container.new()
.register("prisma", () => new PrismaClient())
.register("redis", () => new Redis())
.context()
Expand All @@ -79,7 +80,7 @@ Container.register("users", (context) => new UsersService(context)) // => Contai
> ⚠️ The `Context` that's sent to the dependency will be fully setup, but this may not match what the compiler sees as TypeScript is only able to gather what's been currently registered. For instance, the following results in a compiler error:
```tsx
const context = Container
const context = Container.new()
.register("tasksCreator", (context) => new TasksCreator(context))
// Argument of type '{}' is not assignable to parameter of type 'AppContext'.
// Type '{}' is missing the following properties from type 'AppContext': tasks, tasksCreator
Expand All @@ -90,7 +91,7 @@ const context = Container

However, a method is included to avoid this boilerplate altogether:

#### `Container.instance`
#### `Container.singleton`

Your goal will often be to simply pass in the fully resolved `Context` to classes. For this reason, `nwire` provides a function that will create a new instance of your class with a fully resolved `Context` whenever the dependency is resolved:

Expand Down Expand Up @@ -119,8 +120,7 @@ Container.instance("users", UsersService, { cookieSecret: process.env.COOKIE_SEC
Sometimes you'll want to group things together within the `Container`. You could technically do this:

```tsx
const context = Container
//
const context = Container.new()
.register("services", (context) => ({
users: new UsersService(context),
tasks: new TasksService(context),
Expand All @@ -139,11 +139,9 @@ However, this has a big issue: once you access `service` for the first time you
`nwire` provides a solution for this: `Container.group`. `Container.group` creates a nested `Container` that will only resolve when you access properties within it. The nested container will be passed as the first argument to the function you pass in:

```tsx
const context = Container
//
const context = Container.new()
.group("services", (services: Container) =>
services
//
.singleton("users", UsersService)
.singleton("tasks", TasksService)
)
Expand All @@ -166,14 +164,18 @@ type AppContext = {
context.services.users.findOne("123")
```
#### `Container.instance`
An alias for `Container.singleton`.
### `Context`
The `Context` class is the dependency proxy that the `Container` produces. This class allows you to access your dependencies using the names you registered them with:
```tsx
const context = Container
const context = Container.new()
.register("users" /** Registry name */, () => new UsersService())
.context()
.context() // Proxy created at this point

const user = await context.users.findOne("123")
```
Expand Down Expand Up @@ -201,7 +203,7 @@ export class MyService extends Injected<MyTypedContext> {
This class will fit into the `Container.prototype.instance` API:

```tsx
const context = Container
const context = Container.new()
.register("banner", () => "Hello world!")
.instance("my", MyService) // No type errors
.context()
Expand All @@ -214,7 +216,7 @@ context.my.helloWorld() // => console output: "Hello world!"
Creates a new `Context` class. This is the class you're meant to pass around to all of your dependencies. It's responsible for resolving dependencies:

```tsx
const context = Container
const context = Container.new()
// ... lots of registrations here
.register("users", () => new UsersService())
.context()
Expand All @@ -236,7 +238,7 @@ export type AppContext = {
tasksCreator: TasksCreator
}

const context = Container
const context = Container.new()
.register("tasksCreator", (context) => new TasksCreator(context))
.register("tasks", (context) => new SQLiteTaskStore(context))
.context<AppContext>()
Expand All @@ -259,7 +261,7 @@ This will cause the compiler to completely brick in any service that uses the `A
You'll also run into issues if you attempt to pass a partial context during registration to constructor:
```tsx
export const context = Container
export const context = Container.new()
.register("users", (contextUpToThisPoint) => new UsersService(contextUpToThisPoint))
// Argument of type '{}' is not assignable to parameter of type 'AppContext'.
// Type '{}' is missing the following properties from type 'AppContext': users
Expand Down Expand Up @@ -296,7 +298,7 @@ container.resolve<RandomizerClass>("randomizer").id // => 248
There is currently no API for transient `instance` registrations, so if you do want to create a unique instance on every call you'll need to provide an initial context:

```tsx
const context = Container.build<AppContext>
const context = Container.new<AppContext>()
.register("users", (context) => new UsersService(context), { transient: true }))
.context()
```
Expand Down Expand Up @@ -381,7 +383,8 @@ and when a dependency is needed the framework will resolve it for you.
Consider the previous example in `nwire`:

```tsx
const context = Container.singleton("users", UsersService)
const context = Container.new()
.singleton("users", UsersService)
.register("prisma", new PrismaClient())
.register("psql", new Postgres())

Expand Down
40 changes: 40 additions & 0 deletions packages/nwire/dist/Container.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export type Context = {
[key: string]: unknown;
};
export type Instance<TValue> = {
new (context: any, ...args: any[]): TValue;
};
type Flatten<T> = {} & {
[P in keyof T]: T[P];
};
type AppendContext<TExisting, TKey extends string, TValue> = Flatten<TExisting & {
[P in TKey]: TValue;
}>;
type RegistrationOptions = {
transient?: boolean;
};
export declare class Container<TContext extends Context = {}> {
private _registry;
private _resolvers;
private _cache;
private _transient;
private _base;
private _rootContainer;
private _parentContainer;
constructor(rootContainer?: Container, _parentContainer?: Container);
get root(): this | Container<{}>;
get parent(): this | Container<{}>;
static new<T extends Context = {}>(): Container<T>;
static build<T extends Context = {}>(): Container<T>;
base<TBase extends Context>(base: TBase): Container<TContext & TBase>;
createContextProxy(): Context;
context<TWriteContext extends Context = TContext, TOverride extends Context = {}>(override?: TOverride | {}): Flatten<TWriteContext & TOverride>;
group<TNewKey extends string, TNewContext extends Context>(key: TNewKey, decorator: (container: Container<{}>) => Container<TNewContext>): Container<TContext & { [key in TNewKey]: TNewContext; }>;
singleton<TNewKey extends string, TValue>(key: TNewKey, ClassConstructor: Instance<TValue>, ...args: any[]): Container<TContext & { [P_1 in TNewKey]: TValue; } extends infer T ? { [P in keyof T]: (TContext & { [P_1 in TNewKey]: TValue; })[P]; } : never>;
instance<TNewKey extends string, TValue>(key: TNewKey, ClassConstructor: Instance<TValue>, ...args: any[]): Container<TContext & { [P_1 in TNewKey]: TValue; } extends infer T ? { [P in keyof T]: (TContext & { [P_1 in TNewKey]: TValue; })[P]; } : never>;
register<TNewKey extends string, TValue>(key: TNewKey, resolver: (context: TContext) => TValue, { transient }?: RegistrationOptions): Container<AppendContext<TContext, TNewKey, TValue>>;
unregister<TNewKey extends string>(key: TNewKey): Container<Omit<TContext, TNewKey>>;
resolve<TValue>(key: keyof TContext): TValue;
middleware<TNewContext extends Context>(middleware: (container: Container<TContext>) => Container<TNewContext>): Container<TNewContext>;
}
export {};
12 changes: 12 additions & 0 deletions packages/nwire/dist/CountingSet.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export declare class CountingSet<T> {
private readonly _map;
private readonly _set;
add(value: T): this;
delete(value: T): boolean;
has(value: T): boolean;
count(value: T): number;
clear(): void;
get size(): number;
[Symbol.iterator](): Iterator<T>;
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void;
}
10 changes: 10 additions & 0 deletions packages/nwire/dist/Singleton.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Context } from "./Container";
type PopulatedSingleton<T> = T & {
[key in keyof T]: T[key];
};
export declare function WithContextProperties<T extends Context>(Base: any): new (context: T) => PopulatedSingleton<T>;
export declare class Singleton<TContext extends Context> {
protected _context: TContext;
constructor(context: TContext);
}
export {};
Loading

0 comments on commit 39eff48

Please sign in to comment.