├── .coveralls.sh ├── .gitbook.yaml ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── .npmignore ├── .npmrc.config ├── LICENSE ├── README.md ├── docs ├── INTRO.md ├── SUMMARY.md ├── core │ ├── di-container.md │ ├── factories-from.md │ ├── is-ready.md │ ├── multiple.md │ ├── prepare-all.md │ └── release-all.md ├── recipes │ ├── accessing-items.md │ ├── creating-all-items.md │ ├── cyclic-dependencies.md │ ├── injection-context.md │ ├── releasing-items.md │ └── separated-factories.md └── utils │ ├── assign-props.md │ └── shallow-merge.md ├── examples ├── getting-started │ ├── .eslintrc.json │ ├── LICENSE │ ├── README.md │ ├── jest.config.json │ ├── package-lock.json │ ├── package.json │ ├── sandbox.config.json │ ├── src │ │ ├── DataSourceService │ │ │ ├── __fake__ │ │ │ │ ├── generators.ts │ │ │ │ └── order-items.ts │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── ECommerceService │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── Logger │ │ │ ├── LogEntry.test.ts │ │ │ ├── LogEntry.ts │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── Orders │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── container.test.ts │ │ ├── container.ts │ │ ├── controller.test.ts │ │ ├── controller.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── IDataSourceService.ts │ │ │ ├── IECommerceService.ts │ │ │ ├── ILogger.ts │ │ │ └── index.ts │ │ ├── middlewares.test.ts │ │ ├── middlewares.ts │ │ └── utils │ │ │ ├── NotFoundError.test.ts │ │ │ ├── NotFoundError.ts │ │ │ ├── groupBy.test.ts │ │ │ ├── groupBy.ts │ │ │ ├── isUUID.test.ts │ │ │ ├── isUUID.ts │ │ │ ├── sendJson.test.ts │ │ │ └── sendJson.ts │ └── tsconfig.json └── plain-http-server │ ├── .eslintrc.json │ ├── LICENSE │ ├── README.md │ ├── client.rest │ ├── jest.config.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── DataSourceService │ │ ├── __fake__ │ │ │ ├── generators.ts │ │ │ └── order-items.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── ECommerceService │ │ ├── index.test.ts │ │ └── index.ts │ ├── Logger │ │ ├── LogEntry.test.ts │ │ ├── LogEntry.ts │ │ ├── LogLevel.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── Orders │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── types.ts │ ├── container.test.ts │ ├── container.ts │ ├── controller.ts │ ├── index.ts │ ├── interfaces │ │ ├── IDataSourceService.ts │ │ ├── IECommerceService.ts │ │ ├── ILogger.ts │ │ ├── IRequestInjected.ts │ │ ├── index.ts │ │ └── request.d.ts │ ├── middlewares.ts │ └── utils │ │ ├── NotFoundError.test.ts │ │ ├── NotFoundError.ts │ │ ├── exec.ts │ │ ├── groupBy.test.ts │ │ ├── groupBy.ts │ │ ├── httpListener.ts │ │ ├── isUUID.test.ts │ │ ├── isUUID.ts │ │ ├── pathParser.test.ts │ │ ├── pathParser.ts │ │ ├── readHeader.ts │ │ ├── sendJson.test.ts │ │ └── sendJson.ts │ └── tsconfig.json ├── index.d.ts ├── index.js ├── package.json ├── src ├── create-instance.test.ts ├── create-instance.ts ├── di-container.test.ts ├── di-container.ts ├── index.test.ts ├── index.ts ├── multiple.test.ts ├── multiple.ts ├── types.ts ├── unique-stack.test.ts ├── unique-stack.ts └── utils │ ├── all-names.test.ts │ ├── all-names.ts │ ├── assert-exists.test.ts │ ├── assert-exists.ts │ ├── assign-props.test.ts │ ├── assign-props.ts │ ├── index.test.ts │ ├── index.ts │ ├── map-object.test.ts │ ├── map-object.ts │ ├── narrow-object.test.ts │ ├── narrow-object.ts │ ├── shallow-merge.test.ts │ ├── shallow-merge.ts │ ├── type-test-utils.test.ts │ └── type-test-utils.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── utils ├── assign-props.d.ts ├── assign-props.js ├── index.d.ts ├── index.js ├── shallow-merge.d.ts └── shallow-merge.js └── version.sh /.coveralls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ ! -z "${COVERALLS_REPO_TOKEN}" ]]; then 4 | cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && echo "Coverage sent" 5 | else 6 | echo "No Coveralls Token assigned" 7 | fi 8 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | structure: 2 | readme: docs/INTRO.md 3 | summary: docs/SUMMARY.md 4 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test on Node ${{ matrix.node }} on ubuntu-latest 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | node: [12, 14, 16] 14 | 15 | steps: 16 | - name: Clone repo 17 | uses: actions/checkout@v2 18 | 19 | - name: Set Node.js version 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node }} 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Lint 28 | run: npm run lint 29 | 30 | - name: Test with Coverage 31 | run: npm run test 32 | 33 | - name: Publish on Coveralls 34 | uses: coverallsapp/github-action@master 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | flag-name: node-${{ matrix.node }} 38 | parallel: true 39 | 40 | report-coverage: 41 | needs: test 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Coveralls Finished 45 | uses: coverallsapp/github-action@master 46 | with: 47 | github-token: ${{ secrets.github_token }} 48 | parallel-finished: true 49 | 50 | publish: 51 | needs: report-coverage 52 | runs-on: ubuntu-latest 53 | if: startsWith(github.ref, 'refs/tags/v') 54 | steps: 55 | - name: Clone repo 56 | uses: actions/checkout@v2 57 | 58 | - name: Set Node.js version 59 | uses: actions/setup-node@v1 60 | with: 61 | node-version: 16 62 | 63 | - name: Install dependencies 64 | run: npm install 65 | 66 | - name: Check Version 67 | run: ./version.sh 68 | 69 | - name: Compile 70 | run: npm run compile 71 | 72 | - name: Publish to npm 73 | run: | 74 | cp .npmrc.config .npmrc 75 | npm publish 76 | env: 77 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | coverage 10 | 11 | # Dependency directories 12 | node_modules 13 | 14 | # npm settings 15 | .npmrc 16 | 17 | # Build artifacts 18 | lib/ 19 | esm/ 20 | 21 | # IDE settings 22 | .vscode 23 | .idea 24 | 25 | # Snyk 26 | .dccache -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | !/lib 4 | !/esm 5 | !/src 6 | !/utils 7 | !index.js 8 | !index.d.ts 9 | 10 | /src/**/*.test.ts 11 | -------------------------------------------------------------------------------- /.npmrc.config: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} 2 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dmitry Scheglov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # true-di 2 | 3 | Zero Dependency, Minimalistic **Type-Safe DI Container** for TypeScript and JavaScript projects 4 | 5 | [![Build Status](https://travis-ci.org/DScheglov/true-di.svg?branch=master)](https://travis-ci.org/DScheglov/true-di) [![Coverage Status](https://coveralls.io/repos/github/DScheglov/true-di/badge.svg?branch=master)](https://coveralls.io/github/DScheglov/true-di?branch=master) [![npm version](https://img.shields.io/npm/v/true-di.svg?style=flat-square)](https://www.npmjs.com/package/true-di) [![npm downloads](https://img.shields.io/npm/dm/true-di.svg?style=flat-square)](https://www.npmjs.com/package/true-di) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/DScheglov/true-di/blob/master/LICENSE) 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i --save true-di 11 | ``` 12 | 13 | ```bash 14 | yarn add true-di 15 | ``` 16 | 17 | ## Documentation 18 | 19 | [Read Documentation on Git Book](https://dscheglov.gitbook.io/true-di/) 20 | 21 | ## Usage Example: 22 | 23 | - [Live Demo on Sandbox](https://codesandbox.io/s/github/DScheglov/true-di/tree/master/examples/getting-started?fontsize=14&hidenavigation=1&initialpath=%2Forders&module=%2Fsrc%2Fcontainer.ts&theme=dark) 24 | - [Example Source Code](./examples/getting-started) 25 | 26 | 27 | **./src/container.ts** 28 | 29 | ```typescript 30 | import diContainer from 'true-di'; 31 | import { ILogger, IDataSourceService, IECommerceService } from './interfaces'; 32 | import Logger from './Logger'; 33 | import DataSourceService from './DataSourceService'; 34 | import ECommerceService from './ECommerceService'; 35 | 36 | type IServices = { 37 | logger: ILogger, 38 | dataSourceService: IDataSourceService, 39 | ecommerceService: IECommerceService, 40 | } 41 | 42 | export default diContainer({ 43 | logger: () => 44 | new Logger(), 45 | 46 | dataSourceService: ({ logger }) => 47 | new DataSourceService(logger), 48 | 49 | ecommerceService: ({ logger, dataSourceService }) => 50 | new ECommerceService(logger, dataSourceService), 51 | }); 52 | ``` 53 | 54 | **./src/ECommerceService/index.ts** 55 | 56 | ```typescript 57 | import { 58 | IECommerceService, IDataSourceService, Order, IInfoLogger 59 | } from '../interfaces'; 60 | 61 | class ECommerceSerive implements IECommerceService { 62 | constructor( 63 | private readonly _logger: IInfoLogger, 64 | private readonly _dataSourceService: IDataSourceService, 65 | ) { 66 | _logger.info('ECommerceService has been created'); 67 | } 68 | 69 | async getOrders(): Promise { 70 | const { _logger, _dataSourceService } = this; 71 | // do something 72 | } 73 | 74 | async getOrderById(id: string): Promise { 75 | const { _logger, _dataSourceService } = this; 76 | // do something 77 | } 78 | } 79 | 80 | export default ECommerceSerive; 81 | ``` 82 | 83 | **./src/controller.ts** 84 | 85 | ```typescript 86 | import { Request, Response, NextFunction as Next } from 'express'; 87 | import { IGetOrderById, IGetOrders } from './interfaces'; 88 | import { sendJson } from './utils/sendJson'; 89 | import { expectFound } from './utils/NotFoundError'; 90 | 91 | export const getOrders = (req: Request, res: Response, next: Next) => 92 | ({ ecommerceService }: { ecommerceService: IGetOrders }) => 93 | ecommerceService 94 | .getOrders() 95 | .then(sendJson(res), next); 96 | 97 | export const getOrderById = ({ params }: Request<{ id: string }>, res: Response, next: Next) => 98 | ({ ecommerceService }: { ecommerceService: IGetOrderById }) => 99 | ecommerceService 100 | .getOrderById(params.id) 101 | .then(expectFound(`Order(${params.id})`)) 102 | .then(sendJson(res), next); 103 | ``` 104 | 105 | **./src/index.ts** 106 | 107 | ```typescript 108 | import express from 'express'; 109 | import createContext from 'express-async-context'; 110 | import container from './container'; 111 | import { getOrderById, getOrders } from './controller'; 112 | import { handleErrors } from './middlewares'; 113 | 114 | const app = express(); 115 | const Context = createContext(() => container); 116 | 117 | app.use(Context.provider); 118 | 119 | app.get('/orders', Context.consumer(getOrders)); 120 | app.get('/orders/:id', Context.consumer(getOrderById)); 121 | 122 | app.use(Context.consumer(handleErrors)); 123 | 124 | app.listen(8080, () => { 125 | console.log('Server is listening on port: 8080'); 126 | console.log('Follow: http://localhost:8080/orders'); 127 | }); 128 | ``` 129 | 130 | ## Motivation 131 | 132 | `true-di` is designed to be used with [Apollo Server](https://github.com/apollographql/apollo-server) considering to make resolvers as thin as possible, to keep all business logic testable and framework agnostic by strictly following **S**.**O**.**L**.**I**.**D** Principles. 133 | 134 | Despite the origin motivation being related to Apollo Server the library could be used with any other framework or library that supports injection through the context. 135 | 136 | The [Getting Started Example](./examples/getting-started) shows how to use `true-di` with one of the most popular nodejs libraries: [Express](https://expressjs.com/). Almost all code in the example (~95%) is covered with tests that prove your business logic could be easily decoupled and kept independent even from dependency injection mechanism. 137 | 138 | Thanking to business logic does not depend on specific injection mechanism you can defer the 139 | choice of framework. Such deferring is suggested by Robert Martin: 140 | 141 | > The purpose of a good architecture is to defer decisions, delay decisions. The job of an architect is not to make decisions, the job of an architect is to build a structure that allows decisions to be delayed as long as possible. 142 | 143 | [© 2014, Robert C. Martin: Clean Architecture and Design, NDC Conference](https://vimeo.com/68215570), 144 | 145 | Summarizing, `true-di` is based on: 146 | - emulattion of a plain object that allows to specify exact type for each item and makes strict static type checking possible 147 | - explicit dependency injection for business logic (see example: [./src/container.ts](./examples/getting-started/src/container.ts)) 148 | - transperent injection through the context for framework-integrated components (see example: [./src/index.ts](./examples/getting-started/src/index.ts)) 149 | 150 | 151 | ## Some useful facts about `true-di`? 152 | 153 | 1. No syntatic decorators (annotations) as well as `reflect-metadata` are needed 154 | 1. `diContainer` uses `getters` under the hood. Container doesn't create an item until your code requests 155 | 1. Constructor-injection of cyclic dependency raises an exception 156 | 1. The property-, setter- dependency injects is also supported (what allows to resolve cyclic dependencies) 157 | 1. `diContainer`-s could be composed 158 | 1. Symbolic names are also supported for items 159 | 160 | -------------------------------------------------------------------------------- /docs/INTRO.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Framework Agnostic, Zero Dependency, Isomorphic & Minimalistic Dependency Injection Container for TypeScript and JavaScript projects 3 | --- 4 | 5 | # TRUE-DI 6 | 7 | ## Motivation 8 | 9 | `true-di` is designed to be used with [Apollo Server](https://github.com/apollographql/apollo-server) considering to make resolvers as thin as possible, to keep all business logic testable and framework agnostic by strictly following **S**.**O**.**L**.**I**.**D** Principles. 10 | 11 | Despite the origin motivation being related to Apollo Server the library could be used with any other framework or library that supports injection through the context. 12 | 13 | The [Getting Started Example](https://github.com/DScheglov/true-di/tree/master/examples/getting-started) shows how to use `true-di` with one of the most popular nodejs libraries: [Express](https://expressjs.com/). Almost all code in the example (~95%) is covered with tests that prove your business logic could be easily decoupled and kept independent even from dependency injection mechanism. 14 | 15 | Thanking to business logic does not depend on specific injection mechanism you can defer the 16 | choice of framework. Such deferring is suggested by Robert Martin: 17 | 18 | > The purpose of a good architecture is to defer decisions, delay decisions. The job of an architect is not to make decisions, the job of an architect is to build a structure that allows decisions to be delayed as long as possible. 19 | 20 | [© 2014, Robert C. Martin: Clean Architecture and Design, NDC Conference](https://vimeo.com/68215570), 21 | 22 | Summarizing, `true-di` is based on: 23 | - emulattion of a plain object that allows to specify exact type for each item and makes strict static type checking possible 24 | - explicit dependency injection for business logic (see example: [./src/container.ts](https://github.com/DScheglov/true-di/tree/master/examples/getting-started/src/container.ts)) 25 | - transperent injection through the context for framework-integrated components (see example: [./src/index.ts](https://github.com/DScheglov/true-di/tree/master/examples/getting-started/src/index.ts)) 26 | 27 | ## Some useful facts about `true-di`? 28 | 29 | 1. No syntatic decorators (annotations) as well as `reflect-metadata` are needed 30 | 1. `diContainer` uses `getters` under the hood. Container doesn't create an item until your code requests 31 | 1. Constructor-injection of cyclic dependency raises an exception 32 | 1. The property-, setter- dependency injects is also supported (what allows to resolve cyclic dependencies) 33 | 1. `diContainer`-s could be composed 34 | 1. Symbolic names are also supported for items 35 | 36 | ## Installation 37 | 38 | ```bash 39 | npm i --save true-di 40 | ``` 41 | 42 | ```bash 43 | yarn add true-di 44 | ``` 45 | 46 | ## Usage Example: 47 | 48 | - [Live Demo on Sandbox](https://codesandbox.io/s/github/DScheglov/true-di/tree/master/examples/getting-started?fontsize=14&hidenavigation=1&initialpath=%2Forders&module=%2Fsrc%2Fcontainer.ts&theme=dark) 49 | - [Example Source Code](https://github.com/DScheglov/true-di/tree/master/examples/getting-started) 50 | 51 | 52 | {% tabs %} 53 | {% tab title="./src/container.ts" %} 54 | 55 | ```typescript 56 | import diContainer from 'true-di'; 57 | import { ILogger, IDataSourceService, IECommerceService } from './interfaces'; 58 | import Logger from './Logger'; 59 | import DataSourceService from './DataSourceService'; 60 | import ECommerceService from './ECommerceService'; 61 | 62 | type IServices = { 63 | logger: ILogger, 64 | dataSourceService: IDataSourceService, 65 | ecommerceService: IECommerceService, 66 | } 67 | 68 | export default diContainer({ 69 | logger: () => 70 | new Logger(), 71 | 72 | dataSourceService: ({ logger }) => 73 | new DataSourceService(logger), 74 | 75 | ecommerceService: ({ logger, dataSourceService }) => 76 | new ECommerceService(logger, dataSourceService), 77 | }); 78 | ``` 79 | {% endtab %} 80 | {% tab title="./src/ECommerceService/index.ts" %} 81 | 82 | ```typescript 83 | import { 84 | IECommerceService, IDataSourceService, Order, IInfoLogger 85 | } from '../interfaces'; 86 | 87 | class ECommerceSerive implements IECommerceService { 88 | constructor( 89 | private readonly _logger: IInfoLogger, 90 | private readonly _dataSourceService: IDataSourceService, 91 | ) { 92 | _logger.info('ECommerceService has been created'); 93 | } 94 | 95 | async getOrders(): Promise { 96 | const { _logger, _dataSourceService } = this; 97 | // do something 98 | } 99 | 100 | async getOrderById(id: string): Promise { 101 | const { _logger, _dataSourceService } = this; 102 | // do something 103 | } 104 | } 105 | 106 | export default ECommerceSerive; 107 | ``` 108 | 109 | {% endtab %} 110 | {% tab title="./src/controller.ts" %} 111 | 112 | ```typescript 113 | import Express from 'express'; 114 | import { IGetOrderById, IGetOrders } from './interfaces'; 115 | import { Injected } from './interfaces/IRequestInjected'; 116 | import { sendJson } from './utils/sendJson'; 117 | import { expectFound } from './utils/NotFoundError'; 118 | 119 | export const getOrders = ( 120 | { injected: { ecommerceService } }: Injected<{ ecommerceService: IGetOrders }>, 121 | res: Express.Response, 122 | next: Express.NextFunction, 123 | ) => 124 | ecommerceService 125 | .getOrders() 126 | .then(sendJson(res), next); 127 | 128 | export const getOrderById = ( 129 | { params, injected: { ecommerceService } }: 130 | { params: { id: string } } & Injected<{ ecommerceService: IGetOrderById }>, 131 | res: Express.Response, 132 | next: Express.NextFunction, 133 | ) => 134 | ecommerceService 135 | .getOrderById(params.id) 136 | .then(expectFound(`Order(${params.id})`)) 137 | .then(sendJson(res), next); 138 | ``` 139 | 140 | {% endtab %} 141 | {% tab title="./src/index.ts" %} 142 | 143 | ```typescript 144 | import express from 'express'; 145 | import container from './container'; 146 | import { getOrderById, getOrders } from './controller'; 147 | import { handleErrors } from './middlewares'; 148 | 149 | const app = express(); 150 | 151 | app.use((req, _, next) => { 152 | req.injected = container; 153 | next(); 154 | }); 155 | 156 | app.get('/orders', getOrders); 157 | app.get('/orders/:id', getOrderById); 158 | 159 | app.use(handleErrors); 160 | 161 | if (module.parent == null) { 162 | app.listen(8080, () => { 163 | console.log('Server is listening on port: 8080'); 164 | console.log('Follow: http://localhost:8080/orders'); 165 | }); 166 | } 167 | 168 | export default app; 169 | ``` 170 | {% endtab %} 171 | {% endtabs %} 172 | 173 | 174 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Introduction](./INTRO.md) 4 | 5 | ## Core 6 | 7 | * [diContainer](./core/di-container.md) 8 | * [multiple](./core/multiple.md) 9 | * [isReady](./core/is-ready.md) 10 | * [prepareAll](./core/prepare-all.md) 11 | * [releaseAll](./core/release-all.md) 12 | * [factoriesFrom](./core/factories-from.md) 13 | 14 | ## Utils 15 | 16 | * [assignProps](./utils/assign-props.md) 17 | * [shallowMerge](./utils/shallow-merge.md) 18 | 19 | ## Recipes 20 | 21 | * [Accessing Items](./recipes/accessing-items.md) 22 | * [Releasing Items](./recipes/releasing-items.md) 23 | * [Creating All Items](./recipes/creating-all-items.md) 24 | * [Cyclic Dependencies](./recipes/cyclic-dependencies.md) 25 | 26 | * [Injection Context](./recipes/injection-context.md) 27 | * [Creating Factories before Container](./recipes/separated-factories.md) 28 | -------------------------------------------------------------------------------- /docs/core/di-container.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: creates and returns an empty IOC-Container. The first item will be created and stored only after it is requested. 3 | --- 4 | 5 | # diContainer 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import diContainer from 'true-di'; 11 | ``` 12 | 13 | ```javascript 14 | const { default: diContainer } = require('true-di'); 15 | ``` 16 | 17 | ## Declaration 18 | 19 | ```typescript 20 | function diContainer(factories: IFactories): IContainer 21 | function diContainer( 22 | privateFactories: Pick, keyof Private>, 23 | publicFactories: Pick, keyof Public>, 24 | ): Public 25 | ``` 26 | 27 | ## Arguments (1st overload) 28 | 29 | * **factories**: `IFactories` -- plain JavaScript object that is used as a map, where field names are item names and values are factory-functions that create correspondent items. To support property/setter injections the values of the factories-object could be a tuple (array), the first item of such tuple is a factory function and the second is an instance initializer. 30 | 31 | ## Returns (1st overload) 32 | 33 | * **container**: `IContainer` 34 | 35 | ## Arguments (2nd overload) 36 | 37 | * **privateFactories**: `Pick, keyof Private>` - the factories of the private services 38 | * **publicFactories**: `Pick, keyof Public>` - the factories of the public services 39 | 40 | ## Returns (2st overload) 41 | 42 | * **container**: `Public` - container resolving public services only 43 | 44 | ## Factory Types 45 | 46 | ### Type `IFactories` 47 | 48 | Type declaration: 49 | 50 | ```typescript 51 | type IFactories = { 52 | [name in keyof IContainer]: 53 | IFactory | IFactoryTuple 54 | } 55 | ``` 56 | 57 | ### Type `IFactory` 58 | 59 | Type declaration: 60 | 61 | ```typescript 62 | type IFactory = 63 | (container: IContainer) => IContainer[name] 64 | ``` 65 | 66 | ### Type `IFactoryTuple` 67 | 68 | Type declaration: 69 | 70 | ```typescript 71 | type IFactoryTuple = 72 | [IFactory, IInstanceInitializer]; 73 | ``` 74 | 75 | ### Type `IInstanceInitializer` 76 | 77 | Type declaraion: 78 | 79 | ```typescript 80 | type IInstanceInitializer = 81 | (instance: IContainer[name], container: IContainer) => void; 82 | ``` 83 | 84 | The container doesn't have any methods but only properties copied from the factories. You can get all property descriptors using standard JS-API. 85 | 86 | ## Example "Getting Started" 87 | 88 | ```typescript 89 | import diContainer from 'true-di'; 90 | import { IContainer } from './interfaces'; 91 | import Logger from './Logger'; 92 | import DataSourceService from './DataSourceService'; 93 | import ECommerceService from './ECommerceService'; 94 | 95 | 96 | const container = diContainer({ 97 | logger: () => 98 | new Logger(), 99 | 100 | dataSourceService: ({ logger }) => 101 | new DataSourceService(logger), 102 | 103 | ecommerceService: ({ logger, dataSourceService }) => 104 | new ECommerceService(logger, dataSourceService), 105 | }); 106 | 107 | export default container; 108 | ``` 109 | 110 | ## Example "Exposing only ECommerceService" 111 | 112 | ```typescript 113 | import diContainer from 'true-di'; 114 | import { IInfoLogger, IDataSourceService } from "./interfaces"; 115 | import Logger from './Logger'; 116 | import DataSourceService from './DataSourceService'; 117 | import ECommerceService from './ECommerceService'; 118 | 119 | const createLogger = () => 120 | new Logger(); 121 | 122 | const createDataSourceService = ({ logger }: { 123 | logger: IInfoLogger 124 | }) => new DataSourceService(logger); 125 | 126 | const createECommerceService = ({ logger, dataSourceService }: { 127 | logger: IInfoLogger, 128 | dataSourceService: IDataSourceService, 129 | }) => new ECommerceService(logger, dataSourceService); 130 | 131 | const { ecommerceService } = diContainer({ 132 | logger: createLogger, 133 | dataSourceService: createDataSourceService, 134 | }, { 135 | ecommerceService: createECommerceService, 136 | }); 137 | 138 | await ecommerceService.getOrders(); 139 | ``` 140 | 141 | ## Example "Working With Cyclic Dependencies" 142 | 143 | ```typescript 144 | type Node = { 145 | child: Node; 146 | parent: Node; 147 | name: string; 148 | } 149 | 150 | type Container = { 151 | parentItem: Node; 152 | childItem: Node; 153 | } 154 | 155 | const createNode = (name: string): Node => ({ child: null, parent: null, name }); 156 | 157 | const container = diContainer({ 158 | parentItem: [ 159 | () => createNode('Parent'), 160 | (parentItem, { childItem }) => { 161 | parentItem.child = childItem 162 | }, 163 | ], 164 | childItem: [ 165 | () => createNode('Child' cxx), 166 | (childItem, { parentItem }) => { 167 | childItem.parent = parentItem; 168 | }, 169 | ], 170 | }); 171 | ``` 172 | 173 | -------------------------------------------------------------------------------- /docs/core/factories-from.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Creates factories object based on the passed container. 3 | --- 4 | 5 | # factoriesFrom 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import { factoriesFrom } from 'true-di'; 11 | ``` 12 | 13 | ```javascript 14 | const { factoriesFrom } = require('true-di'); 15 | ``` 16 | 17 | ## Declaration 18 | 19 | ```typescript 20 | (container: IContainer, names?: Name[]): IPureFactories> 21 | ``` 22 | 23 | ## Arguments 24 | 25 | - **container**: `IContainer` - container to be used as a source of instancies for new factories. 26 | - **names**: `Name[]` - _optional_ - the list of item names to be factoried with the new factories object. If omitted all items from the container are considered to be factored with new factories object. 27 | 28 | ## Returns 29 | 30 | - **factories**: `IPureFactories` factories object. 31 | 32 | ### Types: 33 | 34 | ```typescript 35 | export type IPureFactories = { 36 | [name in keyof IContainer]: () => IContainer[name] 37 | } 38 | ``` 39 | 40 | The main goal of the `factoriesFrom` function was added to the `true-di` core is 41 | to get some way to compose two (or more) containers. 42 | 43 | ## Example 1. Combining global and request containers. 44 | 45 | ```typescript 46 | import diContainer, { factoriesFrom } from 'true-di'; 47 | import Express from 'express'; 48 | 49 | type GlobalContainer = { 50 | logger: ILogger; 51 | } 52 | 53 | type RequestContainer = GlobalContainer & { 54 | dao: IDataAccessObject; 55 | ecommerceService: IECommerceService; 56 | } 57 | 58 | export const globalContainer = diContainer({ 59 | logger: () => new Logger(), 60 | }); 61 | 62 | export const requestContainer = (req: Express.Request, globalContainer: GlobalContainer) => 63 | diContainer({ 64 | ...factoriesFrom(globalContainer), 65 | 66 | dao: ({ logger }) => new DataAccessObject(request.user, logger), 67 | ecommerceService: ({ dao }) => new ECommerceService(dao), 68 | }); 69 | ``` 70 | 71 | In the example above the code: 72 | 73 | ```typescript 74 | ...factoriesFrom(globalContainer) 75 | ``` 76 | 77 | Do exactly the same as: 78 | 79 | ```typescript 80 | logger: () => globalContainer.logger, 81 | ``` 82 | 83 | However if `globalContainer` has a lot of items those should be inherited by the `requestContainer` 84 | to write a lot of new factories could be very annoying. 85 | 86 | ## Example 2. Narrowing container 87 | 88 | In the previous example we extend global container by adding two services on the request container. 89 | 90 | This example shows how to narrow container for some specific routes: 91 | 92 | Define container factory function to create request level container: 93 | 94 | ```typescript 95 | import Express from 'express'; 96 | import diContainer from 'true-di'; 97 | import { IContainer } from './interfaces'; 98 | import Logger from './Logger'; 99 | import DataSourceService from './DataSourceService'; 100 | import ECommerceService from './ECommerceService'; 101 | 102 | 103 | const createContainer = ({ query, headers }: Express.Request) => 104 | diContainer({ 105 | logger: () => 106 | new Logger(), 107 | 108 | dataSourceService: ({ logger }) => 109 | new DataSourceService(logger), 110 | 111 | ecommerceService: ({ logger, dataSourceService }) => 112 | new ECommerceService(logger, dataSourceService), 113 | }); 114 | 115 | export default createContainer; 116 | ``` 117 | 118 | Let's use our container in express: 119 | 120 | ```typescript 121 | import express from 'express'; 122 | import diContainer, { factoriesFrom } from 'true-di'; 123 | import createContainer from './container'; 124 | import { getOrders } from './controller'; 125 | 126 | const app = express(); 127 | 128 | app.use((req, res, next) => { 129 | req.container = createContainer(req); 130 | next(); 131 | }); 132 | 133 | app.use('/orders', (req, res, next) => { 134 | const { container } = req; 135 | req.container = diContainer({ 136 | logger: () => container.logger, 137 | ecommerceService: () => container.ecommerceService, 138 | }); 139 | next(); 140 | }); 141 | 142 | app.get('/orders', getOrders); 143 | app.listen(8080); 144 | ``` 145 | 146 | The code: 147 | 148 | ```typescript 149 | app.use('/orders', (req, res, next) => { 150 | const { container } = req; 151 | req.container = diContainer({ 152 | logger: () => container.logger, 153 | ecommerceService: () => container.ecommerceService, 154 | }); 155 | next(); 156 | }); 157 | ``` 158 | 159 | could be replaced with a litle bit more consice code: 160 | 161 | ```typescript 162 | app.use('/orders', (req, res, next) => { 163 | req.container = diContainer( 164 | factoriesFrom(req.container, ['logger', 'ecommerceService']) 165 | ); 166 | next(); 167 | }); 168 | ``` -------------------------------------------------------------------------------- /docs/core/is-ready.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: returns true if the item with `name` is already instantiated and stored in the `container`. 3 | --- 4 | 5 | # isReady 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import { isReady } from 'true-di'; 11 | ``` 12 | 13 | ```javascript 14 | const { isReady } = require('true-di'); 15 | ``` 16 | 17 | ## Declaration 18 | 19 | ```typescript 20 | function isReady(container: IContainer, name: keyof IContainer): boolean 21 | ``` 22 | 23 | ## Arguments 24 | 25 | * **container**: `IContainer` - container to check if item created 26 | * **name**: `keyof IContainer` - name of container items. 27 | 28 | ## Returns 29 | 30 | * `boolean` - `true` if item is already created and `false` if it is not. 31 | 32 | This function is needed because any attempt to read the item from the container causes the item will be created. `isReady` doesn't affect reading the component so it returns its status. 33 | 34 | ## Example 35 | 36 | ```typescript 37 | import { isReady } from 'true-di/utils'; 38 | import container from './container'; 39 | 40 | console.log(isReady(container, 'logger')); 41 | // prints: false -- if logger was not touched before 42 | 43 | container.logger.info("Let's log a message"); 44 | 45 | console.log(isReady(container, 'logger')); 46 | // prints: true 47 | 48 | console.log(Boolean(container.logger)); 49 | // always prints true, becouse reading logger creates it 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /docs/core/multiple.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: decorates factories to allow multiple instantiation. 3 | --- 4 | 5 | # multiple 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import { multiple } from 'true-di'; 11 | ``` 12 | 13 | ```javascript 14 | const { multiple } = require('true-di'); 15 | ``` 16 | 17 | ## Declaration (3 overloadings) 18 | 19 | ```typescript 20 | ( 21 | factory: IFactory 22 | ): IFactoryTuple 23 | ``` 24 | 25 | ```typescript 26 | ( 27 | [factory, initilizer]: IFactoryTuple, 28 | ): 29 | ``` 30 | 31 | ```typescript 32 | (factories: IFactories): IFactories 33 | ``` 34 | 35 | ## Arguments (1st overloading) 36 | 37 | * **factory**: `IFactory` - factory to be decorated to generate multiple instances. 38 | 39 | ## Arguments (2nd overloading) 40 | 41 | * **factoryTuple**: `IFactoryTuple` - factory tuple to be decorated to generate multiple instances. 42 | 43 | ## Returns 44 | 45 | * `IFactoryTuple` - factory tuple (factory and initializer) that allows to create multiple instances 46 | 47 | ## Arguments (3rd overloading) 48 | 49 | * **factories**: `IFactories` - factories object to decorate all factories inside of it. 50 | 51 | ## Returns 52 | 53 | * `IFactories` - factories object with decorated factories (or factory tuples) 54 | 55 | ## Example 56 | 57 | ```typescript 58 | import diContainer, { multiple } from 'true-di'; 59 | 60 | 61 | type Node = { 62 | title: string, 63 | } 64 | 65 | type IContainer = { 66 | index: number, 67 | node: Node, 68 | } 69 | 70 | let counter = 0; 71 | 72 | const container = diContainer({ 73 | index: multiple(() => ++counter), 74 | node: multiple(({ index }) => ({ title: `Item #${index}` })), 75 | }); 76 | 77 | const nodes = [ 78 | container.node, 79 | container.node, 80 | container.node, 81 | ]; 82 | // nodes is an array: 83 | // [ 84 | // { title: 'Item #1' }, 85 | // { title: 'Item #2' }, 86 | // { title: 'Item #3' } 87 | // ] 88 | ``` 89 | 90 | The same effect could be reached by decorating whole factories object 91 | 92 | ```typescript 93 | const container = diContainer(multiple({ 94 | index: () => ++counter, 95 | node: ({ index }) => ({ title: `Item #${index}` }), 96 | })); 97 | ``` -------------------------------------------------------------------------------- /docs/core/prepare-all.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Creates all items in the container. 3 | --- 4 | 5 | # prepareAll 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import { prepareAll } from 'true-di'; 11 | ``` 12 | 13 | ```javascript 14 | const { isReady } = require('true-di'); 15 | ``` 16 | 17 | ## Typing: 18 | 19 | ```typescript 20 | function prepareAll(container: IContainer): IContainer 21 | ``` 22 | 23 | ## Arguments: 24 | 25 | * **container**: `IContainer` - container to create all its items. 26 | 27 | ## Returns: 28 | 29 | * **static copy of the container**: `IContainer` - plain JS-object with fields those are container items. 30 | 31 | ## Example: 32 | 33 | ```typescript 34 | import { isReady, prepareAll } from 'true-di/utils'; 35 | import container from './container'; 36 | 37 | prepareAll(container); 38 | 39 | console.log( 40 | isReady(container, 'logger'), 41 | isReady(container, 'dataAccessService'), 42 | isReady(container, 'ecommerceService') 43 | ); 44 | // prints: true true true 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /docs/core/release-all.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Releases (unbinds) all items from the container. 3 | --- 4 | 5 | # releaseAll 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import { releaseAll } from 'true-di'; 11 | ``` 12 | 13 | ```javascript 14 | const { releaseAll } = require('true-di'); 15 | ``` 16 | 17 | ## Typing 18 | 19 | ```typescript 20 | function releaseAll(container: IContainer): void 21 | ``` 22 | 23 | ## Arguments: 24 | 25 | * **container**: `IContainer` - container to unbind items from 26 | 27 | ## Returns: 28 | 29 | * _nothing_ 30 | 31 | ## Example: 32 | 33 | ```typescript 34 | import { isReady, releaseAll } from 'true-di/utils'; 35 | import container from './container'; 36 | 37 | releaseAll(container); 38 | 39 | console.log( 40 | isReady(container, 'logger'), 41 | isReady(container, 'dataAccessService'), 42 | isReady(container, 'ecommerceService') 43 | ); 44 | // prints: false false false 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /docs/recipes/accessing-items.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: About quantum nature of container items. 3 | --- 4 | 5 | # Accessing Items 6 | 7 | To access items is the most spreaded use-case for the di-container. 8 | 9 | ```typescript 10 | const item = container.item; 11 | 12 | // or 13 | 14 | const { item } = container; 15 | 16 | // or 17 | 18 | const item = container[item]; 19 | 20 | // or even 21 | 22 | const itemName = 'item'; 23 | const item = container[itemName]; 24 | ``` 25 | 26 | All of that works. 27 | 28 | Container doesn't create any of its items when it is been created. Each time when your code accesses the item, container checks if item is created and if it is returns it, otherwise container calls correspondent factory function and passes itself to the factory. 29 | 30 | When the factory function accesses some other items those are need to build the target one container creates them \(if they are not ready\) and returns them to the factory functions. 31 | 32 | This is the main dependency resolution mechanizm that is exactly the same as just accessing the items. 33 | 34 | -------------------------------------------------------------------------------- /docs/recipes/creating-all-items.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Not too long read about how create all items. 3 | --- 4 | 5 | # Creating All Items 6 | 7 | As usually you don't need to create all items at once, but it is quit convinient way to check if all items could be created and container doesn't have cyclic creation dependencies. 8 | 9 | ```typescript 10 | describe('myContainer', () => { 11 | it('creates all items', () => { 12 | expect({ ...container }).toEqual(container); 13 | }); 14 | }); 15 | ``` 16 | 17 | As you can see all items could be instantiated by using spread or rest operators: 18 | 19 | ```typescript 20 | const items = { ...container }; // using spread operator 21 | // or 22 | const { ...items } = container; // using rest and destructuring 23 | ``` 24 | 25 | However such aproach doesn't work if container has some non-enumerable fields: 26 | 27 | ```typescript 28 | type Container = { 29 | x: number, 30 | y: number, 31 | z: number, 32 | }; 33 | 34 | const container = diContainer(Object.create(null, { 35 | x: { value: () => 1, enumerable: true }, 36 | y: { value: () => 2, enumerable: true }, 37 | z: { value: () => 3, enumerable: false }, 38 | })); 39 | 40 | const items = { ...container }; 41 | 42 | console.log({ z: items.z }); 43 | // prints: { z: undefined } 44 | 45 | console.log({ 46 | isXReady: isReady(container, 'x'), 47 | isYReady: isReady(container, 'y'), 48 | isZReady: isReady(container, 'z'), 49 | }); 50 | 51 | // prints: { isXReady: true, isYReady: true, isZReady: false } 52 | ``` 53 | 54 | To create all items including bound to non-enumerable fields use [`prepareAll`](../core/prepare-all.md) function: 55 | 56 | ```typescript 57 | const container = diContainer(Object.create(null, { 58 | x: { value: () => 1, enumerable: true }, 59 | y: { value: () => 2, enumerable: true }, 60 | z: { value: () => 3, enumerable: false }, 61 | })); 62 | 63 | const items = prepareAll(container); 64 | 65 | console.log({ z: items.z }); 66 | // prints: { z: 3 } 67 | 68 | console.log({ 69 | isXReady: isReady(container, 'x'), 70 | isYReady: isReady(container, 'y'), 71 | isZReady: isReady(container, 'z'), 72 | }); 73 | 74 | // prints: { isXReady: true, isYReady: true, isZReady: true } 75 | ``` 76 | 77 | It is suggested to use [`prepareAll`](../core/prepare-all.md) function raither then spread/rest operators because: 78 | 79 | 1. it is more explicitly creates all items (explicit is better then implicit) 80 | 1. it is most reliable way to create all items 81 | 82 | -------------------------------------------------------------------------------- /docs/recipes/cyclic-dependencies.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: how to tackle a cyclic dependencies. 3 | --- 4 | 5 | # Cyclic Dependencies 6 | 7 | **Don't do cyclic dependencies.** 8 | 9 | In some (rare) case you can need to use cyclic dependencies. The library allows you to specify both: factory- and init- function in factories object. 10 | 11 | See example: 12 | 13 | ```typescript 14 | import diContainer from 'true-di'; 15 | 16 | type Node = { 17 | child: Node; 18 | parent: Node; 19 | name: string; 20 | } 21 | 22 | type Container = { 23 | parentItem: Node; 24 | childItem: Node; 25 | } 26 | 27 | const createNode = (name: string): Node => ({ child: null, parent: null, name }); 28 | 29 | const container = diContainer({ 30 | parentItem: [ 31 | () => createNode('Parent'), 32 | (instance, { childItem }) => { 33 | instance.child = childItem 34 | }, 35 | ], 36 | childItem: [ 37 | () => createNode('Child' cxx), 38 | (instance { parentItem }) => { 39 | instance.parent = parentItem; 40 | }, 41 | ], 42 | }); 43 | ``` 44 | 45 | Simillary to assigning instance property you can use `set*` method of the instance: 46 | 47 | ```typescript 48 | const container = diContainer({ 49 | parentItem: [ 50 | () => createNode('Parent'), 51 | (instance, { childItem }) => instance.setChild(childItem), 52 | ], 53 | childItem: [ 54 | () => createNode('Child' cxx), 55 | (instance { parentItem }) => instance.setParent(parentItem), 56 | ], 57 | }); 58 | ``` 59 | 60 | To write property-assignment code in more declarative way `true-di` ships: [`assignProps`](../utils/assign-props.md) utility function, and we can rewrite basic example with the one: 61 | 62 | ```typescript 63 | import diContainer from 'true-di'; 64 | import { assignProps } from 'true-di/utils'; 65 | 66 | type Node = { 67 | child: Node; 68 | parent: Node; 69 | name: string; 70 | } 71 | 72 | type Container = { 73 | parentItem: Node; 74 | childItem: Node; 75 | } 76 | 77 | const createNodeFactory = (name: string): Node => () => ({ 78 | child: null, parent: null, name 79 | }); 80 | 81 | const container = diContainer({ 82 | parentItem: [ 83 | createNodeFactory('Parent'), 84 | assignProps({ child: 'childItem' }), 85 | ], 86 | childItem: [ 87 | createNodeFactory('Child'), 88 | assignProps({ parent: 'parentItem' }), 89 | ], 90 | }); 91 | ``` 92 | 93 | -------------------------------------------------------------------------------- /docs/recipes/injection-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: How to compose containers 3 | --- 4 | 5 | # Injection Context 6 | 7 | The library keeps container to be idiomatic. That means container should not know 8 | anything about how it is used and how some dependencies are injected. 9 | 10 | All items in the container are singletons until they will be explicitly released. 11 | 12 | However it is obvious that we cannot use only globally created containers 13 | ignoring request-context on the server or rendering-context on the client. 14 | 15 | So, there is a way how context could be considered with `true-di`: 16 | 17 | - let's create a container as part of context (idealy the container should be a context). 18 | - let's compose containers to create subcontext-related container based on the higher-context container. 19 | 20 | ## Example 1. Container Factory 21 | 22 | Let's guess we need to log each request query and some of header, so we should provide 23 | correspondent data to the constructor of `Logger`: 24 | 25 | ```typescript 26 | import Express from "express"; 27 | import diContainer from 'true-di'; 28 | import { IContainer } from './interfaces'; 29 | import Logger from './Logger'; 30 | import DataSourceService from './DataSourceService'; 31 | import ECommerceService from './ECommerceService'; 32 | 33 | 34 | const containerFactory = ({ query, headers }: Express.Request) => diContainer({ 35 | logger: () => 36 | new Logger(query, headers), 37 | 38 | dataSourceService: ({ logger }) => 39 | new DataSourceService(logger), 40 | 41 | ecommerceService: ({ logger, dataSourceService }) => 42 | new ECommerceService(logger, dataSourceService), 43 | }); 44 | 45 | export default containerFactory; 46 | ``` 47 | 48 | And then we should create a container when the context is defined: 49 | 50 | ```typescript 51 | import express from 'express'; 52 | import containerFactory from './container'; 53 | import { getOrders } from './controller'; 54 | 55 | const app = express(); 56 | 57 | app.use((req, res, next) => { 58 | req.injected = containerFactory(req); 59 | next(); 60 | }); 61 | 62 | app.get('/orders', getOrders); 63 | app.listen(8080); 64 | ``` 65 | 66 | ## Example 2. Combining global and request containers. 67 | 68 | ```typescript 69 | import diContainer, { factoriesFrom } from 'true-di'; 70 | import Express from 'express'; 71 | 72 | type GlobalContainer = { 73 | logger: ILogger; 74 | } 75 | 76 | type RequestContainer = GlobalContainer & { 77 | dao: IDataAccessObject; 78 | ecommerceService: IECommerceService; 79 | } 80 | 81 | export const globalContainer = diContainer({ 82 | logger: () => new Logger(), 83 | }); 84 | 85 | export const requestContainer = (req: Express.Request, globalContainer: GlobalContainer) => 86 | diContainer({ 87 | ...factoriesFrom(globalContainer), 88 | 89 | dao: ({ logger }) => new DataAccessObject(request.user, logger), 90 | ecommerceService: ({ dao }) => new ECommerceService(dao), 91 | }); 92 | ``` 93 | 94 | In the example above the code: 95 | 96 | ```typescript 97 | ...factoriesFrom(globalContainer) 98 | ``` 99 | 100 | Do exactly the same as: 101 | 102 | ```typescript 103 | logger: () => globalContainer.logger, 104 | ``` 105 | 106 | However if `globalContainer` has a lot of items those should be inherited by the `requestContainer` 107 | to write a lot of new factories could be very annoying. 108 | 109 | 110 | ## Example 3. Narrowing container 111 | 112 | In the previous example we extend global container by adding two services on the request container. 113 | 114 | This example shows how to narrow container for some specific routes: 115 | 116 | Define container factory function to create request level container: 117 | 118 | ```typescript 119 | import Express from 'express'; 120 | import diContainer from 'true-di'; 121 | import { IContainer } from './interfaces'; 122 | import Logger from './Logger'; 123 | import DataSourceService from './DataSourceService'; 124 | import ECommerceService from './ECommerceService'; 125 | 126 | 127 | const createContainer = ({ query, headers }: Express.Request) => 128 | diContainer({ 129 | logger: () => 130 | new Logger(), 131 | 132 | dataSourceService: ({ logger }) => 133 | new DataSourceService(logger), 134 | 135 | ecommerceService: ({ logger, dataSourceService }) => 136 | new ECommerceService(logger, dataSourceService), 137 | }); 138 | 139 | export default createContainer; 140 | ``` 141 | 142 | Let's use our container in express: 143 | 144 | ```typescript 145 | import express from 'express'; 146 | import diContainer, { factoriesFrom } from 'true-di'; 147 | import createContainer from './container'; 148 | import { getOrders } from './controller'; 149 | 150 | const app = express(); 151 | 152 | app.use((req, res, next) => { 153 | req.container = createContainer(req); 154 | next(); 155 | }); 156 | 157 | app.use('/orders', (req, res, next) => { 158 | const { container } = req; 159 | req.container = diContainer({ 160 | logger: () => container.logger, 161 | ecommerceService: () => container.ecommerceService, 162 | }); 163 | next(); 164 | }); 165 | 166 | app.get('/orders', getOrders); 167 | app.listen(8080); 168 | ``` 169 | 170 | The code: 171 | 172 | ```typescript 173 | app.use('/orders', (req, res, next) => { 174 | const { container } = req; 175 | req.container = diContainer({ 176 | logger: () => container.logger, 177 | ecommerceService: () => container.ecommerceService, 178 | }); 179 | next(); 180 | }); 181 | ``` 182 | 183 | could be replaced with a litle bit more consice code: 184 | 185 | ```typescript 186 | app.use('/orders', (req, res, next) => { 187 | req.container = diContainer( 188 | factoriesFrom(req.container, ['logger', 'ecommerceService']) 189 | ); 190 | next(); 191 | }); 192 | ``` -------------------------------------------------------------------------------- /docs/recipes/releasing-items.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: No memory leaks more. 3 | --- 4 | 5 | # Releasing Items 6 | 7 | To tackle possible memory leaks issues `true-di` allows to control what items are stored in the container. It doesn't allow to replace items, but it allows to unbind it from the container (release item). 8 | 9 | To do that just assign the correspondent container property with `null` or `undefined` (don't try to delete it). 10 | 11 | ```typescript 12 | container.logger = null; 13 | ``` 14 | 15 | It doesn't affect other already created items those are depent on it. They continue to use previously created item. 16 | 17 | When you need to release all items from the container use [`releaseAll`](../core/release-all.md) function: 18 | 19 | ```typescript 20 | releaseAll(container); 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /docs/recipes/separated-factories.md: -------------------------------------------------------------------------------- 1 | --- 2 | decription: code splitting is cool 3 | --- 4 | 5 | # Creating Factories Object Separatelly from Container 6 | 7 | If in some reason you need to define factories separatelly from the container 8 | declaration, you can just declare factories object of type `IFactories`. 9 | 10 | 11 | ## Example 1. Declaring Factories Object 12 | 13 | **./factories.ts** 14 | 15 | ```typescript 16 | import { IFactories } from 'true-di'; 17 | import { IContainer } from './interfaces'; 18 | import Logger from './Logger'; 19 | import DataSourceService from './DataSourceService'; 20 | import ECommerceService from './ECommerceService'; 21 | 22 | 23 | const factories: IFactories = { 24 | logger: () => 25 | new Logger(), 26 | 27 | dataSourceService: ({ logger }) => 28 | new DataSourceService(logger), 29 | 30 | ecommerceService: ({ logger, dataSourceService }) => 31 | new ECommerceService(logger, dataSourceService), 32 | }; 33 | 34 | export default factories; 35 | ``` 36 | 37 | then we can use this factories object to create a container: 38 | 39 | **./index.js** 40 | 41 | ```typescript 42 | import express from 'express'; 43 | import diContainer from 'true-di'; 44 | import factories from './factories'; 45 | import { getOrders } from './controller'; 46 | 47 | const app = express(); 48 | 49 | app.use((req, res, next) => { 50 | req.container = diContainer(factories); 51 | next(); 52 | }); 53 | 54 | app.get('/orders', getOrders); 55 | app.listen(8080); 56 | ``` 57 | 58 | ## Example 2. Partial Factories Object 59 | 60 | You can define several factories objects those could be marged on the container creation stage. 61 | 62 | **./factories.ts** 63 | 64 | ```typescript 65 | import { IFactories } from 'true-di'; 66 | import { IContainer } from './interfaces'; 67 | import Logger from './Logger'; 68 | import DataSourceService from './DataSourceService'; 69 | import ECommerceService from './ECommerceService'; 70 | 71 | type FactoryFor = Pick, Names>; 72 | 73 | export const loggerFactory: FactoryFor<'logger'> = { 74 | logger: () => new Logger(), 75 | }; 76 | 77 | export const dasFactory: FactoryFor<'dataSourceService'> = { 78 | dataSourceService: ({ logger }) => new DataSourceService(logger), 79 | }; 80 | 81 | export const ecommerceServiceFactory: FactoryFor<'ecommerceService'> = { 82 | ecommerceService: ({ logger, dataSourceService }) => 83 | new ECommerceService(logger, dataSourceService), 84 | }; 85 | ``` 86 | 87 | and use these factories to create a container: 88 | 89 | **./index.js** 90 | 91 | ```typescript 92 | import express from 'express'; 93 | import diContainer from 'true-di'; 94 | import { loggerFactory, dasFactory, ecommerceServiceFactory } from './factories'; 95 | import { getOrders } from './controller'; 96 | 97 | const app = express(); 98 | 99 | app.use((req, res, next) => { 100 | req.container = diContainer({ 101 | ...oggerFactory, 102 | ...dasFactory, 103 | ...ecommerceServiceFactory, 104 | }); 105 | next(); 106 | }); 107 | 108 | app.get('/orders', getOrders); 109 | app.listen(8080); 110 | ``` 111 | 112 | If some of partially declared factories contains a non-enumerable property, then use `shallowMerge` 113 | function instead of spread-operator. 114 | 115 | ```typescript 116 | import express from 'express'; 117 | import diContainer from 'true-di'; 118 | import { shallowMerge } from 'true-di/utils'; 119 | import { loggerFactory, dasFactory, ecommerceServiceFactory } from './factories'; 120 | import { getOrders } from './controller'; 121 | 122 | const app = express(); 123 | 124 | app.use((req, res, next) => { 125 | req.container = diContainer(shallowMerge( 126 | oggerFactory, 127 | dasFactory, 128 | ecommerceServiceFactory, 129 | )); 130 | next(); 131 | }); 132 | 133 | app.get('/orders', getOrders); 134 | app.listen(8080); 135 | ``` 136 | 137 | ## Example 3. Direct Factory Declaration 138 | 139 | Sometimes it could be convenient to declare just a factory for an item. 140 | 141 | ```typescript 142 | import { IFactory } from 'true-di'; 143 | import { IContainer } from './interfaces'; 144 | import Logger from './Logger'; 145 | import DataSourceService from './DataSourceService'; 146 | import ECommerceService from './ECommerceService'; 147 | 148 | type FactoryFor = IFactory; 149 | 150 | export const logger: FactoryFor<'logger'> = () => new Logger(); 151 | 152 | export const dataSourceService: FactoryFor<'dataSourceService'> = 153 | ({ logger }) => new DataSourceService(logger); 154 | 155 | export const ecommerceService: FactoryFor<'ecommerceService'> = 156 | ({ logger, dataSourceService }) => new ECommerceService(logger, dataSourceService); 157 | ``` 158 | 159 | or 160 | 161 | ```typescript 162 | import { IContainer } from './interfaces'; 163 | import Logger from './Logger'; 164 | import DataSourceService from './DataSourceService'; 165 | import ECommerceService from './ECommerceService'; 166 | 167 | export const logger = () => 168 | new Logger(); 169 | 170 | export const dataSourceService = ({ logger }: IContainer) => 171 | new DataSourceService(logger); 172 | 173 | export const ecommerceService = ({ logger, dataSourceService }: IContainer) => 174 | new ECommerceService(logger, dataSourceService); 175 | ``` 176 | 177 | or even 178 | 179 | ```typescript 180 | import { ILogger, IDataSourceService } from './interfaces'; 181 | import Logger from './Logger'; 182 | import DataSourceService from './DataSourceService'; 183 | import ECommerceService from './ECommerceService'; 184 | 185 | type DASDeps = { 186 | logger: ILogger; 187 | } 188 | 189 | type EComDeps = { 190 | logger: ILogger; 191 | dataSourceService: IDataSourceService; 192 | } 193 | 194 | export const logger = () => new Logger(); 195 | 196 | export const dataSourceService = ({ logger }: DASDeps) => 197 | new DataSourceService(logger); 198 | 199 | export const ecommerceService = ({ logger, dataSourceService }: EComDeps) => 200 | new ECommerceService(logger, dataSourceService); 201 | ``` 202 | 203 | and then use these factories to create container: 204 | 205 | ```typescript 206 | import express from 'express'; 207 | import diContainer from 'true-di'; 208 | import { logger, dataSourceService, ecommerceService } from './factories'; 209 | import { getOrders } from './controller'; 210 | 211 | const app = express(); 212 | 213 | app.use((req, res, next) => { 214 | req.container = diContainer({ logger, dataSourceService, ecommerceService }); 215 | next(); 216 | }); 217 | ``` 218 | 219 | **Caution** 220 | 221 | Despite pointed bellow way to declare factory seems concise: 222 | 223 | ```typescript 224 | export const logger = () => new Logger(); 225 | ``` 226 | 227 | but the consumers of the container became to be dependent on the interface of specific 228 | logger implementation, that could be wider then `ILogger`. 229 | 230 | So, it is better to declare such factories in this way: 231 | 232 | ```typescript 233 | export const logger = (): ILogger => new Logger(); 234 | ``` -------------------------------------------------------------------------------- /docs/utils/assign-props.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Creates item initializer using passed fields map. `assignProps` builds function that maps some container fields to item fields. 3 | --- 4 | 5 | # assignProps 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import { assignProps } from 'true-di/utils'; 11 | ``` 12 | 13 | ```javascript 14 | const { assignProps } = require('true-di/utils'); 15 | ``` 16 | 17 | ## Declaration 18 | 19 | ```typescript 20 | function assignProps( 21 | mapping: IMapping, 22 | ): (item: IContainer[Name], container: IContainer) => void 23 | ``` 24 | 25 | ## Arguments 26 | 27 | * **mapping**: `IMapping` -- plan JavaScript object which keys are fields of the item 28 | and values are the string names of container items. 29 | 30 | ### Types 31 | 32 | ```typescript 33 | type KeysOfType = { 34 | [P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { [Q in P]: F }, P> 35 | }[keyof T]; 36 | 37 | export type IMapping = { 38 | [p in keyof T]?: KeysOfType 39 | } 40 | ``` 41 | 42 | ## Returns 43 | 44 | * **initializer**: `IInstanceInitializer` 45 | 46 | ## Example: 47 | 48 | ```typescript 49 | import diContainer from 'true-di'; 50 | import { assignProps } from 'true-di/utils'; 51 | 52 | type Node = { 53 | child: Node; 54 | parent: Node; 55 | name: string; 56 | } 57 | 58 | type Container = { 59 | parentItem: Node; 60 | childItem: Node; 61 | } 62 | 63 | const createNodeFactory = (name: string): Node => () => ({ 64 | child: null, parent: null, name 65 | }); 66 | 67 | const container = diContainer({ 68 | parentItem: [ 69 | createNodeFactory('Parent'), 70 | assignProps({ child: 'childItem' }), 71 | ], 72 | childItem: [ 73 | createNodeFactory('Child'), 74 | assignProps({ parent: 'parentItem' }), 75 | ], 76 | }); 77 | ``` -------------------------------------------------------------------------------- /docs/utils/shallow-merge.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: creates new object and copies to the one property descriptors of all source objects. In case of name-conflict last source wins. 3 | --- 4 | 5 | # shallowMerge 6 | 7 | ## Import 8 | 9 | ```typescript 10 | import { shallowMerge } from 'true-di/utils'; 11 | ``` 12 | 13 | ```javascript 14 | const { shallowMerge } = require('true-di/utils'); 15 | ``` 16 | 17 | 18 | ## Declaration 19 | 20 | ```typescript 21 | function shallowMerge(...sourceObject: object[]): object 22 | ``` 23 | 24 | Actually `shallowMerge` is well-typed up to 15 source objects. See [code source](../../src/utils/shallow-merge.ts). 25 | 26 | ## Arguments 27 | 28 | * **sourceObjects**: `object[]` - objects to be merged in new objects. 29 | 30 | ## Returns 31 | 32 | * **new object** 33 | 34 | ### Types 35 | 36 | ```typescript 37 | export type Merge = 38 | Omit & T2; // Overriding Join 39 | ``` 40 | 41 | ## Example 42 | 43 | ```typescript 44 | import diContainer, { factoriesFrom } from 'true-di'; 45 | 46 | type IContainer1 = { 47 | service11: IService11, 48 | service12: IService12, 49 | } 50 | 51 | type IContainer2 = { 52 | service11: IService11, 53 | service12: IService12, 54 | } 55 | 56 | const container1 = diContainer(Object.create(null, { 57 | service11: { enumerable: false, value: () => createService11() }, 58 | service12: { enumerable: false, value: () => createService12() }, 59 | })); 60 | 61 | const container2 = diContainer(Object.create(null, { 62 | service21: { enumerable: false, value: () => createService21() }, 63 | service22: { enumerable: false, value: () => createService22() }, 64 | })); 65 | 66 | export default diContainer(shallowMerge( 67 | factoriesFrom(container1), 68 | factoriesFrom(container2), 69 | )); 70 | ``` 71 | -------------------------------------------------------------------------------- /examples/getting-started/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "airbnb/base" 5 | ], 6 | "plugins": [ 7 | "@typescript-eslint", 8 | "jest" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "rules": { 12 | "class-methods-use-this": "off", 13 | "no-console": "off", 14 | "max-len": [ 15 | "error", 16 | { 17 | "code": 100, 18 | "ignoreStrings": true 19 | } 20 | ], 21 | "implicit-arrow-linebreak": "off", 22 | "import/no-unresolved": 0, 23 | "import/prefer-default-export": 0, 24 | "indent": [ 25 | 2, 26 | 2, 27 | { 28 | "flatTernaryExpressions": true 29 | } 30 | ], 31 | "no-unused-vars": "off", 32 | "no-undef": "error", 33 | "no-tabs": "error", 34 | "no-param-reassign": "off", 35 | "no-nested-ternary": 0, 36 | "import/extensions": 0, 37 | "arrow-parens": [ 38 | "error", 39 | "as-needed" 40 | ], 41 | "operator-linebreak": 0, 42 | "no-underscore-dangle": 0, 43 | "import/no-extraneous-dependencies": ["error", { "devDependencies": ["**/*.test.*"] }], 44 | "@typescript-eslint/no-unused-vars": [ 45 | "error" 46 | ], 47 | "jest/no-disabled-tests": "warn", 48 | "jest/no-focused-tests": "error", 49 | "jest/no-identical-title": "error", 50 | "jest/prefer-to-have-length": "warn", 51 | "jest/valid-expect": "error" 52 | }, 53 | "env": { 54 | "node": true, 55 | "jest/globals": true 56 | }, 57 | "ignorePatterns": [ 58 | "lib/**/*", 59 | "esm/**/*" 60 | ] 61 | } -------------------------------------------------------------------------------- /examples/getting-started/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dmitry Scheglov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started Sample 2 | 3 | ## Install 4 | 5 | ```shell 6 | git clone https://github.com/DScheglov/true-di.git && cd examples/getting-started 7 | npm install 8 | ``` 9 | 10 | ## Start Local Server 11 | 12 | ```shell 13 | npm start 14 | ``` 15 | 16 | ## Runnung Tests 17 | 18 | ```shell 19 | npm test 20 | ``` -------------------------------------------------------------------------------- /examples/getting-started/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "collectCoverage": true, 5 | "collectCoverageFrom": ["src/**/*.ts"], 6 | "coveragePathIgnorePatterns": [ 7 | "src/interfaces", "__fake__" 8 | ] 9 | } -------------------------------------------------------------------------------- /examples/getting-started/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started", 3 | "private": "true", 4 | "version": "1.0.0", 5 | "description": "true-di live demo", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "start": "nodemon --exec ts-node src/index.ts", 9 | "lint": "eslint ./src/**/*.ts", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@types/uuid": "^8.3.0", 14 | "express": "^4.17.1", 15 | "express-async-context": "^1.1.2", 16 | "true-di": "^2.0.0", 17 | "uuid": "^8.3.0" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^4.17.17", 21 | "@types/jest": "^29.4.0", 22 | "@types/node": "^20.12.7", 23 | "@types/supertest": "^2.0.12", 24 | "@typescript-eslint/eslint-plugin": "^5.53.0", 25 | "@typescript-eslint/parser": "^5.53.0", 26 | "eslint": "^8.34.0", 27 | "eslint-config-airbnb": "^19.0.4", 28 | "eslint-plugin-import": "^2.27.5", 29 | "eslint-plugin-jest": "^27.2.1", 30 | "jest": "^29.4.3", 31 | "light-my-request": "^5.13.0", 32 | "nodemon": "^3.1.0", 33 | "ts-jest": "^29.0.5", 34 | "ts-node": "^10.9.1", 35 | "typescript": "^5.6.3" 36 | }, 37 | "keywords": [ 38 | "container", 39 | "inject", 40 | "inversion", 41 | "dependency", 42 | "ioc" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/getting-started/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "view": "browser", 5 | "container": { 6 | "node": "16" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/getting-started/src/DataSourceService/__fake__/generators.ts: -------------------------------------------------------------------------------- 1 | export const fakeFloat = (min: number, max: number) => Math.random() * (max - min) + min; 2 | 3 | export const fakeInt = (min: number, max: number) => Math.round(fakeFloat(min, max)); 4 | 5 | export const fakeItemOf = (list: T[]) => (): T => list[fakeInt(0, list.length - 1)]; 6 | 7 | export const fakePrice = (min: number, max: number, fractionDigits: number = 2) => 8 | parseFloat(fakeFloat(min, max).toFixed(fractionDigits)); 9 | -------------------------------------------------------------------------------- /examples/getting-started/src/DataSourceService/__fake__/order-items.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | import { createOrderItem } from '../../Orders'; 3 | import { fakeItemOf, fakeInt, fakePrice } from './generators'; 4 | 5 | const NUMBER_OF_ORDER_IDS = 5; 6 | const NUMBER_OF_ORDER_ITEMS = 15; 7 | 8 | const randomOrderId = fakeItemOf(Array.from({ length: NUMBER_OF_ORDER_IDS }, () => uuid())); 9 | 10 | export default Array.from({ length: NUMBER_OF_ORDER_ITEMS }, () => createOrderItem( 11 | uuid(), 12 | randomOrderId(), 13 | `stock-id:${fakeInt(1000, 10000)}`, 14 | fakePrice(10, 1000), 15 | fakeInt(1, 10), 16 | )); 17 | -------------------------------------------------------------------------------- /examples/getting-started/src/DataSourceService/index.test.ts: -------------------------------------------------------------------------------- 1 | import DataSourceService from '.'; 2 | import { IDataSourceService, IInfoLogger } from '../interfaces'; 3 | import fakeOrderItems from './__fake__/order-items'; 4 | 5 | const fakeLogger: IInfoLogger = { 6 | info: jest.fn(), 7 | }; 8 | 9 | describe('DataSourceService', () => { 10 | it('allows to instantiate dataSourceService', () => { 11 | const dataSourceService: IDataSourceService = new DataSourceService(fakeLogger); 12 | 13 | expect(dataSourceService).toBeInstanceOf(DataSourceService); 14 | }); 15 | 16 | it('prints message to log on creation', () => { 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | const dataSourceService: IDataSourceService = new DataSourceService(fakeLogger); 19 | 20 | expect(fakeLogger.info).toHaveBeenCalledWith('DataSourseService has been created'); 21 | }); 22 | 23 | it('returns order items with method getOrderItems', async () => { 24 | const dataSourceService: IDataSourceService = new DataSourceService(fakeLogger); 25 | 26 | expect(await dataSourceService.getOrderItems()).toEqual(fakeOrderItems); 27 | }); 28 | 29 | it('returns filtered list of order items with method getOrderItems', async () => { 30 | const dataSourceService: IDataSourceService = new DataSourceService(fakeLogger); 31 | 32 | expect(await dataSourceService.getOrderItems( 33 | ({ id }) => id === fakeOrderItems[0].id, 34 | )).toEqual([fakeOrderItems[0]]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/getting-started/src/DataSourceService/index.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceService, IInfoLogger, OrderItem } from '../interfaces'; 2 | import fakeOrderItems from './__fake__/order-items'; 3 | 4 | class DataSourceService implements IDataSourceService { 5 | private readonly _data: OrderItem[] = fakeOrderItems; 6 | 7 | constructor(_logger: IInfoLogger) { 8 | _logger.info('DataSourseService has been created'); 9 | } 10 | 11 | getOrderItems(predicate?: (orderItem: OrderItem) => boolean): Promise { 12 | return Promise.resolve( 13 | typeof predicate === 'function' 14 | ? this._data.filter(predicate) 15 | : this._data, 16 | ); 17 | } 18 | } 19 | 20 | export default DataSourceService; 21 | -------------------------------------------------------------------------------- /examples/getting-started/src/ECommerceService/index.test.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import ECommerceService from '.'; 3 | import { 4 | IDataSourceService, IECommerceService, IInfoLogger, OrderItem, 5 | } from '../interfaces'; 6 | import { ordersFromItems } from '../Orders'; 7 | 8 | const fakeOrderItems = [ 9 | { 10 | id: '9acec35f-2402-40a7-92cc-664a4ade4778', 11 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 12 | sku: 'stock-id:1397', 13 | unitPrice: 215.1, 14 | quantity: 5, 15 | }, 16 | { 17 | id: '845ef455-0531-487b-b1b9-8d8adf55e606', 18 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 19 | sku: 'stock-id:8255', 20 | unitPrice: 692.63, 21 | quantity: 1, 22 | }, 23 | { 24 | id: 'd2a87c78-e7c2-42ee-9ad0-d88cbd151841', 25 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 26 | sku: 'stock-id:6393', 27 | unitPrice: 360.5, 28 | quantity: 2, 29 | }, 30 | { 31 | id: '20e7ba33-7bd3-4390-853b-55bcad49562b', 32 | orderId: '8db5d840-3a8b-45fe-a6e2-1b202f716717', 33 | sku: 'stock-id:8439', 34 | unitPrice: 530.7, 35 | quantity: 9, 36 | }, 37 | { 38 | id: '9c686fa1-d490-4196-89c5-01a423b35876', 39 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 40 | sku: 'stock-id:1667', 41 | unitPrice: 544.99, 42 | quantity: 7, 43 | }, 44 | ]; 45 | 46 | describe('ECommerceService', () => { 47 | it('allows to instantiate ecommerceService', () => { 48 | const fakeLogger: IInfoLogger = { 49 | info: jest.fn(), 50 | }; 51 | 52 | const fakeDataSource: IDataSourceService = { 53 | getOrderItems: jest.fn().mockResolvedValue(fakeOrderItems), 54 | }; 55 | const ecommerceService: IECommerceService = new ECommerceService(fakeLogger, fakeDataSource); 56 | 57 | expect(ecommerceService).toBeInstanceOf(ECommerceService); 58 | }); 59 | 60 | it('returns orders with method .getOrders', async () => { 61 | expect.assertions(1); 62 | 63 | const fakeLogger: IInfoLogger = { 64 | info: jest.fn(), 65 | }; 66 | 67 | const fakeDataSource: IDataSourceService = { 68 | getOrderItems: jest.fn().mockResolvedValue(fakeOrderItems), 69 | }; 70 | const ecommerceService: IECommerceService = new ECommerceService(fakeLogger, fakeDataSource); 71 | 72 | expect(await ecommerceService.getOrders()).toEqual(ordersFromItems(fakeOrderItems)); 73 | }); 74 | 75 | it('returns order with method .getOrderById', async () => { 76 | expect.assertions(1); 77 | 78 | const fakeLogger: IInfoLogger = { 79 | info: jest.fn(), 80 | }; 81 | 82 | const fakeDataSource = { 83 | getOrderItems: jest.fn( 84 | (predicate?: (orderItem: OrderItem) => boolean) => 85 | Promise.resolve(fakeOrderItems.filter(predicate != null ? predicate : () => true)), 86 | ), 87 | }; 88 | const ecommerceService: IECommerceService = new ECommerceService(fakeLogger, fakeDataSource); 89 | 90 | expect( 91 | await ecommerceService.getOrderById(fakeOrderItems[0].orderId), 92 | ).toEqual(ordersFromItems(fakeOrderItems)[0]); 93 | }); 94 | 95 | it('returns null with method .getOrderById if no order is found', async () => { 96 | expect.assertions(1); 97 | 98 | const fakeLogger: IInfoLogger = { 99 | info: jest.fn(), 100 | }; 101 | 102 | const fakeDataSource = { 103 | getOrderItems: jest.fn().mockResolvedValue([]), 104 | }; 105 | 106 | const ecommerceService: IECommerceService = new ECommerceService(fakeLogger, fakeDataSource); 107 | expect( 108 | await ecommerceService.getOrderById('9acec35f-2402-40a7-92cc-664a4ade4778'), 109 | ).toEqual(null); 110 | }); 111 | 112 | it('throws AssertionError with method .getOrderById if id is not a UUID', async () => { 113 | expect.assertions(1); 114 | 115 | const fakeLogger: IInfoLogger = { 116 | info: jest.fn(), 117 | }; 118 | 119 | const fakeDataSource = { 120 | getOrderItems: jest.fn().mockResolvedValue([]), 121 | }; 122 | 123 | const ecommerceService: IECommerceService = new ECommerceService(fakeLogger, fakeDataSource); 124 | 125 | const error = await ecommerceService.getOrderById('9acec35f-2402-40a7-92cc').catch(err => err); 126 | 127 | expect(error).toBeInstanceOf(AssertionError); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /examples/getting-started/src/ECommerceService/index.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { 3 | IECommerceService, IDataSourceService, Order, IInfoLogger, 4 | } from '../interfaces'; 5 | import { ordersFromItems } from '../Orders'; 6 | import { isUUID } from '../utils/isUUID'; 7 | 8 | class ECommerceSerive implements IECommerceService { 9 | constructor( 10 | private readonly _logger: IInfoLogger, 11 | private readonly _dataSourceService: IDataSourceService, 12 | ) { 13 | _logger.info('ECommerceService has been created'); 14 | } 15 | 16 | async getOrders(): Promise { 17 | const { _logger, _dataSourceService } = this; 18 | 19 | _logger.info('getOrders has been called'); 20 | 21 | const orderItems = await _dataSourceService.getOrderItems(); 22 | 23 | return ordersFromItems(orderItems); 24 | } 25 | 26 | async getOrderById(id: string): Promise { 27 | assert(isUUID(id), `${id} is not a valid UUID`); 28 | 29 | const { _logger, _dataSourceService } = this; 30 | 31 | _logger.info('getOrderById has been called'); 32 | 33 | const orderItems = await _dataSourceService.getOrderItems( 34 | ({ orderId }) => orderId === id, 35 | ); 36 | 37 | return orderItems.length > 0 38 | ? ordersFromItems(orderItems)[0] 39 | : null; 40 | } 41 | } 42 | 43 | export default ECommerceSerive; 44 | -------------------------------------------------------------------------------- /examples/getting-started/src/Logger/LogEntry.test.ts: -------------------------------------------------------------------------------- 1 | import { infoLogEntry, warnLogEntry, errorLogEntry } from './LogEntry'; 2 | 3 | describe('LogEntry', () => { 4 | beforeEach(() => { 5 | jest.spyOn(Date, 'now').mockReturnValue(Date.now()); 6 | }); 7 | 8 | afterEach(() => { 9 | jest.restoreAllMocks(); 10 | }); 11 | 12 | it('allows to create INFO Log Entry with infoLogEntry factory', () => { 13 | expect(infoLogEntry('Some Message')).toEqual({ 14 | type: 'INFO', 15 | timestamp: Math.floor(Date.now() / 1000), 16 | message: 'Some Message', 17 | }); 18 | }); 19 | 20 | it('allows to create WARNING Log Entry with errorLogEntry factory', () => { 21 | expect(warnLogEntry('Some Warning')).toEqual({ 22 | type: 'WARNING', 23 | timestamp: Math.floor(Date.now() / 1000), 24 | message: 'Some Warning', 25 | }); 26 | }); 27 | 28 | it('allows to create ERROR Log Entry with errorLogEntry factory', () => { 29 | expect(errorLogEntry('Some Error Message')).toEqual({ 30 | type: 'ERROR', 31 | timestamp: Math.floor(Date.now() / 1000), 32 | message: 'Some Error Message', 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/getting-started/src/Logger/LogEntry.ts: -------------------------------------------------------------------------------- 1 | export type LogEntry = { 2 | type: 'INFO' | 'WARNING' | 'ERROR', 3 | timestamp: number, 4 | message: string, 5 | } 6 | 7 | const logEntryOfType = (type: LogEntry['type']) => 8 | (message: LogEntry['message']): LogEntry => ({ 9 | type, 10 | timestamp: Math.floor(Date.now() / 1000), 11 | message, 12 | }); 13 | 14 | export const infoLogEntry = logEntryOfType('INFO'); 15 | export const warnLogEntry = logEntryOfType('WARNING'); 16 | export const errorLogEntry = logEntryOfType('ERROR'); 17 | -------------------------------------------------------------------------------- /examples/getting-started/src/Logger/index.test.ts: -------------------------------------------------------------------------------- 1 | import Logger, { LogLevel } from '.'; 2 | 3 | describe('Logger', () => { 4 | beforeEach(() => { 5 | jest.spyOn(console, 'log').mockImplementation(() => {}); 6 | jest.spyOn(console, 'info').mockImplementation(() => {}); 7 | jest.spyOn(console, 'warn').mockImplementation(() => {}); 8 | jest.spyOn(console, 'error').mockImplementation(() => {}); 9 | jest.spyOn(Date, 'now').mockReturnValue(Date.now()); 10 | }); 11 | 12 | afterEach(() => { 13 | jest.restoreAllMocks(); 14 | }); 15 | 16 | it('creates a logger object with new operator', () => { 17 | const logger = new Logger(); 18 | 19 | expect(logger).toBeInstanceOf(Logger); 20 | }); 21 | 22 | it('prints to console info message with .info method', () => { 23 | const logger = new Logger(); 24 | 25 | logger.info('Some Message'); 26 | 27 | expect(console.info).toHaveBeenCalledTimes(1); 28 | expect(console.info).toHaveBeenCalledWith({ 29 | type: 'INFO', 30 | timestamp: Math.floor(Date.now() / 1000), 31 | message: 'Some Message', 32 | }); 33 | }); 34 | 35 | it('doesn\'t print to console info message with .info method if logLevel is less then INFO', () => { 36 | const logger = new Logger(LogLevel.INFO - 1); 37 | 38 | logger.info('Some Message'); 39 | 40 | expect(console.info).not.toHaveBeenCalled(); 41 | }); 42 | 43 | it('prints to console warning message with .warn method', () => { 44 | const logger = new Logger(); 45 | 46 | logger.warn('Some Warning'); 47 | 48 | expect(console.warn).toHaveBeenCalledTimes(1); 49 | expect(console.warn).toHaveBeenCalledWith({ 50 | type: 'WARNING', 51 | timestamp: Math.floor(Date.now() / 1000), 52 | message: 'Some Warning', 53 | }); 54 | }); 55 | 56 | it('doesn\'t print to console warning message with .warn method if logLevel is less then WARINING', () => { 57 | const logger = new Logger(LogLevel.WARNING - 1); 58 | 59 | logger.warn('Some Warning'); 60 | 61 | expect(console.warn).not.toHaveBeenCalled(); 62 | }); 63 | 64 | it('prints to console error message with .error method', () => { 65 | const logger = new Logger(); 66 | 67 | logger.error(new Error('Some Error Message')); 68 | 69 | expect(console.error).toHaveBeenCalledTimes(1); 70 | expect(console.error).toHaveBeenCalledWith({ 71 | type: 'ERROR', 72 | timestamp: Math.floor(Date.now() / 1000), 73 | message: 'Some Error Message', 74 | }); 75 | }); 76 | 77 | it('doesn\'t print to console error message with .error method if logLevel is less then ERROR', () => { 78 | const logger = new Logger(LogLevel.SILENT); 79 | 80 | logger.error(new Error('Some Error Message')); 81 | 82 | expect(console.error).not.toHaveBeenCalled(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /examples/getting-started/src/Logger/index.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../interfaces'; 2 | import { infoLogEntry, warnLogEntry, errorLogEntry } from './LogEntry'; 3 | 4 | export enum LogLevel { 5 | SILENT = 0, 6 | ERROR = 1, 7 | WARNING = 2, 8 | INFO = 3, 9 | VERBOSE = 4 10 | } 11 | 12 | class Logger implements ILogger { 13 | constructor(private readonly _level: LogLevel = LogLevel.VERBOSE) { 14 | console.log('Logging level is', _level); 15 | } 16 | 17 | public info(message: string) { 18 | if (this._level < LogLevel.INFO) return; 19 | console.info(infoLogEntry(message)); 20 | } 21 | 22 | public warn(message: string) { 23 | if (this._level < LogLevel.WARNING) return; 24 | console.warn(warnLogEntry(message)); 25 | } 26 | 27 | public error(err: Error) { 28 | if (this._level < LogLevel.ERROR) return; 29 | console.error(errorLogEntry(err.message)); 30 | } 31 | } 32 | 33 | export default Logger; 34 | -------------------------------------------------------------------------------- /examples/getting-started/src/Orders/index.test.ts: -------------------------------------------------------------------------------- 1 | import { createOrder, createOrderItem, ordersFromItems } from '.'; 2 | import { Order, OrderItem } from './types'; 3 | 4 | describe('Orders', () => { 5 | describe('createOrderItem', () => { 6 | it('creates an orderItem with default values', () => { 7 | const orderItem: OrderItem = createOrderItem(); 8 | 9 | expect(orderItem).toEqual({ 10 | id: '', 11 | orderId: '', 12 | sku: '', 13 | unitPrice: 0, 14 | quantity: 0, 15 | }); 16 | }); 17 | 18 | it('creates an orderItem with specified values', () => { 19 | const orderItem: OrderItem = createOrderItem('orderItemId', 'orderId', 'sku', 10, 1); 20 | 21 | expect(orderItem).toEqual({ 22 | id: 'orderItemId', 23 | orderId: 'orderId', 24 | sku: 'sku', 25 | unitPrice: 10, 26 | quantity: 1, 27 | }); 28 | }); 29 | }); 30 | 31 | describe('createOrder', () => { 32 | it('creates an Order with default values', () => { 33 | const order: Order = createOrder(); 34 | 35 | expect(order).toEqual({ 36 | id: '', 37 | items: [], 38 | total: 0, 39 | }); 40 | }); 41 | 42 | it('creates an Order with specified values', () => { 43 | const order: Order = createOrder('id', [], 0); 44 | 45 | expect(order).toEqual({ 46 | id: 'id', 47 | items: [], 48 | total: 0, 49 | }); 50 | }); 51 | }); 52 | 53 | describe('ordersFromItems', () => { 54 | it('returns empty list of orders for empty list of orderItems', () => { 55 | const orders: Order[] = ordersFromItems([]); 56 | 57 | expect(orders).toEqual([]); 58 | }); 59 | 60 | it('returns single order for single orderItem', () => { 61 | const orderItems: OrderItem[] = [ 62 | createOrderItem('orderItemId', 'orderId', 'sku', 10, 10), 63 | ]; 64 | 65 | const orders: Order[] = ordersFromItems(orderItems); 66 | 67 | expect(orders).toEqual([ 68 | createOrder('orderId', orderItems, 100), 69 | ]); 70 | }); 71 | 72 | it('returns groups orderItems with the same id to the single order', () => { 73 | const orderItems: OrderItem[] = [ 74 | createOrderItem('orderItemId-1', 'orderId', 'sku-1', 10, 10), 75 | createOrderItem('orderItemId-2', 'orderId', 'sku-2', 100, 2), 76 | ]; 77 | 78 | const orders: Order[] = ordersFromItems(orderItems); 79 | 80 | expect(orders).toEqual([ 81 | createOrder('orderId', orderItems, 300), 82 | ]); 83 | }); 84 | 85 | it('returns groups orderItems with the different orderId to the diferent orders', () => { 86 | const orderItems1: OrderItem[] = [ 87 | createOrderItem('orderItemId-1', 'orderId-1', 'sku-1', 10, 10), 88 | createOrderItem('orderItemId-2', 'orderId-1', 'sku-2', 100, 2), 89 | ]; 90 | 91 | const orderItems2: OrderItem[] = [ 92 | createOrderItem('orderItemId-3', 'orderId-2', 'sku-1', 50, 10), 93 | createOrderItem('orderItemId-4', 'orderId-2', 'sku-2', 5, 20), 94 | ]; 95 | 96 | const orders: Order[] = ordersFromItems([...orderItems1, ...orderItems2]); 97 | 98 | expect(orders).toEqual([ 99 | createOrder('orderId-1', orderItems1, 300), 100 | createOrder('orderId-2', orderItems2, 600), 101 | ]); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /examples/getting-started/src/Orders/index.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../utils/groupBy'; 2 | import { Order, OrderItem } from './types'; 3 | 4 | const round2 = (value: number): number => Math.round(value * 100) / 100; 5 | 6 | export const createOrderItem = ( 7 | id: string = '', 8 | orderId: string = '', 9 | sku: string = '', 10 | unitPrice: number = 0, 11 | quantity: number = 0, 12 | ): OrderItem => ({ 13 | id, orderId, sku, unitPrice, quantity, 14 | }); 15 | 16 | const orderItemPrice = ({ unitPrice, quantity }: OrderItem): number => 17 | round2(unitPrice * quantity); 18 | 19 | export const createOrder = ( 20 | id: string = '', 21 | items: OrderItem[] = [], 22 | total: number = 0, 23 | ): Order => ({ id, items, total }); 24 | 25 | export const ordersFromItems = groupBy( 26 | ({ orderId }: OrderItem) => orderId, 27 | (order: Order = createOrder(), orderItem) => { // eslint-disable-line default-param-last 28 | order.id = orderItem.orderId; 29 | order.items.push(orderItem); 30 | order.total = round2(order.total + orderItemPrice(orderItem)); 31 | return order; 32 | }, 33 | ); 34 | -------------------------------------------------------------------------------- /examples/getting-started/src/Orders/types.ts: -------------------------------------------------------------------------------- 1 | export type OrderItem = { 2 | id: string; 3 | orderId: string; 4 | sku: string; 5 | unitPrice: number; 6 | quantity: number; 7 | } 8 | 9 | export type Order = { 10 | id: string; 11 | items: OrderItem[], 12 | total: number, 13 | } 14 | -------------------------------------------------------------------------------- /examples/getting-started/src/container.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { prepareAll, releaseAll } from 'true-di'; 3 | import container from './container'; 4 | import { IDataSourceService, IECommerceService, ILogger } from './interfaces'; 5 | 6 | type AssertTypeEqual = T1 extends T2 ? (T2 extends T1 ? true : never) : never; 7 | 8 | describe('container', () => { 9 | beforeAll(() => { 10 | jest.spyOn(console, 'log').mockImplementation(() => {}); 11 | jest.spyOn(console, 'info').mockImplementation(() => {}); 12 | jest.spyOn(console, 'error').mockImplementation(() => {}); 13 | }); 14 | 15 | afterEach(() => { 16 | releaseAll(container); 17 | }); 18 | 19 | afterAll(() => { 20 | jest.restoreAllMocks(); 21 | }); 22 | 23 | it('allows to get logger', () => { 24 | expect(container.logger).toBeDefined(); 25 | 26 | const typecheck: AssertTypeEqual = true; 27 | }); 28 | 29 | it('allows to get dataSourceService', () => { 30 | expect(container.dataSourceService).toBeDefined(); 31 | 32 | const typecheck: AssertTypeEqual = true; 33 | }); 34 | 35 | it('allows to get ecommerceService', () => { 36 | expect(container.ecommerceService).toBeDefined(); 37 | 38 | const typecheck: AssertTypeEqual = true; 39 | }); 40 | 41 | // or just single test 42 | it('allows to instantiate all items', () => { 43 | const items = { ...container }; 44 | 45 | expect(items.logger).toBeDefined(); 46 | expect(items.dataSourceService).toBeDefined(); 47 | expect(items.ecommerceService).toBeDefined(); 48 | 49 | const typecheck: AssertTypeEqual< 50 | typeof items, { 51 | logger: ILogger, 52 | dataSourceService: IDataSourceService, 53 | ecommerceService: IECommerceService, 54 | }> = true; 55 | }); 56 | 57 | // or the same but with prepareAll 58 | it('allows to instantiate all items (prepareAll)', () => { 59 | const items = prepareAll(container); 60 | 61 | expect(items.logger).toBeDefined(); 62 | expect(items.dataSourceService).toBeDefined(); 63 | expect(items.ecommerceService).toBeDefined(); 64 | 65 | const typecheck: AssertTypeEqual< 66 | typeof items, { 67 | logger: ILogger, 68 | dataSourceService: IDataSourceService, 69 | ecommerceService: IECommerceService, 70 | }> = true; 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /examples/getting-started/src/container.ts: -------------------------------------------------------------------------------- 1 | import diContainer from 'true-di'; 2 | import { ILogger, IDataSourceService, IECommerceService } from './interfaces'; 3 | import Logger from './Logger'; 4 | import DataSourceService from './DataSourceService'; 5 | import ECommerceService from './ECommerceService'; 6 | 7 | type IServices = { 8 | logger: ILogger, 9 | dataSourceService: IDataSourceService, 10 | ecommerceService: IECommerceService, 11 | } 12 | 13 | export default diContainer({ 14 | logger: () => 15 | new Logger(), 16 | 17 | dataSourceService: ({ logger }) => 18 | new DataSourceService(logger), 19 | 20 | ecommerceService: ({ logger, dataSourceService }) => 21 | new ECommerceService(logger, dataSourceService), 22 | }); 23 | -------------------------------------------------------------------------------- /examples/getting-started/src/controller.test.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import Express from 'express'; 3 | import { getOrderById, getOrders } from './controller'; 4 | import { IGetOrderById, IGetOrders, Order } from './interfaces'; 5 | import { NotFoundError } from './utils/NotFoundError'; 6 | 7 | const fakeGetOrdersService = (orders: Order[]): IGetOrders => ({ 8 | getOrders: jest.fn(async (): Promise => orders), 9 | }); 10 | 11 | const fakeGetOrderByIdService = (orders: Order[]): IGetOrderById => ({ 12 | getOrderById: jest.fn(async (): Promise => orders[0]), 13 | }); 14 | 15 | function returnThis(this: T):T { return this; } 16 | 17 | const fakeResponse = (): Express.Response => ({ 18 | type: jest.fn(returnThis), 19 | send: jest.fn(returnThis), 20 | }) as any; 21 | 22 | const fakeRequest =

(params: P = {} as P): Express.Request

=> ({ params }) as any; 23 | 24 | describe('controller.getOrders', () => { 25 | it('sends json received from the ecommerceService.getOrders', async () => { 26 | expect.assertions(4); 27 | 28 | const ecommerceService = fakeGetOrdersService([]); 29 | const res = fakeResponse(); 30 | const next = jest.fn(); 31 | 32 | await getOrders(fakeRequest(), res, next)({ ecommerceService }); 33 | 34 | expect(ecommerceService.getOrders).toHaveBeenCalledTimes(1); 35 | expect(res.type).toHaveBeenCalledWith('application/json'); 36 | expect(res.send).toHaveBeenCalledWith('[]'); 37 | expect(next).not.toHaveBeenCalled(); 38 | }); 39 | }); 40 | 41 | describe('controller.getOrderById', () => { 42 | it('sends json received from the ecommerceService.getOrderById', async () => { 43 | expect.assertions(5); 44 | const ecommerceService = fakeGetOrderByIdService([{} as Order]); 45 | const res = fakeResponse(); 46 | const next = jest.fn(); 47 | 48 | await getOrderById( 49 | fakeRequest({ id: '0c6dc1ff-b678-475a-a8cd-13a05525ab11' }), 50 | res, 51 | next, 52 | )({ ecommerceService }); 53 | 54 | expect(ecommerceService.getOrderById).toHaveBeenCalledTimes(1); 55 | expect(ecommerceService.getOrderById).toHaveBeenCalledWith('0c6dc1ff-b678-475a-a8cd-13a05525ab11'); 56 | expect(res.type).toHaveBeenCalledWith('application/json'); 57 | expect(res.send).toHaveBeenCalledWith('{}'); 58 | expect(next).not.toHaveBeenCalled(); 59 | }); 60 | 61 | it('calls next function with NotFoundError if order is not found', async () => { 62 | expect.assertions(4); 63 | const ecommerceService = fakeGetOrderByIdService([null as any]); 64 | const res = fakeResponse(); 65 | const next = jest.fn(); 66 | 67 | await getOrderById( 68 | fakeRequest({ id: '0c6dc1ff-b678-475a-a8cd-13a05525ab11' }), 69 | res, 70 | next, 71 | )({ ecommerceService }); 72 | 73 | expect(ecommerceService.getOrderById).toHaveBeenCalledTimes(1); 74 | expect(res.send).not.toHaveBeenCalled(); 75 | expect(next).toHaveBeenCalledTimes(1); 76 | expect(next).toHaveBeenCalledWith(new NotFoundError('Order(0c6dc1ff-b678-475a-a8cd-13a05525ab11)')); 77 | }); 78 | 79 | it('calls next function with AssertionError if id is not a valid UUID', async () => { 80 | expect.assertions(5); 81 | const ecommerceService = { 82 | getOrderById: jest.fn(async (id: string): Promise => { 83 | throw new AssertionError({ message: `${id} is not a UUID` }); 84 | }), 85 | }; 86 | const res = fakeResponse(); 87 | const next = jest.fn(); 88 | 89 | await getOrderById( 90 | fakeRequest({ id: '0c6dc1ff-b678-475a-a8cd' }), 91 | res, 92 | next, 93 | )({ ecommerceService }); 94 | 95 | expect(ecommerceService.getOrderById).toHaveBeenCalledTimes(1); 96 | expect(ecommerceService.getOrderById).toHaveBeenCalledWith('0c6dc1ff-b678-475a-a8cd'); 97 | expect(res.send).not.toHaveBeenCalled(); 98 | expect(next).toHaveBeenCalledTimes(1); 99 | expect(next).toHaveBeenCalledWith(new AssertionError({ message: '0c6dc1ff-b678-475a-a8cd is not a UUID' })); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /examples/getting-started/src/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction as Next } from 'express'; 2 | import { IGetOrderById, IGetOrders } from './interfaces'; 3 | import { sendJson } from './utils/sendJson'; 4 | import { expectFound } from './utils/NotFoundError'; 5 | 6 | export const getOrders = (req: Request, res: Response, next: Next) => 7 | ({ ecommerceService }: { ecommerceService: IGetOrders }) => 8 | ecommerceService 9 | .getOrders() 10 | .then(sendJson(res), next); 11 | 12 | export const getOrderById = ({ params }: Request, res: Response, next: Next) => 13 | ({ ecommerceService }: { ecommerceService: IGetOrderById }) => 14 | ecommerceService 15 | .getOrderById(params.id) 16 | .then(expectFound(`Order(${params.id})`)) 17 | .then(sendJson(res), next); 18 | -------------------------------------------------------------------------------- /examples/getting-started/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import app from '.'; 3 | 4 | describe('Express App', () => { 5 | const PORT = 11111; 6 | const HOST = 'localhost'; 7 | const baseUrl = `http://${HOST}:${PORT}`; 8 | let server: Server; 9 | 10 | beforeAll(done => { 11 | jest.spyOn(console, 'log').mockImplementation(() => {}); 12 | jest.spyOn(console, 'info').mockImplementation(() => {}); 13 | jest.spyOn(console, 'error').mockImplementation(() => {}); 14 | 15 | server = app.listen(PORT, HOST, done); 16 | }); 17 | 18 | afterAll(() => { 19 | jest.restoreAllMocks(); 20 | server?.close(); 21 | }); 22 | 23 | it('responds with 200 on /orders', async () => { 24 | const res = await fetch(`${baseUrl}/orders`); 25 | expect(res.status).toBe(200); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/getting-started/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import createContext from 'express-async-context'; 3 | import container from './container'; 4 | import { getOrderById, getOrders } from './controller'; 5 | import { handleErrors } from './middlewares'; 6 | 7 | const app = express(); 8 | const Context = createContext(() => container); 9 | 10 | app.use(Context.provider); 11 | 12 | app.get('/orders', Context.consumer(getOrders)); 13 | app.get('/orders/:id', Context.consumer(getOrderById)); 14 | 15 | app.use(Context.consumer(handleErrors)); 16 | 17 | if (module === require.main) { 18 | app.listen(8080, () => { 19 | console.log('Server is listening on port: 8080'); 20 | console.log('Follow: http://localhost:8080/orders'); 21 | }); 22 | } 23 | 24 | export default app; 25 | -------------------------------------------------------------------------------- /examples/getting-started/src/interfaces/IDataSourceService.ts: -------------------------------------------------------------------------------- 1 | import { OrderItem } from '../Orders/types'; 2 | 3 | export interface IDataSourceService { 4 | getOrderItems(predicate?: (orderItem: OrderItem) => boolean): Promise 5 | } 6 | -------------------------------------------------------------------------------- /examples/getting-started/src/interfaces/IECommerceService.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '../Orders/types'; 2 | 3 | export interface IGetOrders { 4 | getOrders(): Promise 5 | } 6 | 7 | export interface IGetOrderById { 8 | getOrderById(id: string): Promise 9 | } 10 | 11 | export interface IECommerceService extends 12 | IGetOrders, 13 | IGetOrderById 14 | {} 15 | -------------------------------------------------------------------------------- /examples/getting-started/src/interfaces/ILogger.ts: -------------------------------------------------------------------------------- 1 | export interface IInfoLogger { 2 | info(message: string) : void; 3 | } 4 | 5 | export interface IWarnLogger { 6 | warn(message: string) : void; 7 | } 8 | 9 | export interface IErrorLogger { 10 | error(err: E): void; 11 | } 12 | 13 | export interface ILogger extends 14 | IInfoLogger, 15 | IWarnLogger, 16 | IErrorLogger 17 | {} 18 | -------------------------------------------------------------------------------- /examples/getting-started/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ILogger'; 2 | export * from '../Orders/types'; 3 | export * from './IDataSourceService'; 4 | export * from './IECommerceService'; 5 | -------------------------------------------------------------------------------- /examples/getting-started/src/middlewares.test.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import Express from 'express'; 3 | import { IErrorLogger, IWarnLogger } from './interfaces'; 4 | import { handleErrors } from './middlewares'; 5 | import { NotFoundError } from './utils/NotFoundError'; 6 | 7 | function returnThis(this: T): T { return this; } 8 | 9 | const fakeResponse = (): Express.Response => ({ 10 | status: jest.fn(returnThis), 11 | json: jest.fn(returnThis), 12 | }) as any; 13 | 14 | describe('handleErrors', () => { 15 | it('handles AssertionError: status = 400, warning: err.message, response: err.message', () => { 16 | const fakeLogger: IWarnLogger & IErrorLogger = { 17 | warn: jest.fn(), 18 | error: jest.fn(), 19 | }; 20 | 21 | const res = fakeResponse(); 22 | const next = jest.fn(); 23 | const error = new AssertionError({ message: 'Something is wrong' }); 24 | 25 | handleErrors(error, {} as Express.Request, res, next)({ logger: fakeLogger }); 26 | 27 | expect(next).not.toHaveBeenCalled(); 28 | expect(fakeLogger.warn).toHaveBeenCalledWith('Something is wrong'); 29 | expect(res.status).toHaveBeenCalledWith(400); 30 | expect(res.json).toHaveBeenCalledWith({ error: 'Something is wrong' }); 31 | }); 32 | 33 | it('handles NotFoundError: status = 404, warning: err.message, response: err.message', () => { 34 | const fakeLogger: IWarnLogger & IErrorLogger = { 35 | warn: jest.fn(), 36 | error: jest.fn(), 37 | }; 38 | 39 | const res = fakeResponse(); 40 | const next = jest.fn(); 41 | const error = new NotFoundError('Entity'); 42 | 43 | handleErrors(error, {} as Express.Request, res, next)({ logger: fakeLogger }); 44 | 45 | expect(next).not.toHaveBeenCalled(); 46 | expect(fakeLogger.warn).toHaveBeenCalledWith('The Entity is not found.'); 47 | expect(res.status).toHaveBeenCalledWith(404); 48 | expect(res.json).toHaveBeenCalledWith({ error: 'The Entity is not found.' }); 49 | }); 50 | 51 | it('handles unrecognized error', () => { 52 | const fakeLogger: IWarnLogger & IErrorLogger = { 53 | warn: jest.fn(), 54 | error: jest.fn(), 55 | }; 56 | 57 | const res = fakeResponse(); 58 | const next = jest.fn(); 59 | const error = new Error('Some Error'); 60 | 61 | handleErrors(error, {} as Express.Request, res, next)({ logger: fakeLogger }); 62 | 63 | expect(next).not.toHaveBeenCalled(); 64 | expect(fakeLogger.error).toHaveBeenCalledWith(error); 65 | expect(res.status).toHaveBeenCalledWith(500); 66 | expect(res.json).toHaveBeenCalledWith({ error: 'Internal Server Error' }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /examples/getting-started/src/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import Express from 'express'; 3 | import { IErrorLogger, IWarnLogger } from './interfaces'; 4 | import { NotFoundError } from './utils/NotFoundError'; 5 | 6 | export const handleErrors = ( 7 | err: Error, 8 | req: Express.Request, 9 | res: Express.Response, 10 | next: any, // eslint-disable-line @typescript-eslint/no-unused-vars 11 | ) => ({ logger }: { logger: IWarnLogger & IErrorLogger }) => { 12 | const statusCode = 13 | err instanceof AssertionError ? 400 : 14 | err instanceof NotFoundError ? 404 : 15 | 500; 16 | 17 | if (statusCode === 500) { 18 | logger.error(err); 19 | res.status(statusCode).json({ error: 'Internal Server Error' }); 20 | return; 21 | } 22 | 23 | logger.warn(err.message); 24 | res.status(statusCode).json({ error: err.message }); 25 | }; 26 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/NotFoundError.test.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError, expectFound } from './NotFoundError'; 2 | 3 | describe('NotFoundError', () => { 4 | it('creates an instance of Error', () => { 5 | expect(new NotFoundError('Entity')).toBeInstanceOf(Error); 6 | }); 7 | 8 | it('created an instance of NotFoundError', () => { 9 | expect(new NotFoundError('Entity')).toBeInstanceOf(NotFoundError); 10 | }); 11 | 12 | it('creates an error with message: "The Entity is not found."', () => { 13 | expect(new NotFoundError('Entity').message).toBe('The Entity is not found.'); 14 | }); 15 | }); 16 | 17 | describe('expectFound', () => { 18 | it('is a closure', () => { 19 | expect(expectFound('Entity')).toBeInstanceOf(Function); 20 | }); 21 | 22 | it('returns value if it is not null or undefined', () => { 23 | const obj = {}; 24 | 25 | expect(expectFound('Entity')(obj)).toBe(obj); 26 | }); 27 | 28 | it('throws an error if value is null', () => { 29 | expect( 30 | () => expectFound('Entity')(null), 31 | ).toThrow(new NotFoundError('Entity')); 32 | }); 33 | 34 | it('throws an error if value is undefined', () => { 35 | expect( 36 | () => expectFound('Entity')(undefined), 37 | ).toThrow(new NotFoundError('Entity')); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class NotFoundError extends Error { 2 | constructor(entityTypeName: string) { 3 | super(`The ${entityTypeName} is not found.`); 4 | Object.setPrototypeOf(this, NotFoundError.prototype); 5 | Error.captureStackTrace(this, NotFoundError); 6 | } 7 | } 8 | 9 | export const expectFound = (entityTypeName: string) => (value: T | null | undefined): T => { 10 | if (value == null) throw new NotFoundError(entityTypeName); 11 | return value; 12 | }; 13 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/groupBy.test.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from './groupBy'; 2 | 3 | describe('groupBy', () => { 4 | it('creates grouping function', () => { 5 | expect( 6 | groupBy( 7 | (x: number) => x, 8 | (list: number[] = [], item: number) => { 9 | list.push(item); 10 | return list; 11 | }, 12 | ), 13 | ).toBeInstanceOf(Function); 14 | }); 15 | 16 | describe('grouping', () => { 17 | it('returns empty list for empty list', () => { 18 | const groupByValue = groupBy( 19 | (x: number) => x, 20 | (list: number[] = [], item: number) => { 21 | list.push(item); 22 | return list; 23 | }, 24 | ); 25 | 26 | expect(groupByValue([])).toEqual([]); 27 | }); 28 | 29 | it('counts number of same items', () => { 30 | const groupByValue = groupBy( 31 | (x: number) => x, 32 | (count: number = 0) => count + 1, 33 | ); 34 | 35 | expect(groupByValue([1, 2, 3, 4])).toEqual([1, 1, 1, 1]); 36 | expect(groupByValue([1, 2, 1, 4])).toEqual([2, 1, 1]); 37 | }); 38 | 39 | it('groups same items', () => { 40 | const groupByValue = groupBy( 41 | (x: number) => x, 42 | (list: number[] = [], item) => { 43 | list.push(item); 44 | return list; 45 | }, 46 | ); 47 | 48 | expect(groupByValue([1, 2, 3, 4])).toEqual([[1], [2], [3], [4]]); 49 | expect(groupByValue([1, 2, 1, 4])).toEqual([[1, 1], [2], [4]]); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/groupBy.ts: -------------------------------------------------------------------------------- 1 | type KeyResolver = (record: R) => K; 2 | type Reducer = (groupTotal: T, record: R, index: number, list: R[]) => T; 3 | 4 | export const groupBy = (getKey: KeyResolver, reducer: Reducer) => 5 | (records: R[]): T[] => Array.from(records.reduce( 6 | (groups, record: R, index, list) => { 7 | const key = getKey(record); 8 | return groups.set(key, reducer(groups.get(key), record, index, list)); 9 | }, 10 | new Map(), 11 | ).values()); 12 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/isUUID.test.ts: -------------------------------------------------------------------------------- 1 | import { isUUID } from './isUUID'; 2 | 3 | describe('isUUID', () => { 4 | it.each([ 5 | ['25c6633c-08c4-45ec-b553-d70106963c14', true], 6 | ['25c6633c-08c4-45ec-b553-d70106963c14'.toUpperCase(), true], 7 | ['60e660b7-6833-443f-a736-dd1e2ab37649', true], 8 | ['23cfab4d-103b-475f-830a-79e7f8c93c05', true], 9 | ['', false], 10 | ['25c6633c-08c4-45ec-b553-d70106963c1Z', false], 11 | ['25c6633c-08c4-45ec-d70106963c14', false], 12 | ['25c6633c-08c4-45ec-b553-d70106963c142-12313123', false], 13 | ])("isUUID('%s') is %s", (value, expected) => { 14 | expect(isUUID(value)).toBe(expected); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/isUUID.ts: -------------------------------------------------------------------------------- 1 | export const isUUID = (value: string): boolean => 2 | /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); 3 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/sendJson.test.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import { sendJson } from './sendJson'; 3 | 4 | function returnThis(this: T): T { return this; } 5 | 6 | const fakeResponse: Express.Response = { 7 | type: jest.fn(returnThis), 8 | send: jest.fn(returnThis), 9 | } as any; 10 | 11 | describe('Utils.sendJson', () => { 12 | afterEach(() => { 13 | jest.restoreAllMocks(); 14 | }); 15 | 16 | it('stringifies object to JSON and sends via res.send', () => { 17 | sendJson(fakeResponse)({ title: 'The Title' }); 18 | 19 | expect(fakeResponse.type).toHaveBeenCalledWith('application/json'); 20 | expect(fakeResponse.send).toHaveBeenCalledWith( 21 | JSON.stringify({ title: 'The Title' }, null, 2), 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/getting-started/src/utils/sendJson.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | 3 | export const sendJson = (res: Express.Response) => (data: object) => 4 | res 5 | .type('application/json') 6 | .send(JSON.stringify(data, null, 2)); 7 | -------------------------------------------------------------------------------- /examples/getting-started/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/plain-http-server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "airbnb/base" 5 | ], 6 | "plugins": [ 7 | "@typescript-eslint", 8 | "jest" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "rules": { 12 | "class-methods-use-this": "off", 13 | "no-console": "off", 14 | "max-len": [ 15 | "error", 16 | { 17 | "code": 100, 18 | "ignoreStrings": true 19 | } 20 | ], 21 | "implicit-arrow-linebreak": "off", 22 | "import/no-unresolved": 0, 23 | "import/prefer-default-export": 0, 24 | "indent": [ 25 | 2, 26 | 2, 27 | { 28 | "flatTernaryExpressions": true 29 | } 30 | ], 31 | "no-unused-vars": "off", 32 | "no-undef": "error", 33 | "no-tabs": "error", 34 | "no-param-reassign": "off", 35 | "no-nested-ternary": 0, 36 | "import/extensions": 0, 37 | "arrow-parens": [ 38 | "error", 39 | "as-needed" 40 | ], 41 | "operator-linebreak": 0, 42 | "no-underscore-dangle": 0, 43 | "import/no-extraneous-dependencies": ["error", { "devDependencies": ["**/*.test.*"] }], 44 | "@typescript-eslint/no-unused-vars": [ 45 | "error" 46 | ], 47 | "jest/no-disabled-tests": "warn", 48 | "jest/no-focused-tests": "error", 49 | "jest/no-identical-title": "error", 50 | "jest/prefer-to-have-length": "warn", 51 | "jest/valid-expect": "error" 52 | }, 53 | "env": { 54 | "node": true, 55 | "jest/globals": true 56 | }, 57 | "ignorePatterns": [ 58 | "lib/**/*", 59 | "esm/**/*" 60 | ] 61 | } -------------------------------------------------------------------------------- /examples/plain-http-server/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dmitry Scheglov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/plain-http-server/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started Sample 2 | 3 | ## Install 4 | 5 | ```shell 6 | git clone https://github.com/DScheglov/true-di.git && cd examples/plain-http-server 7 | npm install 8 | ``` 9 | 10 | ## Start Local Server 11 | 12 | ```shell 13 | npm start 14 | ``` 15 | 16 | ## Runnung Tests 17 | 18 | ```shell 19 | npm test 20 | ``` -------------------------------------------------------------------------------- /examples/plain-http-server/client.rest: -------------------------------------------------------------------------------- 1 | GET http://localhost:8080/orders -------------------------------------------------------------------------------- /examples/plain-http-server/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "collectCoverage": true, 5 | "collectCoverageFrom": ["src/**/*.ts"], 6 | "coveragePathIgnorePatterns": [ 7 | "src/interfaces", "__fake__" 8 | ] 9 | } -------------------------------------------------------------------------------- /examples/plain-http-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-fp-ctx", 3 | "private": "true", 4 | "version": "1.0.0", 5 | "description": "true-di live demo", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "start": "nodemon --exec ts-node src/index.ts", 9 | "lint": "eslint ./src/**/*.ts", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@types/path-to-regexp": "^1.7.0", 14 | "@types/uuid": "^8.3.0", 15 | "express": "^4.21.1", 16 | "path-to-regexp": "^8.1.0", 17 | "true-di": "^2.0.0", 18 | "uuid": "^8.3.0" 19 | }, 20 | "devDependencies": { 21 | "@types/express": "^4.17.21", 22 | "@types/jest": "^29.5.12", 23 | "@types/node": "^22.5.4", 24 | "@types/supertest": "^6.0.2", 25 | "@typescript-eslint/eslint-plugin": "^2.34.0", 26 | "@typescript-eslint/parser": "^2.34.0", 27 | "eslint": "^6.7.2", 28 | "eslint-config-airbnb": "^18.1.0", 29 | "eslint-plugin-import": "^2.21.2", 30 | "eslint-plugin-jest": "^23.13.2", 31 | "jest": "^29.7.0", 32 | "nodemon": "^3.1.4", 33 | "supertest": "^7.0.0", 34 | "ts-jest": "^29.2.5", 35 | "ts-node": "^10.9.2", 36 | "typescript": "^5.6.2" 37 | }, 38 | "keywords": [ 39 | "container", 40 | "inject", 41 | "inversion", 42 | "dependency", 43 | "ioc" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/DataSourceService/__fake__/generators.ts: -------------------------------------------------------------------------------- 1 | export const fakeFloat = (min: number, max: number) => Math.random() * (max - min) + min; 2 | 3 | export const fakeInt = (min: number, max: number) => Math.round(fakeFloat(min, max)); 4 | 5 | export const fakeItemOf = (list: T[]) => (): T => list[fakeInt(0, list.length - 1)]; 6 | 7 | export const fakePrice = (min: number, max: number, fractionDigits: number = 2) => 8 | parseFloat(fakeFloat(min, max).toFixed(fractionDigits)); 9 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/DataSourceService/__fake__/order-items.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | import { createOrderItem } from '../../Orders'; 3 | import { fakeItemOf, fakeInt, fakePrice } from './generators'; 4 | 5 | const NUMBER_OF_ORDER_IDS = 5; 6 | const NUMBER_OF_ORDER_ITEMS = 15; 7 | 8 | const randomOrderId = fakeItemOf(Array.from({ length: NUMBER_OF_ORDER_IDS }, () => uuid())); 9 | 10 | export default Array.from({ length: NUMBER_OF_ORDER_ITEMS }, () => createOrderItem( 11 | uuid(), 12 | randomOrderId(), 13 | `stock-id:${fakeInt(1000, 10000)}`, 14 | fakePrice(10, 1000), 15 | fakeInt(1, 10), 16 | )); 17 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/DataSourceService/index.test.ts: -------------------------------------------------------------------------------- 1 | import DataSourceService from '.'; 2 | import { IDataSourceService, IInfoLogger } from '../interfaces'; 3 | import fakeOrderItems from './__fake__/order-items'; 4 | 5 | const fakeLogger: IInfoLogger = { 6 | info: jest.fn(), 7 | }; 8 | 9 | describe('DataSourceService', () => { 10 | it('prints message to log on creation', () => { 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | const dataSourceService: IDataSourceService = DataSourceService({ logger: fakeLogger }); 13 | 14 | expect(fakeLogger.info).toHaveBeenCalledWith('DataSourseService has been created'); 15 | }); 16 | 17 | it('returns order items with method getOrderItems', async () => { 18 | const dataSourceService: IDataSourceService = DataSourceService({ logger: fakeLogger }); 19 | 20 | expect(await dataSourceService.getOrderItems()).toEqual(fakeOrderItems); 21 | }); 22 | 23 | it('returns filtered list of order items with method getOrderItems', async () => { 24 | const dataSourceService: IDataSourceService = DataSourceService({ logger: fakeLogger }); 25 | 26 | expect(await dataSourceService.getOrderItems( 27 | ({ id }) => id === fakeOrderItems[0].id, 28 | )).toEqual([fakeOrderItems[0]]); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/DataSourceService/index.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceService, IInfoLogger, OrderItem } from '../interfaces'; 2 | import fakeOrderItems from './__fake__/order-items'; 3 | 4 | type DataSourceServiceDeps = { 5 | logger: IInfoLogger, 6 | } 7 | 8 | const DataSourceService = ({ logger }: DataSourceServiceDeps): IDataSourceService => { 9 | logger.info('DataSourseService has been created'); 10 | 11 | const data = fakeOrderItems; 12 | 13 | const getOrderItems = (predicate?: (orderItem: OrderItem) => boolean): Promise => 14 | Promise.resolve( 15 | typeof predicate === 'function' ? data.filter(predicate) : data, 16 | ); 17 | 18 | return { 19 | getOrderItems, 20 | }; 21 | }; 22 | 23 | export default DataSourceService; 24 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/ECommerceService/index.test.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import ECommerceService from '.'; 3 | import { 4 | IDataSourceService, IECommerceService, IInfoLogger, OrderItem, 5 | } from '../interfaces'; 6 | import { ordersFromItems } from '../Orders'; 7 | 8 | const fakeOrderItems = [ 9 | { 10 | id: '9acec35f-2402-40a7-92cc-664a4ade4778', 11 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 12 | sku: 'stock-id:1397', 13 | unitPrice: 215.1, 14 | quantity: 5, 15 | }, 16 | { 17 | id: '845ef455-0531-487b-b1b9-8d8adf55e606', 18 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 19 | sku: 'stock-id:8255', 20 | unitPrice: 692.63, 21 | quantity: 1, 22 | }, 23 | { 24 | id: 'd2a87c78-e7c2-42ee-9ad0-d88cbd151841', 25 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 26 | sku: 'stock-id:6393', 27 | unitPrice: 360.5, 28 | quantity: 2, 29 | }, 30 | { 31 | id: '20e7ba33-7bd3-4390-853b-55bcad49562b', 32 | orderId: '8db5d840-3a8b-45fe-a6e2-1b202f716717', 33 | sku: 'stock-id:8439', 34 | unitPrice: 530.7, 35 | quantity: 9, 36 | }, 37 | { 38 | id: '9c686fa1-d490-4196-89c5-01a423b35876', 39 | orderId: '9b85794c-80d2-4f79-b024-901d0cb230a7', 40 | sku: 'stock-id:1667', 41 | unitPrice: 544.99, 42 | quantity: 7, 43 | }, 44 | ]; 45 | 46 | describe('ECommerceService', () => { 47 | it('returns orders with method .getOrders', async () => { 48 | expect.assertions(1); 49 | 50 | const fakeLogger: IInfoLogger = { 51 | info: jest.fn(), 52 | }; 53 | 54 | const fakeDataSource: IDataSourceService = { 55 | getOrderItems: jest.fn().mockResolvedValue(fakeOrderItems), 56 | }; 57 | const ecommerceService: IECommerceService = ECommerceService( 58 | { logger: fakeLogger, dataSourceService: fakeDataSource }, 59 | ); 60 | 61 | expect(await ecommerceService.getOrders()).toEqual(ordersFromItems(fakeOrderItems)); 62 | }); 63 | 64 | it('returns order with method .getOrderById', async () => { 65 | expect.assertions(1); 66 | 67 | const fakeLogger: IInfoLogger = { 68 | info: jest.fn(), 69 | }; 70 | 71 | const fakeDataSource = { 72 | getOrderItems: jest.fn( 73 | (predicate?: (orderItem: OrderItem) => boolean) => 74 | Promise.resolve(fakeOrderItems.filter(predicate)), 75 | ), 76 | }; 77 | const ecommerceService: IECommerceService = ECommerceService( 78 | { logger: fakeLogger, dataSourceService: fakeDataSource }, 79 | ); 80 | 81 | expect( 82 | await ecommerceService.getOrderById(fakeOrderItems[0].orderId), 83 | ).toEqual(ordersFromItems(fakeOrderItems)[0]); 84 | }); 85 | 86 | it('returns null with method .getOrderById if no order is found', async () => { 87 | expect.assertions(1); 88 | 89 | const fakeLogger: IInfoLogger = { 90 | info: jest.fn(), 91 | }; 92 | 93 | const fakeDataSource = { 94 | getOrderItems: jest.fn().mockResolvedValue([]), 95 | }; 96 | 97 | const ecommerceService: IECommerceService = ECommerceService( 98 | { logger: fakeLogger, dataSourceService: fakeDataSource }, 99 | ); 100 | expect( 101 | await ecommerceService.getOrderById('9acec35f-2402-40a7-92cc-664a4ade4778'), 102 | ).toEqual(null); 103 | }); 104 | 105 | it('throws AssertionError with method .getOrderById if id is not a UUID', async () => { 106 | expect.assertions(1); 107 | 108 | const fakeLogger: IInfoLogger = { 109 | info: jest.fn(), 110 | }; 111 | 112 | const fakeDataSource = { 113 | getOrderItems: jest.fn().mockResolvedValue([]), 114 | }; 115 | 116 | const ecommerceService: IECommerceService = ECommerceService( 117 | { logger: fakeLogger, dataSourceService: fakeDataSource }, 118 | ); 119 | 120 | const error = await ecommerceService.getOrderById('9acec35f-2402-40a7-92cc').catch(err => err); 121 | 122 | expect(error).toBeInstanceOf(AssertionError); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/ECommerceService/index.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { 3 | IECommerceService, IDataSourceService, Order, IInfoLogger, OrderItem, 4 | } from '../interfaces'; 5 | import { ordersFromItems } from '../Orders'; 6 | import { isUUID } from '../utils/isUUID'; 7 | 8 | const belongsToOrder = (id: string) => ({ orderId }: OrderItem): boolean => orderId === id; 9 | 10 | type ECommerceSeriveDeps = { 11 | logger: IInfoLogger, 12 | dataSourceService: IDataSourceService, 13 | } 14 | 15 | const ECommerceSerive = ({ logger, dataSourceService }: ECommerceSeriveDeps): IECommerceService => { 16 | logger.info('ECommerceService has been created'); 17 | 18 | const getOrders = async (): Promise => { 19 | logger.info('getOrders has been called'); 20 | 21 | const orderItems = await dataSourceService.getOrderItems(); 22 | return ordersFromItems(orderItems); 23 | }; 24 | 25 | const getOrderById = async (id: string): Promise => { 26 | assert(isUUID(id), `${id} is not a valid UUID`); 27 | 28 | logger.info('getOrderById has been called'); 29 | 30 | const orderItems = await dataSourceService.getOrderItems(belongsToOrder(id)); 31 | 32 | return orderItems.length > 0 33 | ? ordersFromItems(orderItems)[0] 34 | : null; 35 | }; 36 | 37 | return { 38 | getOrders, 39 | getOrderById, 40 | }; 41 | }; 42 | 43 | export default ECommerceSerive; 44 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/Logger/LogEntry.test.ts: -------------------------------------------------------------------------------- 1 | import { infoLogEntry, warnLogEntry, errorLogEntry } from './LogEntry'; 2 | 3 | describe('LogEntry', () => { 4 | beforeEach(() => { 5 | jest.spyOn(Date, 'now').mockReturnValue(Date.now()); 6 | }); 7 | 8 | afterEach(() => { 9 | jest.restoreAllMocks(); 10 | }); 11 | 12 | it('allows to create INFO Log Entry with infoLogEntry factory', () => { 13 | expect(infoLogEntry('Some Message')).toEqual({ 14 | type: 'INFO', 15 | timestamp: Math.floor(Date.now() / 1000), 16 | message: 'Some Message', 17 | }); 18 | }); 19 | 20 | it('allows to create WARNING Log Entry with errorLogEntry factory', () => { 21 | expect(warnLogEntry('Some Warning')).toEqual({ 22 | type: 'WARNING', 23 | timestamp: Math.floor(Date.now() / 1000), 24 | message: 'Some Warning', 25 | }); 26 | }); 27 | 28 | it('allows to create ERROR Log Entry with errorLogEntry factory', () => { 29 | expect(errorLogEntry('Some Error Message')).toEqual({ 30 | type: 'ERROR', 31 | timestamp: Math.floor(Date.now() / 1000), 32 | message: 'Some Error Message', 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/Logger/LogEntry.ts: -------------------------------------------------------------------------------- 1 | export type LogEntry = { 2 | type: 'INFO' | 'WARNING' | 'ERROR', 3 | timestamp: number, 4 | message: string, 5 | } 6 | 7 | const logEntryOfType = (type: LogEntry['type']) => 8 | (message: LogEntry['message']): LogEntry => ({ 9 | type, 10 | timestamp: Math.floor(Date.now() / 1000), 11 | message, 12 | }); 13 | 14 | export const infoLogEntry = logEntryOfType('INFO'); 15 | export const warnLogEntry = logEntryOfType('WARNING'); 16 | export const errorLogEntry = logEntryOfType('ERROR'); 17 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/Logger/LogLevel.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | SILENT = 0, 3 | ERROR = 1, 4 | WARNING = 2, 5 | INFO = 3, 6 | VERBOSE = 4 7 | } 8 | 9 | const levelsMap = new Map([ 10 | ['SILENT', LogLevel.SILENT], 11 | ['ERROR', LogLevel.ERROR], 12 | ['WARNING', LogLevel.WARNING], 13 | ['INFO', LogLevel.INFO], 14 | ['VERBOSE', LogLevel.VERBOSE], 15 | ]) 16 | 17 | export const logLevelFromStr = (value: string = "") => 18 | levelsMap.get(value.trim().toUpperCase()) ?? LogLevel.VERBOSE; -------------------------------------------------------------------------------- /examples/plain-http-server/src/Logger/index.test.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from './LogLevel'; 2 | import Logger from '.'; 3 | 4 | describe('Logger', () => { 5 | beforeEach(() => { 6 | jest.spyOn(console, 'log').mockImplementation(() => {}); 7 | jest.spyOn(console, 'info').mockImplementation(() => {}); 8 | jest.spyOn(console, 'warn').mockImplementation(() => {}); 9 | jest.spyOn(console, 'error').mockImplementation(() => {}); 10 | jest.spyOn(Date, 'now').mockReturnValue(Date.now()); 11 | }); 12 | 13 | afterEach(() => { 14 | jest.restoreAllMocks(); 15 | }); 16 | 17 | it('prints to console info message with .info method', () => { 18 | const logger = Logger(); 19 | expect(console.info).toHaveBeenCalledTimes(0); 20 | 21 | logger.info('Some Message'); 22 | 23 | expect(console.info).toHaveBeenCalledTimes(1); 24 | expect(console.info).toHaveBeenCalledWith({ 25 | type: 'INFO', 26 | timestamp: Math.floor(Date.now() / 1000), 27 | message: 'Some Message', 28 | }); 29 | }); 30 | 31 | it('doesn\'t print to console info message with .info method if logLevel is less then INFO', () => { 32 | const logger = Logger({ logLevel: LogLevel.INFO - 1 }); 33 | 34 | logger.info('Some Message'); 35 | 36 | expect(console.info).not.toHaveBeenCalled(); 37 | }); 38 | 39 | it('prints to console warning message with .warn method', () => { 40 | const logger = Logger(); 41 | 42 | logger.warn('Some Warning'); 43 | 44 | expect(console.warn).toHaveBeenCalledTimes(1); 45 | expect(console.warn).toHaveBeenCalledWith({ 46 | type: 'WARNING', 47 | timestamp: Math.floor(Date.now() / 1000), 48 | message: 'Some Warning', 49 | }); 50 | }); 51 | 52 | it('doesn\'t print to console warning message with .warn method if logLevel is less then WARINING', () => { 53 | const logger = Logger({ logLevel: LogLevel.WARNING - 1 }); 54 | 55 | logger.warn('Some Warning'); 56 | 57 | expect(console.warn).not.toHaveBeenCalled(); 58 | }); 59 | 60 | it('prints to console error message with .error method', () => { 61 | const logger = Logger(); 62 | 63 | logger.error(new Error('Some Error Message')); 64 | 65 | expect(console.error).toHaveBeenCalledTimes(1); 66 | expect(console.error).toHaveBeenCalledWith({ 67 | type: 'ERROR', 68 | timestamp: Math.floor(Date.now() / 1000), 69 | message: 'Some Error Message', 70 | }); 71 | }); 72 | 73 | it('doesn\'t print to console error message with .error method if logLevel is less then ERROR', () => { 74 | const logger = Logger({ logLevel: LogLevel.SILENT }); 75 | 76 | logger.error(new Error('Some Error Message')); 77 | 78 | expect(console.error).not.toHaveBeenCalled(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/Logger/index.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../interfaces'; 2 | import { LogLevel } from "./LogLevel"; 3 | import { infoLogEntry, warnLogEntry, errorLogEntry } from './LogEntry'; 4 | 5 | 6 | const silent = () => {}; 7 | const info = (message: string) => console.info(infoLogEntry(message)); 8 | const warn = (message: string) => console.warn(warnLogEntry(message)); 9 | const error = (error: Error) => console.error(errorLogEntry(error.message)); 10 | 11 | type LoggerDeps = { 12 | logLevel?: LogLevel 13 | } 14 | 15 | const Logger = ({ logLevel = LogLevel.VERBOSE }: LoggerDeps = {}): ILogger => { 16 | const logger = { 17 | info: logLevel >= LogLevel.INFO ? info : silent, 18 | warn: logLevel >= LogLevel.WARNING ? warn : silent, 19 | error: logLevel >= LogLevel.ERROR ? error : silent, 20 | }; 21 | 22 | console.log(`Logger Created. Log Level is ${logLevel}`); 23 | 24 | return logger; 25 | } 26 | 27 | export default Logger; 28 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/Orders/index.test.ts: -------------------------------------------------------------------------------- 1 | import { createOrder, createOrderItem, ordersFromItems } from '.'; 2 | import { Order, OrderItem } from './types'; 3 | 4 | describe('Orders', () => { 5 | describe('createOrderItem', () => { 6 | it('creates an orderItem with default values', () => { 7 | const orderItem: OrderItem = createOrderItem(); 8 | 9 | expect(orderItem).toEqual({ 10 | id: null, 11 | orderId: null, 12 | sku: null, 13 | unitPrice: 0, 14 | quantity: 0, 15 | }); 16 | }); 17 | 18 | it('creates an orderItem with specified values', () => { 19 | const orderItem: OrderItem = createOrderItem('orderItemId', 'orderId', 'sku', 10, 1); 20 | 21 | expect(orderItem).toEqual({ 22 | id: 'orderItemId', 23 | orderId: 'orderId', 24 | sku: 'sku', 25 | unitPrice: 10, 26 | quantity: 1, 27 | }); 28 | }); 29 | }); 30 | 31 | describe('createOrder', () => { 32 | it('creates an Order with default values', () => { 33 | const order: Order = createOrder(); 34 | 35 | expect(order).toEqual({ 36 | id: null, 37 | items: [], 38 | total: 0, 39 | }); 40 | }); 41 | 42 | it('creates an Order with specified values', () => { 43 | const order: Order = createOrder('id', [], 0); 44 | 45 | expect(order).toEqual({ 46 | id: 'id', 47 | items: [], 48 | total: 0, 49 | }); 50 | }); 51 | }); 52 | 53 | describe('ordersFromItems', () => { 54 | it('returns empty list of orders for empty list of orderItems', () => { 55 | const orders: Order[] = ordersFromItems([]); 56 | 57 | expect(orders).toEqual([]); 58 | }); 59 | 60 | it('returns single order for single orderItem', () => { 61 | const orderItems: OrderItem[] = [ 62 | createOrderItem('orderItemId', 'orderId', 'sku', 10, 10), 63 | ]; 64 | 65 | const orders: Order[] = ordersFromItems(orderItems); 66 | 67 | expect(orders).toEqual([ 68 | createOrder('orderId', orderItems, 100), 69 | ]); 70 | }); 71 | 72 | it('returns groups orderItems with the same id to the single order', () => { 73 | const orderItems: OrderItem[] = [ 74 | createOrderItem('orderItemId-1', 'orderId', 'sku-1', 10, 10), 75 | createOrderItem('orderItemId-2', 'orderId', 'sku-2', 100, 2), 76 | ]; 77 | 78 | const orders: Order[] = ordersFromItems(orderItems); 79 | 80 | expect(orders).toEqual([ 81 | createOrder('orderId', orderItems, 300), 82 | ]); 83 | }); 84 | 85 | it('returns groups orderItems with the different orderId to the diferent orders', () => { 86 | const orderItems1: OrderItem[] = [ 87 | createOrderItem('orderItemId-1', 'orderId-1', 'sku-1', 10, 10), 88 | createOrderItem('orderItemId-2', 'orderId-1', 'sku-2', 100, 2), 89 | ]; 90 | 91 | const orderItems2: OrderItem[] = [ 92 | createOrderItem('orderItemId-3', 'orderId-2', 'sku-1', 50, 10), 93 | createOrderItem('orderItemId-4', 'orderId-2', 'sku-2', 5, 20), 94 | ]; 95 | 96 | const orders: Order[] = ordersFromItems([...orderItems1, ...orderItems2]); 97 | 98 | expect(orders).toEqual([ 99 | createOrder('orderId-1', orderItems1, 300), 100 | createOrder('orderId-2', orderItems2, 600), 101 | ]); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/Orders/index.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../utils/groupBy'; 2 | import { Order, OrderItem } from './types'; 3 | 4 | const round2 = (value: number): number => Math.round(value * 100) / 100; 5 | 6 | export const createOrderItem = ( 7 | id: string = null, 8 | orderId: string = null, 9 | sku: string = null, 10 | unitPrice: number = 0, 11 | quantity: number = 0, 12 | ): OrderItem => ({ 13 | id, orderId, sku, unitPrice, quantity, 14 | }); 15 | 16 | const orderItemPrice = ({ unitPrice, quantity }: OrderItem): number => 17 | round2(unitPrice * quantity); 18 | 19 | export const createOrder = ( 20 | id: string = null, 21 | items: OrderItem[] = [], 22 | total: number = 0, 23 | ): Order => ({ id, items, total }); 24 | 25 | export const ordersFromItems = groupBy( 26 | ({ orderId }: OrderItem) => orderId, 27 | (order: Order = createOrder(), orderItem) => { 28 | order.id = orderItem.orderId; 29 | order.items.push(orderItem); 30 | order.total = round2(order.total + orderItemPrice(orderItem)); 31 | return order; 32 | }, 33 | ); 34 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/Orders/types.ts: -------------------------------------------------------------------------------- 1 | export type OrderItem = { 2 | id: string; 3 | orderId: string; 4 | sku: string; 5 | unitPrice: number; 6 | quantity: number; 7 | } 8 | 9 | export type Order = { 10 | id: string; 11 | items: OrderItem[], 12 | total: number, 13 | } 14 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/container.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { IncomingMessage } from 'http'; 3 | import { prepareAll, releaseAll } from 'true-di'; 4 | import createContainer from './container'; 5 | import { IDataSourceService, IECommerceService, ILogger } from './interfaces'; 6 | import { LogLevel } from './Logger/LogLevel'; 7 | 8 | type AssertTypeEqual = T1 extends T2 ? (T2 extends T1 ? true : never) : never; 9 | 10 | describe('container', () => { 11 | let container: ReturnType; 12 | 13 | beforeAll(() => { 14 | jest.spyOn(console, 'log').mockImplementation(() => {}); 15 | jest.spyOn(console, 'info').mockImplementation(() => {}); 16 | jest.spyOn(console, 'error').mockImplementation(() => {}); 17 | }); 18 | 19 | beforeEach(() => { 20 | container = createContainer({ headers: {} } as IncomingMessage); 21 | }); 22 | 23 | afterEach(() => { 24 | releaseAll(container); 25 | }); 26 | 27 | afterAll(() => { 28 | jest.restoreAllMocks(); 29 | }); 30 | 31 | it('allows to get logger', () => { 32 | expect(container.logger).toBeDefined(); 33 | 34 | const typecheck: AssertTypeEqual = true; 35 | }); 36 | 37 | it('allows to get dataSourceService', () => { 38 | expect(container.dataSourceService).toBeDefined(); 39 | 40 | const typecheck: AssertTypeEqual = true; 41 | }); 42 | 43 | it('allows to get eCommerceService', () => { 44 | expect(container.eCommerceService).toBeDefined(); 45 | 46 | const typecheck: AssertTypeEqual = true; 47 | }); 48 | 49 | // or just single test 50 | it('allows to instantiate all items', () => { 51 | const items = { ...container }; 52 | 53 | expect(items.logger).toBeDefined(); 54 | expect(items.dataSourceService).toBeDefined(); 55 | expect(items.eCommerceService).toBeDefined(); 56 | 57 | const typecheck: AssertTypeEqual< 58 | typeof items, { 59 | logLevel: LogLevel, 60 | logger: ILogger, 61 | dataSourceService: IDataSourceService, 62 | eCommerceService: IECommerceService, 63 | }> = true; 64 | }); 65 | 66 | // or the same but with prepareAll 67 | it('allows to instantiate all items (prepareAll)', () => { 68 | const items = prepareAll(container); 69 | 70 | expect(items.logger).toBeDefined(); 71 | expect(items.dataSourceService).toBeDefined(); 72 | expect(items.eCommerceService).toBeDefined(); 73 | 74 | const typecheck: AssertTypeEqual< 75 | typeof items, { 76 | logLevel: LogLevel, 77 | logger: ILogger, 78 | dataSourceService: IDataSourceService, 79 | eCommerceService: IECommerceService, 80 | }> = true; 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/container.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | import diContainer from 'true-di'; 3 | import logger from './Logger'; 4 | import dataSourceService from './DataSourceService'; 5 | import eCommerceService from './ECommerceService'; 6 | import { logLevelFromStr } from './Logger/LogLevel'; 7 | import { readHeader } from './utils/readHeader'; 8 | 9 | export default (req: IncomingMessage) => diContainer({ 10 | logLevel: () => logLevelFromStr(readHeader(req, 'X-Log-Level')), 11 | logger, 12 | dataSourceService, 13 | eCommerceService, 14 | }); 15 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/controller.ts: -------------------------------------------------------------------------------- 1 | import { IECommerceService } from './interfaces'; 2 | import { expectFound } from './utils/NotFoundError'; 3 | 4 | type Context = { 5 | eCommerceService: IECommerceService 6 | } 7 | 8 | type GetOrderByIdParams = { 9 | id: string 10 | } 11 | 12 | export const getOrders = ({ eCommerceService }: Context) => 13 | eCommerceService.getOrders(); 14 | 15 | export const getOrderById = ({ eCommerceService }: Context, { id }: GetOrderByIdParams) => 16 | eCommerceService 17 | .getOrderById(id) 18 | .then(expectFound(`Order ${id}`)); 19 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import match, { firstOf } from './utils/exec'; 3 | import httpListener from './utils/httpListener'; 4 | 5 | import createContainer from './container'; 6 | import { getOrderById, getOrders } from './controller'; 7 | import { handleErrors } from './middlewares'; 8 | 9 | const handler = firstOf( 10 | match('/orders', getOrders), 11 | match('/orders/:id', getOrderById), 12 | ); 13 | 14 | http.createServer( 15 | httpListener( 16 | createContainer, // create request context 17 | handler, 18 | handleErrors, 19 | ), 20 | ).listen(8080, () => console.log('Server is ready: http://localhost:8080')); 21 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/interfaces/IDataSourceService.ts: -------------------------------------------------------------------------------- 1 | import { OrderItem } from '../Orders/types'; 2 | 3 | export interface IDataSourceService { 4 | getOrderItems(predicate?: (orderItem: OrderItem) => boolean): Promise 5 | } 6 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/interfaces/IECommerceService.ts: -------------------------------------------------------------------------------- 1 | import { Order } from '../Orders/types'; 2 | 3 | export interface IGetOrders { 4 | getOrders(): Promise 5 | } 6 | 7 | export interface IGetOrderById { 8 | getOrderById(id: string): Promise 9 | } 10 | 11 | export interface IECommerceService extends 12 | IGetOrders, 13 | IGetOrderById 14 | {} 15 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/interfaces/ILogger.ts: -------------------------------------------------------------------------------- 1 | export interface IInfoLogger { 2 | info(message: string) : void; 3 | } 4 | 5 | export interface IWarnLogger { 6 | warn(message: string) : void; 7 | } 8 | 9 | export interface IErrorLogger { 10 | error(error: Error): void; 11 | } 12 | 13 | export interface ILogger extends 14 | IInfoLogger, 15 | IWarnLogger, 16 | IErrorLogger 17 | {} 18 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/interfaces/IRequestInjected.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from './ILogger'; 2 | import { IECommerceService } from './IECommerceService'; 3 | 4 | export type Injected = { 5 | injected: T 6 | } 7 | 8 | export type RequestInjected = { 9 | logger: ILogger, 10 | ecommerceService: IECommerceService, 11 | } 12 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import './request'; 2 | 3 | export * from './ILogger'; 4 | export * from '../Orders/types'; 5 | export * from './IDataSourceService'; 6 | export * from './IECommerceService'; 7 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/interfaces/request.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | injected: import('./IRequestInjected').RequestInjected; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import { NotFoundError } from './utils/NotFoundError'; 3 | 4 | export const handleErrors = (err: Error): [number, { error: string}] | null => 5 | err instanceof AssertionError 6 | ? [400, { error: err.message }] : 7 | err instanceof NotFoundError 8 | ? [404, { error: err.message }] : 9 | null; 10 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/NotFoundError.test.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError, expectFound } from './NotFoundError'; 2 | 3 | describe('NotFoundError', () => { 4 | it('creates an instance of Error', () => { 5 | expect(new NotFoundError('Entity')).toBeInstanceOf(Error); 6 | }); 7 | 8 | it('created an instance of NotFoundError', () => { 9 | expect(new NotFoundError('Entity')).toBeInstanceOf(NotFoundError); 10 | }); 11 | 12 | it('creates an error with message: "The Entity is not found."', () => { 13 | expect(new NotFoundError('Entity').message).toBe('The Entity is not found.'); 14 | }); 15 | }); 16 | 17 | describe('expectFound', () => { 18 | it('is a closure', () => { 19 | expect(expectFound('Entity')).toBeInstanceOf(Function); 20 | }); 21 | 22 | it('returns value if it is not null or undefined', () => { 23 | const obj = {}; 24 | 25 | expect(expectFound('Entity')(obj)).toBe(obj); 26 | }); 27 | 28 | it('throws an error if value is null', () => { 29 | expect( 30 | () => expectFound('Entity')(null), 31 | ).toThrow(new NotFoundError('Entity')); 32 | }); 33 | 34 | it('throws an error if value is undefined', () => { 35 | expect( 36 | () => expectFound('Entity')(undefined), 37 | ).toThrow(new NotFoundError('Entity')); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class NotFoundError extends Error { 2 | constructor(entityTypeName: string) { 3 | super(`The ${entityTypeName} is not found.`); 4 | Object.setPrototypeOf(this, NotFoundError.prototype); 5 | Error.captureStackTrace(this, NotFoundError); 6 | } 7 | } 8 | 9 | export const expectFound = (entityTypeName: string) => (value: T | null | undefined): T => { 10 | if (value == null) throw new NotFoundError(entityTypeName); 11 | return value; 12 | }; 13 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | import { Path } from 'path-to-regexp'; 3 | import { pathParser } from './pathParser'; 4 | 5 | const match = (pathPattern: Path, fn: (context:C, params: P) => T) => { 6 | const matcher = pathParser(pathPattern); 7 | return (context: C, { url }: IncomingMessage) => { 8 | const params = matcher

(url); 9 | return params != null ? fn(context, params) : null; 10 | }; 11 | }; 12 | 13 | const execOrNext = ( 14 | h1: (...args: A) => R1, 15 | h2: (...args: A) => R2, 16 | ) => (...args: A) => h1(...args) ?? h2(...args); 17 | 18 | export const firstOf = (...handlers: Array<(context: C, req: IncomingMessage) => any>) => 19 | (handlers.length > 0 20 | ? handlers.reduce(execOrNext) 21 | : () => null); 22 | 23 | export default match; 24 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/groupBy.test.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from './groupBy'; 2 | 3 | describe('groupBy', () => { 4 | it('creates grouping function', () => { 5 | expect( 6 | groupBy( 7 | (x: number) => x, 8 | (list: number[] = [], item: number) => { 9 | list.push(item); 10 | return list; 11 | }, 12 | ), 13 | ).toBeInstanceOf(Function); 14 | }); 15 | 16 | describe('grouping', () => { 17 | it('returns empty list for empty list', () => { 18 | const groupByValue = groupBy( 19 | (x: number) => x, 20 | (list: number[] = [], item: number) => { 21 | list.push(item); 22 | return list; 23 | }, 24 | ); 25 | 26 | expect(groupByValue([])).toEqual([]); 27 | }); 28 | 29 | it('counts number of same items', () => { 30 | const groupByValue = groupBy( 31 | (x: number) => x, 32 | (count: number = 0) => count + 1, 33 | ); 34 | 35 | expect(groupByValue([1, 2, 3, 4])).toEqual([1, 1, 1, 1]); 36 | expect(groupByValue([1, 2, 1, 4])).toEqual([2, 1, 1]); 37 | }); 38 | 39 | it('groups same items', () => { 40 | const groupByValue = groupBy( 41 | (x: number) => x, 42 | (list: number[] = [], item) => { 43 | list.push(item); 44 | return list; 45 | }, 46 | ); 47 | 48 | expect(groupByValue([1, 2, 3, 4])).toEqual([[1], [2], [3], [4]]); 49 | expect(groupByValue([1, 2, 1, 4])).toEqual([[1, 1], [2], [4]]); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/groupBy.ts: -------------------------------------------------------------------------------- 1 | type KeyResolver = (record: R) => K; 2 | type Reducer = (groupTotal: T, record: R, index: number, list: R[]) => T; 3 | 4 | export const groupBy = (getKey: KeyResolver, reducer: Reducer) => 5 | (records: R[]): T[] => Array.from(records.reduce( 6 | (groups, record: R, index, list) => { 7 | const key = getKey(record); 8 | return groups.set(key, reducer(groups.get(key), record, index, list)); 9 | }, 10 | new Map(), 11 | ).values()); 12 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/httpListener.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http"; 2 | 3 | const createHttpListener = ( 4 | buildContext: (req: IncomingMessage) => C, 5 | handler: (context: C, eq: IncomingMessage) => R, 6 | mapErrors: (e: Error) => [number, { error: string }] | null, 7 | ) => 8 | (req: IncomingMessage, res: ServerResponse) => 9 | Promise.resolve() 10 | .then(() => buildContext(req)) 11 | .then(context => 12 | handler(context, req) 13 | ) 14 | .then(result => 15 | result != null ? [200, result] : [404, { error: 'Not Found' }] 16 | ) 17 | .catch((err: Error) => 18 | mapErrors(err) ?? [500, { error: err.message }] 19 | ) 20 | .then(([code, data]: [number, any]) => { 21 | res.statusCode = code; 22 | res.end(JSON.stringify(data, null, 2)) 23 | }); 24 | 25 | export default createHttpListener; -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/isUUID.test.ts: -------------------------------------------------------------------------------- 1 | import { isUUID } from './isUUID'; 2 | 3 | describe('isUUID', () => { 4 | it.each([ 5 | ['25c6633c-08c4-45ec-b553-d70106963c14', true], 6 | ['25c6633c-08c4-45ec-b553-d70106963c14'.toUpperCase(), true], 7 | ['60e660b7-6833-443f-a736-dd1e2ab37649', true], 8 | ['23cfab4d-103b-475f-830a-79e7f8c93c05', true], 9 | ['', false], 10 | ['25c6633c-08c4-45ec-b553-d70106963c1Z', false], 11 | ['25c6633c-08c4-45ec-d70106963c14', false], 12 | ['25c6633c-08c4-45ec-b553-d70106963c142-12313123', false], 13 | ])("isUUID('%s') is %s", (value, expected) => { 14 | expect(isUUID(value)).toBe(expected); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/isUUID.ts: -------------------------------------------------------------------------------- 1 | export const isUUID = (value: string): boolean => 2 | /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); 3 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/pathParser.test.ts: -------------------------------------------------------------------------------- 1 | import { pathParser } from './pathParser'; 2 | 3 | describe('pathParser', () => { 4 | it('creates a function', () => { 5 | expect(pathParser('/')).toBeInstanceOf(Function); 6 | }); 7 | 8 | it('throws an exception if pathPattern is not valid', () => { 9 | expect( 10 | () => pathParser('?'), 11 | ).toThrow(TypeError); 12 | }); 13 | 14 | it.each([ 15 | ['/', '/', { pathname: '/' }], 16 | ['/', '/root', null], 17 | ['/root', '/root', { pathname: '/root' }], 18 | ['/root', '/', null], 19 | ['/root/node/node', '/root/node/node', { pathname: '/root/node/node' }], 20 | ['/root/node/node', '/root/node/', null], 21 | ['/root/:id', '/root/123', { pathname: '/root/123', id: '123' }], 22 | ])( 23 | 'pathParser(%j)(%j) -> %j', (pattern, path, result) => { 24 | expect( 25 | pathParser(pattern)(path), 26 | ).toEqual(result); 27 | }, 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/pathParser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pathToRegexp, Path, PathToRegexpOptions, ParseOptions, 3 | } from 'path-to-regexp'; 4 | 5 | export const pathParser = (pathPattern: Path, options?: PathToRegexpOptions & ParseOptions) => { 6 | const { keys, regexp: pathMatcher } = pathToRegexp(pathPattern, options); // throws TypeError 7 | 8 | keys.unshift({ name: 'pathname', type: 'param' }); 9 | 10 | return (path: string): T | null => { 11 | const parsingResult = pathMatcher.exec(path); 12 | 13 | return parsingResult && parsingResult.reduce( 14 | (params, value, index) => Object.defineProperty( 15 | params, keys[index].name, { value, enumerable: true }, 16 | ), 17 | Object.create(null) as T, 18 | ); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/readHeader.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from "http"; 2 | 3 | export const readHeader = (req: IncomingMessage, headerName: string): string => { 4 | const headerValue = req.headers[headerName.toLocaleLowerCase()]; 5 | return Array.isArray(headerValue) ? headerValue[0] : headerValue; 6 | } -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/sendJson.test.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import { sendJson } from './sendJson'; 3 | 4 | function returnThis() { return this; } 5 | 6 | const fakeResponse: Express.Response = { 7 | type: jest.fn(returnThis), 8 | send: jest.fn(returnThis), 9 | } as any; 10 | 11 | describe('Utils.sendJson', () => { 12 | afterEach(() => { 13 | jest.restoreAllMocks(); 14 | }); 15 | 16 | it('stringifies object to JSON and sends via res.send', () => { 17 | sendJson(fakeResponse)({ title: 'The Title' }); 18 | 19 | expect(fakeResponse.type).toHaveBeenCalledWith('application/json'); 20 | expect(fakeResponse.send).toHaveBeenCalledWith( 21 | JSON.stringify({ title: 'The Title' }, null, 2), 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/plain-http-server/src/utils/sendJson.ts: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | 3 | export const sendJson = (res: Express.Response) => (data: object) => 4 | res 5 | .type('application/json') 6 | .send(JSON.stringify(data, null, 2)); 7 | -------------------------------------------------------------------------------- /examples/plain-http-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/*", 4 | ], 5 | "compilerOptions": { 6 | "noEmit": true, 7 | "noImplicitAny": false, 8 | "types": ["node", "jest"], 9 | "lib": [ 10 | "esnext" 11 | ], 12 | "esModuleInterop": true 13 | } 14 | } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import diContainer from './lib/index'; 2 | 3 | export * from './lib/index'; 4 | export default diContainer; 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "true-di", 3 | "version": "3.0.0", 4 | "description": "Zero Dependency, Minimalistic **Type-Safe DI Container** for TypeScript and JavaScript projects", 5 | "main": "./lib/index.js", 6 | "module": "./esm/index.js", 7 | "types": "./lib/index.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "test": "jest", 11 | "coveralls": "./.coveralls.sh", 12 | "lint": "eslint ./src/**/*.ts", 13 | "clear": "rm -rf ./lib; rm -rf ./esm", 14 | "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json", 15 | "build": "npm run clear; npm run compile" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/DScheglov/true-di.git" 20 | }, 21 | "keywords": [ 22 | "Dependency", 23 | "Injection", 24 | "Inject", 25 | "Dependency", 26 | "di", 27 | "container", 28 | "service", 29 | "instance" 30 | ], 31 | "author": "Dmitry Scheglov ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/DScheglov/true-di/issues" 35 | }, 36 | "homepage": "https://github.com/DScheglov/true-di#readme", 37 | "devDependencies": { 38 | "@types/jest": "^28.1.2", 39 | "@types/node": "^14.0.13", 40 | "@typescript-eslint/eslint-plugin": "^5.28.0", 41 | "@typescript-eslint/parser": "^5.28.0", 42 | "coveralls": "^3.1.1", 43 | "eslint": "^8.18.0", 44 | "eslint-config-airbnb": "^19.0.4", 45 | "eslint-plugin-import": "^2.26.0", 46 | "eslint-plugin-jest": "^26.5.3", 47 | "jest": "^28.1.1", 48 | "ts-jest": "^28.0.5", 49 | "typescript": "^4.7.4" 50 | }, 51 | "eslintConfig": { 52 | "extends": [ 53 | "airbnb/base" 54 | ], 55 | "plugins": [ 56 | "@typescript-eslint", 57 | "jest" 58 | ], 59 | "parser": "@typescript-eslint/parser", 60 | "rules": { 61 | "max-len": [ 62 | "error", 63 | { 64 | "code": 100, 65 | "ignoreStrings": true 66 | } 67 | ], 68 | "no-param-reassign": "off", 69 | "implicit-arrow-linebreak": "off", 70 | "import/no-unresolved": 0, 71 | "import/prefer-default-export": 0, 72 | "indent": [ 73 | 2, 74 | 2, 75 | { 76 | "flatTernaryExpressions": true 77 | } 78 | ], 79 | "no-unused-vars": "off", 80 | "no-undef": "error", 81 | "no-tabs": "error", 82 | "no-nested-ternary": 0, 83 | "import/extensions": 0, 84 | "arrow-parens": [ 85 | "error", 86 | "as-needed" 87 | ], 88 | "operator-linebreak": 0, 89 | "no-underscore-dangle": 0, 90 | "@typescript-eslint/no-unused-vars": [ 91 | "error" 92 | ], 93 | "jest/no-disabled-tests": "warn", 94 | "jest/no-focused-tests": "error", 95 | "jest/no-identical-title": "error", 96 | "jest/prefer-to-have-length": "warn", 97 | "jest/valid-expect": "error" 98 | }, 99 | "env": { 100 | "jest/globals": true 101 | }, 102 | "ignorePatterns": [ 103 | "examples/**/*", 104 | "lib/**/*", 105 | "esm/**/*" 106 | ] 107 | }, 108 | "jest": { 109 | "preset": "ts-jest", 110 | "testEnvironment": "node", 111 | "collectCoverage": true, 112 | "collectCoverageFrom": [ 113 | "src/**/*.ts" 114 | ], 115 | "testPathIgnorePatterns": [ 116 | "/lib/', '/es/", 117 | "/examples" 118 | ], 119 | "globals": { 120 | "ts-jest": { 121 | "tsconfig": "tsconfig.esm.json" 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/create-instance.test.ts: -------------------------------------------------------------------------------- 1 | import UniqueStack from './unique-stack'; 2 | import createInstance from './create-instance'; 3 | import { IFactories, VoidFn } from './types'; 4 | 5 | describe('getInstance', () => { 6 | it('creates new instance if it is not created yet', () => { 7 | type Container = { 8 | vector: { 9 | x: number, 10 | y: number 11 | } 12 | } 13 | const instances = new Map(); 14 | const factories: IFactories = { 15 | vector: jest.fn(() => ({ x: 1, y: 2 })), 16 | }; 17 | 18 | const container = Object.freeze({}) as Container; 19 | const newVector = createInstance(factories, instances)(container, 'vector'); 20 | 21 | expect(newVector).toEqual({ x: 1, y: 2 }); 22 | expect(instances.get('vector')).toBe(newVector); 23 | expect(factories.vector).toHaveBeenCalledWith(container); 24 | }); 25 | 26 | it('throws an error in case of cyclic dependencies', () => { 27 | type Vector = { x: number, y: number }; 28 | type Container = { 29 | vector: Vector, 30 | } 31 | const instances = new Map(); 32 | const stack = UniqueStack(); 33 | const factories: IFactories = { 34 | vector: () => ({ x: 1, y: 2 }), 35 | }; 36 | const container = Object.freeze({}) as Container; 37 | const initializers: VoidFn[] = []; 38 | 39 | stack.push('vector'); 40 | 41 | expect( 42 | () => createInstance(factories, instances, stack, initializers)(container, 'vector'), 43 | ).toThrow(new Error('Cyclic dependencies couldn\'t be resolved.\n\nRequested: vector\nResolution stack:\n\tvector')); 44 | }); 45 | 46 | it('throws an error in case of intercepted exception', () => { 47 | type Vector = { x: number, y: number }; 48 | type Container = { 49 | vector: Vector, 50 | } 51 | const instances = new Map(); 52 | const stack = UniqueStack(); 53 | const factories: IFactories = { 54 | vector: () => { 55 | stack.push('vector2' as 'vector'); 56 | return ({ x: 1, y: 2 }); 57 | }, 58 | }; 59 | const container = Object.freeze({}) as Container; 60 | 61 | expect( 62 | () => createInstance(factories, instances, stack)(container, 'vector'), 63 | ).toThrow('Not all dependencies resolved correctly.'); 64 | }); 65 | 66 | it('throws an Error in attempt to create instance for name without factory', () => { 67 | type Vector = { x: number, y: number }; 68 | type Container = { 69 | vector: Vector, 70 | } 71 | const instances = new Map(); 72 | const stack = UniqueStack(); 73 | const factories: IFactories = { 74 | vector: () => ({ x: 1, y: 2 }), 75 | }; 76 | const container = Object.freeze({}) as Container; 77 | const initializers: VoidFn[] = []; 78 | 79 | expect( 80 | () => createInstance(factories, instances, stack, initializers)(container, 'vector2' as 'vector'), 81 | ).toThrow('Factory is not defined for name "vector2"'); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/create-instance.ts: -------------------------------------------------------------------------------- 1 | import UniqueStack from './unique-stack'; 2 | import { IFactories, VoidFn } from './types'; 3 | 4 | const call = (fn: VoidFn) => fn(); 5 | 6 | const createInstanceFactory = ( 7 | factories: IFactories, 8 | instances: Map, 9 | stack = UniqueStack(), 10 | initializers: VoidFn[] = [], 11 | ) => (container: C, name: N): C[N] => { 12 | if (instances.has(name)) return instances.get(name); 13 | 14 | if (stack.push(name)[0] != null) { 15 | throw new Error( 16 | 'Cyclic dependencies couldn\'t be resolved.\n\n' + 17 | `Requested: ${String(name)}\nResolution stack:\n\t${stack.items.join('\n\t')}`, 18 | ); 19 | } 20 | 21 | const itemFactory = factories[name]; 22 | 23 | const [factory, initializer] = ( 24 | Array.isArray(itemFactory) ? itemFactory : [itemFactory, null] 25 | ); 26 | 27 | if (typeof factory !== 'function') { 28 | throw new Error(`Factory is not defined for name "${String(name)}"`); 29 | } 30 | 31 | const instance = factory(container); 32 | instances.set(name, instance); 33 | 34 | if (typeof initializer === 'function') { 35 | initializers.push(() => initializer(instance, container, name)); 36 | } 37 | 38 | if (stack.pop(name)[0] != null) { 39 | throw new Error('Not all dependencies resolved correctly.'); 40 | } 41 | 42 | if (stack.size === 0) { 43 | initializers.forEach(call); 44 | initializers = []; 45 | } 46 | 47 | return instance; 48 | }; 49 | 50 | export default createInstanceFactory; 51 | -------------------------------------------------------------------------------- /src/di-container.ts: -------------------------------------------------------------------------------- 1 | import createInstanceFactory from './create-instance'; 2 | import { IFactories, IPureFactories } from './types'; 3 | import allNames from './utils/all-names'; 4 | import assertExists from './utils/assert-exists'; 5 | import mapObject from './utils/map-object'; 6 | import narrowObject from './utils/narrow-object'; 7 | import { shallowMerge } from './utils/shallow-merge'; 8 | 9 | const $INSTANCES = Symbol('TRUE-DI::INSTANCES'); 10 | 11 | const itemsOf = (container: C): Map => 12 | assertExists( 13 | (container as any)[$INSTANCES], 14 | 'Argument is not a container', 15 | ); 16 | 17 | export const isReady = (container: C, name: keyof C): boolean => 18 | itemsOf(container).has(name); 19 | 20 | export const prepareAll = (container: C): C => 21 | mapObject(container, name => container[name]); 22 | 23 | export const releaseAll = (container: C): void => { 24 | itemsOf(container).clear(); 25 | }; 26 | 27 | export const factoriesFrom = ( 28 | container: C, 29 | names?: N[], 30 | ): IPureFactories> => 31 | mapObject>>(container, name => () => container[name], names); 32 | 33 | const diContainer: { 34 | (factories: IFactories): Public; 35 | ( 36 | privateFactories: Pick, keyof Private>, 37 | publicFactories?: Pick, keyof Public>, 38 | ): Public 39 | } = ( 40 | privateFactories: Pick, keyof Private>, 41 | publicFactories?: Pick, keyof Public>, 42 | ): Public => { 43 | const instances = new Map(); 44 | const factories: IFactories = 45 | (publicFactories != null 46 | ? shallowMerge(privateFactories, publicFactories) 47 | : privateFactories) as any; 48 | 49 | const createInstance = createInstanceFactory(factories, instances); 50 | 51 | const container: Private & Public = allNames(factories).reduce( 52 | (containerObj, name) => 53 | Object.defineProperty(containerObj, name, { 54 | configurable: false, 55 | enumerable: Object.getOwnPropertyDescriptor(factories, name)!.enumerable, 56 | get: () => createInstance(container, name), 57 | set: (value: null | undefined) => { 58 | if (value != null) { 59 | throw new Error('Container does\'t allow to replace items'); 60 | } 61 | instances.delete(name); 62 | }, 63 | }), 64 | Object.create( 65 | Object.create(null, { 66 | [$INSTANCES]: { 67 | configurable: false, writable: false, enumerable: false, value: instances, 68 | }, 69 | }), 70 | ), 71 | ); 72 | 73 | return publicFactories != null ? narrowObject(container, allNames(publicFactories)) : container; 74 | }; 75 | 76 | export default diContainer; 77 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import diContainer from './index'; 2 | 3 | describe('interface', () => { 4 | test('diContainer is defined', () => { 5 | expect(diContainer).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import diContainer from './di-container'; 2 | import multiple from './multiple'; 3 | 4 | export * from './types'; 5 | export * from './di-container'; 6 | export { multiple }; 7 | 8 | export default diContainer; 9 | -------------------------------------------------------------------------------- /src/multiple.test.ts: -------------------------------------------------------------------------------- 1 | import diContainer from './di-container'; 2 | import multiple from './multiple'; 3 | 4 | describe('multiple', () => { 5 | it('decorates a factory', () => { 6 | type IContainer = { 7 | datetime: number, 8 | }; 9 | 10 | const factoryTuple = multiple( 11 | () => Date.now(), 12 | ); 13 | 14 | expect(factoryTuple).toBeInstanceOf(Array); 15 | expect(factoryTuple[0]).toBeInstanceOf(Function); 16 | expect(factoryTuple[1]).toBeInstanceOf(Function); 17 | }); 18 | 19 | it('could be used to multiplicate container item (factory)', () => { 20 | type IContainer = { 21 | index: number, 22 | }; 23 | 24 | let counter = 0; 25 | const container = diContainer({ 26 | index: multiple(() => ++counter), // eslint-disable-line no-plusplus 27 | }); 28 | 29 | expect(container.index).toBeDefined(); 30 | expect(container.index).not.toBe(container.index); 31 | }); 32 | 33 | it('decorates a factory tuple', () => { 34 | type IContainer = { 35 | datetime: number, 36 | }; 37 | 38 | const factoryTuple = multiple( 39 | [() => Date.now(), () => {}], 40 | ); 41 | 42 | expect(factoryTuple).toBeInstanceOf(Array); 43 | expect(factoryTuple[0]).toBeInstanceOf(Function); 44 | expect(factoryTuple[1]).toBeInstanceOf(Function); 45 | }); 46 | 47 | it('could be used to multiplicate container item (factory tuple)', () => { 48 | type Node = { 49 | index: number, 50 | parent: Node | null, 51 | children: Node[], 52 | } 53 | 54 | type IContainer = { 55 | index: number, 56 | node: Node, 57 | root: Node, 58 | }; 59 | 60 | let counter = 0; 61 | const container = diContainer({ 62 | index: multiple(() => ++counter), // eslint-disable-line no-plusplus 63 | node: multiple([ 64 | ({ index }) => ({ index, parent: null, children: [] }), 65 | (node, { root }) => { 66 | if (node === root) return; 67 | node.parent = root; 68 | root.children.push(node); 69 | }, 70 | ]), 71 | root: ({ node }) => node, 72 | }); 73 | 74 | expect(container.root).toEqual({ index: 1, parent: null, children: [] }); 75 | 76 | expect([ 77 | container.node, 78 | container.node, 79 | container.node, 80 | ]).toEqual(container.root.children); 81 | 82 | expect(container.root).toEqual({ 83 | index: 1, 84 | parent: null, 85 | children: [ 86 | { index: 2, parent: container.root, children: [] }, 87 | { index: 3, parent: container.root, children: [] }, 88 | { index: 4, parent: container.root, children: [] }, 89 | ], 90 | }); 91 | }); 92 | 93 | it('could be used to decorate whole factories', () => { 94 | type Node = { 95 | index: number, 96 | } 97 | 98 | type IContainer = { 99 | index: number, 100 | node: Node, 101 | }; 102 | 103 | let counter = 0; 104 | const container = diContainer(multiple({ 105 | index: () => ++counter, // eslint-disable-line no-plusplus 106 | node: ({ index }) => ({ index }), 107 | })); 108 | 109 | expect(container.node).toEqual({ index: 1 }); 110 | expect(container.node).toEqual({ index: 2 }); 111 | expect(container.node).toEqual({ index: 3 }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/multiple.ts: -------------------------------------------------------------------------------- 1 | import { IFactory, IFactoryTuple, IFactories } from './types'; 2 | import allNames from './utils/all-names'; 3 | 4 | const cleaner = (instance: any, container: C, name: keyof C) => { 5 | container[name] = null as any; 6 | }; 7 | 8 | const decorateFactory = (factory: IFactory): IFactoryTuple => [ 9 | factory, 10 | cleaner, 11 | ]; 12 | 13 | const decorateTupple = ( 14 | [factory, initilizer]: IFactoryTuple, 15 | ): IFactoryTuple => [ 16 | factory, 17 | (instance: C[N], container: C, name: N) => { 18 | initilizer(instance, container, name); 19 | cleaner(instance, container, name); 20 | }, 21 | ]; 22 | 23 | const decorateItemBinding = ( 24 | itemBinding: IFactory | IFactoryTuple, 25 | ): IFactoryTuple => ( 26 | typeof itemBinding === 'function' 27 | ? decorateFactory(itemBinding) 28 | : decorateTupple(itemBinding) 29 | ); 30 | 31 | const decorateFactories = (factories: IFactories): IFactories => 32 | allNames(factories).reduce( 33 | (newObject, name) => Object.defineProperty(newObject, name, { 34 | ...Object.getOwnPropertyDescriptor(factories, name), 35 | value: decorateItemBinding(factories[name]), 36 | }), 37 | Object.create(null), 38 | ); 39 | 40 | const multiple: { 41 | (itemBinding: IFactory | IFactoryTuple): IFactoryTuple 42 | (factories: IFactories): IFactories 43 | } = ( 44 | bindingOrFactories: IFactory | IFactoryTuple | IFactories, 45 | ) => ( 46 | !Array.isArray(bindingOrFactories) && typeof bindingOrFactories === 'object' 47 | ? decorateFactories(bindingOrFactories) 48 | : decorateItemBinding(bindingOrFactories) 49 | ) as any; 50 | 51 | export default multiple; 52 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type IInstanceInitializer = ( 2 | instance: IContainer[N], 3 | container: IContainer, 4 | name: N, 5 | ) => void; 6 | 7 | export type VoidFn = () => void; 8 | 9 | export type IFactory = 10 | (container: IContainer) => IContainer[N] 11 | 12 | export type IFactoryTuple = 13 | [IFactory, IInstanceInitializer]; 14 | 15 | export type IFactories = { 16 | [name in keyof IContainer]: 17 | IFactory | IFactoryTuple 18 | } 19 | 20 | export type IPureFactories = { 21 | [name in keyof C]: () => C[name] 22 | } 23 | -------------------------------------------------------------------------------- /src/unique-stack.test.ts: -------------------------------------------------------------------------------- 1 | import UniqueStack from './unique-stack'; 2 | import { expectStrictType } from './utils/type-test-utils'; 3 | 4 | describe('UniqueStack', () => { 5 | it('implements UniqueStack Interface', () => { 6 | const stack = UniqueStack(); 7 | 8 | type Expected = { 9 | push:(value: string) => [Error, null] | [null, string], 10 | pop: (expected?: string) => [Error, null] | [null, string], 11 | readonly size: number, 12 | readonly items: string[], 13 | }; 14 | 15 | expectStrictType(stack); 16 | }); 17 | 18 | it('allows to add value to stack', () => { 19 | const stack = UniqueStack(); 20 | const result = stack.push(1); 21 | 22 | expect(result[1]).toBeTruthy(); 23 | }); 24 | 25 | it('allows to get value from stack', () => { 26 | const stack = UniqueStack(); 27 | stack.push(1); 28 | expect(stack.pop(1)[1]).toBe(1); 29 | }); 30 | 31 | it('allows to work with stack', () => { 32 | const stack = UniqueStack(); 33 | stack.push(1); 34 | stack.push(2); 35 | stack.push(3); 36 | 37 | const [, res1] = stack.pop(); 38 | const [, res2] = stack.pop(); 39 | const [, res3] = stack.pop(); 40 | 41 | expect(res1).toBe(3); 42 | 43 | expect(res2).toBe(2); 44 | 45 | expect(res3).toBe(1); 46 | }); 47 | 48 | it('allows to get stack items', () => { 49 | const stack = UniqueStack(); 50 | 51 | stack.push('alpha'); 52 | stack.push('betta'); 53 | stack.push('gamma'); 54 | 55 | expect(stack.items).toEqual(['gamma', 'betta', 'alpha']); 56 | }); 57 | 58 | it('returns an error when duplicated item is pushed', () => { 59 | const stack = UniqueStack(); 60 | stack.push(1); 61 | expect(stack.push(1)[0]).toBeTruthy(); 62 | }); 63 | 64 | it('returns an StackRecursionError when duplicated item is pushed', () => { 65 | const stack = UniqueStack(); 66 | stack.push(1); 67 | const [error] = stack.push(1); 68 | expect(error).toBeInstanceOf(Error); 69 | expect(error!.message).toBe('Duplicated item has been pushed to the stack.'); 70 | }); 71 | 72 | it('returns an StackSequenceError on poping from empty stack', () => { 73 | const stack = UniqueStack(); 74 | const [error] = stack.pop()!; 75 | expect(error).toBeInstanceOf(Error); 76 | expect(error!.message).toBe('Trying to extract item from the empty stack'); 77 | }); 78 | 79 | it('returns an StackSequenceError if extracted item is unexpected', () => { 80 | const stack = UniqueStack(); 81 | stack.push(1); 82 | const [error] = stack.pop(2); 83 | expect(error).toBeInstanceOf(Error); 84 | expect(error!.message).toBe('Extracted item is not equal to expected one'); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/unique-stack.ts: -------------------------------------------------------------------------------- 1 | export type ETuple = [E, null] | [null, T]; 2 | 3 | export type IUniqueStack = { 4 | push:(value: T) => ETuple, 5 | pop: (expected?: T) => ETuple, 6 | readonly items: T[], 7 | readonly size: number, 8 | }; 9 | 10 | const _push = (stack: T[], set: Set) => (item: T): ETuple => { 11 | if (set.has(item)) return [new Error('Duplicated item has been pushed to the stack.'), null]; 12 | stack.unshift(item); 13 | set.add(item); 14 | return [null, item]; 15 | }; 16 | 17 | const _pop = (stack: T[], set: Set) => (expected?: T): ETuple => { 18 | if (stack.length === 0) { 19 | return [new Error('Trying to extract item from the empty stack'), null]; 20 | } 21 | 22 | const last = stack.shift()!; 23 | set.delete(last); 24 | 25 | return ( 26 | expected === undefined || last === expected 27 | ? [null, last] 28 | : [new Error('Extracted item is not equal to expected one'), null] 29 | ); 30 | }; 31 | 32 | const UniqueStackApi = (stack: T[], set: Set): IUniqueStack => ({ 33 | push: _push(stack, set), 34 | pop: _pop(stack, set), 35 | get size(): number { 36 | return stack.length; 37 | }, 38 | get items(): T[] { 39 | return stack.slice(); 40 | }, 41 | }); 42 | 43 | const UniqueStack = () => UniqueStackApi([], new Set()); 44 | 45 | export default UniqueStack; 46 | -------------------------------------------------------------------------------- /src/utils/all-names.test.ts: -------------------------------------------------------------------------------- 1 | import allNames from './all-names'; 2 | 3 | describe('allNames', () => { 4 | it('returns field names from object', () => { 5 | expect( 6 | allNames({ x: 1, y: 2 }), 7 | ).toEqual(['x', 'y']); 8 | }); 9 | 10 | it('returns symbolic names too', () => { 11 | const $field = Symbol('symbolic field'); 12 | expect( 13 | allNames({ x: 1, [$field]: $field }), 14 | ).toEqual(['x', $field]); 15 | }); 16 | 17 | it('works with null-prototpyed objects', () => { 18 | const obj = Object.create(null); 19 | obj.x = 1; 20 | obj.y = 'y'; 21 | 22 | expect( 23 | allNames(obj), 24 | ).toEqual(['x', 'y']); 25 | }); 26 | 27 | it('works with null-prototpyed objects and symbolic names', () => { 28 | const obj = Object.create(null); 29 | const $field = Symbol('symbolic name'); 30 | obj.x = 1; 31 | obj.y = 'y'; 32 | obj[$field] = $field; 33 | 34 | expect( 35 | allNames(obj), 36 | ).toEqual(['x', 'y', $field]); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/utils/all-names.ts: -------------------------------------------------------------------------------- 1 | const allNames = (obj: C): Array => 2 | (Object.getOwnPropertyNames(obj) as Array) 3 | .concat(Object.getOwnPropertySymbols(obj)) as Array; 4 | 5 | export default allNames; 6 | -------------------------------------------------------------------------------- /src/utils/assert-exists.test.ts: -------------------------------------------------------------------------------- 1 | import assertExists from './assert-exists'; 2 | 3 | describe('assertExists', () => { 4 | it('returns object if it is not null and not undefined', () => { 5 | const $s = Symbol('an object'); 6 | expect( 7 | assertExists($s, 'object is not exists'), 8 | ).toBe($s); 9 | }); 10 | 11 | it('throws an error if null is passed', () => { 12 | expect( 13 | () => assertExists(null, 'it is a null'), 14 | ).toThrow(new TypeError('it is a null')); 15 | }); 16 | 17 | it('throws an error if undefined passed', () => { 18 | expect( 19 | () => assertExists(undefined, 'it is an undefined'), 20 | ).toThrow(new TypeError('it is an undefined')); 21 | }); 22 | 23 | it('returns "" for ""', () => { 24 | expect(assertExists('', 'error')).toBe(''); 25 | }); 26 | 27 | it('return 0 for 0', () => { 28 | expect(assertExists(0, 'error')).toBe(0); 29 | }); 30 | 31 | it('returns false for false', () => { 32 | expect(assertExists(false, 'error')).toBeFalsy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/utils/assert-exists.ts: -------------------------------------------------------------------------------- 1 | function assertExists(obj: T, message: string): T { 2 | if (obj == null) throw new TypeError(message); 3 | return obj; 4 | } 5 | 6 | export default assertExists; 7 | -------------------------------------------------------------------------------- /src/utils/assign-props.test.ts: -------------------------------------------------------------------------------- 1 | import { assignProps } from './assign-props'; 2 | 3 | describe('assignProps', () => { 4 | it('returns mapping function', () => { 5 | type Person = { 6 | name: string, 7 | age: number, 8 | } 9 | 10 | type IContainer = { 11 | x: number, 12 | s: string, 13 | z: string, 14 | p: Person, 15 | } 16 | 17 | const mapping = assignProps({ 18 | name: 'z', 19 | age: 'x', 20 | }); 21 | 22 | expect(mapping).toBeInstanceOf(Function); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/utils/assign-props.ts: -------------------------------------------------------------------------------- 1 | import allNames from './all-names'; 2 | 3 | type KeysOfType = { 4 | [P in keyof T]: T[P] extends F ? P : never; 5 | }[keyof T]; 6 | 7 | type ObjectFieldKeys = { 8 | [P in keyof T]: T[P] extends { [k in any]: any } ? P : never; 9 | }[keyof T] 10 | 11 | export type IMapping = { 12 | [p in keyof T]?: keyof IContainer & KeysOfType 13 | } 14 | 15 | export const assignProps = >( 16 | mapping: IMapping, 17 | ) => (instance: IContainer[N], container: IContainer): void => 18 | allNames>(mapping).forEach( 19 | name => { 20 | // @ts-ignore 21 | instance[name] = container[mapping[name]!]; 22 | }, 23 | ); 24 | -------------------------------------------------------------------------------- /src/utils/index.test.ts: -------------------------------------------------------------------------------- 1 | import { assignProps, shallowMerge } from '.'; 2 | 3 | describe('Exported utils', () => { 4 | it('exports assignProps', () => { 5 | expect(assignProps).toBeDefined(); 6 | }); 7 | 8 | it('exports shallowMerge', () => { 9 | expect(shallowMerge).toBeDefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './assign-props'; 2 | export * from './shallow-merge'; 3 | -------------------------------------------------------------------------------- /src/utils/map-object.test.ts: -------------------------------------------------------------------------------- 1 | import mapObject from './map-object'; 2 | import { expectStrictType } from './type-test-utils'; 3 | 4 | describe('mapOject', () => { 5 | it('maps empty object to empty object', () => { 6 | expect( 7 | mapObject({}, () => { throw new Error(); }), 8 | ).toEqual({}); 9 | }); 10 | 11 | it('maps each field', () => { 12 | const obj = { 13 | x: 1, 14 | s: 'hello', 15 | }; 16 | 17 | type Lazy = () => T; 18 | type LazyObj = { [p in keyof T]: Lazy } 19 | 20 | const result = mapObject>( 21 | obj, 22 | (name, self) => () => self[name] as any, 23 | ); 24 | 25 | expectStrictType<() => number>(result.x); 26 | }); 27 | 28 | it('maps some fields', () => { 29 | const obj = { 30 | x: 1, 31 | y: 2, 32 | s: 'hello', 33 | }; 34 | 35 | type Lazy = () => T; 36 | type LazyObj = { [p in keyof T]: Lazy }; 37 | type N = 'x' | 'y'; 38 | type T = typeof obj; 39 | type D = LazyObj>; 40 | 41 | const result = mapObject(obj, (name, self) => () => self[name] as any, ['x', 'y']); 42 | 43 | expectStrictType<{ x:() => number; y: () => number; }>(result); 44 | expect(result.x).toBeInstanceOf(Function); 45 | expect(result.y).toBeInstanceOf(Function); 46 | expect((result as any).z).not.toBeDefined(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/utils/map-object.ts: -------------------------------------------------------------------------------- 1 | import allNames from './all-names'; 2 | 3 | const mapObject = < 4 | SrcType extends object, 5 | Names extends keyof SrcType, 6 | DestType extends { [p in Names]: any } 7 | >( 8 | obj: SrcType, 9 | mapper:

(name: p, self: SrcType) => DestType[p], 10 | names: Names[] = allNames(obj) as Names[], 11 | ): DestType => names.reduce( 12 | (newObj, key) => Object.defineProperty(newObj, key, { 13 | enumerable: Object.getOwnPropertyDescriptor(obj, key)!.enumerable, 14 | value: mapper(key, obj), 15 | }), 16 | Object.create(null), 17 | ); 18 | 19 | export default mapObject; 20 | -------------------------------------------------------------------------------- /src/utils/narrow-object.test.ts: -------------------------------------------------------------------------------- 1 | import narrowObject from './narrow-object'; 2 | 3 | describe('narrowObject', () => { 4 | it('returns the copy if no names are specified', () => { 5 | const object = { 6 | x: 1, y: 2, 7 | }; 8 | 9 | expect(narrowObject(object)).toEqual(object); 10 | }); 11 | 12 | it('returns the narrowed object if names are specified', () => { 13 | const object = { x: 1, y: 2, z: 3 }; 14 | const newObject = narrowObject(object, ['x']); 15 | expect(newObject).toEqual({ x: 1 }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/utils/narrow-object.ts: -------------------------------------------------------------------------------- 1 | import allNames from './all-names'; 2 | 3 | const narrowObject = ( 4 | obj: T, 5 | names: N[] = allNames(obj) as N[], 6 | ): Pick => names.reduce( 7 | (newObj, key) => Object.defineProperty(newObj, key, Object.getOwnPropertyDescriptor(obj, key)!), 8 | Object.create(null), 9 | ); 10 | 11 | export default narrowObject; 12 | -------------------------------------------------------------------------------- /src/utils/shallow-merge.test.ts: -------------------------------------------------------------------------------- 1 | import { expectStrictType } from './type-test-utils'; 2 | import { shallowMerge } from './shallow-merge'; 3 | 4 | describe('shallowMerge', () => { 5 | it('copies an object if only one is passed', () => { 6 | const obj = { 7 | x: 1, y: 'y', 8 | }; 9 | 10 | const newObj: { 11 | x: number, 12 | y: string 13 | } = shallowMerge(obj); 14 | 15 | expect(newObj).toEqual(obj); 16 | expect(newObj).not.toBe(obj); 17 | }); 18 | 19 | it('copies an object if only one is passed including symbolic names', () => { 20 | const $field = Symbol('symbolic name'); 21 | const obj = { 22 | x: 1, y: 'y', [$field]: new Date(), 23 | }; 24 | 25 | const newObj: { 26 | x: number, 27 | y: string, 28 | [$field]: Date, 29 | } = shallowMerge(obj); 30 | 31 | expect(newObj).toEqual(obj); 32 | expect(newObj).not.toBe(obj); 33 | }); 34 | 35 | it('merges two objects', () => { 36 | const t = () => {}; 37 | 38 | const obj1 = { 39 | x: 1, y: 'y', 40 | }; 41 | 42 | const obj2 = { 43 | z: true, t, 44 | }; 45 | 46 | const newObj = shallowMerge(obj1, obj2); 47 | 48 | type ResultObjType = { 49 | x: number, 50 | y: string, 51 | z: boolean, 52 | t: () => void, 53 | }; 54 | 55 | expectStrictType(newObj); 56 | 57 | expect(newObj).toEqual({ 58 | x: 1, y: 'y', z: true, t, 59 | }); 60 | 61 | expect(newObj).not.toBe(obj1); 62 | expect(newObj).not.toBe(obj2); 63 | }); 64 | 65 | it('merges two objects with intercepted sets of fields', () => { 66 | const t = () => {}; 67 | 68 | const obj1 = { 69 | x: 1, y: 'y', t: false, 70 | }; 71 | 72 | const obj2 = { 73 | z: true, t, 74 | }; 75 | 76 | const newObj = shallowMerge(obj1, obj2); 77 | 78 | type ResultObjType = { 79 | x: number; 80 | y: string; 81 | z: Boolean; 82 | t:() => void; 83 | } 84 | 85 | expectStrictType(newObj); 86 | 87 | expect(newObj).toEqual({ 88 | x: 1, y: 'y', z: true, t, 89 | }); 90 | 91 | expect(newObj).not.toBe(obj1); 92 | expect(newObj).not.toBe(obj2); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/utils/shallow-merge.ts: -------------------------------------------------------------------------------- 1 | export type Merge = 2 | Omit & T2; // Overriding Join 3 | 4 | type IShallowMergeFn = { 5 | ( 6 | obj1: T1, 7 | ): T1; 8 | 9 | < 10 | T1 extends object, 11 | T2 extends object 12 | >( 13 | obj1: T1, 14 | obj2: T2, 15 | ): Merge; 16 | 17 | < 18 | T1 extends object, 19 | T2 extends object, 20 | T3 extends object 21 | >( 22 | obj1: T1, 23 | obj2: T2, 24 | obj3: T3, 25 | ): Merge< 26 | Merge, T3>; 27 | 28 | < 29 | T1 extends object, 30 | T2 extends object, 31 | T3 extends object, 32 | T4 extends object 33 | >( 34 | obj1: T1, 35 | obj2: T2, 36 | obj3: T3, 37 | obj4: T4, 38 | ): Merge< 39 | Merge< 40 | Merge, T3>, T4>; 41 | 42 | < 43 | T1 extends object, 44 | T2 extends object, 45 | T3 extends object, 46 | T4 extends object, 47 | T5 extends object, 48 | >( 49 | obj1: T1, 50 | obj2: T2, 51 | obj3: T3, 52 | obj4: T4, 53 | obj5: T5, 54 | ): Merge< 55 | Merge< 56 | Merge< 57 | Merge, T3>, T4>, T5>; 58 | 59 | < 60 | T1 extends object, 61 | T2 extends object, 62 | T3 extends object, 63 | T4 extends object, 64 | T5 extends object, 65 | T6 extends object, 66 | >( 67 | obj1: T1, 68 | obj2: T2, 69 | obj3: T3, 70 | obj4: T4, 71 | obj5: T5, 72 | obj6: T6, 73 | ): Merge< 74 | Merge< 75 | Merge< 76 | Merge< 77 | Merge, T3>, T4>, T5>, T6>; 78 | 79 | < 80 | T1 extends object, 81 | T2 extends object, 82 | T3 extends object, 83 | T4 extends object, 84 | T5 extends object, 85 | T6 extends object, 86 | T7 extends object, 87 | >( 88 | obj1: T1, 89 | obj2: T2, 90 | obj3: T3, 91 | obj4: T4, 92 | obj5: T5, 93 | obj6: T6, 94 | obj7: T7, 95 | ): Merge< 96 | Merge< 97 | Merge< 98 | Merge< 99 | Merge< 100 | Merge, T3>, T4>, T5>, T6>, T7>; 101 | 102 | < 103 | T1 extends object, 104 | T2 extends object, 105 | T3 extends object, 106 | T4 extends object, 107 | T5 extends object, 108 | T6 extends object, 109 | T7 extends object, 110 | T8 extends object, 111 | >( 112 | obj1: T1, 113 | obj2: T2, 114 | obj3: T3, 115 | obj4: T4, 116 | obj5: T5, 117 | obj6: T6, 118 | obj7: T7, 119 | obj8: T8, 120 | ): Merge< 121 | Merge< 122 | Merge< 123 | Merge< 124 | Merge< 125 | Merge< 126 | Merge, T3>, T4>, T5>, T6>, T7>, T8>; 127 | 128 | < 129 | T1 extends object, 130 | T2 extends object, 131 | T3 extends object, 132 | T4 extends object, 133 | T5 extends object, 134 | T6 extends object, 135 | T7 extends object, 136 | T8 extends object, 137 | T9 extends object, 138 | >( 139 | obj1: T1, 140 | obj2: T2, 141 | obj3: T3, 142 | obj4: T4, 143 | obj5: T5, 144 | obj6: T6, 145 | obj7: T7, 146 | obj8: T8, 147 | obj9: T9, 148 | ): Merge< 149 | Merge< 150 | Merge< 151 | Merge< 152 | Merge< 153 | Merge< 154 | Merge< 155 | Merge, T3>, T4>, T5>, T6>, T7>, T8>, T9>; 156 | 157 | < 158 | T1 extends object, 159 | T2 extends object, 160 | T3 extends object, 161 | T4 extends object, 162 | T5 extends object, 163 | T6 extends object, 164 | T7 extends object, 165 | T8 extends object, 166 | T9 extends object, 167 | TA extends object, 168 | >( 169 | obj1: T1, 170 | obj2: T2, 171 | obj3: T3, 172 | obj4: T4, 173 | obj5: T5, 174 | obj6: T6, 175 | obj7: T7, 176 | obj8: T8, 177 | obj9: T9, 178 | objA: TA, 179 | ): Merge< 180 | Merge< 181 | Merge< 182 | Merge< 183 | Merge< 184 | Merge< 185 | Merge< 186 | Merge< 187 | Merge, T3>, T4>, T5>, T6>, T7>, T8>, T9>, TA>; 188 | 189 | < 190 | T1 extends object, 191 | T2 extends object, 192 | T3 extends object, 193 | T4 extends object, 194 | T5 extends object, 195 | T6 extends object, 196 | T7 extends object, 197 | T8 extends object, 198 | T9 extends object, 199 | TA extends object, 200 | TB extends object, 201 | >( 202 | obj1: T1, 203 | obj2: T2, 204 | obj3: T3, 205 | obj4: T4, 206 | obj5: T5, 207 | obj6: T6, 208 | obj7: T7, 209 | obj8: T8, 210 | obj9: T9, 211 | objA: TA, 212 | objB: TB, 213 | ): Merge< 214 | Merge< 215 | Merge< 216 | Merge< 217 | Merge< 218 | Merge< 219 | Merge< 220 | Merge< 221 | Merge< 222 | Merge< 223 | T1, T2>, T3>, T4>, T5>, T6>, T7>, T8>, T9>, TA>, TB 224 | >; 225 | 226 | < 227 | T1 extends object, 228 | T2 extends object, 229 | T3 extends object, 230 | T4 extends object, 231 | T5 extends object, 232 | T6 extends object, 233 | T7 extends object, 234 | T8 extends object, 235 | T9 extends object, 236 | TA extends object, 237 | TB extends object, 238 | TC extends object, 239 | >( 240 | obj1: T1, 241 | obj2: T2, 242 | obj3: T3, 243 | obj4: T4, 244 | obj5: T5, 245 | obj6: T6, 246 | obj7: T7, 247 | obj8: T8, 248 | obj9: T9, 249 | objA: TA, 250 | objB: TB, 251 | objC: TC, 252 | ): Merge< 253 | Merge< 254 | Merge< 255 | Merge< 256 | Merge< 257 | Merge< 258 | Merge< 259 | Merge< 260 | Merge< 261 | Merge< 262 | Merge< 263 | T1, T2>, T3>, T4>, T5>, T6>, T7>, T8>, T9>, TA>, TB>, TC 264 | >; 265 | } 266 | 267 | export const shallowMerge: IShallowMergeFn = (...objects: C[]): C => 268 | objects.reduce( 269 | (newObject, obj) => Object.defineProperties(newObject, Object.getOwnPropertyDescriptors(obj)), 270 | Object.create(null), 271 | ); 272 | -------------------------------------------------------------------------------- /src/utils/type-test-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { expectStrictType } from './type-test-utils'; 2 | 3 | describe('type-test-utils', () => { 4 | test('expectStrinctType is defined', () => { 5 | expect(expectStrictType).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/utils/type-test-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | type Fn = () => T extends R ? 1 : 2; 4 | 5 | export type IfEquals = Fn extends Fn ? A : B; 6 | 7 | export const expectStrictType = (value: expected): void => { 8 | const newValue: IfEquals = true; 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictFunctionTypes": true, 5 | "strictNullChecks": true, 6 | "sourceMap": false, 7 | "target": "es5", 8 | "outDir": "./lib", 9 | "baseUrl": "./src", 10 | "alwaysStrict": true, 11 | "noImplicitAny": true, 12 | "experimentalDecorators": false, 13 | "emitDecoratorMetadata": false, 14 | "module": "CommonJS", 15 | "moduleResolution": "node", 16 | "allowSyntheticDefaultImports": false, 17 | "esModuleInterop": false, 18 | "resolveJsonModule": true, 19 | "removeComments": true, 20 | "declaration": true 21 | }, 22 | "types": ["jest", "node"], 23 | "lib": ["es5", "es6"], 24 | "include": ["src/**/*.ts"], 25 | "exclude": ["node_modules", "src/**/*.test.ts"] 26 | } -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictFunctionTypes": true, 5 | "strictNullChecks": true, 6 | "sourceMap": false, 7 | "target": "es6", 8 | "outDir": "./esm", 9 | "baseUrl": "./src", 10 | "alwaysStrict": true, 11 | "noImplicitAny": true, 12 | "experimentalDecorators": false, 13 | "emitDecoratorMetadata": false, 14 | "module": "ES6", 15 | "moduleResolution": "node", 16 | "allowSyntheticDefaultImports": false, 17 | "esModuleInterop": true, 18 | "resolveJsonModule": true, 19 | "removeComments": true, 20 | "declaration": true, 21 | "importHelpers": false 22 | }, 23 | "types": ["jest", "node"], 24 | "lib": ["es6", "es7"], 25 | "include": ["src/**/*.ts"], 26 | "exclude": ["node_modules", "src/**/*.test.ts"] 27 | } -------------------------------------------------------------------------------- /utils/assign-props.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../lib/utils/assign-props'; 2 | -------------------------------------------------------------------------------- /utils/assign-props.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../lib/utils/assign-props'); 2 | -------------------------------------------------------------------------------- /utils/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../lib/utils'; 2 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../lib/utils'); 2 | -------------------------------------------------------------------------------- /utils/shallow-merge.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../lib/utils/shallow-merge'; 2 | -------------------------------------------------------------------------------- /utils/shallow-merge.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../lib/utils/shallow-merge'); 2 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME=$(npm pkg get name | tr -d '"') 2 | PACKAGE_VERSION=$(npm pkg get version | tr -d '"') 3 | PACKAGE=$PACKAGE_NAME@$PACKAGE_VERSION 4 | 5 | PUBLISHED_VERSION=$(npm view $PACKAGE version || echo "") 6 | if [[ -n $PUBLISHED_VERSION ]]; then echo "The version is already published"; exit 1; fi 7 | --------------------------------------------------------------------------------