@greeneyesai/api-utils v1.30.0
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
- Create a model file like:
./models/user.ts
- 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;
}
- Create a controller file like:
./controllers/example-users.ts
- 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);
}
}
}
- 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 asError->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 andRequestIdMiddleware
- 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
andRequestWithId
type
1.18.*
Cryptography
- logger debug mode:
LoggerAccessor.setLogLevel(LogLevel.DEBUG)
StoreProviderAbstract
renamed toStoreProvider
IProvider
renamed toProviderDefinitionType
1.17.*
Worker
abstraction refactoring, addedWorkerInstance
class (see example code) andIWorkerFactory
1.16.*
- introducing
SecretResolver
provider for AWS Secrets Manager
1.15.*
- introducing
Worker
class
1.14.*
- introducing
CronProvider
,CacheTokenizer
,CacheTokenizerError
implementations - introducing
CronTokenizer
andCronJob
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 ofProviderError
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 returnsCacheProviderWithProxiedClient
- added support for
Singleton.create
factory method, which will be used in theSingletonFactory
onSingleton
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 Provider
s which useInjectorToken
from parent class, if not modified, can extend parent Providers whileDependencies[]
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
andModelTraitStatic
types
1.3.0
- new types:
ModelTrait
andModelTraitStatic
- new helpers:
DatabaseModelHelper.attachTraitToModel
andDatabaseModelHelper.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.
11 days ago
11 days ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
6 months ago
6 months ago