@adr-express-ts/coredeprecated

This package is a dependency injection for express with typescript using decorators and the Action–Domain–Responder pattern

Usage no npm install needed!

<script type="module">
  import adrExpressTsCore from 'https://cdn.skypack.dev/@adr-express-ts/core';
</script>

README

ADR Express TypeScript

Build & Testing CodeQL OSSAR

Template - Build & Testing Template - CodeQL Template - OSSAR

CLI - Build CLI - CodeQL CLI - OSSAR

This package is a dependency injection for express with typescript using decorators and the Action–Domain–Responder pattern.

OAuth2 for ADR-Express-TS:

In the Dependency Selector you can select the OAuth2 module which comes with a template.

Getting Started

Create

Use my CLI to create the application. Instead of "myFolderName" you can add "." to install it in the current directory. Always use npx to use the latest version of the CLI.

npx @adr-express-ts/cli create myFolderName

Path Check

You will get the following question:

? Is the path correct? ('/my/path/to/application') (Y/n)

If the path is correct respond with Y or press Enter

Yarn Check

If you have Yarn installed, the CLI will ask if you want to use it, if you say N npm will be used.

? Yarn was detected on your system, do you want to use Yarn? (Y/n)

Dependency Selector

For the next part you can choose if you need more dependencies (They will be automatically injected). Question 2

Dialect Selector (Only for Sequelize) and complete extra information

If you choose Sequelize, you will also be asked to choose which dialect you want and to complete the connection information. Question 3

Check if directory is empty

If the directory you want to create the application is not empty, you will be asked if you want to remove all files from it. If you don't remove the files the project will not be created.

? The current directory is not empty, do you want to clear it? (y/N)

Finish

After everything is installed, you should see the following output. Final

About Action–Domain–Responder

  • The action takes HTTP requests (URLs and their methods) and uses that input to interact with the domain, after which it passes the domain's output to one and only one responder.
  • The domain can modify state, interacting with storage and/or manipulating data as needed. It contains the business logic.
  • The responder builds the entire HTTP response from the domain's output which is given to it by the action.
  • The entity is the model for the database

Project structure

/src

# Here you define the tests, there are default tests defined.

- /**tests**

- /actions

# Here you define the route and the methods (get, post, put, delete)

# If you have /actions/v1/Action.ts, all the actions in v1 will have the prefix v1

# Are automatically injected!

- - action.ts

- /app

# Here you start the server (app.listen) and other checks before/after starting.

- - Server.ts

- /domain
- - /entities

# Here you define the entity (They are accessed with @Retrieve('Entity.NAME') or from your ORM)

# Are automatically injected!

- - - entity.ts

# Here you define the domain (They are accessed with @Retrieve('Domain.NAME'))

# Are automatically injected!

- - domain.ts

- /middlewares

# Here you define middlewares (as classes).

# You inject them manually

- - middleware.ts

/public

# In public (depends on the Configuration made in index.ts) you have the front-end,

# all routes point to index.html (i made it so it can work with React)

- - index.html

- /responders

# Here you define the domains (They are accessed with @Retrieve('Responder.NAME'))

# Are automatically injected!

- - responder.ts

# Here you inject classes, middlewares, variables, functions, server, router, etc.

# And you have to call Injector.ready(); when you finished all your injections.

- index.ts

Configuration

Injector.setup({
  rootFile: __filename,
  apiPrefix: '/api',
  debug: {
    log: console.log,
    error: console.error
  },
  staticFiles: {
    path: '/',
    directory: ['public']
  },
  errorHandler:
    undefined /* If undefined, the default error handler will be used. */,
  notFoundHandler:
    undefined /* If undefined, the default not found handler will be used. */
});

Entity Model

This model is for Mongoose.

@Inject
@Entity('User')
export default class UserEntity implements InjectedEntity {
  @Retrieve('Mongoose')
  private mongoose?: MongooseClass;

  async onLoad(): Promise<void> {
    if (!this.mongoose) {
      return;
    }

    const { ObjectId } = Schema as any;

    this.mongoose.model(
      'User',
      new Schema({
        id: ObjectId,
        email: {
          type: String,
          min: 3,
          max: 255,
          required: true
        },
        password: {
          type: String,
          min: 8,
          required: true
        }
      })
    );
  }
}

Responder Model

import { Inject, Responder } from '@adr-express-ts/core';
import { Response } from 'express';

@Inject
@Responder('Demo')
export default class DemoResponder {
  public success(res: Response) {
    return res.status(200).json({
      success: true
    });
  }

  public demo(res: Response, status: number, output: any) {
    return res.status(status).json(output);
  }
}

Middleware Model

import { Request, Response, NextFunction } from 'express';
import { Middleware } from '@adr-express-ts/core/lib/@types';
import { Inject } from '@adr-express-ts/core';

@Inject
export default class AuthenticationMiddleware implements Middleware {
  public async middleware(
    req: Request,
    res: Response,
    next: NextFunction
  ): Promise<any> {
    (req as any).myData = 'My custom request data!';
    return next();
  }
}

Action Model

import {
  Action,
  Get,
  Post,
  Put,
  Delete,
  Request,
  Response,
  Retrieve
} from '@adr-express-ts/core';
import {
  Request as ExpressRequest,
  Response as ExpressResponse
} from 'express';

import DemoResponder from '../responders/DemoResponder';
import DemoDomain from '../domain/DemoDomain';

@Action('/demo', ['MiddlewareHere'])
export default class DemoAction {
  @Retrieve('Responder.Demo')
  private responder?: DemoResponder;

  @Retrieve('Domain.Demo')
  private domain?: DemoDomain;

  @Get('/demo1', ['MiddlewareHere'])
  public findAll(
    @Request req: ExpressRequest,
    @Response res: ExpressResponse
  ): any {
    return res.send({
      success: true,
      test: (req as any).myData
    });
  }

  @Post()
  public async saveX(
    @Request req: ExpressRequest,
    @Response res: ExpressResponse
  ): Promise<any> {
    const dataFromDatabase = await this.domain!.test('parameter from action');
    return this.responder!.demo(res, 201, dataFromDatabase);
  }

  @Delete()
  public async deleteY(
    @Request req: ExpressRequest,
    @Response res: ExpressResponse
  ): Promise<any> {
    return this.responder!.success(res);
  }
}

Domain Model

import { Inject, Domain } from '@adr-express-ts/core';

@Inject
@Domain('Demo')
export default class DemoDomain {
  public async test(someParameter: string) {
    return {
      success: true,
      data: 'Data from database',
      someParameter
    };
  }
}

Server.ts Example

import { Configuration, Inject, Retrieve } from '@adr-express-ts/core';
import { InjectedClass } from '@adr-express-ts/core/lib/@types';
import { Application } from 'express';

@Inject
export default class Server implements InjectedClass {
  @Retrieve('ExpressApp')
  private application?: Application;

  @Retrieve('Configuration')
  private config?: Configuration;

  @Retrieve('Middlewares')
  private middlewares?: any;

  public async onReady(): Promise<void> {
    try {
      if (!this.application || !this.config) {
        return;
      }

      const log = this.config.debug.log ?? console.log;

      this.application.listen(4000, '0.0.0.0', async () => {
        log('Server started %o', '0.0.0.0:4000');
      });
    } catch (e) {
      const error = this.config?.debug.error ?? console.error;
      error(e);
    }
  }
}