npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@greeneyesai/api-utils

v1.30.0

Published

API utilities for devs to help them build REST APIs. By GreenEyes.AI

Downloads

225

Readme

API Utils, by GreenEyes.AI

By using this code, you can quickly setup a NodeJS REST API. Classes and utilities cover the most common patterns and practices of enterprise-grade, secure and scaling, MVC-like application architectures, suitable for production usage right out of the box.

Usage, application bootstrap

import * as path from "path";
import {
  Application,
  ApplicationEvents,
  ExecutionContext,
  IRoute,
  IRouteFactory,
  createRouteBuilder,
  CorsMiddleware,
  HeartBeatController,
  HttpMethod,
  S3Provider,
  DatabaseProvider,
  ILoggerInstance,
  IS3Config,
  ProviderDefinitionType,
  IDatabaseConfig,
  LoggerAccessor,
  NativeMiddleware,
} from "@greeneyesai/api-utils";
import BodyParserMiddleware from "body-parser"; // included in the package

const port: number = !!process.env.PORT ? Number(process.env.PORT) : 1337;

const logger: ILoggerInstance = LoggerAccessor.logger;

const routeFactory: IRouteFactory = {
  create(): IRoute[] {
    const commonApiRouteBuilder = createRouteBuilder("/common", [
      new CorsMiddleware({
        wildcard: "greeneyesai.com",
      }),
    ]);

    const commonRoutes = [
      commonApiRouteBuilder<HeartBeatController>(
        "/status",
        HttpMethod.GET,
        HeartBeatController,
        "heartBeat",
        [], // Middleware[]
      ),
    ];

    return [...commonRoutes];
  }
};

const bodyParserMiddleware: NativeMiddleware
  = BodyParserMiddleware.json({ limit: "6mb" });

const s3ProviderDefinition: ProviderDefinitionType<S3Provider, IS3Config> = {
  class: S3Provider,
  config: {
    objectTypes: ['temp', 'thumbnails'],
    baseUrl: ...,
    key: ...,
    endpoint: ...,
    accessKeyId: ...,
    secretAccessKey: ...,
    region: ...,
  }
};

const databaseProviderDefinition: ProviderDefinitionType<DatabaseProvider, IDatabaseConfig> = {
  class: DatabaseProvider,
  config: {
    debug: process.env.NODE_ENV !== "production",
    host: ...,
    port: ...,
    username: ...,
    password: ...,
    database: ...,
    ssl: true,
    modelsPath: path.resolve(__dirname, "./models"),
  }
};

const providers: ProviderDefinitionType<any, any>[] = [
  s3ProviderDefinition,
  databaseProviderDefinition,
];

(async function() {
  try {
    await new Application(port)
      .attachToContext(process)
      .setLoggerInterface(logger)
      .bindHandlerToContextEvents(
        ["uncaughtException", "unhandledRejection"],
        (_app: Application, _context: ExecutionContext, err: Error) => {
          _app.logger?.error(err);
        }
      )
      .once(
        ApplicationEvents.Closed,
        (_app: Application) => {
          _app.logger?.info(`Application closed`);
          !!_app.getContext() &&
            _app.getContext()!.exit &&
            _app.getContext()!.exit!();
        }
      )
      .bindHandlerToContextEvents(
        ["SIGTERM", "SIGUSR2"],
        (_app: Application) => {
          _app.logger?.info(`Received SIGTERM`);
          _app.logger?.onExit && _app.logger?.onExit();
          _app.notify("sigtermFromOS").close();
        }
      )
      .disableApplicationSignature()
      .allowCors()
      .addNativeMiddleware(bodyParserMiddleware) // body parser mw from above
      ...
      .configureProviders(providers)
      .mountRoutes(routeFactory)
      .addRouteNotFoundHandler()
      .addDefaultErrorHandler([
        "SequelizeDatabaseError",
        "DataCloneError",
        "connect ECONNREFUSED" /* Axios */
      ])
      .once(
        ApplicationEvents.Listening,
        (_app: Application) => {
          _app.logger?.info(
            `Application launched on port ${_app.getPort()}`
          );
        }
      )
      .listen();
  } catch (e) {
    console.error(e);
    process.exit(1);
  }
})();

Using the DatabaseProvider, with examples

  1. Create a model file like: ./models/user.ts
  2. Repurpose this example code and place it into into the file:
import { BIGINT, BOOLEAN, STRING } from "sequelize"; // included in the package
import {
  DatabaseProvider,
  DatabaseModelHelper,
  Model,
  ModelTrait,
  ModelTraitStatic,
  ModelDefinition,
  ModelStatic,
  ISchema,
  IParanoidAttributes,
  ITimestampsAttributes,
} from "@greeneyesai/api-utils";
import { v4 } from "uuid"; // included in the package

export interface IUser
  extends ISchema,
    IParanoidAttributes,
    ITimestampsAttributes {
  id: number;
  email: string;
  password: string;
  fullName: string;
  enabled: boolean;
}

export interface IUserPreview extends Omit<IUser, "password" | "deletedAt"> {}

export type ViewsTraitType = ModelTrait<
  IUser,
  {
    getPublicView(): IUserPreview;
  }
>;

export type StaticHelpersTraitType = ModelTraitStatic<
  IUser,
  {
    createWithUUID(
      this: UserModelTypeStatic,
      objectIdSelector: keyof IUser,
      seed: Partial<IUser> & Pick<IUser, "email" | "password">
    ): Promise<UserModelType>;
  }
>;

export type UserModelType = Model<IUser> & ViewsTraitType;

export type UserModelTypeStatic = ModelStatic<UserModelType> &
  StaticHelpersTraitType;

/* This is the entry function for the DatabaseProvider#getModelByName
 * to work (dynamic linking to database network connection) */
export function factory(
  databaseProvider: DatabaseProvider
): UserModelTypeStatic {
  const ViewsTrait: ViewsTraitType = {
    getPublicView: function (): IUserPreview {
      const json = DatabaseModelHelper.PATCHED_GETTER(this);
      delete json.password;
      return json;
    },
  };

  const StaticHelpersTrait: StaticHelpersTraitType = {
    createWithUUID: function (
      objectIdSelector: keyof IUser,
      seed: Partial<IUser> & Pick<IUser, "email" | "password">
    ): Promise<UserModelType> {
      return this.build({
        email: seed.email,
        password: seed.password,
        fullName: seed.fullName || "",
        enabled: seed.enabled || false,
      })
        .set(objectIdSelector, v4())
        .save();
    },
  };

  const model: ModelDefinition = DatabaseModelHelper.buildModel(
    // Table name, export this value if you use the Sequelize CLI
    "user",
    // Schema, export this value if you use the Sequelize CLI
    {
      id: {
        field: "id",
        type: BIGINT,
        primaryKey: true,
        autoIncrement: true,
      },
      email: {
        type: STRING(100),
        allowNull: false,
        unique: true,
      },
      password: {
        type: STRING(64),
        allowNull: false,
      },
      fullName: {
        type: STRING(100),
        field: "full_name",
        allowNull: false,
      },
      enabled: {
        type: BOOLEAN,
        defaultValue: false,
      },
    },
    // Traits
    [
      DatabaseModelHelper.PARANOID_MODEL_SETTINGS, // deletedAt
      DatabaseModelHelper.TIMESTAMPS_SETTINGS, // createdAt / updatedAt
    ]
  );

  const UserModel: ModelStatic<Model<IUser>> =
    databaseProvider.connection.define("User", model.schema, model.settings);

  DatabaseModelHelper.attachTraitToModel(UserModel, ViewsTrait);
  DatabaseModelHelper.attachTraitToModelStatic(UserModel, StaticHelpersTrait);

  return UserModel as UserModelTypeStatic;
}
  1. Create a controller file like: ./controllers/example-users.ts
  2. Place this example code into the file:
import { NextFunction, Request, Response } from "express"; // included in the package
import {
  Controller,
  ControllerError,
  DatabaseProvider,
  SingletonFactory,
  ModelStatic,
  ResponseFormatter,
  SingletonClassType,
} from "@greeneyesai/api-utils";
import {
  IUserPreview,
  UserModelType,
  UserModelTypeStatic,
} from "../models/user";

export class ExampleUsersController extends Controller {
  static get Dependencies(): [SingletonClassType<DatabaseProvider>] {
    return [DatabaseProvider];
  }

  constructor(protected _databaseProvider: DatabaseProvider) {
    super();
  }

  async getUserById(
    req: Request<{ userId: string }>,
    res: Response,
    next: NextFunction
  ) {
    try {
      const userId: number = parseInt(req.params.userId, 10);

      if (Number.isNaN(userId)) {
        throw new ControllerError(`User id ${userId} invalid`);
      }

      this.logger?.info(`Getting user by id ${userId}`);

      const UserModel: UserModelTypeStatic =
        this._databaseProvider.getModelByName<UserModelTypeStatic>("user")!;

      const user: UserModelType | null = await UserModel.findOne({
        where: {
          id: userId,
        },
      });

      if (!user) {
        const notFoundError = new ControllerError("User not found");
        res
          .status(404)
          .json(new ResponseFormatter(notFoundError).toErrorResponse());
        throw notFoundError;
      }

      const view: IUserPreview = user.getPublicView();
      res.status(200).json(new ResponseFormatter(view).toResponse());
    } catch (e) {
      const err = (
        e instanceof ErrorLike ? e : ErrorLike.createFromError(e as Error)
      )
        .clone()
        .setContext(this.context)
        .setOriginator(this)
        .setMetadata({
          path: req.path,
        });
      return next(err);
    }
  }
}
  1. Wire in the route similarly to how it was described above:
...
    ...
        publicApiRouteBuilder<ExampleUsersController>(
          "/users/:userId",
          HttpMethod.GET,
          ExampleUsersController,
          "getUserById",
          [],
        ),
    ...
...

Migrations should be prepared separatelly. You can use the sequelize-cli to run them, or alternativelly you can use the Model.sync() method like this:

1.) Create DatabaseSynchronizer provider class and place it in the ./providers/database-synchronizer.ts file:

import {
  SingletonClassType,
  DatabaseProvider,
  StoreProviderEvents,
  StoreProviderError,
  Provider,
  ModelStatic,
} from "@greeneyesai/api-utils";

export interface IDatabaseSynchronizerConfig extends Array<string> {}

export class DatabaseSynchronizer extends Provider<IDatabaseSynchronizerConfig> {
  static get Dependencies(): [SingletonClassType<DatabaseProvider>] {
    return [DatabaseProvider];
  }

  constructor(protected _databaseProvider: DatabaseProvider) {
    super();
  }

  configure(config: IDatabaseSynchronizerConfig) {
    super.configure(config);
    this._databaseProvider.once(
      StoreProviderEvents.Connected,
      this.createModelsSyncHandler()
    );
  }

  protected createModelsSyncHandler(): (
    _databaseProvider: DatabaseProvider
  ) => Promise<void> {
    return async (_databaseProvider: DatabaseProvider): Promise<void> => {
      try {
        for (let modelPath of this._config!) {
          const CurrentModel: ModelStatic<any> =
            _databaseProvider.getModelByName<ModelStatic<any>>(modelPath)!;
          await CurrentModel.sync();
          _databaseProvider.logger?.info(
            `${this.className}->createModelsSyncHandler()[nested] Model for table "${CurrentModel.tableName}" synced.`
          );
        }
      } catch (e) {
        const err = (e as StoreProviderError).clone();
        err.stack = `${this.className}->createModelsSyncHandler()[nested] Error: ${err.stack}`;
        _databaseProvider.logger?.error(err);
      }
    };
  }
}

2.) Add the provider to the providers array in the application boostrap file:

...

const databaseSynchronizerDefinition: ProviderDefinitionType<
  DatabaseSynchronizer,
  IDatabaseSynchronizerConfig
> = {
  class: DatabaseSynchronizer,
  config: ["user"],
};

...

providers.push(databaseSynchronizerDefinition);

...

(async function() {
  try {
    await new Application(port)
      ...
      .configureProviders(providers)
      ...
      listen();
  } catch (e) {
    console.error(e);
    process.exit(1);
  }
})();

...

Thats it.


What about middlewares?

A recommended pattern:

import {
  CacheProvider,
  CacheProviderWithProxiedClientType,
  Middleware,
} from "@greeneyesai/api-utils";
import { NextFunction, Request, Response } from "express";

export interface IXYZMiddlewareConfig {
  prefix: string;
}

export class XYZMiddleware extends Middleware {
  protected get _defaultOpts(): IXYZMiddlewareConfig {
    return {
      prefix: "XYZPrefix",
    };
  }

  ...
  static create(opts: Partial<IXYZMiddlewareConfig> = {}): XYZMiddleware {
    const cacheProvider = CacheProvider.instance;
    return new this(cacheProvider, opts);
  }

  constructor(
    protected _cacheProvider: CacheProviderWithProxiedClientType,
    protected _opts: Partial<IXYZMiddlewareConfig> = {},
  ) {
    super();
    this._opts = Object.assign({}, this._defaultOpts, this._opts);
  }

  async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const yourKey = `${this._opts.prefix}:${req.params.userId}`;
      ...
      const whateverValue = await this._cacheProvider.get(yourKey);
      ...
      await this._cacheProvider.set(yourKey, yourValue);
      ...
    } catch (e) {
      return next(e);
    }
    return next();
  }
}

Usage:

...
    ...
        publicApiRouteBuilder<ExampleUsersController>(
          "/users/:userId",
          HttpMethod.GET,
          ExampleUsersController,
          "getUserById",
          [
            ...
            XYZMiddleware.create({
              prefix: "XYZPrefixAltered",
            }),
            ...
          ],
        ),
    ...
...

Cron jobs?

See the example:

import { CronJob } from "@greeneyesai/api-utils";
...
export class XYZCronJob extends CronJob {
  public static get ScheduledFor(): string {
    return "0 0 * * *"; // every midnight
  }

  public static create(token: string) {
    return new this(token);
  }

  async run(): Promise<void> {
    this.logger?.info(
      `${this.className}->run Hello from cronjob, token: ${this.token}`
    );
  }
}
...

Configure provider:

import {
  ...,
  ProviderDefinitionType,
  CacheProvider, // CronProvider depends on this
  CronProvider,
  ICronProviderConfig,
  ...
} from "@greeneyesai/api-utils";
import { XYZCronJob } from "./jobs/xzy"
...
const cronProviderDefinition: ProviderDefinitionType<CronProvider, ICronProviderConfig> = {
  class: CronProvider,
  config: [XYZCronJob]
};
...
providers.push(cronProviderDefinition);
...

Workers?

Create a TestWorker class like this:

import {
  Worker,
  IWorkerFactory,
  IWorkerConfig,
  SingletonClassType,
} from "@greeneyesai/api-utils";

export type TestWorkerResultType = boolean;

export interface ITestWorkerConfig extends IWorkerConfig {}

export class TestWorker extends Worker<
  TestWorkerResultType,
  ITestWorkerConfig
> {
  // Must implement:
  protected static getCurrentFilePath(): string {
    return super.resolve(__dirname, __filename);
  }

  public static get InjectorToken(): symbol {
    return Symbol.for(TestWorker.name);
  }

  public static get Dependencies(): SingletonClassType<any>[] {
    return [];
  }

  public configure(config: ITestWorkerConfig): void {
    super.configure(config);
  }

  public async run(): Promise<TestWorkerResultType> {
    this.logger?.info(`Worker runs: ${this.token}`);
    return true;
  }
}

export const TestWorkerFactory: IWorkerFactory<
  TestWorkerResultType,
  ITestWorkerConfig
> = TestWorker.getWorkerFactory<TestWorkerResultType, ITestWorkerConfig>();

Usage:

import { WorkerInstance, WorkerError } from "@greeneyesai/api-utils";
import {
  TestWorkerFactory,
  TestWorkerResultType,
  ITestWorkerConfig,
} from "./workers/test";
...
  ...
    try {
      const config: ITestWorkerConfig = {};
      const worker: WorkerInstance<TestWorkerResultType> =
        TestWorkerFactory.createAndRunWorker(config);
      this.logger?.info(`Worker created with token: ${worker.token}`);
      const result: TestWorkerResultType = await worker.getResult();
      this.logger?.info(`Result: ${result}`);
    } catch (e) {
      const err = (
        (e instanceof WorkerError)
          ?
          e
          :
          WorkerError.createFromError(e as Error)
        ).clone();
      throw err;
    }
  ...
...

Cryptography

import {
  ...,
  ProviderDefinitionType,
  Cryptography,
  ICryptographyConfig,
  ...
} from "@greeneyesai/api-utils";
...
const cryptographyDefinition: ProviderDefinitionType<Cryptography, ICryptographyConfig> = {
  class: Cryptography,
  config: {
    passwordSalt: process.env.PASSWORD_SALT!,
    passwordSaltInTransmit: process.env.PASSWORD_IN_TRANSMIT_SALT,
    ...
  }
};
...
providers.push(cryptographyDefinition);
...


To see the complete source of the package, please use the Code tab on the NPM page.


Release notes

1.29.0

  • type added for createRouteBuilder: RouteBuilder

1.27.*

  • native Error extensions via CallTracingErrorWrapper, such as Error->setToken(token: string)

1.25.*

  • error constructor extensions via CallTracingErrorWrapper
  • error format, token unwrap
  • ChainingHasher<T>, ChainingHasherType, ChainingHasherError:
...
  const hasher: Cryptography.ChainingHasherType = 
    this._cryptography.createChainingHasher("text to hash");
  const hash: string = 
    hasher.pipe("hashPassword")
      .pipe("hashPasswordForTransmission")
      .pipe("hashWithCustomSalt", someRandomSalt)
      .getHash();
...

1.22.*

  • error handling in CacheProviderClientProxy

1.20.*

  • Cryptography.JSONWebToken class and RequestIdMiddleware
  • patching logger instance commands to accept callbacks
  • stack trace filtering (based on context switches within the logger transport)
  • CallTracingErrorWrapper
  • logger.info(\\MSG\, { shouldTrace: true }); will print the stack trace under the message

1.19.*

  • RequestIdMiddleware and RequestWithId type

1.18.*

  • Cryptography
  • logger debug mode: LoggerAccessor.setLogLevel(LogLevel.DEBUG)
  • StoreProviderAbstract renamed to StoreProvider
  • IProvider renamed to ProviderDefinitionType

1.17.*

  • Worker abstraction refactoring, added WorkerInstance class (see example code) and IWorkerFactory

1.16.*

  • introducing SecretResolver provider for AWS Secrets Manager

1.15.*

  • introducing Worker class

1.14.*

  • introducing CronProvider, CacheTokenizer, CacheTokenizerError implementations
  • introducing CronTokenizer and CronJob abstract classes, CronJobError and bunch of new helper interfaces

1.13.*

  • Circular dependency detection in SingletonFactory.
  • Added SingletonFactoryError type.
  • Type tweaks around Singleton abstract class.

1.11.*

  • Timestamps and Paranoid fields now exported from DatabaseModelHelper, so you can add them to your migrations from reference
  • typings for mutating singleton getters (see CacheProvider.instance)
  • Singleton.castToSelf(...) now does run-time assertion of the class specified as target before casting the type
  • smaller improvements like StoreProviderError now a subclass of ProviderError

1.10.*

  • Fixing a bug in CacheProviderClientProxy
  • Patches around the native Object extensions

1.7.*

  • added LoggerAccessor built on top of @greeneyesai/winston-console-transport-in-worker NPM package, for performant logging

1.6.*

  • cache provider CacheProvider.instance now returns CacheProviderWithProxiedClient
  • added support for Singleton.create factory method, which will be used in the SingletonFactory on Singleton instance creation, if the child class defines it
  • ILoggerInstance#onExit introduced (optional, if you need to flush the logger sometime)
  • Object.respondsToSelector introduced globally using the reflection API, so you can assert if any Object is capable of receiving calls under a selector you pass
  • improved type safety

1.5.*

  • resolved ErrorLike message supression
  • Providers which use InjectorToken from parent class, if not modified, can extend parent Providers while Dependencies[] in all other places does not need to be changed anywhere
  • default error handler of Application only emits the error if headers are not sent prior

1.4.0

  • New types and breaking changes
  • New singleton creation pattern, introducing SingletonFactory

1.3.8

  • readme update

1.3.7

  • fixing Application->allowCors method

1.3.5

  • readme update

1.3.3

  • change in the DatabaseProvider->getModelByName type definition
  • readme update regarding to the model construction (UserModelTypeStatic)

1.3.1

  • improved ModelTrait and ModelTraitStatic types

1.3.0

  • new types: ModelTrait and ModelTraitStatic
  • new helpers: DatabaseModelHelper.attachTraitToModel and DatabaseModelHelper.attachTraitToModelStatic
  • improved error handling
  • improved base class for providers
  • improved logging in default providers

License

GNU Lesser General Public License v3.0

© GreenEyes Artificial Intelligence Services, LLC.