@nxara/olio-di 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,103 +1,11 @@
|
|
|
1
1
|
# @nxara/olio-di
|
|
2
2
|
|
|
3
|
-
Typed dependency injection for olio apps, on
|
|
4
|
-
|
|
5
|
-
Modules already share code through plain imports. DI adds what imports can't:
|
|
6
|
-
|
|
7
|
-
- **Per-operation instances:** a DataLoader, a transaction or a "current user" repository, one per request and shared by every resolver in it.
|
|
8
|
-
- **Swapping implementations in tests,** without rewiring imports.
|
|
9
|
-
- **Lifecycle:** lazy creation, and cleanup when an operation or the app ends.
|
|
10
|
-
|
|
11
|
-
Awilix does the work underneath: lifetimes, lazy proxy injection, catching a singleton that holds a per-operation instance, cycle detection, disposal. This package puts typed providers on top: resolvers reach services by name, with autocomplete, and a typo is a compile error.
|
|
3
|
+
Typed dependency injection for olio apps, on Awilix: singletons and per-operation instances, reached as `ctx.di`.
|
|
12
4
|
|
|
13
5
|
```sh
|
|
14
6
|
bun add @nxara/olio-di
|
|
15
7
|
```
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Providers are plain values, declared next to the code that uses them. A factory asks for what it needs with `get(Provider)`.
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { operation, singleton } from "@nxara/olio-di";
|
|
23
|
-
|
|
24
|
-
export const Db = singleton(() => new Pool(env.DATABASE_URL), {
|
|
25
|
-
dispose: (pool) => pool.end()
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
export const ProductLoader = operation(
|
|
29
|
-
({ get }) =>
|
|
30
|
-
new DataLoader((skus: readonly string[]) => get(Db).products.bySkus(skus))
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
export const Orders = operation(
|
|
34
|
-
({ get, ctx }) => new OrderRepo(get(Db), ctx.viewer)
|
|
35
|
-
);
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
| | Lives | Its factory gets |
|
|
39
|
-
| --- | --- | --- |
|
|
40
|
-
| `singleton(factory, { dispose? })` | As long as the container | `{ get }`, for other singletons only |
|
|
41
|
-
| `operation(factory, { dispose? })` | One operation (one scope) | `{ get, ctx }`, for any provider, plus the operation's context |
|
|
42
|
-
|
|
43
|
-
A singleton that asks for an operation provider is a type error, and Awilix rejects it at runtime too. So do cycles (`Cyclic dependencies detected`).
|
|
44
|
-
|
|
45
|
-
## With olio
|
|
46
|
-
|
|
47
|
-
Tell DI what an operation's context looks like, the same way you register olio's:
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
declare module "@nxara/olio-di" {
|
|
51
|
-
interface Register {
|
|
52
|
-
ctx: RequestInfo; // what your context has, apart from `di` itself
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Create one container, naming the services resolvers use. Give every operation a scope in its context, and dispose the scope when the operation ends:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
// di.ts: the keys are the names. Providers these depend on (Db) needn't be listed.
|
|
61
|
-
export const container = createContainer({
|
|
62
|
-
productLoader: ProductLoader,
|
|
63
|
-
orders: Orders
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// in your context type
|
|
67
|
-
di: ScopeOf<typeof container>;
|
|
68
|
-
|
|
69
|
-
fetchHandler(app, {
|
|
70
|
-
context: (request) => {
|
|
71
|
-
const info = requestInfo(request);
|
|
72
|
-
return { ...info, di: container.scope(info) };
|
|
73
|
-
},
|
|
74
|
-
onOperationEnd: (ctx) => ctx.di.dispose() // @nxara/olio-http
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// in a resolver
|
|
78
|
-
product: (line, _args, ctx) => ctx.di.productLoader.load(line.sku); // typed, autocompletes, created on first access
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Subscriptions work the same way: give each one a scope in its context and dispose it when it ends. With graphql-ws that's its `context(ctx, id)` hook, which runs once per subscription, and `onComplete`; with graphql-sse, `context` and `onComplete`. `@nxara/olio-http`'s README shows both.
|
|
82
|
-
|
|
83
|
-
## Tests
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
const container = createContainer(services, { override: [override(Db, fakeDb)] }); // fakeDb must fit Db
|
|
87
|
-
await using scope = container.scope(testInfo); // disposed at the end of the block
|
|
88
|
-
expect(await scope.orders.list()).toEqual([...]);
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
`dispose` and `ctx` are reserved and can't be service names. Factories use tokens (`get(Db)`) rather than names: typing a factory's dependencies from the list it's part of is circular, and TypeScript can't resolve it.
|
|
92
|
-
|
|
93
|
-
Each container is independent, so tests don't share singletons.
|
|
94
|
-
|
|
95
|
-
## API
|
|
9
|
+
**[Read the docs](https://olio.nxara.dev/reference/di/)**
|
|
96
10
|
|
|
97
|
-
|
|
98
|
-
| --- | --- |
|
|
99
|
-
| `createContainer(services, { override? })` | The root: `scope(ctx)`, `dispose()`, `Symbol.asyncDispose` |
|
|
100
|
-
| `scope.<name>` | An instance for this operation: its own for `operation` providers, the shared one for singletons |
|
|
101
|
-
| `ScopeOf<typeof container>` | The scope's type, for `ctx.di` |
|
|
102
|
-
| `scope.dispose()` | Disposes this operation's instances, newest first. Also `Symbol.asyncDispose` |
|
|
103
|
-
| `override(provider, value)` | A typed replacement for tests |
|
|
11
|
+
Part of [olio](https://olio.nxara.dev): modular, type-safe GraphQL for TypeScript. MIT licensed.
|