@greeneyesai/api-utils
v1.30.0
Published
API utilities for devs to help them build REST APIs. By GreenEyes.AI
Downloads
225
Maintainers
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
- 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.