Skip to content

Commit

Permalink
[dsch] module examples
Browse files Browse the repository at this point in the history
  • Loading branch information
DScheglov committed Apr 23, 2024
1 parent bedc565 commit 7d924b1
Show file tree
Hide file tree
Showing 17 changed files with 106 additions and 64 deletions.
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,23 @@ More details on the example could be found in the "Getting Started" Example Proj
### ./src/main.ts - composition root

```typescript
import Module from 'true-di';
import Module, { asUseCases, scope } from 'true-di';
import { DiscountService } from './DiscountService';
import { Product } from './domain/products';
import { ProductRepoMock } from './ProductRepoMock';
import { ProductService } from './ProductService';
import { UserService } from './UserService';
import * as productController from './ProductController';
import PRODUCTS_JSON from './products.json';

const main = Module()
.private({
productRepo: () =>
new ProductRepoMock(PRODUCTS_JSON as Product[]),
})
.public({
userService: (_, { token }: { token: string | null }) =>
new UserService(token),
.public(scope.async, {
userService: () =>
new UserService(),
})
.private({
discountService: ({ userService }) =>
Expand All @@ -50,9 +51,14 @@ const main = Module()
.public({
productService: ({ productRepo, discountService }) =>
new ProductService(productRepo, discountService),
});
})
.public({
productController: asUseCases(productController),
})
.create();

export default main;

```

### ./src/DiscountService/index.ts
Expand Down Expand Up @@ -146,7 +152,7 @@ export class ProductService implements IProductService {
}
```

### ./src/products-controller.ts
### ./src/ProductController/index.ts

```typescript
import type { Request, Response } from 'express';
Expand Down Expand Up @@ -174,19 +180,22 @@ export const getFeaturedProducts =

```typescript
import express from 'express';
import createContext from 'express-async-context';
import main from './main';
import { getFeaturedProducts } from './products-controller';
import { scope } from '../../../src';
import { JSONMoneyReplacer } from './domain/money';

const app = express();

const Context = createContext(
req => main.create({ token: req.headers.authorization ?? null }),
);
app.use((req, res, next) => {
scope.async.run(next);
});

app.use(Context.provider);
app.use((req, res, next) => {
main.userService.setToken(req.headers.authorization ?? null);
next();
});

app.get('/featured-products', Context.consumer(getFeaturedProducts));
app.get('/featured-products', main.productController.getFeaturedProducts);

if (module === require.main) {
app.listen(8080, () => {
Expand Down
4 changes: 3 additions & 1 deletion examples/getting-started/src/DiscountService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export const NO_DISCOUNT: number = 0;
export class DiscountService implements IDiscountService {
constructor(
private readonly userService: IUserProvider,
) {}
) {
console.log('Creating a Discounting Service');
}

async getDiscountRate() {
const user = await this.userService.getCurrentUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {
describe, expect, it, jest,
} from '@jest/globals';
import Express from 'express';
import { IFeaturedProductProvider } from './interfaces/IProductService';
import { getFeaturedProducts } from './products-controller';
import { IFeaturedProductProvider } from '../interfaces/IProductService';
import { getFeaturedProducts } from '.';

function returnThis<T>(this: T) { return this; }

Expand All @@ -20,7 +20,7 @@ describe('productsController.getFeaturedProducts', () => {
getFeaturedProducts: jest.fn<IFeaturedProductProvider['getFeaturedProducts']>(),
};

it('sends json recieved from the productsService.getFeaturedProducs', async () => {
it('sends json received from the productsService.getFeaturedProducts', async () => {
expect.assertions(4);

const res = fakeResponse();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Request, Response } from 'express';
import { JSONMoneyReplacer } from './domain/money';
import { IFeaturedProductProvider } from './interfaces/IProductService';
import { JSONMoneyReplacer } from '../domain/money';
import { IFeaturedProductProvider } from '../interfaces/IProductService';

type GetFeaturedProductsDeps = {
productService: IFeaturedProductProvider;
Expand Down
4 changes: 3 additions & 1 deletion examples/getting-started/src/ProductRepoMock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { IProductRepo } from '../interfaces';
import { matches } from '../utils/matches';

export class ProductRepoMock implements IProductRepo {
constructor(private readonly products: Product[]) {}
constructor(private readonly products: Product[]) {
console.log('Creating a ProductRepoMock');
}

async getProducts(match?: Partial<Product>): Promise<Product[]> {
return match != null ? this.products.filter(matches(match)) : this.products.slice();
Expand Down
4 changes: 3 additions & 1 deletion examples/getting-started/src/ProductService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export class ProductService implements IProductService {
constructor(
private readonly products: IProductsProvider,
private readonly discountService: IDiscountRateProvider,
) {}
) {
console.log('Creating a Product Service');
}

async getFeaturedProducts() {
const discountRate = await this.discountService.getDiscountRate();
Expand Down
9 changes: 6 additions & 3 deletions examples/getting-started/src/UserService/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ describe('UserService', () => {
describe('getCurrentUser', () => {
it('returns the User if token is correct', async () => {
const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InNvbWUtdXNlci1pZCIsImVtYWlsIjoic29tZS11c2VyQGVtYWlsLm5vdCIsImlzUHJlZmVycmVkQ3VzdG9tZXIiOnRydWV9.K4TrvKFYjjcxZ-auoziZkkSiarnoYYfFHXLfbI1yrTs';
const userService = new UserService(validToken);
const userService = new UserService();
userService.setToken(validToken);
await expect(userService.getCurrentUser()).resolves.toEqual({
id: 'some-user-id',
email: '[email protected]',
Expand All @@ -15,12 +16,14 @@ describe('UserService', () => {

it('returns null if token is invalid', async () => {
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.CiAgICAgICAgaWQ6ICdzb21lLXVzZXItaWQnLAogICAgICAgIGVtYWlsOiAnc29tZS11c2VyQGVtYWlsLm5vdCcsCiAgICAgICAgaXNQcmVmZXJyZWRDdXN0b21lcjogdHJ1ZSwK.K4TrvKFYjjcxZ-auoziZkkSiarnoYYfFHXLfbI1yrTs';
const userService = new UserService(invalidToken);
const userService = new UserService();
userService.setToken(invalidToken);
await expect(userService.getCurrentUser()).resolves.toEqual(null);
});

it('returns null if token is null', async () => {
const userService = new UserService(null);
const userService = new UserService();
userService.setToken(null);
await expect(userService.getCurrentUser()).resolves.toEqual(null);
});
});
Expand Down
12 changes: 5 additions & 7 deletions examples/getting-started/src/UserService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ import { IUserService } from '../interfaces';
export class UserService implements IUserService {
#user: User | null = null;

#token: string | null = null;
constructor() {
console.log('Creating a User Service');
}

constructor(readonly token: string | null) {
this.#token = token;
async setToken(token: string | null) {
this.#user = token !== null ? parseToken(token) : null;
}

async getCurrentUser(): Promise<User | null> {
if (this.#user == null && this.#token != null) {
this.#user = parseToken(this.#token);
}

return this.#user;
}
}
26 changes: 19 additions & 7 deletions examples/getting-started/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import express from 'express';
import createContext from 'express-async-context';
import main from './main';
import { getFeaturedProducts } from './products-controller';
import { scope } from '../../../src';
import { JSONMoneyReplacer } from './domain/money';

const app = express();

const Context = createContext(
req => main.create({ token: req.headers.authorization ?? null }),
);
app.use((req, res, next) => {
scope.async.run(next);
});

app.use(Context.provider);
app.use((req, res, next) => {
main.userService.setToken(req.headers.authorization ?? null);
next();
});

app.get('/featured-products', Context.consumer(getFeaturedProducts));
app.get('/featured-products', main.productController.getFeaturedProducts);

app.get('/featured-products-2', async (req, res) => {
const featuredProducts = await main.productService.getFeaturedProducts();

res
.status(200)
.type('application/json')
.send(JSON.stringify(featuredProducts, JSONMoneyReplacer, 2));
});

if (module === require.main) {
app.listen(8080, () => {
Expand Down
6 changes: 5 additions & 1 deletion examples/getting-started/src/interfaces/IUserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ export interface IUserProvider {
getCurrentUser(): Promise<User | null>
}

export interface IUserService extends IUserProvider {}
export interface ITokenInitializer {
setToken(token: string | null): Promise<void>;
}

export interface IUserService extends IUserProvider, ITokenInitializer {}
3 changes: 2 additions & 1 deletion examples/getting-started/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { UserService } from './UserService';

describe('main', () => {
it('allows to get create all module items', () => {
expect({ ...main.create({ token: null }) }).toEqual({
expect({ ...main }).toEqual({
userService: expect.any(UserService),
productService: expect.any(ProductService),
productController: expect.any(Object),
});
});
});
15 changes: 10 additions & 5 deletions examples/getting-started/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import Module from '../../../src';
import Module, { asUseCases, scope } from '../../../src';
import { DiscountService } from './DiscountService';
import { Product } from './domain/products';
import { ProductRepoMock } from './ProductRepoMock';
import { ProductService } from './ProductService';
import { UserService } from './UserService';
import PRODUCTS_JSON from './products.json';
import * as productController from './ProductController';

const main = Module()
.private({
productRepo: () =>
new ProductRepoMock(PRODUCTS_JSON as Product[]),
})
.public({
userService: (_, { token }: { token: string | null }) =>
new UserService(token),
.public(scope.async, {
userService: () =>
new UserService(),
})
.private({
discountService: ({ userService }) =>
Expand All @@ -22,6 +23,10 @@ const main = Module()
.public({
productService: ({ productRepo, discountService }) =>
new ProductService(productRepo, discountService),
});
})
.public({
productController: asUseCases(productController),
})
.create();

export default main;
3 changes: 3 additions & 0 deletions examples/getting-started/src/utils/getFirst.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const getFirst = <T>(value: T | T[]): T | undefined => (
Array.isArray(value) ? value[0] : value
);
2 changes: 1 addition & 1 deletion examples/getting-started/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

/* Language and Environment */
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"lib": ["ESNext", "DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
Expand Down
2 changes: 1 addition & 1 deletion src/als-context-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const diAsyncContext = new AsyncLocalStorage();

const get = <T>() => diAsyncContext.getStore() as T;

const run = <T, R>(context: T, cb: () => R) => {
const run = <T, R>(context: T, cb: () => R): R => {
diAsyncContext.enterWith(context);
return cb();
};
Expand Down
23 changes: 11 additions & 12 deletions src/async-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,38 @@ import { ASYNC } from './life-cycle';
import { Resolver } from './types';

export interface ContextProvider<T> {
run(context: T, callback: () => any): void;
run<R>(context: T, callback: () => R): R;
get(): T;
}

let _contextProvider: ContextProvider<WeakMap<any, any>> =
let contextProvider: ContextProvider<Map<any, any>> =
typeof window === 'undefined'
? require('./als-context-provider').default
: require('./sync-context-provider').default;

export const run = <R>(cb: () => R) => contextProvider.run(new Map(), cb);

export const initAsyncContextProvider = (
asyncContextProvider: ContextProvider<WeakMap<any, any>>,
asyncContextProvider: ContextProvider<Map<any, any>>,
) => {
_contextProvider = asyncContextProvider;
contextProvider = asyncContextProvider;
run(() => {});
};

const contextProvider = (): ContextProvider<WeakMap<any, any>> => _contextProvider;

export const run = (cb: () => void) => contextProvider().run(new WeakMap(), cb);
run.init = initAsyncContextProvider;

if (typeof window !== 'undefined') {
run(() => {});
}
run(() => {});

export const asyncScope = <PrM extends {}, PbM extends {}, ExtD extends {}, T>(
resolver: Resolver<PrM, PbM, ExtD, T>,
initial?: [any, T],
force: boolean = true,
): Resolver<PrM, PbM, ExtD, T> => {
if (initial) contextProvider().get().set(resolver, initial[1]);
if (initial) contextProvider.get().set(resolver, initial[1]);

return decorated(
(internal: PrM & PbM, external: ExtD): T => {
const cache = contextProvider().get();
const cache = contextProvider.get();

if (cache == null) {
console.warn('Async Context is not defined. Use scope.async.run to handle request with async di scope');
Expand Down
Loading

0 comments on commit 7d924b1

Please sign in to comment.