1.1.0 • Published 6 months ago

matheusicaro-node-framework v1.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

@mi-node-framework (matheusicaro)

This framework is a pack of @matheusicaro custom basic configurations and setups for quickly building services and APIs in Node.js for short projects like hackathons, studies, challenges, etc. A bunch of resources here might be useful for our next project 😃👍

Installing

npm package

npm i matheusicaro-node-framework

Local Test

  1. in the your project folder:
npm uninstall matheusicaro-node-framework
npm link matheusicaro-node-framework

Resources

Dependency Injection

An abstraction class for an injection container for TypeScript using TSyring.

This abstraction allows me to use dependency injection in a contract defined in this framework without knowing the main provider (TSyring in v1.0.0).

If we decide to use another dependency injection provider, it will be easier for me to implement it here and update the following services with the new @mi-node-framework version.

How to use it:

function registerProviders(this: DependencyRegistry): void {
  //
  this.register(RegistryScope.SINGLETON, ProviderTokens.MyProvider, new MyProvider());
}

export { registerProviders };
import { DependencyRegistry } from 'matheusicaro-node-framework';

let dependencyRegistry: DependencyRegistry;

const getDependencyRegistryInstance = (): DependencyRegistry => {
  if (!dependencyRegistry) {
    dependencyRegistry = new DependencyRegistry([ registerProviders, ...and others]);
  }

  return dependencyRegistry;
};

export { getDependencyRegistryInstance };
// application layer

import { inject } from 'matheusicaro-node-framework';

class MyController {
  constructor(
    @inject(ProviderTokens.MyProvider)
    private myProvider: MyProviderPort
  ) {}

  public handler(): Promise<void> {
    this.myProvider.run();
  }
}

export { MyController };
// tests layer

describe('MyController', () => {
  const provider = getDependencyRegistryInstance().resolve(ProviderTokens.MyProvider);

  //...
});

Logger

This is a custom logger already setup with winston. The logger will be printed in the files app console

How to use it:

import { DependencyInjectionTokens } from 'matheusicaro-node-framework';

class MyController {
  constructor(
    @inject(DependencyInjectionTokens.Logger)
    private logger: LoggerPort
  ) {}

  public handler(): Promise<void> {
    this.logger.info('trace handler');
  }
}
  const logger = getDependencyRegistryInstance().resolve(ProviderTokens.MyProvider)

  logger.info(message)
  logger.info(message, { id: "...", status: "..." })

  logger.error(message)
  logger.error(message, { id: "...", status: "...", error })

  logger.exception(error): void;

Files location:

  • file: logs/exceptions.log
2024-11-27 14:47:58 [ ERROR ]==> uncaughtException: failed on starting the app Error: failed on starting the app
    at Timeout._onTimeout (/Users/matheus.icaro/DEVELOPMENT/repositories/test/mi-gateway-service/src/app.ts:41:9)
    at listOnTimeout (node:internal/timers:573:17)
    at processTimers (node:internal/timers:514:7)
  • file: logs/combined.log
2024-11-27 14:50:53 [ ERROR ]==> {"message":"failed on starting the app","logData":{"trace_id":"fake_id","originalError":{"message":"its fail","stack":"Error: its fail\n    at Timeout._onTimeout (/Users/matheus.icaro/DEVELOPMENT/repositories/test/mi-gateway-service/src/app.ts:44:11)\n    at listOnTimeout (node:internal/timers:573:17)\n    at processTimers (node:internal/timers:514:7)"}}}

2024-11-27 14:53:37 [ INFO ]==> {"message":"logging data for trace","logData":{"id":"fake_id"}}

Controller Base

The controller base is an abstract class with some useful resources to use, like handling errors and responding to the client with a pre-defined payload.

RestControllerBase

RestControllerBase is the a base controller to be used in rest implementations, recommended express.

import { RestControllerBase } from 'matheusicaro-node-framework';

class HealthController extends RestControllerBase {
  constructor() {
    super();
  }

  public async getHealth(_req: Request, res: Response): Promise<Response<HealthResponse>> {
    try {
      return res.status(200).json({ message: 'success' });
    } catch (error) {
      return this.handleErrorThenRespondFailedOnRequest({
        error,
        response: res,
        responseData: {
          status: 'FAILED',
          time: new Date()
        }
      });
    }
  }
}

export { HealthController };

Testing

Factory

A factory to build objects easier with overrides for their props.

How to use it:

  • src/application/domain/entities/my-object.ts
// your entity/object

export interface MyObject {
  id: string;
  status: 'OPEN' | 'CLOSED' | 'IN_PROGRESS';
}
  • tests/factories/my-object.factory.ts:
// declare any custom function to override specific params and make it easier to build specific objects
class MyObjectFactory extends Factory<MyObject> {
  closed() {
    return this.params({
      status: 'CLOSED',
    });
  }

  open() {
    return this.params({
      status: 'OPEN',
    });
  }
}

// return the default object when build it
const myObjectFactory = MyObjectFactory.define(() => ({
  id: 'some-id',
  status: 'IN_PROGRESS',
}));

export { myObjectFactory };
    import { myObjectFactory } from '../factories/my-object.factory.ts';


    it('should find all closed status', async () => {
      
      // build the object in the desired state pre-defined
      const myObject = myObjectFactory.closed().build();

      stubDatabase.findOne.mockResolvedValueOnce(myObject);

      const result = await provider.findAllClosedStatus();

      expect(result).toEqual([myObject]);
    });


    it('should find by id', async () => {

      //override the build with any value for the fields from your object
      const myObject = myObjectFactory.build({ id: "any id" });

      stubDatabase.findOne.mockResolvedValueOnce(myObject);

      const result = await provider.findById("any id");

      expect(result).toEqual(myObject);
    });

JestStub

JestStub is a stub using (jest.fn()) for interface, types and objects. You can easily stub/mock when you are testing.

  import { jestStub } from 'matheusicaro-node-framework';

  //...

  const stubMyInterface = jestStub<MyInterface>();

  const myClass = new MyClass(stubMyInterface)

  //...

  test('should stub function correctly and set id', async () => {
    const userId = "id",

    stubMyInterface.anyMethod.mockResolvedValueOnce(100);

    const result = myClass.run(userId)

    expect(result).toEqual(100);
    expect(stubMyInterface).toHaveBeenCalledTimes(1);
    expect(stubMyInterface).toHaveBeenCalledWith(userId);
  });

VitestStub

VitestStub is a stub that uses Vitest (vi.fn()) for interfaces, types, and objects. You can easily stub/mock when testing with Vitest.

  import { vitestStub } from 'matheusicaro-node-framework';

  //...

  const stubMyInterface = vitestStub<MyInterface>();

  const myClass = new MyClass(stubMyInterface)

  //...

  test('should stub function correctly and set id', async () => {
    const userId = "id",

    stubMyInterface.anyMethod.mockResolvedValueOnce(100);

    const result = myClass.run(userId)

    expect(result).toEqual(100);
    expect(stubMyInterface).toHaveBeenCalledTimes(1);
    expect(stubMyInterface).toHaveBeenCalledWith(userId);
  });

DeepStubObject

DeepStubObject is a deep typer to be used when JestStub or VitesStub don't return the deeper prop.

  import { jestStub, vitestStub, DeepStubObject } from 'matheusicaro-node-framework';

  //...

  let stubProvider: ProviderInterface & DeepStubObject<ProviderInterface>;

  beforeAll(() => {

    // with jestStub
    stubProvider = jestStub<ProviderInterface>();

    // with vitestStub
    stubProvider = vitestStub<ProviderInterface>();

    myClass = new MyClass(stubProvider);
  });

  test('should stub function correctly and set id', async () => {
    //...
    stubMyInterface.anyProp.deepProp.deeperProp.mockResolvedValueOnce(100);
  });

Errors

You can use some custom errors in your business logic already implemented from ErrorBase which handles with logger and traces.

ErrorBase

you can implement your own errors from this ErrorBase.

class MyCustomErrorError extends ErrorBase {
  constructor(message: string);
  constructor(trace: InvalidStateErrorTrace);
  constructor(message: string, trace?: InvalidStateErrorTrace);
  constructor(messageOrTrace: string | InvalidStateErrorTrace, _trace?: InvalidStateErrorTrace) {
    const { message, trace } = alignArgs(messageOrTrace, _trace);

    super(ErrorCode.INVALID_STATE, InvalidStateError.name, message, {
      userMessage: trace?.userMessage,
      originalError: trace?.logData?.error,
      ...(trace?.logData && {
        logs: {
          data: trace?.logData,
          level: LogLevel.ERROR,
          instance: container.resolve<LoggerPort>(DependencyInjectionTokens.Logger)
        }
      })
    });
  }
}

export { InvalidStateError };

InvalidArgumentError

InvalidArgumentError is a type of error recommended to be used when an invalid argument is informed.

  • surface to the user with a known message for the invalid argument.
  • Log automatically the error & "trace" field when it is present in the args
    • new InvalidArgumentError(message) => do not error & message -new InvalidArgumentError(message, trace) => do log message and trace fields
new InvalidArgumentError('invalid argument', { userMessage: 'friendly user message', logData: { traceId: 'id' } });
  • userMessage can be sent in the response automatically when using RestControllerBase (here)

InvalidRequestError

InvalidRequestError is a type of error recommended to be used when an invalid argument is informed.

  • surface to the user with a known message for the invalid request.
  • Log automatically the error & "trace" field when it is present in the args
    • new InvalidRequestError(message) => do not error & message
    • new InvalidRequestError(message, trace) => do log message and trace fields
new InvalidRequestError('invalid request', { userMessage: 'friendly user message', logData: { traceId: 'id' } });
  • userMessage can be sent in the response automatically when using RestControllerBase (here)

InvalidStateError

InvalidStateError is a type of error recommended to be used when an invalid state is found and your app is not able to handle with.

  • surface to the user as a default error message (if not informed) once there is nothing the user can do at this point to fix the request
  • Log automatically the error & "trace" field when it is present in the args
    • new InvalidStateError(message) => do not error & message
    • new InvalidStateError(message, trace) => do log message and trace fields
new InvalidStateError('invalid state found', { userMessage: 'friendly user message', logData: { traceId: 'id' } });
  • userMessage can be sent in the response automatically when using RestControllerBase (here)

NotFoundError

NotFoundError is a type of error recommended to be used when a resource is not found.

  • surface to the user as an unknown error once there is nothing the user can do at this point to fix the request.
  • Log automatically the error & "trace" field when it is present in the args
    • new InvalidStateError(message) => do not error & message
    • new InvalidStateError(message, trace) => do log message and trace fields
new NotFoundError('doc was not found', { userMessage: 'friendly user message', logData: { docId: 'id' } });
  • userMessage can be sent in the response automatically when using RestControllerBase (here)

1.1.0

6 months ago

1.1.0-next.2.0

6 months ago

1.1.0-alpha.1.0

6 months ago

1.1.0-next.1

6 months ago

1.0.10

7 months ago

1.0.9

7 months ago

1.1.9

7 months ago

1.0.8

7 months ago

1.0.7

7 months ago

1.0.7-next.2

7 months ago

1.0.7-next.1

7 months ago

1.0.6

7 months ago

1.0.6-next.3

7 months ago

1.0.6-next.2

7 months ago

1.0.6-next.1

7 months ago

1.0.5

7 months ago

1.0.4

7 months ago

1.0.3

7 months ago

1.0.2-next.1

7 months ago

1.0.1-next.1

7 months ago

1.0.2

7 months ago

1.0.1

7 months ago

1.0.0

7 months ago

1.0.0-next.4

7 months ago

1.0.0-next.3

7 months ago

1.0.0-next.2

7 months ago

1.0.0-next.1

7 months ago