# Adaptivestone Framework - Complete Documentation

> This file is auto-generated from the Docusaurus documentation.
> Generated on: 2026-05-11T15:50:09.956Z
> Total documents: 17

---

## Table of Contents

1. 01-intro
2. 02-configs
3. 03-files-inheritance
4. 04-base
5. 05-models
6. 05-modelsOld
7. 06-Controllers / 01-intro
8. 06-Controllers / 02-routes
9. 06-Controllers / 03-middleware
10. 07-logging
11. 08-i18n
12. 09-testsing
13. 10-cli
14. 11-cache
15. 12-email
16. 13-deploy
17. 14-helpers

---



# Document 1: 01-intro

<!-- Source: 01-intro.md -->

# Intro

Welcome to the AdaptiveStone framework documentation. We hope that the documentation is clean and short. Please feel free to edit it via a Git merge request and contact us.

## History

You must be wondering - why another framework, as we currently have a lot of them? But each of them is different.

The Adaptive Stone framework was born around 2016 and has been growing since.

There were a few requirements that no one existing framework could provide us for SAAS at that time - working with multiple databases, scaling out of the box, and a modern approach.

That is why the Adaptive Stone framework was born.

## Features

- Automatically initialized controllers (no config needed)
- Mongoose models
- Winston logger (Sentry logger as a part of Winston logger)
- Node cluster enabled by default (possibility to use with no cluster (dev mode))
- Docker development environment (and production too)
- Integrated Biome (moder alternative to ESLint and Prettier) for code style
- Cache system
- Ability to overwrite any controller, model, and config that came with the framework
- Multi-language support out of the box
- ESM and CommonJS compatibility
- TypeScript support (you are able to write everything in JavaScript, but in JS you will have types as a bonus)

## Folder Structure

```js
framework/
├─ commands/  // Contains the command folder (CLI)
├─ config/ // Contains config files
├─ controllers/ // Contains controller files
│  ├─ {controller_group_folder}/ // Can contain a folder (will be added to the route)
├─ locales/ // i18n folder with translations
│  ├─ {locale_1}/ // Locale name (en, fr, etc.)
│  │  ├─ translation.json // Translation JSON file
│  ├─ {locale_2}/
│  │  ├─ translation.json
├─ migrations/ // Folder where migration files are stored
├─ models/ // Contains model files
├─ modules/ // Main folder with abstract stuff
├─ public/ // Public stuff (served statically)
├─ services/ // Some services (email, http, etc.)
├─ tests/ // Folder contains basic tests
├─ views/ // The framework is able to respond with a view (not only API). View files are stored here.
├─ Cli.ts // Main CLI class
├─ cliCommands.ts // CLI implementation
├─ cluster.ts  // Entry point for production for the cluster module
├─ folderConfig.ts // Folder configuration
├─ server.ts // Main entry point to the framework
```

## Framework Structure

![Framework](/img/AdaptiveStroneFramework.jpg)

## Getting Started

Get started by **creating a new project**.

The simplest way to create a new project is to clone the **template** and customize it:

```shell
git clone git@gitlab.com:adaptivestone/example-project.git adaptivestone-example-rename-me
```

## Start Your Project

You should have **[Docker](https://www.docker.com/products/docker-desktop)** and **[Docker Compose](https://docs.docker.com/compose/install/)** installed.

Run the development server:

```shell
cd adaptivestone-example-rename-me
docker compose up
```

Your site starts at `http://localhost:3300`.

Open `src/controllers/Person.ts` and edit some lines: the site **reloads automatically** and applies your changes.

## TypeScript Support

The framework itself is written in TypeScript (erasable syntax), but the project is flexible to use either TypeScript or JavaScript.

You are able to use the modern Node.js runtime to run TypeScript files without any compilation.

You are also able to mix TypeScript and JavaScript files in one project. Just remember that types will only work if the imported file is a TypeScript file and it is imported into a TypeScript file.


---


# Document 2: 02-configs

<!-- Source: 02-configs.md -->

# Configs

Every modern application runs on multiple environments and should have the ability to configure itself without changing the code.

## Config Files

Config files are located in ‘src/config/\{filename\}.(js|ts)’. Each config file is mostly unique, and you should refer to the module documentation (or the framework config section).

:::note

Config files are part of the [framework inheritance process](03-files-inheritance.md).

:::

## Environment Variables

Out of the box, the framework supports environment variables with [process.loadEnvFile](https://nodejs.org/api/process.html#processloadenvfilepath). This is part of Node.js since v20.12.0.

The framework will grab and parse the .env file in the root of the project directory to fill environment variables.

The sample project ships with a basic .env.example file.

:::tip

Do not use environment variables in the code. Use them only inside config files. This will allow you to track variables and split configs and the codebase.

:::

:::tip

Do not push the .env file to the version control system, as it can contain passwords and keys. The file should be unique per environment.
:::

:::tip

Try not to overload the .env file with configuration and keep only private data in it. To manage non-private data, check the [NODE_ENV](#node_env) section.
:::

## NODE_ENV

Depending on the NODE_ENV environment variable, the framework will load additional config files. Data from these files will be merged into the main config with an overwrite.

This is useful when you want to have something that depends on the environment but do not want to overload the .env file.

Also, it's a good practice to keep the basic config in files rather than in the .env file.

**Example:**

```js title="/src/config/sample.ts"
export default {
  variable1: 1,
  variable2: 2,
};
```

```js title="/src/config/sample.production.ts"
export default {
  variable2: 3,
  variable3: 4,
};
```

On a NON-production environment:

```js
const sampleConfig = this.app.getConfig("sample");
//     variable1: 1,
//     variable2: 2
```

On a PRODUCTION environment:

```js
const sampleConfig = this.app.getConfig("sample");
//     variable1: 1,
//     variable2: 3, <-- DIFFERENT
//     variable3: 4  <-- NEW
```

You can use the same approach for different environments (dev, stage, testing, etc.).

## API

```ts
getConfig(configName: string): {};
updateConfig(configName: string, config: {}): {};
```

Returns the config based on the config name. It also caches it in memory.

```js
const sampleConfig = this.app.getConfig("sample");
```

Updates the config based on the config name. Returns the updated config.

```js
const sampleConfig = this.app.updateConfig("sample", {
  variableToUpdate: 3,
  anotherVariable: 4,
});
```


---


# Document 3: 03-files-inheritance

<!-- Source: 03-files-inheritance.md -->

# File Inheritance

The framework provides a flexible way to overwrite some functionalities from the core. You can create a file with the same name in the same folder, and the framework will use this file instead of the built-in one.

Let’s go with an example. We have a User model built directly with the framework with some user-related stuff. We want to provide a fully different implementation of this model.

To do that, we will create a user model file at the project level in the model folder.

```js
project/
├─ node_modules/
│  ├─ @adaptivestone/
│  │  ├─ framework/
│  │  │  ├─ models/
│  │  │  │  ├─ User.ts // Built-in model. Mark it as "User_original"
├─ src/
│  ├─ models/ // Contains model files
│  │  ├─ User.ts // File that will be used. Mark it as "User_project"

```

```js
const User = this.app.getModel("User"); // Will return the "User_project" model
```

This also happens at **all** levels of the code. If some code inside the framework asks for the “User” model, it will get the “User_project” model.

The same approach works for models, controllers, and configs.

## How To

### Extend a module with new functionality instead of completely overwriting it

That's easy. Just require the original file and extend it.
Please note: TypeScript types are fully optional. You are able to use plain JavaScript too.

```ts
import OriginalUserModel from "@adaptivestone/framework/models/User.ts";

class User extends OriginalUserModel {
  static get modelStatics() {
    type UserModelLite = GetModelTypeLiteFromSchema<
      typeof User.modelSchema,
      ExtractProperty<typeof User, "schemaOptions">
    >;

    return {
      ...OriginalUserModel.modelStatics, // Grab the original model methods
      getPublic: function getPublic(this: InstanceType<UserModel>) {
        return {
          userName: this.name,
        };
      },
    };
  }
}
```

### Disable functions completely

To disable something (like a default controller), the best way is to overwrite it with an empty implementation.

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class Auth extends AbstractController {}

export default Auth;
```


---


# Document 4: 04-base

<!-- Source: 04-base.md -->

# Base Class

Each class extends the Base class to have some basic functions, mostly related to logging and the inheritance process.

The Base class implements on-demand logging instance loading. That means the logger will be united only when you first request it. This is an important part of loading speed optimization.

## API

```js
class A extends Base {
  async someFunction() {
    // Access to the logger instance (please follow the logger documentation for more details).
    this.logger;

    // Get files with inheritance.
    const files = await this.getFilesPathWithInheritance(
      `${__dirname}/../../migrations`,
      this.app.foldersConfig.migrations
    );
  }

  /**
   * Returns the logger group. Just to have all logs grouped logically.
   */
  static get loggerGroup() {
    return "Base_please_overwrite_";
  }

  /**
   * In case of logging, sometimes we might need to replace the name.
   */
  getConstructorName() {
    return this.constructor.name;
  }
}
```

```js
getFilesPathWithInheritance(internalFolder, externalFolder);
```

Will scan two folders and provide a path with inheritance. If the same file is present in both paths, the priority will be given to the external file.


---


# Document 5: 05-models

<!-- Source: 05-models.md -->

# Models

The framework is based on the [Mongoose](https://mongoosejs.com/) library and provides direct access to it.

The model handles database connections.

:::note

Model files are part of the [framework inheritance process](03-files-inheritance.md).

:::

The model uses a class with static methods and properties, providing auto-typing for each model. TypeScript helpers are also available to extract types from the model class.

## Lifecycle

The framework can do the following:

1. Load model files
2. Initialize model files

There are internal options for this (see the Commands section), which are mostly used in commands where you can skip model initialization or load the model without initializing it. This is useful in a few use cases, primarily for type generation.

Under normal conditions, the framework scans the `model` folder and loads all models using the inheritance process.

This is primarily to avoid model 'circular dependencies.'

## Base Model

The base model is the core of your models. It handles their structure and initialization.
We have provided a TypeScript example, but you can ignore the types if you only want to use JavaScript.
Using types is fully optional.

```ts
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";

// in case you need to access appInstance - appInstance.getConfig('s3');
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";

// These are TypeScript helpers.
import type {
  GetModelTypeFromClass, // `GetModelTypeFromClass` returns model types from the class.
  GetModelTypeLiteFromSchema, // Same as above, but only uses the schema to avoid circular linking.
} from "@adaptivestone/framework/modules/BaseModel.js";

import type { Schema } from "mongoose";

// Type helper for static and instance methods.
type SomeModelLite = GetModelTypeLiteFromSchema<typeof SomeModel.modelSchema>;

class SomeModel extends BaseModel {
  static initHooks(schema: Schema): void {
    // A place to initialize plugins, indexes, and so on.
    // This happens after the class is loaded into Mongoose but before Mongoose initializes it.
    // schema.plugin(PLUGIN_NAME);
    schema.index({ name: "text" }); // or indexes.

    // For hooks, there are two types of `this`: model and queries.
    // https://mongoosejs.com/docs/middleware.html#types-of-middleware
    schema.pre(
      'save',
      async function (this: InstanceType<SomeModelLite>) {
        ...
      }
    );
    schema.pre('findOneAndDelete', async function () {
      const docToDelete = await this.model.findOne<SomeModelLite>( // Helps to return the correct model.
        this.getQuery(),
      );
    });
  }

  // The Mongoose schema goes here.
  // This is a complete Mongoose schema.
  // Please refer to the Mongoose documentation: https://mongoosejs.com/docs/guide.html
  static get modelSchema() {
    return {
      someString: { type: String, required: true },
      firstName: String,
      lastName: String,
      email: String,
      orders: {
        type: mongoose.Schema.Types.ObjectId, // This is the correct type for an ObjectID reference. Only this type generates valid types.
        ref: "Order", // You don't need to worry about initialized model schemas; the framework will load and initialize all models for you.
      },
    } as const; // This helps generate better types (TypeScript only).
  }

  // The Mongoose schema options go here.
  // Please refer to the Mongoose documentation: https://mongoosejs.com/docs/guide.html#options
  static get schemaOptions() {
    return {
      read: "primary",
    } as const; // This helps to generate better types. (TypeScript only)
  }

  /**
   *  Object with static methods.
   * this.app.getModel('SomeModel').findByEmail('email');
   * this.app.getModel('SomeModel').getInfoStatic();
   *
   */
  static get modelStatics() {
    type OrderModelType = GetModelTypeFromClass<typeof Order>; // To help with the `populate` method.

    return {
      findByEmail: async function findByEmail(
        this: SomeModelLite, // A type helper to map to the correct `this` context.
        email: string
      ) {
        const instance = await this.find({ email });
        return instance;
      },
      getInfoStatic: async function getInfoStatic(
        model: InstanceType<SomeModelLite> // A TypeScript type helper.
      ) {
        await model.populate("orders");
        return {
          _id: model.id,
          email: model.email,
        };
      },
      getInfoStaticWithOrders: async function getInfoStatic(
        // Intercepts model types to ensure that the `orders` type is correct (without interception, it will be just an `ObjectID`).
        model: InstanceType<SomeModelLite> & {
          orders: InstanceType<OrderModelType>[];
        }
      ) {
        await model.populate("orders");
        return {
          _id: model.id,
          email: model.email,
          orders: model.orders,
        };
      },
    };
  }

  /**
   * We should also have instance methods for the model to interact with.
   * const SomeModel = appInstance.getModel('SomeModel');
   * const someModel = await SomeModel.findOne({email:"cfff"});
   * const data = await someModel.getInfo(); // call instance method
   */
  static get modelInstanceMethods() {
    type ShippingInstanceType = InstanceType<SomeModelLite>;

    return {
      getInfo: async function getInfo(this: SomeModelLite) {
        return {
          _id: this._id,
          email: this.email,
        };
      },
            // anotherMethod,
    };
  }

  /**
   * We should also have virtual methods for the model to interact with.
   * const SomeModel = appInstance.getModel('SomeModel');
   * const someModel = await SomeModel.findOne({email:"cfff"});
   * const fullName = await someModel.fullName // virtual field
   * someModel.fullName = 'Jean-Luc Picard';
   */
  static get modelVirtuals() {
    return {
      fullName: {
        // virtual field
        options: {
          type: Object, // schema
        },
        get(this: InstanceType<SomeModelLite>) {
          // Getter
          return `${this.firtsName} ${this.lastName}`;
        },
        async set(this: InstanceType<SomeModelLite>, v: string) {
          // Setter
          const firstName = v.substring(0, v.indexOf(" "));
          const lastName = v.substring(v.indexOf(" ") + 1);
          this.set({ firstName, lastName });
        },
      },
    }; // make sure that you not put it as a const
  }

}

export default SomeModel;

// It's good practice to return the type from the model.
export type TSomeModel = GetModelTypeFromClass<SomeModel>;
```

:::warning

Models are instantiated once per process, and their instances are then cached. Do not expect constructor or `init` hook calls on every model load.

:::

:::tip

Please do not use the plural form for model names.

**Bad** - Coin**s**

**Good** - Coin

:::

## API

```ts
getModel(modelName: string): MongooseModel<any>;
```

Example:

```js
const UserModel = this.app.getModel("User");
const userInstance = await UserModel.findOne({ email: "user@email.com" });
```

## Configuration

The main configuration variable is the `MONGO_DSN` environment variable, which the model uses to connect to the database.

## Built-in Models

The framework comes with a few built-in models.

### User

It is part of the authorization system and handles user storage, password hashing, and provides basic functions for token generation and user retrieval.

If you want to create your own user implementation, you should override or disable this one.

The authentication controller depends on this model.

#### API

```js
const UserModel = this.app.getModel("User");
const user = await UserModel.getUserByEmailAndPassword("email", "password");
const userToken = await user.generateToken(); // Generates and stores a token in the database
const userPublic = await user.getPublic();
const hashedPassword = await UserModel.hashPassword("password");
const sameUser = await UserModel.getUserByToken(userToken);
const sameUserAgain = await UserModel.getUserByEmail(user.email);
const recoveryToken = await UserModel.generateUserPasswordRecoveryToken(user);
const sameUserAgain2 = await UserModel.getUserByPasswordRecoveryToken(
  recoveryToken
);
const isSuccess = await user.sendPasswordRecoveryEmail(i18n);
const verificationToken = await UserModel.generateUserVerificationToken(user);
const sameUserAgain3 = await UserModel.getUserByVerificationToken(
  verificationToken
);
await UserModel.removeVerificationToken(verificationToken);
const isSuccess2 = await user.sendVerificationEmail(i18n);
```

### Migration

The migration model is a helper for the migration subsystem. It stores the names of migrated files to ensure that each migration is only executed once.

Please refer to the `CLI/migrations` section for more details.

You should probably not use this model directly.

### Sequence

The Sequence model allows you to generate sequences by name. This is a cross-server-safe method for generating sequences in a distributed environment.

```javascript
const SequenceModel = this.app.getModel("Sequence");
// Will be 1.
const someTypeSequence = await SequenceModel.getSequence("someType");
// Will be 2.
const someTypeSequence2 = await SequenceModel.getSequence("someType");
// Will be 1, as the type is different.
const someAnotherTypeSequence = await SequenceModel.getSequence(
  "someAnotherType"
);
```

### Lock

The Lock model provides the ability to lock resources in a distributed environment.

This can be used for external requests, system actions, etc.

Imagine you have a high volume of traffic requesting data from an external system. You also have a cache for this data, but you must initially query the internal API to retrieve it. To prevent overwhelming the API, you want to ensure that you only request the data once and that other simultaneous requests wait for the result instead of making redundant calls. This is where the Lock model can help.

```javascript

const LockModel = this.app.getModel("Lock");

  /**
   * Acquires a lock based on the lock name.
   * @param {string} name
   * @param {number} [ttlSeconds=30]
   * @returns {Promise<boolean>}
   */
  async acquireLock(name, ttlSeconds = 30)

  /**
   * Releases a lock based on the lock name.
   * @param {string} name
   * @returns {Promise<boolean>}
   */
  async releaseLock(name)

  /**
   * Waits for a lock based on the lock name.
   * @param {string} name
   * @returns {Promise}
   */
  async waitForUnlock(name)

  /**
   * Gets the lock's remaining time based on the lock name.
   * @param {string} name
   * @returns {Promise<{ttl: number}>}
   */
  async getLockData(name)


  /**
   * Gets the locks' remaining time based on the lock names.
   * @param {string[]} names
   * @returns {Promise<{name: string, ttl: number}[]>}
   */
  static async getLocksData(names)

```

Example of usage:

```javascript

async someHTTPRequestWithExpensiveExternalAPI(req, res) {
  // We have some external requests, which can be simultaneous requests from different users.
  const LockModel = this.app.getModel("Lock");
  // Let's say it's AI processing of a video, for example.
  const { videoId } = req.appInfo.request;

  // Check if we already have it.
  const VideoAIModel = this.app.getModel("VideoAIModel");
  const videoAI = await VideoAIModel.findOne({ videoId });
  if (videoAI) {
    return res.json(videoAI.getPublic());
  }

  const lockName = `video-ai-processing-${videoId}`;

  // We don't have that video, so let's send it for processing using a lock.
  const isLockAcquired = await LockModel.acquireLock(lockName);
  if (isLockAcquired) {
    const result = await videoAIService.processVideo(videoId);
    const videoModel = await VideoAIModel.create({ videoId, result });
    // Release the lock.
    await LockModel.releaseLock(lockName);
    // Return the result.
    return res.json(videoModel.getPublic());
  }

  // We don't have a lock, so let's wait for one.
  await LockModel.waitForUnlock(lockName);
  // It looks like the external process is finished, so let's check for the result.
  const videoAI2 = await VideoAIModel.findOne({ videoId });
  if (videoAI2) {
    return res.json(videoAI.getPublic());
  }

  // If there's no result, we'll return an error.
  return res.status(500).json({ error: "Something went wrong" });
}
```


---


# Document 6: 05-modelsOld

<!-- Source: 05-modelsOld.md -->

# Models (OLD)

:::warning

This documentation for old model defenitions. Please do not use this type of models at all. Use ceuurnt model versions
:::

Framework based on [mongoose](https://mongoosejs.com/) library and provide direct access to mongoose.

It uses [ES6 class variant](https://mongoosejs.com/docs/guide.html#es6-classes) of mongoose to init. Also you can access framework

Model take care about database connection

:::note

Models files part of [framework inheritance process ](03-files-inheritance.md).

:::

## Access mongoose instance

Inside the class mongoose avaialable as

```js
this.mongooseModel;
```

## Basic model

```js
import AbstractModel from "@adaptivestone/framework/modules/AbstractModel.js";

class SomeModel extends AbstractModel {
  constructor(app) {
    super(app);
    // you can put some init stuff
  }

  initHooks() {
    super.initHooks();
    // place to init plugins, indexes, etc.
    // As it happens after loaded class into mongoos, but before mongoose inited class
    // this.mongooseSchema.plugin(PLUGIN_NAME);
  }

  // here mongoose scheme go
  // this is a fullu mongoose scheme
  // Please reffer to mongoose documentation https://mongoosejs.com/docs/guide.html
  get modelSchema() {
    return {
      someString: { type: String, required: true },
      firstName: String,
      lastName: String,
      email: String,
    };
  }

  // here mongoose scheme options go
  // this is a fullu mongoose scheme
  // Please reffer to mongoose documentation https://mongoosejs.com/docs/guide.html#options
  get modelSchemaOptions() {
    return {
      read: "primary",
    };
  }

  // Static method will be part on monngose class
  // this.app.getModel('SomeModel').someStaticMethod()
  static async someStaticMethod() {
    const somedata = await this
      .findByIdAndUpdate
      //.....
      ();
    const { app } = this.getSuper();
    return somedata;
  }

  // any methods will be part on instance method
  // await this.app.getModel('SomeModel').findById(124).someInstanceMethod()
  async someInstanceMethod() {
    // you can access app into the instance method
    const { app } = this.getSuper();

    // inside of instance method you can access model data
    this.someString;
  }

  // any getters will became a mongoose virtual
  // const SomeModels = this.app.getModel("SomeModel").
  // const someModelInstance = await SomeModel().create({ email: 'test@gmail.com' });;
  // `domain` is now a property on SomeModels documents.
  // someModelInstance.domain; // 'gmail.com'
  get domain() {
    return this.email.slice(this.email.indexOf("@") + 1);
  }

  // setters also be an virtual
  // const SomeModels = this.app.getModel("SomeModel").
  // const someModelInstance = new SomeModel();
  // Vanilla JavaScript assignment triggers the setter
  // someModelInstance.fullName = 'Jean-Luc Picard';
  set fullName(v) {
    // `v` is the value being set, so use the value to set
    // `firstName` and `lastName`.
    const firstName = v.substring(0, v.indexOf(" "));
    const lastName = v.substring(v.indexOf(" ") + 1);
    this.set({ firstName, lastName });
  }
}

export default SomeModel;
```

:::tip

If you have some relations ("ref") on a mongoose model that you should care to load schema. As mongoose can only build relationships with schemas in memory. Google place to do that - inside constructor. Be aware on loop linking models

```js
constructoror(app){
	super(app);
	this.app.getModel(“ReferenceModelName”);
}
```

:::

:::warning

Models united onse per process and then united instances cached. Do NOT expect constructor or init hook calls on every model loading

:::

:::tip

Please do not name the model in plural form.

**Bad** - Coin**s**

**Good** - Coin

:::

## API

```js
getModel(modelName: string): MongooseModel<any>;
```

Example:

```js
const UserModel = this.app.getModel("User");
const userInstace = await UserModel.findOne({ email: "user@email.com" });
```

## Configuration

Main configuration variable "MONGO_DSN" environment variable. Based on it model will made connection to the database

## Built-in models

Framework came with few built-in models.

### User

It's a part of the authorization system. It takes care of storing users, hash passwords and provides some basic functions for token generation and getting users.

If you want to have your own user implementation you should overwrite it or disable.

Auth controller depends on this model

#### API

```js
const UserModel = this.app.getModel("User");
const user = await UserModel.getUserByEmailAndPassword("email","password"):
const userToken = await user.generateToken(); // generate and store token on databse
const userPublic = await user.getPublic();
const hashedPassword = await UserModel.hashPassword("password");
const sameUser = await UserModel.getUserByToken(userToken);
const sameUserAgain = await UserModel.getUserByEmail(user.email);
const recoveryToken = await UserModel.generateUserPasswordRecoveryToken(user);
const sameUSerAgain2 = await UserModel.getUserByPasswordRecoveryToken(recoveryToken);
const isSuccess = await user.sendPasswordRecoveryEmail(i18n);
const verificationToken = await UserModel.generateUserVerificationToken(user);
const sameUSerAgain3 = awaitUserModel.getUserByVerificationToken(verificationToken);
await UserModel.removeVerificationToken(verificationToken);
const isSuccess2 = await user.sendVerificationEmail(i18n);
```

### Migration

Migration model it's helper for migration subsystems. It stores migrated files to make sure that migrated executed once

Please refer to CLI/migrations for more details

You probably should not use this model directly

### Sequence

Sequence allows you to generate sequences by name. This is cross server safe method to generate sequences in distributed environment

```javascript
const SequenceModel = this.app.getModel("Sequence");
// will be 1
const someTypeSequence = await SequenceModel.getSequence("someType");
// will be 2
const someTypeSequence2 = await SequenceModel.getSequence("someType");
// will be 1 as type is another
const someAnotherTypeSequence = await SequenceModel.getSequence(
  "someAnotherType"
);
```

### Lock

Lock model is designed to provide ability to lock some resources in distributed environment.

This can be external requests, some actions in the system, etc.

Imagine that you have a log of traffic that asks external system for some data. You have cache of this data as weel, but initially you shaul asks internal api to get this data. And you want to make sure that you asking this api for that data only one time and other same time requests will wait for the result instead of asking api. This is where Lock model can help you.

```javascript

const LockModel = this.app.getModel("Lock");

  /**
   * acquire lock based on lock name
   * @param {string} name
   * @param {number} [ttlSeconds=30]
   * @returns {Promise<boolean>}
   */
  async acquireLock(name, ttlSeconds = 30)

  /**
   * release lock based on lock name
   * @param {string} name
   * @returns {Promise<boolean>}
   */
  async releaseLock(name)

  /**
   * wait lock based on lock name
   * @param {string} name
   * @returns {Promise}
   */
  async waitForUnlock(name)

  /**
   * get lock remaining time based on lock name
   * @param {string} name
   * @returns {Promise<{ttl: number}>}
   */
  async getLockData(name)


  /**
   * get lock remaining time based on lock name
   * @param {string[]} names
   * @returns {Promise<{name: string, ttl: number}[]>}
   */
  static async getLocksData(names)

```

Example of usage:

```javascript

async someHTTPRequestWithExpensiveExternalAPI(req,res){
  // we have some external request (this can be same time request from different users)
  const LockModel = this.app.getModel("Lock");
  // lets say its a AI processing of video (for example)
  const {videoId} = req.appInfo.request;

  // check if we already have it
  const VideoAIModel = this.app.getModel("VideoAIModel");
  const videoAI = await VideoAIModel.findOne({videoId});
  if(videoAI){
    return res.json(videoAI.getPublic());
  }

  const lockName = `video-ai-processing-${videoId}`;

  // we have no that video, lets send it otp rocessing with lock
  const isLockAcquired = await LockModel.acquireLock(lockName);
  if(isLockAcquired){
    const result = await videoAIService.processVideo(videoId);
    const videoModel = await VideoAIModel.create({videoId, result});
    // release lock
    await LockModel.releaseLock(lockName);
    // return result
    return res.json(videoModel.getPublic());
  }

  // we have no lock, lets wait for it
  await LockModel.waitForUnlock(lockName);
  // lookslikeexternal process is done, lets check if we have result
  const videoAI2 = await VideoAIModel.findOne({videoId});
  if(videoAI2){
    return res.json(videoAI.getPublic());
  }

  // we have no result, lets return error
  return res.status(500).json({error: "something went wrong"});
}

```


---


# Document 7: 06-Controllers > 01-intro

<!-- Source: 06-Controllers/01-intro.md -->

# Controllers

Controllers are a crucial component of the framework. The framework uses [Express.js](https://expressjs.com/) for the HTTP lifecycle (listening, body parsing, response API, the third-party middleware ecosystem) and a tree-based `RouteRegistry` for path matching, parameter extraction, method dispatch, and middleware ordering. Controllers contribute subtrees to the global registry — auto-loaded from the controllers folder, mounted via a single Express middleware.

:::note

Controller files are part of the [framework inheritance process](03-files-inheritance.md).

:::

The framework provides built-in error handling, automatic controller loading (including from subfolders), and request validation and casting.

:::note

In production, the framework uses the `cluster` module to start multiple instances (based on the number of CPU cores) and provide load balancing between them.
Keep in mind that you cannot access one process from another. For complex scenarios (like a WebSocket server), you will need to use inter-process communication techniques to send messages between processes.
:::

## Controller Structure

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    // Return routes info.
    // NECESSARY part.
  }

  getHttpPath() {
    // URL prefix for this controller. Default: `/{constructor-name-lowercase}`.
    // Override in a subclass to customize (e.g., `Home` → `/`).
  }

  static get middleware() {
    return new Map();
    // Path-/method-scoped middlewares for routes in THIS controller.
  }
}
export default ControllerName;
```

:::tip

Only the `routes` getter is required; other parts of the controller can be omitted if not needed.
:::

:::warning

Controllers should extend the "AbstractController" module.
:::

## Name Convention and Loading

The framework will load any file (except for `*.test.js` and `*.test.ts` files) and initialize it as an HTTP module. By default, the filename will be used as the route name. This behavior can be customized by providing your own `getHttpPath` function.

### Explicit registration

In addition to file-based auto-loading, you can register a controller programmatically via `app.controllerManager.registerController(ControllerClass, prefix?)`. This is useful for:

- **Test fixtures** — register a controller only for a specific test (see the [Testing](../09-testsing.md) chapter).
- **Late registration** — controllers added after `Server.startServer()` boots, via the `callbackBefore404` hook so routes mount before the 404 handler.
- **Conditional controllers** — only register based on config, feature flags, or runtime detection.

```js
import MyController from "./controllers/MyController.js";

await server.startServer(async () => {
  // Runs after framework controllers init, before the 404 handler.
  // Routes mount on `/my/mycontroller/*` (prefix + lowercase class name).
  server.app.controllerManager?.registerController(MyController, "my");
});
```

Auto-loading internally uses the same `registerController` entry point — both paths produce identical instances.

For the example above:

```js
class ControllerName extends AbstractController {
```

The route will be “http://localhost:3300/controllername”.

Then, any method from the router will be accessible via the URL.

If you want to define a custom path, you can provide your own implementation of the `getHttp-Path` function.

```js
  getHttpPath() {
    return "superDuperMegaSpecialRoute";
  }
```

By default, `getHttpPath` resolves the current folder and filename and uses them to construct the route name.

## Request Flow

![RequestFlow](/img/requestFlow.jpg)

## View

By default, the framework uses Express to render views with the Pug template engine. To render a view, create a template file in the `views` folder and then call it with the required parameters:
++++...
[message is too long]

```js
res.render("template", { title: "Hey", message: "Hello there!" });
```

## JSON

JSON is the most common data format for modern web applications, but its flexibility can sometimes lead to confusion.

We provide [basic documentation](https://andrey-systerr.notion.site/API-JSON-41f2032055ae4bddae5d033dc28eb1d3) on how we recommend working with JSON, and the framework is designed to follow these guidelines.

## Configuration

The configuration file is located at `config/http.js`.

Please take a moment to review it.

The most notable options are:

```js
port; // Port that the server will use. By default, process.env.HTTP_PORT or port 3300.
hostname; // IP to bind to. By default, process.env.HTTP_HOST or '0.0.0.0' (any). Could be dangerous.
corsDomains; // CORS-allowed domains.
```


---


# Document 8: 06-Controllers > 02-routes

<!-- Source: 06-Controllers/02-routes.md -->

# Routes

Routes are relative to the controller route. You SHOULD NOT use the full route here.

Route objects have multiple levels.

## Route First Level (Method Level)

On the first level, only the ‘method’ (post, put, delete, etc.) exists. Only requests with these methods will go deeper into the real routes.

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      post: {
        // post routes
      },
      get: {
        // get routes
      },
      put: {
        // put routes
      },
      // etc.
    };
  }
}
export default ControllerName;
```

## Route Second Level (Path Level)

Inside the methods (second level), we have a path. The framework's tree-based router supports a small, opinionated set of patterns:

```js
"/fullpath"                          // literal path
"/fullpath/:paramOne/:paramTwo"      // named params → req.params.paramOne, paramTwo
"/api/{*rest}"                       // catch-all splat → req.params.rest = "/v1/users/42"
```

| Syntax | Matches | Captures |
|---|---|---|
| `/literal` | exact segment | nothing |
| `:name` | exactly one segment | `req.params.name` |
| `{*name}` | zero or more segments to end of path | `req.params.name` (joined with `/`) |

**Specificity** (when patterns overlap): static segments win, then `:param`, then `{*splat}`. So `/users/me` registered alongside `/users/:id` always matches the literal first.

**URL decoding** is per-segment (Spring `PathPatternParser` model). `%2F` inside a `:param` value stays as `/`; the matcher does not split on it. For `{*splat}` captures, segments are decoded individually then re-joined — encoded-slash distinction is lost (documented trade-off; use raw-body mode for the rare encoded-slash case).

**Defaults**: case-insensitive, lenient trailing slash. Both flip in v6.

:::note

The order of routes matters when patterns overlap at the same specificity tier (e.g., two `:param` siblings). The first matched route is executed.
:::

Example:

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      post: {
        "/someUrl": {
          handler: this.postSomeUrl,
          request: yup.object().shape({
            count: yup.number().max(100).required(),
          }),
        },
      },
    };
  }
}
export default ControllerName;
```

## Route Third Level (Route Object Level)

On the third level, we have a "route object," a special object that will describe our route.

```js
{
  handler: this.postSomeUrl, // required
  request: yup.object().shape({ // optional
    count: yup.number().max(100).required(),
  }),
  query: yup.object().shape({ // optional
    page: yup.number().required(),
  }),
  middleware: [RateLimiter], // optional
  description: yup.string() // optional
}

```

Here:

```js
Handler; // Some async function (most likely in this controller file) that will do all the work.
Request; // A special interface that will do validation of body parameters for you.
Query; // A special interface that will do validation of query parameters for you.
Middleware; // An array of middlewares specially for the current route.
Description; // A description of this route (used when generating documentation).
```

## Request

Request does validation and casting of an upcoming `req.body`.

As we want to use already well-defined solutions, we believe that [yup](https://github.com/jquense/yup) is a great example of how a schema should be validated.

But you still have the ability to provide your own validation based on an interface.

:::warning
Request works on a body level.
:::

Request contains all fields from `req.body` and passes them into validation.

:::warning
Please note that GET methods have no BODY.
:::

Parameters after validation are available as `req.appInfo.request`.

:::warning
Do not use `req.body` directly. Always use parameters via `req.appInfo.request`.

:::

## Query

Query does validation and casting of an upcoming `req.query`.

The Yup schema is described similarly to the request.

:::warning
Query works on a query level.
:::

Query contains all fields from `req.query` and passes them into validation.

Parameters after validation are available as `req.appInfo.query`.

:::warning
Do not use `req.query` directly. Always use parameters via `req.appInfo.query`.

:::

## Validation

The framework dispatches validation through [Standard Schema](https://standardschema.dev/) — a vendor-neutral interface. Any conforming validator works as a route's `request:` or `query:` schema with no glue code:

| Validator | Standard Schema support |
|---|---|
| [Yup](https://github.com/jquense/yup) | ≥1.7 |
| [Zod](https://zod.dev/) | ≥3.24 |
| [Valibot](https://valibot.dev/) | all current versions |
| [ArkType](https://arktype.io/) | all current versions |

Yup is shown in the examples below since the framework historically taught it, but the same shapes are accepted from any Standard Schema-conforming library.

### Yup example

```js
request: yup.object().shape({
  count: yup.number().max(100).required("error text"),
});
query: yup.object().shape({
  page: yup.number(),
});
```

A more complete example:

```js
request: yup.object().shape({
  name: yup.string().required("validation.name"), // You can use i18n keys here.
  email: yup.string().email().required("Email required field"), // Or just text.
  message: yup
    .string()
    .required("Message required field")
    .min(30, "minimum 30 chars"), // Additional validators for different types exist.
  pin: yup.number().integer().min(1000).max(9999).required("pin.pinProvided"),
  status: yup
    .string()
    .required("Status required field")
    .oneOf(["WAITING", "CANCELED"]), // One of.
  transaction: yup
    .object() // Deep-level object.
    .shape({
      to: yup.string().required(),
      amount: yup.number().required(),
      coinName: yup.string().oneOf(["btc", "etc"]).default("etc"), // Default.
    })
    .required(),
});
```

For Yup schemas, the framework automatically strips unknown fields (security-relevant when handlers spread `req.appInfo.request` into model creates). You don't need `.noUnknown()` — the framework calls `cast(data, { stripUnknown: true })` for you.

### Zod example

```js
import { z } from "zod";

request: z.object({
  count: z.number().max(100),
  message: z.string().min(30, "minimum 30 chars"),
});
query: z.object({
  page: z.number().optional(),
});
```

Zod (and Valibot, ArkType) strip unknown fields by default — no extra configuration needed. To allow unknown fields explicitly, use the library's pass-through API (Zod's `.passthrough()`, Valibot's `looseObject`, ArkType's `'+': 'ignore'`).

### Typed handler signatures (codegen)

Running `npm run gen` (alias for `npm run cli generatetypes`) emits a `<File>.routes.gen.ts` next to every controller. The gen file exports one type alias per handler method (PascalCase suffixed with `Request`) — handlers import the alias instead of hand-writing `FrameworkRequest & { appInfo: { request: { ... } } }`:

```ts
// src/controllers/Auth.ts
import type { PostLoginRequest } from "./Auth.routes.gen.ts";
import { object, string } from "yup";

class Auth extends AbstractController {
  get routes() {
    return {
      post: {
        "/login": {
          handler: this.postLogin,
          request: object().shape({
            email: string().email().required(),
            password: string().required(),
          }),
        },
      },
    };
  }

  async postLogin(req: PostLoginRequest, res: Response) {
    // req.appInfo.request.email is typed as string (from the schema)
    // req.appInfo.user is typed as InstanceType<TUser> | undefined
    //   (from GetUserByToken middleware's `static get provides()`)
    // req.appInfo.i18n.t(...) is typed (from BaseAppInfo)
  }
}
```

The gen file uses `InstanceType<typeof Controller>['routes'][...]['request']` type navigation, so schemas stay inline in the `routes` getter — no extracted named consts required. It also intersects in `provides` shapes from the middleware tuple at this route, so `req.appInfo.user` (and any other middleware-contributed fields) are typed automatically.

The middleware chain in the gen file is read from the same `RouteRegistry.flatten()` the runtime uses — so the types you see at compile time match the middlewares that actually run at request time. No parallel matcher to drift.

#### Setup

Add to `package.json`:

```json
"gen": "node cliCommand.ts generatetypes",
"check:types": "npm run gen && tsc --noEmit"
```

Add to `.gitignore`:

```
**/*.routes.gen.ts
```

The gen files are regenerated on every type-check, so they stay fresh; CI doesn't need any extra step.

#### Naming convention

Handler method `postLogin` → type `PostLoginRequest`. Method `verifyUser` → `VerifyUserRequest`. The convention is method name in PascalCase + `Request` suffix. Renames flow naturally with editor refactor tools.

If the same handler method serves multiple routes (e.g., a backward-compatible POST and GET sharing one method), the type is a union of the per-route shapes — narrow with `req.method` inside the handler.

#### Routes without schemas

Bare-method-ref routes like `'/logout': this.postLogout` (no `request:` field) get a type that omits the schema-output override; `req.appInfo.request` falls through to the default `Record<string, unknown>` from `BaseAppInfo`.

#### Middleware-provided types

To make a middleware contribute typed fields to `req.appInfo`, add a `static get provides()` getter:

```ts
class GetUserByToken extends AbstractMiddleware {
  static get provides() {
    return {} as { user?: InstanceType<TUser> };
  }

  async middleware(req, res, next) {
    // ... runtime logic ...
  }
}
```

The returned object is always `{}` — only the cast type matters. Codegen reads this; the runtime ignores it. Handlers downstream of this middleware (per the route's middleware chain) get `req.appInfo.user` typed.

For app-wide globals (e.g., `requestId`, `sentryTransaction`) that aren't tied to a specific middleware, augment `AppInfoExtensions`:

```ts
declare module "@adaptivestone/framework/services/http/types" {
  interface AppInfoExtensions {
    requestId: string;
  }
}
```

#### Manual fallback (without codegen)

If you'd rather not run codegen, you can pull a typed shape from any Standard Schema validator with `StandardSchemaV1.InferOutput`:

```ts
import type { StandardSchemaV1 } from "@adaptivestone/framework/services/validate/types";
import { object, string } from "yup";

const loginSchema = object({
  email: string().email().required(),
  password: string().required(),
});

type LoginRequest = StandardSchemaV1.InferOutput<typeof loginSchema>;

async postLogin(
  req: FrameworkRequest & { appInfo: { request: LoginRequest } },
  res: Response,
) {
  // req.appInfo.request.email is typed
}
```

This works for Zod, Valibot, ArkType too. Trade-off: middleware-contributed fields (`req.appInfo.user` and friends) need to be intersected by hand on every handler, and renames don't propagate.

### File Validation

For file validation, we provide a special Yup class, `YupFile`. It is really simple to use.

```js
import { YupFile } from "@adaptivestone/framework/helpers/yup.js";

request: yup.object().shape({
  someFileName: new YupFile().required("error text"),
  otherFiled: yup.string().required(), // Yes, you can mix it with regular data.
});
```

`YupFile` validates a single file. For multi-file uploads (e.g., `<input type="file" multiple>`), wrap with `yup.array`:

```js
request: yup.object().shape({
  avatars: yup.array(new YupFile()).min(1, "at least one file"),
});
```

For other Standard Schema validators (Zod, Valibot, ArkType), validate against the framework-exported `File` type:

```ts
import type { File } from "@adaptivestone/framework/types";
import { z } from "zod";

request: z.object({
  avatar: z.instanceof(File),
  avatars: z.array(z.instanceof(File)).nonempty(),
});
```

:::warning
Please be aware that a file can only be uploaded by ‘multipart/form-data’, and because of this, you can’t use nested objects.
:::

### Custom validators

To plug in a validator that doesn't already implement Standard Schema (e.g., raw [Joi](https://joi.dev/), or a hand-rolled function), implement the `~standard` slot directly. About 10 lines of glue:

```ts
import type { StandardSchemaV1 } from "@adaptivestone/framework/services/validate/types";

interface ProductInput { sku: string; price: number }

const productSchema: StandardSchemaV1<unknown, ProductInput> = {
  "~standard": {
    version: 1,
    vendor: "mycustom",
    validate(value) {
      const data = value as Partial<ProductInput>;
      if (typeof data.sku !== "string") {
        return { issues: [{ message: "sku is required", path: ["sku"] }] };
      }
      if (typeof data.price !== "number") {
        return { issues: [{ message: "price is required", path: ["price"] }] };
      }
      return { value: { sku: data.sku, price: data.price } };
    },
  },
};

// Use it on a route:
request: productSchema;
```

You also get `InferOutput<typeof productSchema>` for free. Standard Schema's spec lives at https://standardschema.dev/.

### Registering vendor drivers

For library-specific behavior (custom strip semantics, native JSON Schema export for OpenAPI, etc.), register a `ValidatorDriver`:

```ts
import { ValidateService, type ValidatorDriver } from "@adaptivestone/framework/services/validate/ValidateService";

const myJoiDriver: ValidatorDriver = {
  canHandle: (body) => body?.isJoi === true,
  async validate(body, data) {
    const { value, error } = body.validate(data, { stripUnknown: true });
    if (error) throw new ValidationError(joiToFrameworkPayload(error));
    return value;
  },
  toJsonSchema: (body) => myJoiToJsonSchema(body), // optional; for OpenAPI later
};

ValidateService.register(myJoiDriver);
```

Drivers are matched in registration order; user-registered drivers take priority over the built-ins.

### ValidationError

When validation fails, the framework throws a `ValidationError`. The instance's `.message` is the path-keyed payload object that ships out via `res.json({ errors: err.message })`, producing:

```json
{
  "errors": {
    "fieldName": ["error description"],
    "anotherField": ["another field error"]
  }
}
```

Each value is always an array of messages. A field that fails multiple validators surfaces all of them: `{password: ["min8", "startUpper"]}`.

For structured access (logging, observability), use `.issues`:

```ts
import { ValidationError } from "@adaptivestone/framework/services/validate/ValidationError";

try {
  /* ... */
} catch (e) {
  if (e instanceof ValidationError) {
    for (const issue of e.issues) {
      console.error(`[${issue.path?.join(".") ?? "root"}] ${issue.message}`);
    }
  }
}
```

### i18n

In any fields that can generate an error (required, etc.), you can use i18n keys to translate. The framework will handle the translation for you.

Please refer to the [i18n documentation](08-i18n.md).

## Handler

Handler - some async function (most likely in this controller file) that will do all the work. It is better to write the function in the same file.

:::warning
The handler can only be an **async** function.
:::

`req.appInfo.app`

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      post: {
        '/': {
          handler: this.postSample,
          request: yup.object().shape({
            count: yup.number().max(100).required(),
          })
        }
      }
    }
  }
  // Send a request with data {count: "5000"}.
  // Will produce an error with status 400 and {errors: {count:['Text error']}}.

  postSample(req, res) {
    // On success validation, we pass here.
    // {count: "5000"}
    console.log(req.appInfo.request)
    // {count: 5000} -> casted to a number

    const SomeModel = this.app.getModel('SomeModel');
    const SomeModelAlternativeWay = req.appInfo.app.getModel('SomeModel');

    const { count } = req.appInfo.request;

    const someModel = await SomeModel.findOne({count});

    return res.status(200).json({modelId: someModel.id});
  }

}
export default ControllerName;
```

### Middleware

Middleware - an array of middlewares specially for the current route.

:::warning

Route middlewares take precedence over middlewares in controllers.

:::

```javascript
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      get: {
        '/routeName': {
          handler: ...,
          middleware: [MiddlewareName, MiddlewareName, etc]
        }
      },
    };
  }
}
export default ControllerName;
```

Similarly to controller middlewares, you can use middlewares with parameters.

:::note

The rules for the design of middlewares with parameters are described in the subsection "Middleware".

:::

Sample:

```javascript
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
import RoleMiddleware from "@adaptivestone/framework/services/http/middleware/Role.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      get: {
        '/routeName': {
          handler: ...,
          middleware: [[RoleMiddleware, { roles: ['client'] }]]
        }
      },
    };
  }
}
export default ControllerName;
```

## Debugging your routes

### Boot-time route tree

After all controllers are registered, the framework prints the full route tree at the `verbose` log level — useful for spotting cross-controller middleware accumulation, splat scopes, or unexpected route shapes. Set `LOGGER_CONSOLE_LEVEL=verbose` (or your transport's equivalent) to see it.

```
/  (mw: GetUserByToken)
├── GET → home
├── auth  (mw: GetUserByToken, RateLimiter)
│   ├── login
│   │   └── POST → postLogin  [request]
│   └── logout
│       └── POST → postLogout
└── v1  (mw: ApiLimiter)
    └── container  (mw: ApiLimiter)
        ├── GET → getContainers  [query]
        └── POST → getContainers  [query]
```

`[request]` / `[query]` markers indicate routes with body / query schemas. `{…}` after a middleware name means it was registered with parameters.

### Warnings on misconfiguration

The framework logs a `warn`-level message and skips the offending entry when it sees these problems in your `routes` getter or middleware `Map`:

| Warning | Triggered when |
|---|---|
| `unknown verb 'X' in routes getter` | A key in `get routes()` isn't one of `get/post/put/patch/delete/head/options` |
| `route X Y has no handler function` | A route's value is an object but its `handler` field is missing or not callable |
| `middleware Map key is not a string` | A key in `static get middleware()`'s `Map` is not a string |
| `middleware Map key 'X' has unknown method prefix 'Y'` | A `Map` key looks like `METHOD/path` but `METHOD` is not a known HTTP verb — the whole key is treated as a path, which is usually a typo |

These warnings catch the common typos that used to silently produce 404s at request time.


---


# Document 9: 06-Controllers > 03-middleware

<!-- Source: 06-Controllers/03-middleware.md -->

# Middleware

You can read more about middlewares at [https://expressjs.com/en/guide/using-middleware.html](https://expressjs.com/en/guide/using-middleware.html).

In general, it’s a function that accepts a request, a response, and a `next` callback. This function can analyze requests and add more details to them (like parsing JSON, getting query params, or checking a user token). Middleware can pass requests to the next level (the next middleware or handler) or can respond directly and finish the request.

This is really powerful and allows developers to reuse simple logic and build routes on these simple building blocks.

Default:

```js
  static get middleware() {
    return new Map([['/{*splat}', [GetUserByToken, Auth]]]);
  }
```

## Middleware Order

Middleware will be executed in the order provided. Based on that, you can chain middleware where the input of the second middleware depends on the output of the first middleware.

## Global Middlewares

The framework internally uses a few middlewares. These middlewares are not adjustable (for now) and are executed on each request.

[Cors](#cors)

[PrepareAppInfo](#prepareappinfo)

[IpDetector](#ipdetector)

[RequestLogger](#requestlogger)

[RequestParser](#requestparser)

[I18n](#i18n)

## Including Middlewares into Controllers

Controller-level middleware is adjusted based on the “middleware” getter.

```js
  static get middleware() {
    return new Map([['METHOD/path', ["Middleware","Array"]]]);
    // return middlewares for THIS route only
  }
```

Where 'METHOD/path' is a method with a path. Supported methods are `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`, and the pseudo-verb `ALL` (matches all methods). If the middleware key starts with `/`, then `ALL` methods are used.

The path follows the same patterns as routes — literals, `:name` params, and `{*name}` splats. See [Routes › path syntax](02-routes.md#route-second-level-path-level) for the full reference.

The middleware array is an array of middlewares (with params).

Sample:

```javascript
  static get middleware() {
    return new Map([['GET/{*splat}', [GetUserByToken]]]);
  }
```

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        GetUserByToken,
        [RoleMiddleware, { roles: ['admin'] }]
      ]]
    ]);
  }
```

:::warning
The middleware here are not raw Express middlewares. Please see below.
:::

## Including Middlewares into a Route Object

Middlewares can also be added into a route object (subchapter “Routes”).

## Middleware Parameters

Some middleware accept initial parameters passed into them.

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        GetUserByToken, // middleware with no parameters
        [RoleMiddleware, { roles: ['admin'] }] // middleware with parameters
      ]]
    ]);
  }
```

To pass parameters, wrap the middleware in an array. The first element will be the middleware itself, and the second one will be the middleware parameters. The second one will be passed as is into the middleware constructor.

## Built-in Middleware

The framework has a few middlewares that you can use.

### Auth

```js
import Auth from "@adaptivestone/framework/services/http/middleware/Auth.js";
```

Allows passing only if the user is provided. Please use any middleware that provides a user instance beforehand (like `GetUserByToken`).

#### Parameters

No parameters.

### Cors

```js
import Cors from "@adaptivestone/framework/services/http/middleware/Cors.js";
```

Adds CORS headers if the origin matches the config.

#### Parameters

`origins` - an array of strings or regex to check the origin. Required parameter.

```javascript
  static get middleware() {
    return new Map([
      ['GET/someUrl', [
        [Cors, { origins: ['http://localhost',/./] }]
      ]]
    ]);
  }
```

### GetUserByToken

```js
import GetUserByToken from "@adaptivestone/framework/services/http/middleware/GetUserByToken.js";
```

Grabs a token and tries to parse the user from it. It will find the user in the database by the token. If the user exists, it will add the `req.appInfo.user` variable.

#### Parameters

No parameters.

### I18n

```js
import I18n from "@adaptivestone/framework/services/http/middleware/I18n.js";
```

An internationalization module based on [i18next](https://www.npmjs.com/package/i18next). It provides `req.appInfo.i18n` that can be used for translation.

The middleware provides a few detectors:

- X-Lang header
- Query
- User

Please check the [i18n documentation](08-i18n.md) for more details.

#### Parameters

No parameters.

### IpDetector

```js
import IpDetector from "@adaptivestone/framework/services/http/middleware/IpDetecor.js";
```

This middleware will detect the client's IP. It works well with different proxies (AWS ELB, Nginx, etc.) and detects the real client IP.

:::note
If the request IP is from a `trustedProxy` (trusted source) only, then the module will try to parse the IP from the provided `X-Forwarded-For` header and grab the client IP from there. Otherwise, the request IP will be used.
:::
This is a core middleware, and some other middlewares (like `RateLimiter`) depend on it.

#### Parameters

All parameters go into the config file. There are two parameters there: `headers` and `trustedProxy`.

`headers` is an array of headers to parse the IP address from. By default, it is 'X-Forwarded-For'.

`trustedProxy` is an IP, CIDR, or range of IPv4 and IPv6 that is trusted to parse headers from.

```javascript
  headers: ['X-Forwarded-For'],
  trustedProxy: [ // list of trusted proxies.
    '169.254.0.0/16', // ipv4 cidr
    'fe80::/10', // ipv6 cidr
    '127.0.0.1', // ip itself
    '1.1.1.1-1.1.1.3', // ip range
  ],
```

IP data is available at:

```javascript
req.appInfo.ip;
```

:::warning
Select `trustedProxy` really carefully, as anyone can add any headers to your request.
:::

Nginx sample to add a header:

```bash
server {
    location xxxx/ {
      proxy_set_header  X-Forwarded-For $remote_addr;
    }
  }

```

### Pagination

```js
import Pagination from "@adaptivestone/framework/services/http/middleware/Pagination.js";
```

The pagination middleware provides a helper that grabs URL search parameters (`page`, `limit`) and calculates the necessary `appInfo` properties (`skip`, `limit`, and `page`).

#### Parameters

`limit` = 10 - default limit if not provided.

`maxLimit` = 100 - max limit for documents.

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        [Pagination, { limit: 10,maxLimit: 50}]
      ]]
    ]);
  }
```

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [Pagination]
    ]);
  }
```

```javascript
// http://localhost:3300/someUrl?limit=10&page=2
const { limit, skip, page } = req.appInfo.pagination;
```

### PrepareAppInfo

```js
import PrepareAppInfo from "@adaptivestone/framework/services/http/middleware/PrepareAppInfo.js";
```

`PrepareAppInfo` is a special small middleware that generates `res.appInfo = {}`. This is to make sure that all subsequent middleware can use `appInfo` without checking if it exists.
It is for internal use.

#### Parameters

No parameters.

### RateLimiter

A rate limiter middleware. Limits the amount of requests.

```js
import RateLimiter from "@adaptivestone/framework/services/http/middleware/RateLimiter.js";
```

For rate limiting, we are using the [node-rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) module. Please refer to the module documentation for more details.

The basic idea of a rate limiter is that we have some weight for the call and some key that has ‘credits’. Each call consumes ‘credits’, and when it reaches 0, the request is blocked.

Some samples - login protection. We can generate rate limiters based on the user's email and limit each email to only 5 calls per minute. Or we can construct a more complex login that includes user IPs, etc.

#### Parameters

By default, the rate key is generated based on the Route, IP, and userID. But you can adjust it via the config (globally) or via middleware parameters.

```javascript
  static get middleware() {
    return new Map([
      [
        'POST/login',
        [
          GetUserByToken,
          [
            RateLimiter,
            {
              consumeKeyComponents: { ip: false },
              limiterOptions: { points: 5 },
            },
          ],
        ],
      ],
    ]);
  }
```

The rate limiter middleware allows you to include request components (`req.body`) for key generation. Please note that you have no access to `req.appInfo.request` at this stage.

```javascript
  static get middleware() {
    return new Map([
      ['POST/login', [
        GetUserByToken,
        [RateLimiter,{consumeKeyComponents: { ip: false, request:['email','phone'] }}]
      ]]
    ]);
  }
```

You can find the default parameters in ‘config/rateLimiter.js’. These parameters are used if other parameters are not provided.

The rate limiter has multiple backends (memory, Redis, and Mongo). By default, the 'memory' backend is activated.

### RequestLogger

```js
import RequestLogger from "@adaptivestone/framework/services/http/middleware/RequestLogger.js";
```

A small middleware that logs request info (route, method, status, and time).

Logs example:

```js
[middlewareRequestLogger]  2023-01-24T07:00:35.680Z  info : Request is  [GET] /project/123
[middlewareRequestLogger]  2023-01-24T07:00:35.747Z  info : Finished Request is  [GET] /project/123.  Status: 200. Duration 67 ms
```

#### Parameters

No parameters.

### RequestParser

```js
import RequestParser from "@adaptivestone/framework/services/http/middleware/RequestParser.js";
```

This is the main middleware to parse requests (`application/json`, `multipart/form-data`, `application/octet-stream`, `application/x-www-form-urlencoded`).
It is based on the [formidable](https://www.npmjs.com/package/formidable) package.
After parsing, the data is available in `req.body`.

#### Parameters

No parameters.

### Role

Checks the user role (`user.roles` property). If the user does not have the role, it stops the request and returns an error. It uses OR logic (any of the specified roles will allow the user to pass).

```js
import Role from "@adaptivestone/framework/services/http/middleware/Role.js";
```

#### Parameters

`roles` - an array of roles to check. It uses OR logic (any role will pass).

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        [RoleMiddleware, { roles: ['admin','moderator'] }]
      ]]
    ]);
  }
```

### StaticFiles

:::warning

Deprecated and removed in version 5. It is better to use an HTTP server (Nginx, etc.) to handle static files.
:::

```bash
# nginx sample
server {

	root /var/www/application/src/public;

	server_name _;
	client_max_body_size 64M;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ @backend;
	}

	location @backend {
		proxy_pass http://localhost:3300;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_cache_bypass $http_upgrade;
	}
}

```

```js
import StaticFiles from "@adaptivestone/framework/services/http/middleware/StaticFiles.js";
```

Handles static files. Mostly for development purposes. In production, it is better to handle files via a web server.

#### Parameters

`folders` - an array of folders to handle files from. Required parameter.

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        [StaticFiles, { folders: ['/var/www/public','/opt/public'] }]
      ]]
    ]);
  }
```

## Creating Your Own Middlewares (or Integrating External Ones)

You can create your own middleware. To do that, you should extend `AbstractMiddleware` and provide at least two of your own functions: `description` and `middleware`. Please check the code below.

```js
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class CustomMiddleware extends AbstractMiddleware {
  static get description() {
    return "Middleware description";
  }

  // optional
  static get usedAuthParameters() {
    // Array of parameters that are used for authorization within the middleware
    return [
      {
        name: "Authorization", // name of the parameter
        type: "apiKey", // apiKey, http, oauth2, openIdConnect
        in: "header", // header, query, cookie
        description: this?.description,
      },
    ];
  }

  // optional
  get relatedQueryParameters() {
    // A Standard Schema-conformant schema (Yup, Zod, Valibot, ArkType, ...) that defines middleware-related `req.query` parameters. It allows you to validate and get those parameters in `req.appInfo.query` relative to the route in which the middleware is declared.
    return yup.object().shape({
      limit: yup.number(), // For example
    });
  }

  // optional
  get relatedRequestParameters() {
    // A Standard Schema-conformant schema that defines middleware-related `req.body` parameters. It allows you to validate and get those parameters in `req.appInfo.request` relative to the route in which the middleware is declared.
    return yup.object().shape({
      name: yup.string().required(), // For example
    });
  }

  async middleware(req, res, next) {
    // check something
    if (!req.body.yyyyy) {
      //  return and stop processing
      return res.status(400).json({});
    }
    if (this.params.iiii) {
      // we can also check all the params that we passed during init
    }
    // go to the next one
    return next();
  }
}

export default CustomMiddleware;
```


---


# Document 10: 07-logging

<!-- Source: 07-logging.md -->

# Logging

The framework uses the [Winston](https://github.com/winstonjs/winston) logger as the main subsystem for logging.

You can adjust different transports (console and Sentry are available by default) and add your own transport with your own parameters.

The framework provides a basic initialization of Winston so you can easily use it out of the box, but you still have the ability to adjust it as you want.

## Logging Levels

[RFC5424](https://datatracker.ietf.org/doc/html/rfc5424) defines logger levels. Higher levels will include messages from lower levels. In other words, if you set the ‘warn’ level, the logger will report ‘error’ as well, but will not report the ‘debug’ level.

You can read more about levels in the [Winston levels documentation](https://github.com/winstonjs/winston#logging-levels).

But in short, these are the default levels:

```js
{
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6
}
```

## API

Each class has access to the logger instance via:

```js
this.logger;
```

```js
this.logger.error("error message");
this.logger.warn("warn message");
this.logger.info("info message");
this.logger.http("http message");
this.logger.verbose("verbose message");
this.logger.debug("debug message");
this.logger.silly("silly message");
```

:::tip
Please note that `this.logger` is an instance of `winston.logger`, and you can use any methods from it.
:::

## Default Transports

By default, the framework provides two transports: **console** and **Sentry**

## Configuration

We try to keep the configuration simple and powerful - you are able to enable/disable loggers and add your own with all available options passed to the transport.

Configuration files are located in ‘config/log.js’.

```js
export default {
  transports: [
    {
      transport: "sentry", // transport name (specail name or npm package name)
      transportOptions: {
        // options that will be passed to the transport instance
        level: process.env.LOGGER_SENTRY_LEVEL || "info",
      },
      enable: process.env.LOGGER_SENTRY_ENABLE || false, // whether the transport is enabled or not
    },
    {
      // ....
    },
    /// more transports
  ],
};
```

The config contains a “transports” array. Each transport can be included in the logger.

The transport **name** is an npm package name that the framework will require. You can use any transport from NPM.

The **transportOptions** field contains the transport options - you can pass any options here for the transport. Please refer to the transport documentation.

And finally, the **enable** field will enable/disable modules for the logger. You can check [“NODE_ENV” in the config documentation](02-configs.md#node_env) to learn more about how you can use it depending on your environment.

## Sentry transport 

For Sentry, we assume it is already configured in the project and we will reuse the existing setup. If Sentry is not present in the system, the framework will display a message indicating that this feature is only available when Sentry is configured.

## Add Your Own Transport

Adding your own transport is a simple process.

```js
npm i ${WINSTON_TRANSPORT_PACKAGE}
```

Then add it to the ‘config/log.ts’ config file.

```js
export default {
  transports: [
    {
      // .....
      // some already existing transports
    },
    {
      // ....
    },
    {
      transport: "WINSTON_TRANSPORT_PACKAGE",
      transportOptions: {
        // options that will be passed to the transport instance
        // .... your transport options
      },
      enable: true,
    },
  ],
};
```

:::tip

Feel free to use environment variables in your transport config as well. That simplifies working with multiple environments.

:::

## Console Output for Logger

Console loggers act in a customized way to provide more verbose info. The default console output is constructed as:

`(${process.pid}) ${info.label}  ${info.timestamp}  ${info.level} : ${info.message}`

Where:
“process.pid” - the PID of the Node process. Useful in a cluster environment.
“info.label” - the label of the place. More info below.
“info.timestamp” - the date of the event.
“info.level” - the log level (“error”, ”warn”, etc.).
“info.message” - the message that was passed to the logger (e.g., `this.logger.info(“this is a message”)`).

### info.label

Inside the base class, we have a method [“loggerGroup”](04-base.md#api) that is used as the first part of the info message generation. This is useful for grouping messages like “controllers”, ”models”, etc.

The framework uses the following groups: “command”, “connector”, “controller”, “model”, and “CLI\_”.

:::tip
Feel free to introduce your own groups.
:::

The second part is generated by the base class function “getConstructorName”, which by default grabs the constructor name and adds it to the info string. You can overwrite this method for your classes.

Example:

```
(15950)  [modelCoin]  2021-10-18T11:17:54.746Z  verbose : Model have no hooks
```

Here:

```
(15950) - process PID
[modelCoin] -  info message ("model" - group, "Coin" - model name)
2021-10-18T11:17:54.746Z - time
verbose - level
Model have no hooks - message
```

### Environment Variables

#### Sentry Transport

**LOGGER_SENTRY_DSN** - Sentry DSN.

**LOGGER_SENTRY_LEVEL** - the log level that should go into Sentry. Default: 'info'.

**LOGGER_SENTRY_ENABLE** - enable or disable the Sentry logger. Default: 'false'.

#### Console Transport

**LOGGER_CONSOLE_LEVEL** - the log level. Default: 'silly' (includes all).

**LOGGER_CONSOLE_ENABLE** - enable or disable the console logger. Default: 'true'.


---


# Document 11: 08-i18n

<!-- Source: 08-i18n.md -->

# i18n

No modern app can avoid multi-language support. The framework supports internationalization out of the box.

All internationalization is based on the [i18next](https://www.i18next.com/) library.

### Middlewares 

Framework provides I18n middleware that uses for each http requuest.

## Detectors

As we are talking about languages, we need some way for the codebase to understand what language the user should use.

This feature is called detectors.

Order of detection:

- X-Lang header
- Query
- User

### X-Lang Header Detector

This detector will parse the “X-Lang” header on the request to detect the user's language. The frontend should add the user's language here (“en”, ”fr”, etc.), and if the backend supports it, the app will use the given user language.

:::tip
“xLang” is the preferred way to work with languages in the framework.
:::

Example for the frontend with Fetch:

```js
fetch("https://example.com/getSomething", {
  headers: {
    "X-Lang": "en", // Added language
  },
});
```
### Query Detector

The query is a simple detector. Just add the 'lookupQuerystring' parameter to your query string. 'lookupQuerystring' by default is **lng**, but you can change it as you want inside the config file.

```js
const res = await fetch("https://someUrl.com?lng=en");
```

### User Detector

The user detector tries to find an authorized user (provided by middleware) and grab the ‘locale’ field from this user.

### Adding Your Own Detector

At this time, you are not able to add your own detector. Please contact us if you need that option, and we will be happy to help you.

## Configuration

Please look at the ‘config/i18n.js’ file for all configuration options. There are a limited number of options available.

## Language Files

All files in JSON format are located in ‘src/locales/\{localeCode\}/translations.json’.

:::note

Backends besides files are not supported.

:::

You can find detailed documentation about JSON files in the [i18next JSON documentation](https://www.i18next.com/misc/json-format).

## API

The framework provides easy integration for controllers. You can grab the i18n instance with:

```js
req.appInfo.i18n;
req.appInfo.i18n.language; // current language
req.appInfo.i18n.t("some.phrase"); // translate some phrase https://www.i18next.com/overview/api#t
```

:::tip

You can pass controller `request` / `query` error messages as i18n keys (or plain strings). The framework processes them with i18next before sending the response, regardless of which validator library produced the error — Yup, Zod, Valibot, ArkType, or a custom Standard Schema validator.

```js
class SomeController extends AbstractController {
  get routes() {
    return {
      post: {
        "/login": {
          handler: this.postLogin,
          request: yup.object().shape({
            email: yup.string().email().required("auth.emailProvided"), // <-- look here i18n
          }),
        },
      },
    };
  }
}
```

### Interpolation

Validators that produce parameters (yup's `min` / `max` / `length`, etc.) forward those parameters to i18next, so locale strings can use `{{placeholder}}` syntax:

```js
// schema
request: yup.object().shape({
  password: yup.string().min(8, "auth.passwordTooShort").required(),
});

// locales/en/translation.json
// "passwordTooShort": "Password must be at least {{min}} characters"

// response when password is too short:
// { "errors": { "password": ["Password must be at least 8 characters"] } }
```

:::


## Direct usage. 

Sometimes you may want to use i18n outside of HTTP requests (such as for emails, WebSockets, etc.). For that purpose, frameworks provide an easy way to interact with i18n using the same configuration (translations, languages, etc.).

App instanse ptovides lang services for you

```ts
import { appInstance } from '@adaptivestone/framework/helpers/appInstance.js';

const i18nService = await appInstance.getI18nService();
const i18n = await i18nService.getI18nForLang(lang);
```

and this is preconfigured i18next instance for you.

### Validation outside HTTP

`ValidateService.validate` accepts an optional `i18n` argument. Pass it to get the same auto-translation the HTTP path provides; omit it to receive raw keys (useful in workers / RPC where you'd rather forward structured errors than translated text).

```ts
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
import ValidateService from "@adaptivestone/framework/services/validate/ValidateService.js";
import { ValidationError } from "@adaptivestone/framework/services/validate/ValidationError.js";

async function processQueueMessage(payload, schema) {
  const i18nService = await appInstance.getI18nService();
  const i18n = await i18nService.getI18nForLang("en");

  try {
    return await new ValidateService(appInstance, schema).validate(payload, i18n);
  } catch (err) {
    if (err instanceof ValidationError) {
      // err.message → translated wire-shape; err.issues → structured
      logger.error({ issues: err.issues }, "queue payload invalid");
    }
    throw err;
  }
}
```


---


# Document 12: 09-testsing

<!-- Source: 09-testsing.md -->

# Testing

The framework comes with [Vitest](https://vitest.dev/) support. When you name files with the `.test.(js|ts)` extension, they will be added to the tests.

:::tip
Please put test files near the main files that you are testing and give them the same name.
If you want to test “Auth.js”, please create a file named “Auth.test.js” and put it in the same folder.

:::

## Framework (app) Instance

Of course, inside a test, you need to have access to the framework instance. It will be available via appInstanceHelper

```js
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
```

## Run Tests

```bash
npm test
```

## Before Scripts

The test entry point is at the project level in ‘src/test/setupVite.js’. This file prepares all folder configs, requires the framework setup, and prepares the global setup for tests.

The minimum Vite config file should contain:

```js
  test: {
    globalSetup: [ // This script will start Mongo (DIST in important there)
      'node_modules/@adaptivestone/framework/dist/tests/globalSetupVitest',
    ],
    setupFiles: [
      './src/tests/setup.ts', // this is a config files with directory location
      '@adaptivestone/framework/tests/setupVitest', // This is the entry point for testing from the  framework
      './src/tests/setupHook.ts', // This is a local config file (see below)
    ],
  }
```

### Global setup (once per test running)

You are able to provide additional setup and teardown functions to global setup.
Just add your implementation in an additional (or instead of) globalSetup

```js
  test: {
    globalSetup: [
      'node_modules/@adaptivestone/framework/dist/tests/globalSetupVitest',
      './src/tests/globalSetup.ts', // custom file for global setup and teardown
    ],
    // ...
  }
```

### Custom hooks (beforeAll, etc) per test

Testing helpers provide isolation of modules, and we run preparation of the framework and teardown for each test as well.

```js
  test: {
    //...
    setupFiles: [
      './src/tests/setup.ts', // this is a config files with directory location
      '@adaptivestone/framework/tests/setupVitest', // This is the entry point for testing from the  framework
      './src/tests/setupHooks.ts', // <-- we are able to provide custom logic there
    ],
  }
```

You can provide any amount of testing configs

Example:

```ts ./src/tests/setupHooks.ts
import { beforeAll, afterAll, beforeEach, afterEach } from "vitest";
import { createDefaultTestUser } from "./testHelpers.ts";

beforeAll(async () => {
  await createDefaultTestUser();
});

afterAll(async () => {
  // do something
});

beforeEach(async () => {
  // do something
});

afterEach(async () => {
  // do something
});
```

### Default User for Testing

You are able to call the creation of a default user. The user is not created by default. You should call it manually.

```js
import {
  defaultUser, // instance of user if default user was created
  defaultAuthToken, // default token for auth if user was created
  createDefaultTestUser, // create default user and populate defaultUser and defaultAuthToken.
} from "@adaptivestone/framework/tests/testHelpers.js";

const { user, token } = await createDefaultTestUser();
// defaultUser - same user
// defaultAuthToken - same token
```

## Mongo Instance

As the framework is designed to work with MongoDB and provide easy integration with it, it also comes with MongoDB integration in tests.

The integration is done with the help of the [MongoDbMemoryServer](https://github.com/nodkz/mongodb-memory-server) package.

By default, the framework starts the Mongo memory server and stops it afterward. So you can use Mongo during your tests.

### Mongo Tests on ARM64 Machines (Docker)

For ARM64, we have an interesting situation. Mongo Inc. provides binaries for Ubuntu but not for Debian, but official Node images exist for Debian but not for Ubuntu.

To solve this situation, we provide our own Node Docker image based on Ubuntu. You can find it here: [ubuntu-node-docker](https://gitlab.com/adaptivestone/ubuntu-node).

## Running Tests in CI (GitLab)

An important thing about testing is that tests should be executed automatically on every Git commit. That is where CI (Continuous Integration) comes in.

A `.gitlab-ci.yml` sample is below:

```yaml
stages:
  - install
  - checks

install:
  stage: install
  image: registry.gitlab.com/adaptivestone/ubuntu-node:latest
  script:
    - node -v
    - npm install
  artifacts:
    paths:
      - node_modules/
    expire_in: 2 hour

codestyle:
  stage: checks
  image: registry.gitlab.com/adaptivestone/ubuntu-node:latest
  needs:
    - install
  dependencies:
    - install
  allow_failure: true
  script:
    - npm run codestyle

tests:
  stage: checks
  image: registry.gitlab.com/adaptivestone/ubuntu-node:latest
  needs:
    - install
  dependencies:
    - install
  script:
    - npm run test
```

## Running Tests in CI (GitHub)

It is better to look at the [repo](https://github.com/adaptivestone/framework/blob/main/.github/workflows/test.yml).

```yml
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: Test

on:
  push:
    branches: ["*"]

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    services:
      redis:
        image: redis:latest
        ports:
          - 6379:6379

    env:
      LOGGER_CONSOLE_LEVEL: "error"
      REDIS_URI: redis://localhost

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: "latest"
          cache: "npm"

      - name: npm clean install
        run: npm ci

      - name: Run Test
        run: npm test

      - name: Upload results to Codecov
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
```

## Server instance access

It's possible that in testing you will need to have low-level access to the server itself. We have a helper there too.

```js
import { serverInstance } from "@adaptivestone/framework/tests/testHelpers.ts";
```

## Test-only controllers

When testing edge cases — broken handlers, unusual middleware combinations, schemas designed to fail — you don't have to put fixture controllers in your production controllers folder. Register them explicitly via `app.controllerManager.registerController(ControllerClass, prefix?)`.

The framework's `Server.startServer()` accepts a `callbackBefore404` hook that runs after the controller manager initializes the auto-loaded controllers but before the 404 handler is attached, so explicitly registered controllers mount in the right order.

```ts ./src/tests/setupHooks.ts
import { beforeAll } from "vitest";
import { serverInstance } from "@adaptivestone/framework/tests/testHelpers.js";
import BrokenController from "./fixtures/BrokenController.ts";
import FakeAuthMiddleware from "./fixtures/FakeAuthMiddleware.ts";

// If you control startServer yourself, register inside callbackBefore404:
//   await server.startServer(async () => {
//     server.app.controllerManager?.registerController(BrokenController, "broken");
//   });
//
// If you use the framework's standard test setup, register in beforeAll —
// late registration still works for tests because the test's HTTP requests
// only fire AFTER setup completes (so the 404 handler ordering is irrelevant
// in practice; the controller's routes are reachable on Express).
beforeAll(() => {
  serverInstance.app.controllerManager?.registerController(BrokenController, "broken");
});
```

`prefix` is the URL prefix segment — `registerController(BrokenController, "broken")` mounts on `/broken/brokencontroller/*`. Pass `''` (or omit) to mount at `/<classname>/`.

This pattern keeps fixture controllers out of your production controllers folder and gives each test full control over which controllers exist for it.

## HTTP Endpoint Testing

The framework provides a special function `getTestServerURL` to help you construct a full URL for testing.

```js
import { getTestServerURL } from "@adaptivestone/framework/tests/testHelpers.js";
const url = getTestServerURL("/auth");
```

Full example:

```js
import {
  getTestServerURL,
  defaultAuthToken,
} from "@adaptivestone/framework/tests/testHelpers.ts";

describe("module", () => {
  describe("function", () => {
    it("test", async () => {
      expect.assertions(1);
      const { status } = await fetch(getTestServerURL("/some/endpoint"), {
        method: "POST",
        headers: {
          "Content-type": "application/json",
          Authorization: defaultAuthToken,
        },
        body: JSON.stringify({
          // request object
          oneData: 1,
          secondDate: 2,
        }),
      }).catch(() => {});
      expect(status).toBe(400);
    });
  });
});
```

## Test Helpers

The framework provides a set of helpers to simplify testing.

```js
import {
  getTestServerURL, // return server URL for testing  getTestServerURL('auth');
  defaultUser, // instance of user if default user was created
  defaultAuthToken, // default token for auth if user was created
  serverInstance, // server instance for low level interaction
  createDefaultTestUser, // create default user and populate defaultUser and defaultAuthToken.
  setDefaultUser, // in case you want to have own user implementation setDefaultUser(yourUser)
  setDefaultAuthToken, // in case you want to have own user implementation setDefaultAuthToken(token)
} from "@adaptivestone/framework/tests/testHelpers.js";
```

## Mock

In most cases, your code depends on external services, but you still need to perform testing. Calling an external service for each test can be expensive and is not necessary. For this problem, Vitest provides mock options. This is when, instead of calling the real SDK of a service, you call a fake function that provides the result without API calls.

[https://vitest.dev/api/vi.html#vi-mock](https://vitest.dev/api/vi.html#vi-mock)

### Mocking a Function

[https://vitest.dev/api/vi.html#mocking-functions-and-objects](https://vitest.dev/api/vi.html#mocking-functions-and-objects)

You can redefine an import for your own import.

```js
vi.doMock("../file.js", () => ({
  fileFunction: async () => ({
    isSuccess: true,
  }),
}));
```

Redefine one method in a file:

```javascript
import S3 from "../S3.js";
vi.spyOn(S3, "validateCreds").mockImplementation(() => true);
```

There are many more mocking options. Please refer to the Vitest documentation for others.

{/*
Manual mocks are defined by writing a module in a **mocks** subdirectory immediately adjacent to the module. For example, to mock a module called user in the models directory, create a file called user.js and put it in the models/**mocks** directory. Note that the **mocks** folder is case-sensitive, so naming the directory **MOCKS** will break on some systems.

:::note
You should call moch load function before performing any operation on it

```js
jest.mock("path");
jest.createMockFromModule("module");
```

:::

### @google-cloud/translate example (NODE_MODULES)

Assume that we have some translation helper (synthetic example) that just does a translation and registers it in the database to speed it up for the next time.

/src/helpers/translateHelper.js

```js
import { v2 } from "@google-cloud/translate";
const translate = new v2.Translate();

// no error handling because it's an example. You should handle errors in production mode
// no model passing
const translateHelper = async (text, language) => {
  const alreadyTranslatedData = await TranslatedModel.find({ text, language });
  if (alreadyTranslatedData) {
    return alreadyTranslatedData.translatedText;
  }

  const translated = await translate.translate(text, language);
  const data = await TranslatedModel.create({
    text,
    language,
    translatedText: translated[0],
  });
  return translated[0];
};

export default translateHelper;
```

Right now you want to test that the function works correctly. So as @google-cloud/translate.js is a node_modules module, we are creating a file:

```js
/__mocks__/@google-cloud/translate.js
```

The file extends the original Google Translate and overwrites some functions to avoid API calls.

```js /__mocks__/@google-cloud/translate.js
const googleTranslate = jest.genMockFromModule("@google-cloud/translate");

class Translate {
  translate(text, lang) {
    return [`${text}_${lang}`, "this is test"];
  }
}

googleTranslate.v2.Translate = Translate;
export default googleTranslate;
```

Now inside your helper test file:

```js
jest.mock('@google-cloud/translate');

import translateHelper from '/src/helpers/translateHelper.js';


describe('mock testing', () => {
  it('should return translated text', async () => {
    expect.assertions(1);
    const translated = await translateHelper("text","fr");
    expect(translated).toBe("text_fr");
  })

  it('should store text in the database', async () => {
    expect.assertions(1);
    const translated = await TranslatedModel.find({text:"text", language:"fr"});
    expect(translated.translatedText).toBe("text_fr");
  })
})


``` 
*/}


---


# Document 13: 10-cli

<!-- Source: 10-cli.md -->

# CLI

The CLI (Command Line Interface) is a part of the framework system that allows you to use the full power of the framework on the command line - manipulate data, import, export, etc.

The framework scans the directory for a list of commands and provides you with the ability to see what commands are available to run.

:::note

CLI commands are part of the [framework inheritance process](03-files-inheritance.md).

:::

## Run Command

You are able to create a group of commands by putting files in a directory. You can see ‘migration’ as an example of grouping commands.

```bash
# On the project level
node src/cli.ts {command}

# OR
npm run cli
```

The command path works the same way as in controllers. Any file will be parsed as a command, and folders will group commands together.
But unlike at the controller level, you are not able to change that behavior.

You are able to create a group of commands by putting files in a directory. You can see ‘migration’ as an example of grouping commands.

```js
commands / migration / create.ts; // migration/create command
commands / migration / migrate.ts; // migration/migrate command
```

## API

You are able to run a command from any place in the framework.

```js
this.app.runCliCommand(commandName: string, args: {}): Promise;
```

Where:
`commandName` - the name of the command that you want to start.
`args` - the arguments that you want to pass to the command.

## Creating Your Own Command

All passed arguments on the command line are parsed with the help of the `parseArgs` module.

```ts
import AbstractCommand from "@adaptivestone/framework/modules/AbstractCommand.js";
import type { CommandArgumentToTypes } from "@adaptivestone/framework/modules/AbstractCommand.js";

class CommandName extends AbstractCommand {
  static get description() {
    return "Some nice description of the command";
  }

  /**
   * You are able to add command arguments for parsing here.
   * https://nodejs.org/api/util.html#utilparseargsconfig
   */
  static get commandArguments() {
    return {
      id: {
        type: "string",
        description: "User ID to find the user",
      },
      email: {
        type: "string",
        description: "User email to find/create the user",
      },
      password: {
        type: "string",
        description: "New password for the user",
      },
      roles: {
        type: "string",
        required: true, // make sure that command will ask this from user
        description:
          "User roles as a comma-separated string (--roles=user,admin,someOtherRoles)",
      },
      update: {
        type: "boolean",
        default: false,
        description: "Update the user if they exist",
      },
    } as const; // <- this is important for type generation
  }

  static isShouldInitModels = true; // Default value. Can be omitted.

  /**
   * If true, then this command will get model paths with inheritance.
   */
  static isShouldGetModelPaths = true;

  /**
   * Return the name of the connection that you want to use.
   */
  static getMongoConnectionName(commandName, commandArguments) {
    return `CLI: ${commandName} ${JSON.stringify(args)}`;
  }

  async run() {
    // CommandArgumentToTypes will generate types from  commandArguments description
    const { id, email, password, roles, update } = this
      .args as CommandArgumentToTypes<typeof CommandName.commandArguments>;

    return new Promise((resolve, reject) => {});
  }
}

export default CommandName;
```

:::tip

For boolean types, we also support negative values (without a prefix).

```js
      update: {
        type: "boolean",
      },
```
```bash
node src/cli.ts ourCommand --update
```
Update Argument will by true

```bash
node src/cli.ts ourCommand --no-update
```
update argumant will be false 

:::

## Framework Commands

The framework comes with a few built-in commands.

### Migration

Migration commands allow you to migrate some data to another, or fill the database with some data.

The key point here is that a migration is executed only once per file.

:::tip

You can use migrations for different cases, not only to modify data in the database.

:::

#### Creating a Migration

The migration command comes with a template to generate a migration.

```js
node src/cli.ts migration/create --name={someName}
```

After creating the migration, please edit it and implement any logic that you want here.

#### Applying a Migration

```js
node src/cli.ts migration/migrate
```

A migration is executed in the order it was created. It is executed only once per life.

### DropIndex

Sometimes it is very useful to drop indexes on already created models. For example, you created a unique index for non-required fields and missed that `null` values also should be unique per collection.

The framework will take care of creating indexes based on your model on the next start, so you can drop an index and be sure that it will be recreated after.

#### Run dropindex

```js
node src/cli.ts dropindex --model={modelName}
```

### SyncIndexes

Synchronizes the indexes defined in the models with the real ones indexed in the database. The command will remove all indexes from the database that do not exist in the model OR have different parameters. Then it will create new indexes.

:::warning

This can be a dangerous command in case you have some unique index features.

:::

#### Run SyncIndexes

```js
node src/cli.ts SyncIndexes
```

### CreateUser

The `createuser` command creates a new user.

#### Run CreateUser

```js
node src/cli.ts createuser --email=somemail@gmail.com --password=somePassword --roles=user,admin,someOtherRoles
```

Only email and password are required.

You are able to update a user as well. You need to specify the email or user ID to find the user and the '--update' flag to allow user updates.

### Generate Random Bytes

In some cases, you need a random byte string. This command helps you to generate a random byte string.

#### Run Generate Random Bytes

```js
node src/cli.ts generateRandomBytes
```

or

```js
npm run cli generateRandomBytes
```

### Generate TypeScript Types

Generates two kinds of TS source from the framework's introspection:

1. **`genTypes.d.ts`** at the project root — augments `IApp` so `getConfig('foo')` and `getModel('Bar')` are typed.
2. **`<File>.routes.gen.ts`** next to every controller — typed `<MethodName>Request` aliases for handler signatures (per-route schema output, middleware-provided `appInfo` fields, etc.). The middleware chain comes from `RouteRegistry.flatten()` — same matcher the runtime uses, so types match runtime behavior. See [Routes → Typed handler signatures (codegen)](06-Controllers/02-routes.md#typed-handler-signatures-codegen) for usage.

#### Run Generate TypeScript Types

```bash
node src/cli.ts generatetypes
# or
npm run cli generatetypes
```

#### Recommended setup

Add to `package.json`:

```json
"gen": "node cliCommand.ts generatetypes",
"check:types": "npm run gen && tsc --noEmit"
```

Add to `.gitignore`:

```
genTypes.d.ts
**/*.routes.gen.ts
```

Gen files regenerate on every type-check — no postinstall hook needed.


---


# Document 14: 11-cache

<!-- Source: 11-cache.md -->

# Cache

The cache subsystem is designed to store some values for quick access and a unified interface. It is useful when you have some values grabbed from an external API or some stuff that requires a lot of calculation and does not change from time to time.

Caches have an expiration time, and the developer should not worry about checking it. If a value has expired or does not exist, a callback will be executed, and its return value will be used as the value to store in the cache.

:::note

The cache subsystem handles all values and takes care of serialization/deserialization.

:::

## API

The API is simple:

```ts
  async getSetValue(
    key: string,
    onNotFound: () => Promise<any>,
    storeTime: number, // in seconds
  ): Promise<any>;
```

By default, the store time is 5 minutes.

Example:

```javascript
const cacheTime = 60 * 5; // seconds
const someValueFromCache = await this.app.cache.getSetValue(
  "someKey",
  async () => {
    const someValue = await someLongAsyncOperation();
    return someValue;
  },
  cacheTime // in seconds
);
```

:::note

You can request the same value multiple times, and only one callback will be executed. All other calls will be resolved as a Promise (the same promise).

```js
const promiseArr = [];
for (let n = 0; n < 100; n++) {
  promiseArr.push(
    this.app.cache.getSetValue(
      "someKey",
      async () => {
        // Will be called once! Other calls will find that "someKey" is already processing and return the same Promise.
        const someValue = await someLongAsyncOperation();
        return someValue;
      },
      3600
    )
  );
}
```

Please note that it works that way **per process**, as checking promises happens at the process level and is not synchronized via a master process.

:::

## Configuration

For now, the cache subsystem has no configuration. But please follow the Redis configuration, as the cache depends on it.


---


# Document 15: 12-email

<!-- Source: 12-email.md -->

# Sending Emails

:::warning

As of framework version 5.0, we have moved the email module to a separate package: [@adaptivestone/framework-module-email](https://www.npmjs.com/package/@adaptivestone/framework-module-email).

:::

The email subsystem is based on [Nodemailer](https://github.com/nodemailer/nodemailer). In addition, we are using [Juice](https://www.npmjs.com/package/juice) to inline CSS and [html-to-text](https://www.npmjs.com/package/html-to-text) to generate text from the HTML of files.

:::note

Sadly, email clients are outdated and do not support a lot of web features. Some clients do not even support “style” tags. That is why all styles should be inlined.

:::

## Installation

```bash
npm i @adaptivestone/framework-module-email
```

## Templates

We support two types of templates: HTML files (not templates) or Pug templates. It’s easy to add your own template language too.

For each email, you should provide an HTML version, a text version (optional), and a subject. These should be provided as separate files inside the template directory.

If the text version of the email is not provided, it will be generated from the HTML version by removing all HTML tags with the help of the [html-to-text](https://www.npmjs.com/package/html-to-text) package.

The template directory is located at `src/services/messaging/email/templates/{templateName}` in your project. You can change it in the config.

Example of a folder:

```js
html.pug; // HTML markup of the email
subject.pug; // Subject to generate
text.pug; // Text version of the email
style.css; // Styles to inline inside the HTML
```

### Inline Images

By default, the framework email module does not inline images and keeps the links as they are.
But if you want to inline some images, you can use the "data-inline" attribute in the "img" tag.

```html
<img src="/cats.jpg" data-inline />
```

The image path is relative to your project's "src/services/messaging/email/resources" folder.

:::note
The best practice is to put your images on a CDN.
:::

### Template Variables

Each template has these variables:

- `locale` - the current locale of the request.
- `t` - the translate function from i18n. Can be a dummy function if i18n is not provided.
- `globalVariablesToTemplates` - from the config.
- User-provided variables (see the API section).

## API

### Init Mailer

```js
import Mailer from "@adaptivestone/framework-module-email";

const mail = new Mailer(
  this.app,
  "recovery", // template name
  {
    // variables for the template. These are user-provided variables. They will be merged with the default variables.
    oneTemplateVariable: "1",
    anotherTemplateVariable: "2",
  },
  req.appInfo.i18n
);
```

Inside the template, `oneTemplateVariable` and `anotherTemplateVariable` will be available as top-level variables.

```pug
p #{oneTemplateVariable} #{anotherTemplateVariable}
```

### Send Email

```js
const result = await mail.send(
  "some@email.com", // To
  "optional@from.com", // OPTIONAL. From email. If not provided, it will be grabbed from the config.
  {} // OPTIONAL. Any additional options for Nodemailer: https://nodemailer.com/message/
);
```

### Send Raw

For advanced usage (your own templates, mail headers, attachments), another low-level method exists.

```js
import Mailer from "@adaptivestone/framework-module-email";

const result = await Mailer.sendRaw(
  this.app, // framework app
  "to@email.com", // To
  "email subject", // topic
  "<html><body><h1>Email html body</h1></body></html>", // HTML body of the email
  "Email text body", // OPTIONAL. If not provided, it will be generated from the HTML string.
  "from@email.com", // OPTIONAL. From email. If not provided, it will be grabbed from the config.
  {} // OPTIONAL. Any additional options for Nodemailer: https://nodemailer.com/message/
);
```

### Render Template

In some cases, you may want to render templates to a string for future usage. For example, to send an email via Gmail OAuth2 authorization on behalf of a user.

```js
const { subject, text, inlinedHTML, htmlRaw } = await mail.renderTemplate();
```

## Configuration

Please look at the ‘config/mail.ts’ file for all configuration options.

### Environment Variables

Here are the most important environment variables:

```js
EMAIL_HOST; // smtp.mailtrap.io by default
EMAIL_PORT; // 2525 by default
EMAIL_USER;
EMAIL_PASSWORD;
EMAIL_TRANSPORT; // smtp by default


---


# Document 16: 13-deploy

<!-- Source: 13-deploy.md -->

# Deploy

There are multiple ways to deploy your app.

## On a Server

This is the simplest way. There are multiple options here, but we are recommending a PM2 + Nginx setup.

### PM2 Setup

You need to install PM2 and make it autoload.

```bash
npm install pm2@latest -g
pm2 startup
```

Then load your code to the server and go to the server directory.

```bash
NODE_ENV=production pm2 start --name YOUR_APP_NAME src/index.js
pm2 save
```

That is all from the Node.js side. The app is already there and listening on localhost:3300.

### Nginx Setup

```
server {

	root /var/www/YOUR_FOLDER/src/public;

	server_name YOUR_SERVER_NAME;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ @backend;
	}

	location @backend {
		proxy_pass http://localhost:3300;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_set_header  X-Forwarded-For $remote_addr;
		proxy_cache_bypass $http_upgrade;
	}

}
```

## Docker

You can create a Docker image with all your data and run it on any server, including Kubernetes.

The Dockerfile can look like this:

```dockerfile
FROM node:latest
RUN mkdir -p /opt/app && chown -R node:node /opt/app
WORKDIR /opt/app
COPY --chown=node:node package.json package-lock.json ./
USER node
RUN npm ci
COPY --chown=node:node src/ ./src/
EXPOSE 3300
CMD [ "node", "src/index.ts"]
```

To build it, use:

```bash
docker build --platform --platform linux/amd64,linux/arm64 -t YOUR_REPO_NAME:TAG .
```

You can also use `buildx`:

```bash
docker buildx build --platform linux/amd64,linux/arm64 -t YOUR_REPO_NAME:TAG . --push
```

Then you can run it:

```bash
docker run -it -p 3300:3300 YOUR_REPO_NAME:TAG
```


---


# Document 17: 14-helpers

<!-- Source: 14-helpers.md -->

# Helpers

The framework provides some helpers to make your code easier to work with.

## App instance

You can access the app instance from anywhere.

```ts
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
```

The app instance is the core of the framework, allowing you to retrieve models, configurations, and more.

```ts
const Model = appInstance.getModel("ModelName");
const s3config = appInstance.getConfig("s3");
// etc
```

## Redis connection

A simplified way to connect to Redis. This helper loads the configuration and adds shutdown hooks.

```ts
import { getRedisClient, getRedisClientSync } from '@adaptivestone/framework/helpers/redis/redisConnection.ts';
const redisClient = await getRedisClient();
const redisClientSync = await getRedisClientSync();
```

The only difference is that `getRedisClientSync` returns the Redis client immediately and establishes the connection in the background.


---


<!-- End of Documentation -->
