# Adaptivestone Framework - Complete Documentation

> This file is auto-generated from the Docusaurus documentation.
> Generated on: 2026-08-08T22:52:05.958Z
> Total documents: 22

---

## 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. 06-Controllers / 04-error-handling
11. 07-logging
12. 08-i18n
13. 09-testsing
14. 10-cli
15. 11-cache
16. 12-email
17. 12-resize
18. 13-deploy
19. 14-helpers
20. 15-recipes
21. 16-anti-patterns
22. 17-openapi

---



# 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 (modern 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 only (no CommonJS)
- TypeScript support (you are able to write everything in JavaScript, but in JS you will have types as a bonus)

:::info Requirements
The framework requires **MongoDB** and an **`AUTH_SALT`** secret — it fails fast at boot if either is missing (set the `MONGO_DSN` env var; generate a salt with `npm run cli generateRandomBytes`). The runtime requires **Node ≥ 24**.
:::

## 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
├─ Cli.ts // Main CLI class
├─ index.ts // Main entry point (creates and starts the Server)
├─ cluster.ts  // Entry point for production (cluster module)
├─ folderConfig.ts // Folder configuration
├─ server.ts // Server class
```

## 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@github.com:adaptivestone/framework-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.

## Guides

- [Recipes](15-recipes.md) — a task-oriented cookbook: add a controller, validate a body, paginate, write middleware, override a built-in, test a controller.
- [Anti-patterns](16-anti-patterns.md) — common mistakes and what to do instead.


---


# 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): Record<string, unknown>;
updateConfig(configName: string, config: Record<string, unknown>): Record<string, unknown>;
```

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,
});
```

## Typed config

The base `getConfig(name)` signature is `Record<string, unknown>`, but the
framework's [type generation](10-cli.md) (`node src/cli.ts generatetypes`) emits
a `genTypes.d.ts` that types each call against your **actual** config file — so
`this.app.getConfig("http").port` is precise, with no cast.

The type is derived from the config's runtime **shape**, not its values, so a
secret value is never written into the generated file. A key read straight from
the environment with **no default** is a special case: at generation time
`process.env.X` may be unset, so the framework reads it from the config
**source** and types it honestly as `string | undefined`.

```ts title="/src/config/hubspot.ts"
export default {
  apiKey: process.env.HUBSPOT_API_KEY, // typed: string | undefined
  region: process.env.HUBSPOT_REGION ?? "eu", // has a default → typed: string
  baseUrl: "https://api.hubspot.com", // literal → typed: string
};
```

```ts
const { apiKey } = this.app.getConfig("hubspot");
// apiKey is `string | undefined` — guard it; no `as` cast needed
if (!apiKey) {
  throw new Error("HUBSPOT_API_KEY is not set");
}
```

:::tip

If a value is guaranteed present (you assert it at boot), add a non-null
assertion in the config so the generated type is `string`:

```ts
apiKey: process.env.HUBSPOT_API_KEY!, // typed: string
```

:::

:::note

Recovery follows only inline `process.env.X` reads in a config's own
default-exported object. A key whose env read is spread in from another config
(`{ ...baseConfig }`) or hidden behind an indirection isn't followed across that
boundary. Regenerate after changing a config's shape; `generatetypes --check`
fails CI if the committed types are stale.

:::

### Generated array and object shapes

Codegen widens a homogeneous array to a reusable array type. Its type therefore
describes the kind of entries the config contains without freezing the number
of entries that happened to be loaded during generation:

```ts title="/src/config/assets.ts"
export default {
  origins: ["https://one.example", "https://two.example"],
  dimensions: [
    [300, 300],
    [1080, 1080],
  ],
  endpoints: [
    { hostname: "one.example", secure: true },
    { hostname: "two.example", secure: false },
  ],
};
```

The generated shapes are `string[]`, `number[][]`, and
`{ hostname: string; secure: boolean }[]`, respectively. A production override
can therefore contain a different number of entries without creating a false
fixed-length tuple error.

Arrays whose entries have different generated types remain tuples. For example,
`["retries", 3]` becomes `[string, number]`, preserving the useful positional
types. An empty array becomes `unknown[]`: a runtime empty value provides no
evidence from which codegen could infer its element type.

Generated object properties remain exact. The framework intentionally does not
add a `[key: string]` index signature, because that would allow misspelled keys
and falsely claim that every possible string exists. When iterating a finite
generated object, narrow `Object.keys()` back to its real keys:

```ts
const { endpointsByRegion } = this.app.getConfig("assets");
const keys = Object.keys(endpointsByRegion) as Array<
  keyof typeof endpointsByRegion
>;

for (const key of keys) {
  const endpoint = endpointsByRegion[key];
}
```

If a config value is genuinely an open string dictionary rather than a finite
object, declare a local `Record<string, Value>` contract at that dynamic access
boundary. Codegen sees only the keys present in the loaded runtime value and
cannot safely infer that arbitrary future keys are supported.


---


# 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.js // Built-in model (published as compiled .js). 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.js";
import type {
  GetModelTypeLiteFromSchema,
  ExtractProperty,
} from "@adaptivestone/framework/modules/BaseModel.js";

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<UserModelLite>) {
        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(
      `${import.meta.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.'

### Duplicate framework copies

Every model extends `BaseModel` from `@adaptivestone/framework`. If **two different copies** of the framework end up installed (a duplicate/undeduped install), a model can extend `BaseModel` from one copy while the loader runs from the other. `instanceof BaseModel` compares prototype identity, so it is `false` across that copy boundary — and boot used to silently misroute such a model into the legacy (`AbstractModel`) branch, surfacing only later as a confusing downstream failure.

Boot now recognizes the model by its static shape (a `BaseModel` subclass without the `instanceof`) and **fails fast**, naming the offending model:

```text
Model 'Article' extends BaseModel from a DIFFERENT copy of @adaptivestone/framework than the one loading models, so `instanceof BaseModel` is false and it cannot be initialized. This means @adaptivestone/framework is installed more than once (a duplicate/undeduped install). Fix the duplication so a single copy is shared: run `npm ls @adaptivestone/framework` to find the extra copy, then dedupe (align versions, delete node_modules and reinstall, and check your lockfile).
```

The model loader prefixes this with the model name and file (`Failed to initialize model '<Name>' (<file>): …`). The fix is to collapse the duplicate so a single copy is shared:

```bash
npm ls @adaptivestone/framework
```

then dedupe — align versions, delete `node_modules` and reinstall, and check your lockfile. Genuinely legacy (`AbstractModel`-based) models still route to the legacy branch unchanged.

:::tip Module authors

If you publish a package that defines framework models (or otherwise imports `@adaptivestone/framework`), declare the framework as a **peer dependency**, not a regular dependency — that way the host app supplies the single shared copy instead of your package pulling in a second one. `npm link` during local development is the most common trigger for a duplicate copy, since the linked package resolves the framework from its own `node_modules`.

:::

## 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, // Schema-derived authoring type that breaks class cycles.
} from "@adaptivestone/framework/modules/BaseModel.js";

import mongoose, {
  type Aggregate,
  type Query,
  type Schema,
} from "mongoose";

// Reduced authoring types for static methods, instance methods, virtuals,
// and hooks while the class is still being defined.
type SomeModelLite = GetModelTypeLiteFromSchema<
  typeof SomeModel.modelSchema,
  typeof SomeModel.schemaOptions
>;
type SomeDocument = InstanceType<SomeModelLite>;

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: SomeDocument) {
        ...
      }
    );
    schema.pre('findOneAndDelete', async function (
      this: Query<unknown, SomeDocument>
    ) {
      const docToDelete = await this.model.findOne(this.getFilter());
      ...
    });
    schema.pre('aggregate', function (
      this: Aggregate<unknown>
    ) {
      this.pipeline().unshift({ $match: { archived: { $ne: true } } });
    });
  }

  // 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, // Use Schema.Types.ObjectId for a stored reference.
        ref: "Order", // Models are resolved after the framework has loaded them.
      }],
    } 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() {
    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: SomeDocument
      ) {
        await model.populate("orders");
        return {
          _id: model.id,
          email: model.email,
        };
      },
      getInfoStaticWithOrders: async function getInfoStatic(
        model: SomeDocument
      ) {
        const populated = await model.populate<{
          orders: Array<{ id: string; total: number }>;
        }>("orders");
        return {
          _id: populated.id,
          email: populated.email,
          orders: populated.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() {
    return {
      getInfo: async function getInfo(this: SomeDocument) {
        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: SomeDocument) {
          // Getter
          return `${this.firstName} ${this.lastName}`;
        },
        async set(this: SomeDocument, 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<typeof SomeModel>;
```

## Authoring types and complete model handles

There is only one runtime schema: `SomeModel.modelSchema`. The two helpers show
that schema at different points in TypeScript's class evaluation:

| Type | Use it for | What it contains |
| --- | --- | --- |
| `GetModelTypeLiteFromSchema<typeof Model.modelSchema, typeof Model.schemaOptions>` | `this:` annotations inside the class while its members are still being inferred | Schema fields and native Mongoose model/document operations |
| `GetModelTypeFromClass<typeof Model>` | Generated types, exports, controllers, services, commands, and tests after the class is complete | Schema fields plus the class's statics, instance methods, and virtuals |

The reduced type cannot include a custom static, method, or virtual that
TypeScript is currently in the middle of inferring. Trying to use the complete
class-derived type inside that same member creates a circular type. This is a
TypeScript evaluation boundary, not a second schema and not a reduced runtime
model.

Keep the reduced alias beside the class-authoring context. Export the complete
class-derived handle:

```ts
type SomeModelLite = GetModelTypeLiteFromSchema<
  typeof SomeModel.modelSchema,
  typeof SomeModel.schemaOptions
>;
type SomeDocument = InstanceType<SomeModelLite>;

export type TSomeModel = GetModelTypeFromClass<typeof SomeModel>;
```

For two complete model classes that refer to each other, put one deliberate
annotation or deferred boundary at the cycle. Renaming the reduced helper or
wrapping the same circular inputs in another conditional type cannot make the
unfinished class available earlier.

## Schema options that affect types

Pass `typeof Model.schemaOptions` as the second argument whenever schema options
affect a document or query result. Keep the options literal with `as const`.

Timestamp options are reflected exactly: either timestamp can be disabled or
renamed, and an omitted key in an object-form timestamp configuration keeps its
default name.

```ts
class AuditModel extends BaseModel {
  static get modelSchema() {
    return { event: { type: String, required: true } } as const;
  }

  static get schemaOptions() {
    return {
      timestamps: { createdAt: "created_on", updatedAt: false },
    } as const;
  }
}

type AuditModelLite = GetModelTypeLiteFromSchema<
  typeof AuditModel.modelSchema,
  typeof AuditModel.schemaOptions
>;
type AuditDocument = InstanceType<AuditModelLite>;

declare const audit: AuditDocument;
audit.created_on; // Date
// audit.createdAt; // Type error: renamed
// audit.updatedAt; // Type error: disabled
```

Mongoose can also make queries lean by default at schema level. With
`lean: true` (or an object-form lean configuration), ordinary reads return plain
objects. Opt out per query when document methods are required:

```ts
class LeanRecord extends BaseModel {
  static get modelSchema() {
    return { title: { type: String, required: true } } as const;
  }

  static get schemaOptions() {
    return { lean: true } as const;
  }
}

type LeanRecordModel = GetModelTypeFromClass<typeof LeanRecord>;
declare const LeanRecordHandle: LeanRecordModel;

const plain = await LeanRecordHandle.findOne();
// plain?.save(); // Type error: the result is a plain object

const document = await LeanRecordHandle.findOne({}, null, { lean: false });
await document?.save(); // Hydrated document
```

If `schemaOptions` is not passed to the schema-derived helper, TypeScript cannot
recover those options from `modelSchema` alone.

:::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

:::

:::tip Annotating `this` on instance methods

An instance method may declare an explicit `this:` to type its body — handy when
the body assumes a narrower shape than the raw document (a populated ref, a
non-null [plugin-reshaped field](#typing-plugin-reshaped-fields), sibling
methods):

```ts
getInfo: async function (this: SomeDocument) {
  return { _id: this._id, email: this.email };
},
```

That annotation types the **body only**. You still call the method directly on
the document — `doc.getInfo()` — on any model handle; the framework drops the
authored `this` from the caller-facing type, since a method accessed on its own
document always has the right `this` at runtime. No `(schema.methods.x as …)
.call(doc, …)` cast is needed.

:::

:::tip Annotating `this` in middleware

Use the context for the middleware category, not one model type everywhere:

```ts
// Document middleware
schema.pre("save", function (this: SomeDocument) {
  this.email;
});

// Query middleware
schema.pre("findOneAndUpdate", function (
  this: Query<unknown, SomeDocument>
) {
  this.getFilter();
  this.getUpdate();
});

// Aggregate middleware
schema.pre("aggregate", function (this: Aggregate<unknown>) {
  this.pipeline();
});
```

The same annotations work when hooks are registered in a loop. A query's
`this` is a Mongoose `Query`; it is not the model and not a hydrated document.

:::

## Typing plugin-reshaped fields

Some Mongoose plugins reshape a field after the schema literal is declared. An
intl plugin may store a locale map behind a `String` path, an encryption plugin
may store a cipher object, and a custom getter may expose a different hydrated
value. The framework can only infer the declared `type:` unless you describe the
plugin's transformation.

Mark the field with `TsTypeOverride<TRaw, THydrated = TRaw>`:

- `TRaw` is the stored shape used by casting/create inputs and lean results;
- `THydrated` is the value exposed by loaded Mongoose documents and hydrated
  subdocuments;
- omitting `THydrated` preserves the original one-type behavior.

Both marker properties are phantom TypeScript fields and are never set at
runtime. The plugin remains responsible for the transformation.

:::info Version requirement

Distinct raw and hydrated overrides require framework **5.2.3 or newer**. The
one-argument `TsTypeOverride<T>` form works in earlier v5 releases and remains
source-compatible.

:::

### One value type on both surfaces

Use one type argument when the plugin exposes the same reshaped value on raw and
hydrated documents:

```ts title="/src/models/Event.ts"
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";
import type { TsTypeOverride } from "@adaptivestone/framework/modules/BaseModel.js";

type EncryptedValue = { ciphertext: string; keyId: string };

function encryptedString<C extends object>(field: C) {
  return field as C & TsTypeOverride<EncryptedValue>;
}

export default class Event extends BaseModel {
  static get modelSchema() {
    return {
      secret: encryptedString({ type: String, encrypted: true }),
      schedule: [{ secret: encryptedString({ type: String, encrypted: true }) }],
      plainField: { type: String }, // unmarked → still `string`
    } as const;
  }
}
```

The static type now follows the runtime value everywhere — no casts:

```ts
const Event = this.app.getModel("Event");
const event = await Event.findOne();
event?.secret?.ciphertext; // `secret` is EncryptedValue
event?.schedule?.[0]?.secret?.keyId; // any depth (nested + subdoc arrays)
event?.plainField; // unmarked field is still `string`
```

### Different raw and hydrated values

A virtual getter can expose a different value from the one stored in MongoDB.
Describe both surfaces once in an application-side schema factory. Include every
state the getter can return—for example, an intl getter may normally return the
selected string but expose the complete locale map after a document method
changes its mode:

```ts title="/src/models/Event.ts"
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";
import type { TsTypeOverride } from "@adaptivestone/framework/modules/BaseModel.js";

type Language = "en" | "fr";
type IntlText = Partial<Record<Language, string>>;
type IntlHydratedValue = string | IntlText;

function intlString<C extends object>(field: C) {
  return field as C & TsTypeOverride<IntlText, IntlHydratedValue>;
}

export default class Event extends BaseModel {
  static get modelSchema() {
    return {
      title: intlString({ type: String, required: true, intl: true }),
      schedule: [
        { title: intlString({ type: String, required: true, intl: true }) },
      ],
    } as const;
  }
}
```

The raw shape is accepted when creating the document. The returned document is
hydrated, while `.lean()` returns the raw locale map:

```ts
const Event = this.app.getModel("Event");

const event = await Event.create({
  title: { en: "Title", fr: "Titre" },
  schedule: [{ title: { en: "Session", fr: "Séance" } }],
});

if (typeof event.title === "string") {
  event.title.toUpperCase(); // selected-language getter state
} else {
  event.title.en; // full-languages getter state
}

const raw = await Event.findById(event._id).lean();
raw?.title.en; // IntlText

const item = event.schedule.create({
  title: { en: "Next", fr: "Suivante" },
});
event.schedule.push(item);
// `item.title` is IntlHydratedValue; its create input was IntlText.
```

Prefer the plugin's nested raw value (`title: { en, fr }`) when its setter
supports it. `TsTypeOverride` describes a field's values; it deliberately does
not synthesize plugin-specific dotted root keys such as `"title.en"` for every
model operation.

:::note

The override is **opt-in** and a strict **no-op** for any field without the
marker — existing models are unaffected. It recurses into nested objects and
subdocument arrays, so a reshaped field can appear at any depth. The same marker
works for any runtime-reshaping plugin (encrypted fields, custom getters, …), not
just intl plugins. The framework does not interpret plugin-specific options such
as `intl: true`; the small application-side factory is the explicit type/runtime
boundary.

:::

## Typing populated references

A reference field (`{ type: Schema.Types.ObjectId, ref: "User" }`) is typed as an
`ObjectId` — that is what is stored, and what you get back when the field is
**not** populated. After `.populate(...)` the runtime value is the referenced
document, but the inferred type stays `ObjectId` (Mongoose cannot know at the
schema level which queries populate it). There are two cast-free ways to type the
populated value, depending on how often you populate the field.

**Per call — `.populate<T>()`.** When you populate occasionally, pass the
populated shape as the type argument at the call site. The returned document is
typed with that field replaced:

```ts
const Boat = this.app.getModel("Boat");
const boat = await Boat.findOne();
const populated = await boat!.populate<{ owner: { email: string } }>("owner");
populated.owner.email; // typed — no cast
```

**Always — mark the field.** When a field is almost always read populated, mark it
with `TsTypeOverride` as the **union** of both states (`ObjectId` when not
populated, the document when it is). Reads then narrow without a cast:

```ts title="/src/models/Boat.ts"
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";
import type { TsTypeOverride } from "@adaptivestone/framework/modules/BaseModel.js";
import { Schema, type Types } from "mongoose";

type PopulatedOwner = { email: string; name: string };

function ref<C extends object, T>(field: C) {
  return field as C & TsTypeOverride<Types.ObjectId | T>;
}

export default class Boat extends BaseModel {
  static get modelSchema() {
    return {
      owner: ref<{ type: typeof Schema.Types.ObjectId; ref: "User" }, PopulatedOwner>({
        type: Schema.Types.ObjectId,
        ref: "User",
      }),
    } as const;
  }
}
```

```ts
const boat = await this.app.getModel("Boat").findOne();
// `owner` is `ObjectId | PopulatedOwner | undefined` — narrow before use:
if (boat?.owner && "email" in boat.owner) {
  boat.owner.email; // typed as PopulatedOwner
}
```

:::note

Refs that are not marked stay plain `ObjectId`, and `.populate<T>()` always works
regardless. Prefer the marker only for fields you consistently populate — the
union forces a narrowing check, which is the honest cost of a field that is
sometimes an id and sometimes a document.

:::

## Recovering a model from a document

Use Mongoose's typed `$model<T>()` method when document code needs its owning
model. Avoid casting `document.constructor`: Mongoose exposes that property as a
general constructor, so it does not preserve the framework model's statics.

```ts
declare const document: SomeDocument;

const SomeModelHandle = document.$model<TSomeModel>();
await SomeModelHandle.findByEmail(document.email ?? "");
```

On an existing document, prefer the no-argument `$model<T>()` overload. The
named overload (`$model<T>("SomeModel")`) passes through a broader Mongoose
constraint that can reject schema-specific model types. For a model selected by
a runtime string, use the application's generated model registry or define a
small local capability type for the operations that branch needs.

## Write inputs and query-local result types

A hydrated document type describes a document returned by Mongoose. It is not a
general create/insert DTO: hydrated subdocuments may contain generated `_id`
fields and document methods that are not present in a plain write payload.

Prefer inference for one-off writes:

```ts
declare const SomeModelHandle: TSomeModel;

await SomeModelHandle.create({
  someString: "required value",
  email: "reader@example.com",
  orders: [],
});
```

### Hydrated subdocuments and array writes

Raw write values and hydrated document properties intentionally have different
types. A plain value passed to `create()` has no generated fields yet. After
Mongoose hydrates it, a subdocument has its generated `_id` and an array of
subdocuments is a real `DocumentArray` with Mongoose's change tracking and
methods.

```ts
const Workflow = this.app.getModel("Workflow");

const workflow = await Workflow.create({
  entries: [{ label: "Initial" }], // no `_id` or defaulted fields required
});

workflow.entries[0]._id; // ObjectId: generated ids are required when read

workflow.entries.push({ label: "Review" }); // Mongoose casts the plain value

const approved = workflow.entries.create({ label: "Approved" });
workflow.entries.push(approved);
workflow.entries.id(approved._id); // hydrated DocumentArray method
```

The same distinction applies to defaulted subdocument fields: callers may omit
them from `Model.create()`, `DocumentArray.push()`, `DocumentArray.create()`, and
`DocumentArray.splice()` input, while the hydrated result exposes the value
Mongoose supplies. An inline `_id: false` removes the generated id instead; it
is not exposed as a usable field on the hydrated subdocument and is absent from
raw/lean results.

### Nested paths are not subdocuments

Only the `{ type: { … } }` spelling builds a subdocument, and only a subdocument
gets a generated `_id`. A plain nested object is a *path grouping*: Mongoose
stores `name.first` and `name.last`, and there is no `name._id` at any point.
The types follow that distinction on every surface, so the runtime shape
assigns without a cast or an `Omit<…>` bridge:

```ts
name: {
  first: { type: String }, // plain nested path — never an `_id`
  last: { type: String },
},
badge: {
  type: { label: { type: String } }, // subdocument — generated `_id`
},
```

```ts
user.name = { first: "Ada" }; // nothing to supply beyond the real fields
user.badge?._id; // ObjectId — a real subdocument
```

Use the `{ type: … }` form when you want a subdocument's own identity and
document methods; keep the plain form when you only want to group related fields
under one prefix.

Replacing a hydrated array with a native JavaScript array by direct property
assignment is intentionally rejected. A native array does not have
`DocumentArray.create()`, `id()`, casting, or change tracking, so accepting it as
the property's read type would make those APIs unsafe. Mutate the existing array
or use Mongoose's setter:

```ts
const replacement = [{ label: "Rebuilt" }];

workflow.entries.splice(
  0,
  workflow.entries.length,
  ...replacement,
);

// Alternatively, replace the whole path through Mongoose's casting setter.
workflow.set("entries", replacement);

// Type error: a native array is not a hydrated DocumentArray.
// workflow.entries = replacement;
```

For filtered hydrated values, keep the `DocumentArray` instance and replace its
contents:

```ts
const remaining = workflow.entries.filter((entry) => entry.label !== "Review");
workflow.entries.splice(0, workflow.entries.length, ...remaining);
```

A lean query returns raw values, so its array fields are ordinary JavaScript
arrays rather than `DocumentArray`s. This raw/hydrated split follows
[Mongoose's TypeScript subdocument model](https://mongoosejs.com/docs/typescript/subdocuments.html)
and does not require a second runtime schema or a separately maintained document
interface.

Primitive schema arrays similarly hydrate as Mongoose arrays rather than native
arrays. Mutate them in place or use `set()` for a whole-path replacement.

When an input crosses a service or command boundary, declare a local input type
containing the writable fields instead of reusing `SomeDocument`:

```ts
type CreateSomeModelInput = {
  someString: string;
  email?: string;
  orders?: mongoose.Types.ObjectId[];
};

const rows: CreateSomeModelInput[] = getRows();
await SomeModelHandle.insertMany(rows);
```

Population and aggregation are also query-local runtime choices. Use
`.populate<T>()` for the populated result and supply an explicit aggregation
result type:

```ts
const totals = await SomeModelHandle.aggregate<{
  _id: string;
  count: number;
}>([
  { $match: { email: { $ne: null } } },
  { $group: { _id: "$email", count: { $sum: 1 } } },
]);
```

Keep these contracts beside the operation that creates the shape. The framework
cannot infer an aggregation projection or a runtime-selected population state
from the static schema alone.

## Looking up models

Run type generation before type-checking. The generated `AppModelTypes` map
contains every resolved application model, including application overrides of
framework models. `getModel()` therefore accepts application models as well as
the built-in models—it is not limited to the framework's model names.

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

// A union of valid generated names produces the corresponding model union.
const modelName: "Article" | "Comment" = chooseModelName();
const Content = this.app.getModel(modelName);

// @ts-expect-error — no model with this generated name
this.app.getModel("Artcle");
```

Use the two lookup methods according to where the name comes from:

| Name source | Method | Return type | Missing model |
|---|---|---|---|
| Generated literal or union | `getModel(name)` | Exact model or model union | Rejected by TypeScript |
| Runtime `string` | `getModelOrThrow(name)` | Broad Mongoose model, never `false` | Throws an `Error` |

For example, a command may receive a model name only at runtime:

```ts
const modelName = process.env.MODEL_NAME;
if (!modelName) throw new Error("MODEL_NAME is required");

const Model = this.app.getModelOrThrow(modelName);
await Model.collection.dropIndexes();
```

`getModelOrThrow()` also has access to the generated map. Passing a known
literal or a valid-name union therefore keeps the same precise return type as
`getModel()`; only an unrestricted `string` widens to the common model type.

An unknown name, or a lookup before `Server.init()` completes, is logged and
then throws. If a model name comes directly from an HTTP request, validate it
against an allowed-name set first so the application can return an appropriate
client error rather than exposing an internal lookup failure.

## 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();
// `hashPassword` is a standalone helper, not a model static:
// import { hashPassword } from "@adaptivestone/framework/helpers/crypto.js";
const hashedPassword = await hashPassword("password");
const sameUser = await UserModel.getUserByToken(userToken);
const sameUserAgain = await UserModel.getUserByEmail(user.email);
// The token generators live in `userHelpers`, not on the model:
// import { userHelpers } from "@adaptivestone/framework/models/User.js";
const recoveryToken = await userHelpers.generateUserPasswordRecoveryToken(user);
const sameUserAgain2 = await UserModel.getUserByPasswordRecoveryToken(
  recoveryToken
);
const isSuccess = await user.sendPasswordRecoveryEmail(i18n);
const verificationToken = await userHelpers.generateUserVerificationToken(user);
const sameUserAgain3 = await UserModel.getUserByVerificationToken(
  verificationToken
);
const isSuccess2 = await user.sendVerificationEmail(i18n);
```

#### Customizing the User model

To replace the framework's `User`, drop your own `User.ts` into your project's
`models/` folder. The [inheritance process](03-files-inheritance.md) makes it win
over the framework's, and `getModel("User")` / `req.appInfo.user` are typed
against **your** model automatically (run `generatetypes` after adding it).

There are two ways to customize it.

**Add fields** — extend the framework's `User` and spread its schema:

```ts title="/src/models/User.ts"
import FrameworkUser from "@adaptivestone/framework/models/User.js";

export default class User extends FrameworkUser {
  static get modelSchema() {
    return {
      ...FrameworkUser.modelSchema,
      company: { type: String },
    } as const;
  }
}
```

The inherited auth statics and instance methods (`getUserByEmailAndPassword`,
`generateToken`, `getPublic`, …) keep working on your model with no casts.

**Reshape fields** — when you need to change a field's _shape_ (for example an
i18n `name`, or a singular `role` instead of `roles[]`), TypeScript can't express
a type _replacement_ through `extends` (the static-getter override is checked
covariantly, so it fails with `TS2417`). Compose instead: extend `BaseModel` and
reuse the framework's auth logic by spreading it in.

```ts title="/src/models/User.ts"
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";
import FrameworkUser from "@adaptivestone/framework/models/User.js";
import type { Schema } from "mongoose";

export default class User extends BaseModel {
  static get modelSchema() {
    return {
      name: { native: { type: String }, machine: { type: String } },
      email: { type: String },
      password: String,
      sessionTokens: [{ token: String, valid: Date }],
      role: { type: String },
      // …the rest of your schema
    } as const;
  }

  static get modelStatics() {
    return { ...FrameworkUser.modelStatics } as const;
  }

  static get modelInstanceMethods() {
    return { ...FrameworkUser.modelInstanceMethods } as const;
  }

  static initHooks(schema: Schema) {
    FrameworkUser.initHooks(schema); // keeps the password-hashing pre-save hook
  }
}
```

The shipped auth helpers are typed against small structural contracts
(`UserAuthDoc` / `UserAuthInstance` / `UserAuthModel`), so they stay callable on
your reshaped model without casts.

:::note

The auth statics (`getUserByEmailAndPassword`, `getUserByToken`, …) only read a
few fields — `email`, `password`, and the token arrays. Any model that keeps
those reuses them as-is. `getPublic` returns the framework's public shape, so
override it if your model reshapes the fields it reads (such as `name`).

:::

### 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" });
}
```

### KeyValue

A minimal persistent key/value store backed by MongoDB. Think of it as a tiny, shared "settings drawer" for your app: a place to keep small pieces of state that should survive restarts and be readable by every process — a lightweight cache, runtime configuration, feature flags, the cursor of a background job, and so on.

The model is intentionally schema-only — it adds no custom methods. The key is the document `_id` (a string), and the value is a `Mixed` field, so it can hold anything Mongoose can serialise (string, number, boolean, array, or nested object). You interact with it through the standard Mongoose API that every model already exposes.

```ts
static get modelSchema() {
  return {
    _id: { type: String, required: true },
    value: { type: Schema.Types.Mixed, required: true },
  } as const;
}
```

#### Usage

```js
const KeyValue = this.app.getModel("KeyValue");

// Set (create or overwrite). `upsert: true` makes it idempotent.
await KeyValue.findByIdAndUpdate(
  "config:theme",
  { value: "dark" },
  { upsert: true },
);

// Get. Returns the document or `null` when the key is missing.
const doc = await KeyValue.findById("config:theme");
const theme = doc?.value ?? "light"; // fall back to a default

// Any serialisable value works.
await KeyValue.findByIdAndUpdate(
  "config:features",
  { value: { newDashboard: true, limits: [10, 50, 100] } },
  { upsert: true },
);

// Read many keys at once.
const docs = await KeyValue.find({ _id: { $in: ["config:theme", "config:features"] } });
const map = new Map(docs.map((d) => [d._id, d.value]));

// Delete.
await KeyValue.deleteOne({ _id: "config:theme" });
```

:::tip

Use a `namespace:key` convention for the `_id` (for example `config:theme`, `cache:user-42`, `flag:beta-signup`). It keeps keys readable and makes prefix queries with a regular expression easy:

```js
const allConfig = await KeyValue.find({ _id: /^config:/ });
```

:::

#### Caching pattern

Because every process reads the same collection, `KeyValue` is a convenient cross-server cache for values that are expensive to compute but cheap to store.

```js
async function getExchangeRates(app) {
  const KeyValue = app.getModel("KeyValue");
  const cached = await KeyValue.findById("cache:exchange-rates");
  if (cached) {
    return cached.value;
  }

  const rates = await fetchExpensiveRatesFromExternalApi();
  await KeyValue.findByIdAndUpdate(
    "cache:exchange-rates",
    { value: rates },
    { upsert: true },
  );
  return rates;
}
```

Pair it with the [`Lock`](#lock) model when several requests might try to populate the same cache key at once, so the expensive work runs only once.

:::note

`KeyValue` is **persistent storage**, not an expiring cache — entries live until you delete them. There is no built-in time-to-live. If you need automatic expiration, add an `expireAt` date field and a TTL index in `initHooks`, the same way the `Lock` model does:

```ts
static initHooks(schema: Schema) {
  schema.index({ expireAt: 1 }, { expireAfterSeconds: 0 });
}
```

For request-scoped or in-memory caching, see the [Cache](11-cache.md) section instead.

:::

#### Concurrency

`value` is a `Mixed` field, so it is replaced as a whole — concurrent writers are last-write-wins. Do not read a value, mutate it in your code, and write it back if multiple processes update the same key; you may lose updates. For counters or fields that must change atomically, use MongoDB update operators directly (`$inc`, `$set` on a sub-path) or reach for the [`Sequence`](#sequence) model.


---


# Document 6: 05-modelsOld

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

# Models (OLD)

:::warning

This documentation is for old model definitions. Please do not use this type of models at all. Use current 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 is available 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 full mongoose schema
  // Please refer 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 full mongoose schema
  // Please refer to mongoose documentation https://mongoosejs.com/docs/guide.html#options
  get modelSchemaOptions() {
    return {
      read: "primary",
    };
  }

  // Static method will be part of the mongoose 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
constructor(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 userInstance = 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 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);
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

Clustering is opt-in. Use the exported `runCluster()` helper when a standalone
deployment should use multiple CPU cores; when systemd, PM2, Docker, or an
orchestrator already manages replicas, run one framework process per replica.
Workers do not share memory, so cross-worker sessions, WebSocket fan-out, and
similar state require external coordination. See [Deployment](../13-deploy.md).
:::

## 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 loads every file in `src/controllers/` except `*.test.js` and
`*.test.ts`. The default mount path comes from the controller's folder prefix
plus its **lowercased class name**. The filename does not determine the URL:
`src/controllers/admin/UserSettings.ts` exporting `class UserSettings`
mounts at `/admin/usersettings`.

Filename still matters for framework-internal controller overrides, so name the
file after the class. Never export the same controller class from two files:
the loader initializes both files and mounts the controller twice. Override
`getHttpPath()` when the default class-name path is not the URL you want.

### Organizational route groups

A controller subfolder normally contributes a URL segment. Wrap a folder name
in parentheses when it should organize source files without changing routes:

```text
src/controllers/(group)/Reports.ts
  → /reports

src/controllers/(group)/admin/Settings.ts
  → /admin/settings
```

Only fully parenthesized path segments are omitted. Ordinary folders still
contribute their lowercased names, and multi-word controller class names are
still lowercased without implicit kebab-casing. Generated `*.routes.gen.ts`
files remain beside their controllers inside the group folder.

Route groups are also ignored when matching a project controller against a
framework-internal controller override. For example, moving an `Auth.ts`
override into `(group)/Auth.ts` still replaces the framework `Auth`
controller; the group changes source organization, not override identity.

Route groups do not create namespaces. If two grouped controllers resolve to
the same HTTP method and path, startup fails with the normal duplicate-route
error; rename or explicitly remount one of them.

### 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` because the class is
named `ControllerName`; renaming only its file would not change this URL.

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

If you want to define a custom path, provide your own implementation of
`getHttpPath()`:

```js
  getHttpPath() {
    return "/super-duper-mega-special-route";
  }
```

By default, `getHttpPath()` combines the folder prefix with the lowercased
class name. It does not kebab-case multi-word names: `UserSettings` becomes
`/usersettings`, so use an override when `/user-settings` is required.

### Project boot hook (`bootHttp`)

For app-wide HTTP wiring that doesn't belong to any single controller — webhooks, healthchecks, OAuth callbacks, or boot-time setup — pass a **`bootHttp`** function to the `Server` constructor. The framework calls it with the live `app` during `startServer`, after controllers are registered but before the adapter mounts, so anything it adds is in place from the first request.

Define it inline — `app` is inferred, no annotation needed:

```ts title="src/index.ts"
import Server from "@adaptivestone/framework/server.js";

const server = new Server({
  folders: {
    /* … */
  },
  bootHttp: async (app) => {
    // `httpServer` is always set by the time bootHttp runs, but it's typed
    // nullable (CLI and worker processes never have one). Narrow it by
    // throwing, not with `?.`: an optional chain on a null would skip your
    // wiring silently and the app would serve without it.
    if (!app.httpServer) {
      throw new Error("bootHttp ran without a live httpServer");
    }

    // An ad-hoc route that doesn't fit the controller convention:
    app.httpServer.routeRegistry.registerRoute("POST", "/webhooks/stripe", {
      handler: stripeWebhookHandler,
    });

    // Or app-wide Express middleware (runs before the router):
    // app.httpServer.express.use(myGlobalMiddleware);
  },
});
await server.startServer();
```

There's no required file or folder for it — it's just a function. If it grows, extract it to its own module and type it with `BootHttpHook`:

```ts title="src/bootHttp.ts"
import type { BootHttpHook } from "@adaptivestone/framework/server.js";

const bootHttp: BootHttpHook = async (app) => {
  /* … */
};

export default bootHttp;
```

It's wired **explicitly**, not auto-discovered from a folder: the framework finds everything else through configured folders, and those are all spoken for — `config/` merges its files as config, `controllers/` auto-loads its files as controllers — so there's no conflict-free folder to scan. `bootHttp` is **HTTP-specific** by design: it needs `app.httpServer`, which only exists once the HTTP server boots (CLI and worker processes never run it). Unlike the `callbackBefore404` hook (which you pass to `startServer` and which runs _after_ the adapter mounts), `bootHttp` runs _before_ the mount.

The framework builds its own `Server` for tests, so pass the same hook to [`configureTestServer`](../09-testsing.md#production-http-wiring-boothttp) in your test setup — otherwise your suites run against a server that never got this wiring.

## Request Flow

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

## View

:::warning

Built-in view rendering was **removed in version 5**. The framework no longer ships a `views/` folder or a default template engine — it is API-first (returns JSON).

:::

If you need server-rendered HTML, the underlying Express 5 instance is available on `app.httpServer.express` (once the HTTP server has booted), so you can register a template engine yourself (see the [Express template-engine guide](https://expressjs.com/en/guide/using-template-engines.html)). For most applications, return JSON and render on the client.

## 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(),
  }),
  params: yup.object().shape({ // optional
    id: yup.string().required(),
  }),
  middleware: [RateLimiter], // optional
  description: "Create a sample" // optional — a plain string; becomes the OpenAPI operation summary
}

```

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.
Params; // A special interface that will do validation of path parameters (:id) for you.
Middleware; // An array of middlewares specially for the current route.
Description; // A description of this route — becomes the OpenAPI operation summary.
```

The route `description` together with the `request:` / `query:` / `params:` schemas and the middleware chain are what the framework reads to generate an [OpenAPI 3.1 document](../17-openapi.md).

## 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`.

:::

## Params

Path params (`:id`, `{*rest}`) arrive as raw strings from the matcher. Declare a `params:` schema to validate and coerce them the same way `request:` handles the body:

```js
get: {
  "/person/:id": {
    handler: this.getPerson,
    params: yup.object().shape({
      id: yup.string().matches(/^[0-9a-fA-F]{24}$/, "must be a valid id"),
    }),
  },
}
```

A failure is a **400** with the framework's usual error shape, returned before your handler runs:

```json
{ "errors": { "id": ["must be a valid id"] } }
```

The validated, coerced values are available as `req.appInfo.params`. Because coercion comes from the validator, a numeric or date param arrives already converted:

```js
"/invoice/:year": {
  handler: this.getInvoice,
  params: yup.object().shape({ year: yup.number().min(2000) }),
}

// in the handler:
req.appInfo.params.year; // 2026    (number)
req.params.year;         // "2026"  (string — raw, untouched)
```

:::warning
Use `req.appInfo.params` for validated values. Raw `req.params` is deliberately left alone so the Express contract (always strings) still holds — which also means it is **not** validated. Passing a raw param straight to Mongoose is the classic way to turn a client typo into a server error.
:::

:::note
`params:` is route-level only — unlike `request:` and `query:`, middleware do not contribute param schemas. It does feed the [OpenAPI document](../17-openapi.md): a declared `params:` schema types the path parameters that would otherwise be documented as plain `string`s. Path params are always emitted as `required: true` — they are part of the URL by construction, whatever the schema says about optionality.

If a route declares no `params:` schema and an unvalidated param reaches Mongoose anyway, the framework still catches the resulting `CastError` and answers **400** rather than 500 — see [Error handling → the Mongoose cast safety net](04-error-handling.md#built-in-the-mongoose-cast-safety-net). That is a floor, not a substitute: a `params:` schema rejects bad input earlier, with wording and coercion you control.
:::

## 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.

:::note
`request:` validates the request **body**, `query:` the **query string**, and `params:` the **path params** (`:id`) — see [Params](#params) below. All three accept any Standard Schema validator and all three report failures the same way.
:::

:::note
The framework no longer bundles a validator. `yup` is an **optional peer dependency** — install it only if you use yup schemas (or the deprecated `YupFile`). For dependency-free validation of simple shapes, use [`defineSchema`](#zero-dependency-schemas-defineschema) below.
:::

### 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'`).

### Zero-dependency schemas (`defineSchema`)

When you want a small schema without pulling in a validator library, use `defineSchema` — a one-function adapter that wraps a plain validate callback into a Standard Schema object:

```ts
import { defineSchema } from "@adaptivestone/framework/services/validate/defineSchema.js";

request: defineSchema<{ email: string }>(
  (value) => {
    const v = (value ?? {}) as Record<string, unknown>;
    if (typeof v.email !== "string" || !v.email.includes("@")) {
      return { issues: [{ message: "validation.email", path: ["email"] }] };
    }
    // Return only known keys — unknown input is stripped by construction.
    return { value: { email: v.email } };
  },
  {
    // Optional: lets OpenAPI document this imperative validator.
    jsonSchema: {
      type: "object",
      properties: { email: { type: "string", format: "email" } },
      required: ["email"],
    },
  },
);
```

- The `Output` generic (`{ email: string }`) is what the codegen reads for the typed handler signature (`req.appInfo.request`) — see below. You declare it; the runtime checks live in the callback.
- Return `{ value }` on success (only the keys you copy survive — this is your strip-unknown), or `{ issues }` on failure. Each issue's `message` is an i18n key (or literal text); the framework auto-translates it like any other validator.
- The optional `jsonSchema` value (or function returning one) describes the wire shape to OpenAPI. It does not change runtime validation. Omit it when the shape should remain undocumented.
- This is the mechanism the built-in `Auth` controller uses, so the framework itself ships validator-free.

`defineSchema` is intentionally minimal — there are no `string()`/`object()` combinators. If you find yourself hand-writing many or deeply nested schemas, reach for a real validator library (Zod, etc.) instead.

### 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.

**What the generated type carries** (per route):

| Source | Emitted into |
|---|---|
| Route path `:name` segments (e.g., `/users/:id`) | `req.params: { id: string }` |
| Route path `{*name}` splats (e.g., `/files/{*path}`) | `req.params: { path: string }` — splats join captured segments with `/` (one string, not an array) |
| Route `request:` schema | `req.appInfo.request: StandardSchemaV1.InferOutput<...>` |
| Route `query:` schema | `req.appInfo.query: StandardSchemaV1.InferOutput<...>` |
| Route `params:` schema | `req.appInfo.params: StandardSchemaV1.InferOutput<...>` — the raw `req.params` row above stays `string`-valued |
| Middleware-chain `static get provides()` returns | Merged into `req.appInfo` |

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.

#### Keep `routes` declarative (codegen reads the source, never your constructor)

`npm run gen` analyzes each controller **statically, from its source AST** — it never imports, constructs, or runs your controller. That keeps type generation free of side effects (config reads, S3/OAuth client construction, timers, DB connections) and fast.

The trade-off: the `routes` (and `static get middleware()` / `getHttpPath()`) getter must return a **plain object literal** that the parser can read directly. It may reference handler methods (`handler: this.postLogin`) and inline schemas, but it must not be computed — no reading constructor state, no loops, conditionals, computed keys, or `super` calls in the returned shape.

```ts
// ✅ fine — the constructor sets up clients, but `routes` returns a literal
class Files extends AbstractController {
  constructor(app, prefix) {
    super(app, prefix);
    this.s3 = new S3Client(app.getConfig('s3')); // used inside handlers, not in routes
  }
  get routes() {
    return { post: { '/upload': { handler: this.upload } } };
  }
}

// ❌ not analyzable — `routes` is computed from constructor state
class Crud extends AbstractController {
  constructor(app, prefix) {
    super(app, prefix);
    this.models = ['User', 'Order'];
  }
  get routes() {
    // a computed return — the AST can't read these route shapes
    return Object.fromEntries(this.models.map((m) => [`/${m}`, { handler: this.list }]));
  }
}
```

If a `routes` getter isn't a static literal, codegen can't analyze it and **`npm run gen` fails** (declarative-only — there is no constructor fallback). Move the dynamic part into handlers or a module-level constant so the returned shape stays literal.

#### Setup

Install the parser code generation uses. `oxc-parser` is an **optional peer dependency** — it reads your controller sources and is never loaded at runtime, so it stays out of production installs:

```sh
npm i -D oxc-parser
```

Add to `package.json`:

```json
"gen": "node src/cli.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.

The same pattern works for **project-side middlewares** that augment `req.appInfo`. Example — a `UserWithRole` middleware that resolves CASL permissions onto the request:

```ts
import type { MongoAbility } from "@casl/ability";
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class UserWithRole extends AbstractMiddleware {
  static get provides() {
    return {} as { permissions: MongoAbility };
  }

  async middleware(req, res, next) {
    req.appInfo.permissions = await buildPermissions(req.appInfo.user);
    next();
  }
}
```

Any route with `UserWithRole` in its middleware chain now has `req.appInfo.permissions` typed as `MongoAbility` automatically.

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;
  }
}
```

#### Extending the framework's User model

If your project's `User` model adds methods or fields beyond the framework's base (`getSuppliers()`, custom relations, etc.), `req.appInfo.user` from `GetUserByToken.provides` will still be typed as the framework's `TUser` — calls to project-specific methods won't compile.

Augment the framework's `User` module to add your project's fields:

```ts
// somewhere in your project, loaded before handlers compile
declare module "@adaptivestone/framework/models/User" {
  interface IUserMethods {
    getSuppliers(): Promise<string[]>;
  }
}
```

Now `req.appInfo.user.getSuppliers()` type-checks across all handlers.

#### 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

Files are uploaded via `multipart/form-data`. The parser preserves the raw form shape, which means **every multipart field arrives as an array** — a single value is a one-element array, several values are a longer array. Validate against the framework-exported `File` type, using your validator's own array support to declare cardinality:

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

request: z.object({
  // one file: validate the one-element array, unwrap to a File for the handler
  avatar: z.array(z.instanceof(File)).length(1).transform(([f]) => f),
  // many files (`<input type="file" multiple>`): keep the array
  avatars: z.array(z.instanceof(File)).nonempty(),
});
```

`req.appInfo.request.avatar` is a `File`; `req.appInfo.request.avatars` is `File[]`.

:::caution Cardinality is declared, not inferred from length
A single value and a one-element array look identical on the wire (`["A"]`), so you must **declare** which fields are scalar — never auto-unwrap by array length. A `multiple` field that receives one file still arrives as `["A"]`; because `avatars` is declared `z.array(...)` (no transform), it correctly stays a one-element `File[]`. Only fields you explicitly transform to a scalar (`.length(1).transform(...)`) are unwrapped — and a scalar field that receives two values fails `.length(1)`, which is the right outcome.
:::

`File` is exported as both a value (so `instanceof` works) and a type. It aliases the parser's file class today and re-points at the web-standard `File` after the transport-neutral parser swap — so your validation code stays stable across that change. The same `File` works with every validator: `z.instanceof(File)`, `v.instance(File)`, `type.instanceOf(File)`, or yup's `mixed().test("file", "not a file", (v) => v instanceof File)`.

#### How route types are generated for multipart

Route-type generation is **parser-agnostic** — the multipart always-array shape never reaches it. Codegen emits `req.appInfo.request: StandardSchemaV1.InferOutput<...["request"]>`, which reads the request schema's **output** type. Because the schema above transforms the one-element array down to a `File`, the generated handler type is the unwrapped shape (`{ avatar: File; avatars: File[] }`) — and it matches the runtime value. The schema is the single source of truth for both runtime and types.

:::info Planned — route-level single-element extraction
A **route-level option** is planned so a route can declare which multipart fields are scalar, and the router unwraps their single-element arrays *before* validation. The schema then stays the clean logical shape (`avatar: z.instanceof(File)`), codegen reads that output type directly, and there's no per-field `.array().length(1).transform(...)` to write. This keeps the array-handling in the parser layer (where the array-ness originates) instead of the schema. Until it ships, use the validator's array handling shown above.
:::

:::warning Deprecated: `YupFile`
The yup-specific `YupFile` helper (`@adaptivestone/framework/helpers/yup.js`) is **deprecated and will be removed in v6** — migrate to the `File` export above. It still works for now (and requires `yup` in your own dependencies, since the framework no longer bundles it):

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

request: yup.object().shape({
  someFileName: new YupFile().required("error text"),
});
```
:::

:::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.
:::

### Different schemas per Content-Type

A single route can accept more than one `Content-Type` with a different body shape for each — for example, create a resource from JSON or upload it as `multipart/form-data`. Instead of one `request` schema, pass a **content-type map** (the shape mirrors OpenAPI's `requestBody.content`):

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

"/avatar": {
  handler: this.setAvatar,
  request: {
    "application/json": z.object({ url: z.string().url() }),
    "multipart/form-data": z.object({
      file: z.array(z.instanceof(File)).length(1).transform(([f]) => f),
    }),
  },
},
```

The body is parsed (the parser is already Content-Type-aware), then validated with the schema matching the request's `Content-Type`. An unmatched type returns **415 Unsupported Media Type**, listing the accepted ones.

`req.appInfo.request` becomes a **discriminated union** keyed by an injected `contentType` field, so the handler narrows cleanly — and codegen emits the union for you:

```ts
async setAvatar(req: SetAvatarRequest, res: Response) {
  if (req.appInfo.request.contentType === "multipart/form-data") {
    req.appInfo.request.file; // File
  } else {
    req.appInfo.request.url; // string
  }
}
```

:::note
Matching is on the media type only and is **case-insensitive** — parameters like `; charset=...` and `; boundary=...` are ignored. The injected `contentType` is the lower-cased media type, and it overwrites any body field of the same name, so don't declare a schema field named `contentType`. A `Content-Type` the body parser itself can't handle (e.g. malformed `multipart/form-data`) is rejected with a `400` by the parser *before* the `415` check. Middleware-declared request schemas (`relatedRequestParameters`) still apply on top, regardless of Content-Type.
:::

### 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 — supplies the OpenAPI body schema
};

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"]}`.

Validation errors cover input that fails the declared schema **before** the handler runs. For errors thrown **inside** the handler — typed HTTP errors like `NotFoundError`, third-party errors you map yourself, escaped Mongoose validation — see [Error handling](04-error-handling.md).

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.

```
Registered routes:
/  (mw: GetUserByToken)
├── GET     /
├── auth  (mw: RateLimiter; pmw: GetUserByToken)
│   ├── login
│   │   └── POST    /auth/login  [request]
│   └── logout
│       └── POST    /auth/logout
└── v1  (mw: ApiLimiter; pmw: GetUserByToken)
    └── container
        ├── GET     /v1/container  [query]
        └── POST    /v1/container  [query]

5 route(s) across 6 node(s) in the tree.
```

Each method line is `METHOD` (padded) followed by the route's full path — handler names aren't printed. `[request]` / `[query]` markers indicate routes with body / query schemas. For middleware: `(mw: …)` lists what's newly attached at that node or route, `pmw: …` lists the inherited chain that already runs above it (deduped, in run order), and `{…}` 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, in this order (security response headers are applied first, before any of these):

[RequestLogger](#requestlogger)

[PrepareAppInfo](#prepareappinfo)

[IpDetector](#ipdetector)

[I18n](#i18n)

[Cors](#cors)

[RequestParser](#requestparser)

## 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/IpDetector.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`).
Routes using it also expose optional numeric `page` and `limit` query
parameters in generated OpenAPI documents automatically.

#### 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 `req.appInfo = { app }`. 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'] }}]
      ]]
    ]);
}
```

#### Typed named policies from config

Keep reusable operational limits in the application's typed configuration
instead of repeating numbers across controllers:

```ts title="src/config/rateLimiter.ts"
import defaults from '@adaptivestone/framework/config/rateLimiter.js';

export default {
  ...defaults,
  policy: {
    personCreate: {
      limiterOptions: { points: 3, duration: 60 * 60 },
    },
  },
};
```

Read the final merged config once when the controller module loads and pass the
selected options object directly to the existing middleware tuple:

```ts
import { getAppInstance } from '@adaptivestone/framework/helpers/appInstance.js';
import AbstractController, {
  type TMiddleware,
} from '@adaptivestone/framework/modules/AbstractController.js';
import RateLimiter from '@adaptivestone/framework/services/http/middleware/RateLimiter.js';

const { policy } = getAppInstance().getConfig('rateLimiter');

class Person extends AbstractController {
  static get middleware(): Map<string, TMiddleware> {
    return new Map([
      ['/{*splat}', []],
      ['POST/', [[RateLimiter, policy.personCreate]]],
    ]);
  }
}
```

Run `npm run gen` after adding the config. Generated config types preserve the
policy names, so `policy.personCreate` is autocompleted and a misspelling fails
TypeScript; `RateLimiter` receives the actual options object and performs no
string lookup.

For route-level policies, a simple initialized `const` config read before the
literal route return is also statically analyzable:

```ts
get routes() {
  const { policy } = this.app.getConfig('rateLimiter');
  return {
    post: {
      '/': {
        handler: this.create,
        middleware: [[RateLimiter, policy.personCreate] as const],
      },
    },
  };
}
```

The returned route tree must remain literal. Loops, conditionals, mutable setup,
computed route keys, and dynamically constructed middleware are still skipped
by route-type generation with a warning. The `as const` keeps the route-level
middleware pair typed as a tuple; the AST extractor safely unwraps this
TypeScript-only annotation. Route-type generation never executes the getter —
the `const` prelude is checked by statement shape only, so an initializer that
happens to have side effects is accepted, and those effects run at runtime only.

Declare the policy catalogue in the base config, then override policy values in
environment-specific config files. Controller modules capture the effective
boot-time value; a later `app.updateConfig()` does not mutate limiter instances
that already exist. Config merging uses `deepmerge`, so array fields are
concatenated rather than replaced—most policies should override scalar
`limiterOptions` such as `points` and `duration`.

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, selected via the `driver` option (`'mongo'`, `'memory'`, or `'redis'`). The default is `'mongo'`. The `'redis'` backend lazy-loads `@redis/client`, which is an **optional peer dependency** — install it (`npm i @redis/client`) when you choose that driver; the `'memory'` and `'mongo'` backends never load it.

### 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;
	}
}

```

## 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() {
    // Security scheme(s) this middleware enforces — read by the OpenAPI
    // generator into `components.securitySchemes` (see the OpenAPI chapter).
    return [
      {
        name: "Authorization", // name of the parameter
        type: "apiKey", // apiKey, http, oauth2, openIdConnect
        in: "header", // header, query, cookie
        description: this?.description,
      },
    ];
  }

  // optional — declared `static` so the framework reads the schema WITHOUT
  // instantiating the middleware (no constructor side effects at route setup /
  // codegen)
  static get relatedQueryParameters() {
    // A Standard Schema-conformant schema (Zod, Valibot, ArkType, yup ≥1.7, or
    // `defineSchema`) for middleware-related `req.query` parameters. Validated
    // and exposed on `req.appInfo.query`, relative to the route the middleware
    // is declared on.
    return yup.object().shape({
      limit: yup.number(), // For example
    });
  }

  // optional
  static get relatedRequestParameters() {
    // Same, for middleware-related `req.body` parameters — exposed on
    // `req.appInfo.request`.
    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;
```

`static get usedAuthParameters()` declares the security scheme(s) the middleware enforces. The OpenAPI generator reads it off the class (no instantiation) to populate `components.securitySchemes` and mark every route in the middleware's chain as secured — see [OpenAPI › Documenting auth](../17-openapi.md#documenting-auth-security-schemes) for the field reference and an `http`/`bearer` example.

:::warning Deprecated: instance schema getters
The non-static form — `get relatedQueryParameters()` / `get relatedRequestParameters()` (and `get relatedReqParameters()`) — is **deprecated and will be removed in v6**. It forces the framework to instantiate the middleware (running its constructor) just to read the schema, so use `static get` instead. The instance form still works through v5: when it's detected, the framework instantiates the middleware as a fallback and emits a one-per-class `DeprecationWarning` (`ASF_DEP_MW_INSTANCE_SCHEMA`).
:::

### Typed contributions to `req.appInfo`

If your middleware sets fields on `req.appInfo`, declare them via `static get provides()` so codegen can type them on every downstream handler — no per-handler intersections required.

```ts
import type { MongoAbility } from "@casl/ability";
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class UserWithRole extends AbstractMiddleware {
  static get provides() {
    // Phantom — the runtime returns this `{}` but only the cast matters.
    return {} as { permissions: MongoAbility };
  }

  async middleware(req, res, next) {
    req.appInfo.permissions = await buildPermissions(req.appInfo.user);
    next();
  }
}
```

Any route with `UserWithRole` in its chain now has `req.appInfo.permissions` typed automatically. The cast type is the contract; the runtime value is ignored.

See the [routes chapter codegen section](./02-routes.md#middleware-provided-types) for the full picture of how generated request types compose middleware-provided fields, schemas, and path params.


---


# Document 10: 06-Controllers > 04-error-handling

<!-- Source: 06-Controllers/04-error-handling.md -->

# Error handling

What happens when a route handler throws? The framework resolves the error through an ordered **error-handler registry**:

1. **Your registered handlers** — checked first, in registration order.
2. **Built-ins** — the `HttpError` mapper, the Mongoose validation safety net, then the Mongoose cast safety net.
3. **Fallback** — nothing matched: the error is logged at `error` level and the client gets `500 {"message": "Platform error. Please check later or contact support"}`.

The first entry whose error class matches (`instanceof`) and whose handler returns a response wins. That gives you two tools: **throw** typed HTTP errors from your own code, and **register** handlers for error types you don't own.

## Throwing HTTP errors from your code

Deep inside business logic you often know the right HTTP answer — the document doesn't exist, the user isn't allowed — but you don't have `res` there, and threading it through every function is noise. Throw instead:

```js
import { NotFoundError, ForbiddenError } from "@adaptivestone/framework/services/http/httpErrors.js";

class Posts extends AbstractController {
  get routes() {
    return {
      get: { "/:id": { handler: this.getOne } },
    };
  }

  async getOne(req, res) {
    const post = await this.app.getModel("Post").findById(req.params.id);
    if (!post) {
      throw new NotFoundError("Post not found");
      // → 404 {"message": "Post not found"}
    }
    if (String(post.ownerId) !== req.appInfo.user.id) {
      throw new ForbiddenError("Not your post");
      // → 403 {"message": "Not your post"}
    }
    return res.status(200).json({ data: post.getPublic() });
  }
}
```

It works from any depth — a service function three calls down can throw the same way.

Available classes (all from `services/http/httpErrors.js`):

| Class | Status | Default message |
| --- | --- | --- |
| `BadRequestError` | 400 | `Bad request` |
| `UnauthorizedError` | 401 | `Unauthorized` |
| `ForbiddenError` | 403 | `Forbidden` |
| `NotFoundError` | 404 | `Not found` |
| `ConflictError` | 409 | `Conflict` |
| `HttpError` | any | — (base class) |

Every constructor accepts `(message, body?)`. The response body is `{ message }` unless you pass an explicit `body`, which replaces it:

```js
throw new HttpError(422, "Unprocessable", { errors: { csv: "row 17 malformed" } });
// → 422 {"errors": {"csv": "row 17 malformed"}}
```

For a status you use often, subclass once and throw everywhere:

```js
import { HttpError } from "@adaptivestone/framework/services/http/httpErrors.js";

export class PaymentRequiredError extends HttpError {
  constructor(message = "Subscription expired") {
    super(402, message);
  }
}
```

Thrown `HttpError`s are logged at `verbose` level — they're deliberate control flow, not defects, so they don't pollute your error logs.

## Mapping errors you don't own

Libraries throw their own error types — the Mongo driver, payment SDKs, queue clients. Register a handler for the class, typically from the [`bootHttp` hook](01-intro.md#project-boot-hook-boothttp) (the `Server` constructor option that runs with the live app):

```js
import { MongoServerError } from "mongodb";

const server = new Server({
  ...folderConfig,
  bootHttp: async (app) => {
    if (!app.httpServer) {
      throw new Error("bootHttp ran without a live httpServer");
    }
    app.httpServer.registerErrorHandler(MongoServerError, (err) =>
      err.code === 11000
        ? { status: 409, body: { message: "Already exists" } }
        : null, // null = "not mine after all" → try the next entry
    );
  },
});
```

:::tip Register them in tests too
The framework builds its own server for tests and does not read your `Server`
options, so handlers registered here are absent under test — the 409 above comes
back as a 500. Pass the same `bootHttp` to
[`configureTestServer`](../09-testsing.md#production-http-wiring-boothttp) in
your test setup.
:::

The handler contract:

- **Signature**: `(err, req) => { status, body } | null` — async is fine, the result is awaited.
- `err` is typed as an instance of the class you registered — `err.code` autocompletes, no casts.
- `req` is the same request the route handler had — `req.appInfo.request`, `req.appInfo.i18n`, etc.
- Return `{ status, body }` to produce the response (the framework sends it — handlers never touch `res`, so double-send protection and logging stay in one place).
- Return `null`/`undefined` to pass the error to the next entry.
- `registerErrorHandler` returns an **unregister function** — handy in tests.
- Third argument `{ logLevel }` controls how the handled error is logged (default `warn`):

```js
app.httpServer.registerErrorHandler(
  StripeCardError,
  (err) => ({ status: 402, body: { message: err.declineReason } }),
  { logLevel: "verbose" },
);
```

If a handler itself throws, the framework logs it (with the stack) and falls back to the 500 — a broken error handler can never crash the request pipeline.

Handler-side types, if you want to extract the function:

```ts
import type { ErrorHandlerFn, ErrorHandlerResult } from "@adaptivestone/framework/services/http/builtinErrorHandlers.js";
```

### Reading the request in a handler

`req` is the full framework request, so a handler can build responses from everything the route knew — path params, validated body and query, locale, client IP. A fuller example: a task-creation route hits a unique index, and the handler turns the raw driver error into an answer that names what collided and where:

```js
// Route: POST /project/:projectId/tasks?notify=email
//        request: object({ title: string().required() })
//        query:   object({ notify: string() })
// Model: title has a unique index per project → E11000 on duplicates.

app.httpServer?.registerErrorHandler(MongoServerError, (err, req) => {
  if (err.code !== 11000) {
    return null; // other driver errors → next entry (→ 500 fallback)
  }

  // Which unique field collided — E11000 carries it in `keyValue`.
  const [field] = Object.keys(err.keyValue ?? {});

  return {
    status: 409,
    body: {
      // Translated for the request's locale, like any handler would do.
      message: req.appInfo.i18n?.t("errors.taskExists") ?? "Task already exists",
      field,                                    // "title"
      projectId: req.params.projectId,          // raw path param (string)
      attempted: req.appInfo.request?.title,    // validated body value
      notify: req.appInfo.query?.notify ?? null, // validated query value
    },
  };
});
```

What's available on `req`:

| Source | What it is | Caveat |
| --- | --- | --- |
| `req.params` | Path params (`:projectId`) | Raw **strings**, never validated — declare a route [`params:` schema](02-routes.md#params) and read `req.appInfo.params` instead |
| `req.appInfo.params` | Validated, coerced path params | Only set when the route declares a `params:` schema |
| `req.appInfo.request` | Validated, cast request body | Only set when the route declares a `request:` schema — guard with `?.` in handlers registered for many routes |
| `req.appInfo.query` | Validated, cast query string | Same — needs a `query:` schema |
| `req.appInfo.i18n` | `t()` + detected `language` | Present on framework routes; typed optional |
| `req.appInfo.ip` | Client IP (proxy-aware) | From the global IP detector |
| `req.appInfo.user` | Authenticated user document | Only on routes running the auth middleware (`GetUserByToken`) |
| `req.method`, `req.path`, `req.headers`, … | Anything Express exposes | — |

One design boundary to keep in mind: the handler decides the **response**; the framework does the sending and the logging. If you find a handler reaching for `res` or a logger, that logic probably belongs in the route handler's own `try/catch` instead.

## Matching order

Entries are matched by `instanceof` in **registration order — not by class specificity**. Your handlers always run before the built-ins, so you can intercept or override anything, including the built-ins themselves.

:::warning
An early handler for a base class shadows later handlers for its subclasses. If you register a handler for `HttpError` and later one for `NotFoundError`, the `HttpError` one wins for every `NotFoundError` thrown — it was registered first and `NotFoundError instanceof HttpError` is true. Register the specific classes first, or branch inside one handler.
:::

## Built-in: the Mongoose validation safety net

The recommended practice is to mirror model constraints in your route schema — a `maxLength: 50` in the model should have a `.max(50)` in the route's `request:` schema, so bad input fails fast with a clean, translated 400 (see [Validation](02-routes.md#validation)).

But when a constraint slips through, `doc.save()` throws a Mongoose `ValidationError`, and a built-in registry entry catches it:

- If **every** failing model path is a field the client actually sent (a key of the validated `request:`/`query:` input), the client gets `400 {"errors": {"name": "..."}}` — same shape as route validation errors — and the framework logs a `warn`: your route schema is missing a constraint worth mirroring.
- If **any** failing path is internal or renamed (the client sent `name`, the model field is `userName`), it stays an honest **500**. Model field names are never leaked to the client, and a server-side data bug is never blamed on the client.

Each message is **rebuilt from the validation kind and the schema constraint** — `maxlength` → `"Must be at most 255 characters"`, a `Number` cast failure → `"Must be a number"`, `enum` → `"Must be one of: …"` — and **never includes the value the client submitted**. Mongoose's own default messages interpolate that value (a phone number, a password pasted into the wrong field), which would otherwise leak it into the response and the log. For the same reason a *custom* message set on the model (`maxLength: [50, 'Name too long']`) is **not** passed through — it's rebuilt generically, since a custom string can't be told apart from a templated default that embedded the value. The `warn` log line for a handled error is sanitized the same way; a failure that stays a 500 logs the original error in full. These fallback messages are plain English and not translated; put user-facing, i18n wording on the route schema.

Note this covers Mongoose *validation* errors only. A duplicate-key violation (`E11000`) is a `MongoServerError` from the driver, not a `ValidationError` — map it yourself as shown above if you want a 409. A standalone **cast** failure is a `CastError`, a sibling of `ValidationError` rather than a subclass, so it has its own built-in — described next.

:::tip
The safety net is a fallback, not the contract. Route schemas are the API's source of truth: they produce field-accurate, i18n-translated errors under the names the client knows. The safety net exists so a missed constraint degrades to a useful 400 instead of a mystery 500.
:::

## Built-in: the Mongoose cast safety net

The classic version of this is a path param handed straight to a model:

```js
async getPerson(req, res) {
  // `req.params.id` is a raw, unvalidated string
  const person = await this.app.getModel("Person").findById(req.params.id);
  return res.json({ data: person });
}
```

`GET /person/abc` cannot be cast to an ObjectId, so Mongoose throws a `CastError`. A `CastError` is **not** a `ValidationError` — they are siblings — so the validation safety net above structurally cannot see it. A built-in entry handles it separately:

- If the rejected value is one the **client actually supplied** — matched by value against the path params and the validated `request:`/`query:` input — the client gets `400 {"errors": {"id": "Must be a valid id"}}`, keyed by the public input name, logged at `warn`.
- If the value was **computed server-side**, nothing matches and it stays an honest **500** at `error` level. A bug in your own code is never blamed on the caller.

The message is rebuilt from the cast *kind* (`Must be a valid id`, `Must be a number`, `Must be a valid date`), so neither the rejected value nor the internal model path (`_id`) reaches the response — and the `warn` log line is sanitized the same way.

:::tip
This is a floor, not a design. Declaring a [`params:` schema](02-routes.md#params) rejects the same request earlier, with your own wording, i18n, and coercion — and it documents the constraint in your [OpenAPI output](../17-openapi.md). Reach for the floor only where you haven't got round to a schema yet.
:::

## What still becomes a 500

- Any error no registry entry claims (including `null` returns all the way down).
- A registry handler that throws while handling.
- Mongoose validation failures on internal/renamed fields (see above).
- Mongoose cast failures on server-computed values (see above).

All of these are logged at `error` level with the original error, so the details are in your logs — the client only ever sees the generic message.


---


# Document 11: 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.ts’.

```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

The framework's Sentry transport reuses the Sentry SDK your app already initialized — it does **not** read a DSN itself. Configure the DSN in your own `Sentry.init({ dsn: ... })` at startup (the project template uses the `LOGGER_SENTRY_DSN` env var there by convention). The framework reads only:

**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 12: 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 

The framework provides an I18n middleware that runs on each HTTP request.

## 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.ts’ 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\}/translation.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 instance provides 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 13: 09-testsing

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

# Testing

The framework supports Node.js's built-in test runner and Vitest. The built-in
runner is the default for new projects: it executes TypeScript test files
natively, has no additional test-runner dependency, and integrates with the
framework's shared MongoDB and per-test isolation helpers. Node.js 24 remains
the minimum supported runtime; the framework's own test infrastructure and CI
run on Node.js 26.

Vitest remains a fully supported alternative. Choose the runner that best fits
the project's existing tooling and plugins; the native runner is not presented
as an execution-speed upgrade.

Name tests `*.test.ts` or `*.test.js` and keep them next to the file they cover.
For example, test `src/controllers/Auth.ts` in
`src/controllers/Auth.test.ts`.

## Install the test database

The framework loads `mongodb-memory-server` lazily, so applications that use
the supplied in-memory MongoDB global setup must install it directly:

```bash
npm install --save-dev mongodb-memory-server
```

Redis is optional during tests. When `REDIS_URI` is configured, the framework
uses a fresh namespace for each test and clears it afterward.

## Node.js test-runner setup

Node.js runs every test file in a separate process. The framework therefore
splits its lifecycle into:

- a global setup that starts one MongoDB replica set for the complete run;
- per-file hooks that start and stop a framework server with a fresh database;
- per-test hooks that isolate the Redis namespace.

### Project test configuration

Keep project-specific test configuration in `src/tests/setup.ts`. This file can
set test folder locations or other environment required before the framework
server starts.

Create one preload file at `src/tests/setupNodeTest.ts`:

```ts
import './setup.ts';
import '@adaptivestone/framework/tests/setupNodeTest.js';
import './setupHooks.ts';
```

The runner preloads this file for every test file. Do not import it from each
individual test.

### Production HTTP wiring (`bootHttp`)

The framework builds the test server itself, so the `Server` options your
`src/index.ts` passes are not picked up automatically — including the
[`bootHttp` hook](06-Controllers/01-intro.md#project-boot-hook-boothttp).
Declare them with `configureTestServer` from `src/tests/setup.ts`, which loads
before the framework preload:

```ts title="src/tests/setup.ts"
import { configureTestServer } from '@adaptivestone/framework/tests/testHelpers.js';
import bootHttp from '../bootHttp.ts';

configureTestServer({ bootHttp });
```

Point it at the **same** function production uses, rather than re-registering
the wiring from a test hook: two copies drift, and the copy that drifts is the
one the tests trust. Without this, tests run against a server that never ran
that wiring at all — an error handler registered in `bootHttp` does not exist,
so a request that returns 409 in production returns 500 under test, with nothing
reporting the difference.

`folders` is not accepted here; the bootstrap owns those (see the
`TEST_FOLDER_*` variables below). Call `configureTestServer` at module scope,
before any hook boots the server — calling it afterwards throws, because a late
call cannot retroactively wire the server that is already running.

### Global MongoDB setup

Create `src/tests/globalSetupNodeTest.ts`:

```ts
export {
  globalSetup,
  globalTeardown,
} from '@adaptivestone/framework/tests/globalSetupNodeTest.js';
```

`globalSetup` starts MongoDB once and passes `TEST_MONGO_URI` to the child test
processes. `globalTeardown` stops it after every test file has finished.

### Custom hooks

Project hooks can live in `src/tests/setupHooks.ts`:

```ts
import { after, afterEach, before, beforeEach } from 'node:test';
import {
  createDefaultTestUser,
  ensureTestServerReady,
} from '@adaptivestone/framework/tests/testHelpers.js';

before(async () => {
  // Root hooks registered by separate modules are siblings and may start
  // concurrently. Always await framework readiness before using app state.
  await ensureTestServerReady();
  await createDefaultTestUser();
});

after(async () => {
  // Clean up project-level test state.
});

beforeEach(async () => {
  // Prepare each test.
});

afterEach(async () => {
  // Clean up each test.
});
```

The framework hooks are already registered by `setupNodeTest.js`. Project hooks
should contain only application-specific preparation and cleanup.

Node.js does not serialize sibling root-level `before()` hooks registered by
different modules. A project root hook that reads config, models, `appInstance`,
or the HTTP server must call `await ensureTestServerReady()` first. The helper
and framework preload share one idempotent startup promise, so concurrent calls
wait for the same server instead of constructing two servers.

Setup used by only one test file should normally live inside the same
`describe()` as those tests. Suite-scoped hooks wait for the root framework
hook and do not accidentally affect unrelated suites.

### Validation messages and application locales

The framework test helper loads its built-in locale folder by default. It does
not automatically load the application's locale folder: set
`TEST_FOLDER_LOCALES` in `src/tests/setup.ts` when a suite specifically needs
rendered application copy.

Without that opt-in, application-specific validation message keys remain raw
in HTTP 400 responses. This is intentional for ordinary API tests—assert the
stable key and status code rather than translated prose that can change between
locales. A copy-specific test may point `TEST_FOLDER_LOCALES` at
`src/locales`, but should do so before `setupNodeTest.js` loads.

### Package scripts

Use a small command for local tests and watch mode. Keep coverage and reporters
in the CI command so ordinary development runs stay fast:

```json
{
  "scripts": {
    "test": "node --import=./src/tests/setupNodeTest.ts --test --test-global-setup=./src/tests/globalSetupNodeTest.ts \"src/**/*.test.ts\"",
    "t": "node --import=./src/tests/setupNodeTest.ts --test --watch --test-global-setup=./src/tests/globalSetupNodeTest.ts \"src/**/*.test.ts\"",
    "test:ci": "mkdir -p coverage && node --import=./src/tests/setupNodeTest.ts --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-exclude=\"src/tests/**\" --test-coverage-lines=80 --test-coverage-branches=80 --test-coverage-functions=75 --test-global-setup=./src/tests/globalSetupNodeTest.ts --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=coverage/junit.xml --test-reporter=lcov --test-reporter-destination=coverage/lcov.info \"src/**/*.test.ts\""
  }
}
```

Run the suite once:

```bash
npm test
```

Run it in watch mode:

```bash
npm run t
```

Run the CI configuration with coverage thresholds and LCOV output:

```bash
npm run test:ci
```

The example thresholds are 80% for lines, 80% for branches, and 75% for
functions. These are enforcement flags: a below-threshold run exits with code
1 even when multiple reporters are enabled. Adjust them deliberately as the
project grows. LCOV is written to `coverage/lcov.info`, and JUnit test results
are written to `coverage/junit.xml`.

Native global setup, whole-module mocks, and test coverage remain experimental
Node.js test-runner surfaces. Pin the test runtime in CI rather than following
an unbounded `latest` release.

## Writing a Node.js test

Use `node:test` with the standard strict assertion module:

```ts
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { getTestServerURL } from '@adaptivestone/framework/tests/testHelpers.js';

describe('person', () => {
  it('creates a person', async () => {
    const response = await fetch(getTestServerURL('/person'), {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        name: 'Example Person',
        age: 30,
      }),
    });

    assert.equal(response.status, 200);
  });
});
```

The preload and global setup belong in the runner command, not in this file.

### Assertion plans

`t.plan(n)` counts assertions made through `t.assert` (and registered
subtests), not calls to a separately imported `node:assert` object. If a test
uses a plan, use `t.assert.*` for every assertion in that test:

```ts
import { it } from 'node:test';

it('returns one result', (t) => {
  t.plan(2);
  t.assert.strictEqual(1, 1);
  t.assert.deepStrictEqual([{ id: 1 }], [{ id: 1 }]);
});
```

Top-level `node:assert/strict` remains appropriate in tests that do not use
`t.plan()`.

### Migration traps from Jest and Vitest

- `assert.partialDeepStrictEqual(actual, expected)` applies subset semantics to
  arrays as well as objects. Assert the array length separately when extra
  elements must fail the test. For rejected errors, use the two-argument
  `assert.rejects(promise, errorPattern)` form. Compare selected Mongoose
  document fields or deliberately normalize the document before deep matching.
- Calling `mockImplementationOnce()` twice before the mock is invoked targets
  the same next call and the later registration replaces the earlier one. Pass
  explicit zero-based `onCall` indices when queuing several results:

  ```ts
  const load = t.mock.method(service, 'load');
  load.mock.mockImplementationOnce(() => firstResult, 0);
  load.mock.mockImplementationOnce(() => secondResult, 1);
  ```

- TypeScript represents `mock.calls[n].arguments` using one overload of an
  overloaded method, which may not be the overload exercised by the test.
  Prefer assertions at the public API boundary. When argument inspection is
  necessary, keep any `unknown`-first tuple cast local to that assertion.

## Framework and server access

Use `appInstance` for the initialized framework application:

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

Use `serverInstance` when a test needs lower-level access to the server:

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

## HTTP endpoint testing

The framework starts each test server on a random available port.
`getTestServerURL()` returns the correct URL:

```ts
import assert from 'node:assert/strict';
import { it } from 'node:test';
import {
  defaultAuthToken,
  getTestServerURL,
} from '@adaptivestone/framework/tests/testHelpers.js';

it('rejects an invalid request', async () => {
  const response = await fetch(getTestServerURL('/some/endpoint'), {
    method: 'POST',
    headers: {
      authorization: defaultAuthToken,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ invalid: true }),
  });

  assert.equal(response.status, 400);
});
```

Do not catch and discard `fetch()` errors in tests. A rejected request should
fail the test and preserve the original error.

## Default test user

The framework does not create a user automatically. Call
`createDefaultTestUser()` from project setup when a suite needs one:

```ts
import {
  createDefaultTestUser,
  defaultAuthToken,
  defaultUser,
} from '@adaptivestone/framework/tests/testHelpers.js';

const { user, token } = await createDefaultTestUser();
```

`defaultUser` and `defaultAuthToken` reference the values created by the helper.
Projects with a custom User model should implement their own creation helper and
use `setDefaultUser()` and `setDefaultAuthToken()`.

## Test helpers

The public helpers are exported from
`@adaptivestone/framework/tests/testHelpers.js`:

```ts
import {
  createDefaultTestUser,
  defaultAuthToken,
  defaultUser,
  ensureTestServerReady,
  getTestServerURL,
  serverInstance,
  setDefaultAuthToken,
  setDefaultUser,
} from '@adaptivestone/framework/tests/testHelpers.js';
```

- `getTestServerURL(path)` returns the active test server URL.
- `ensureTestServerReady()` waits for the initialized per-file server and is
  safe to call concurrently from application root hooks.
- `serverInstance` exposes the current test server.
- `createDefaultTestUser()` creates the framework's default User and token.
- `setDefaultUser()` and `setDefaultAuthToken()` support custom User models.

## MongoDB and Docker

The global setup uses `MongoMemoryReplSet` with one `wiredTiger` member. Each
test file receives a unique database, and the framework drops it during
teardown.

MongoDB publishes ARM64 binaries for Ubuntu, but not for every Debian release
used by official Node.js Docker images. Use the project's Ubuntu-based Node
image for GitLab and ARM64 Docker testing:

```text
registry.gitlab.com/adaptivestone/ubuntu-node:latest
```

When installation and tests run in separate CI jobs, use the same image for
both. This prevents Linux, libc, Node.js, and native dependency mismatches. The
current Ubuntu image supports MongoMemoryServer's automatic distro and MongoDB
version selection; application setup should not normally set
`MONGOMS_DISTRO` or `MONGOMS_VERSION`.

This template uses `npm install` in CI and production image builds. npm can
remove optional dependency records for other CPU architectures when it rewrites
`package-lock.json`; a later `npm ci` on another architecture may then reject
the otherwise valid lockfile. `npm install` repairs those optional records in
the CI workspace and avoids making an ARM-generated lockfile block an x64 job.

## GitLab CI

The following pipeline installs dependencies once and runs quality checks and
tests with the exact same Ubuntu/Node artifact:

```yaml
stages:
  - install
  - checks

default:
  # Keep dependency installation and execution on the same Ubuntu/Node image.
  # Ubuntu is also required for mongodb-memory-server binary availability.
  image: registry.gitlab.com/adaptivestone/ubuntu-node:latest

install:
  stage: install
  script:
    # Keep cross-platform optional dependencies resolvable after an ARM install.
    - npm install
  artifacts:
    paths:
      - node_modules/
    expire_in: 2 hours

quality:
  stage: checks
  needs:
    - install
  script:
    - npm run check

tests:
  stage: checks
  needs:
    - install
  services:
    - redis:latest
  variables:
    EMAIL_TRANSPORT: stub
    REDIS_URI: redis://redis
  script:
    - npm run test:ci
  coverage: '/[Aa]ll files[^|]*\|[^|]*\s+([\d\.]+)/'
  artifacts:
    when: always
    paths:
      - coverage/lcov.info
```

## GitHub Actions

GitHub-hosted Ubuntu runners can use Node.js 26 for the current test
infrastructure. Applications that deliberately verify the minimum supported
runtime may add a separate Node.js 24 job:

```yaml
name: Test

on:
  push:
    branches: ['*']

jobs:
  test:
    name: Node 26
    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@v6
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v6
        with:
          node-version: '26'
          cache: npm

      - name: Install dependencies
        # ARM npm can omit optional x64 peers when it rewrites package-lock.json.
        run: npm install

      - name: Run tests
        run: npm run test:ci

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v6
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

      - name: Upload test results to Codecov
        if: ${{ !cancelled() }}
        uses: codecov/codecov-action@v6
        with:
          report_type: test_results
          files: coverage/junit.xml
          token: ${{ secrets.CODECOV_TOKEN }}
```

## Mocking

Node.js provides function and method mocks through each test's `MockTracker`:

```ts
import assert from 'node:assert/strict';
import { it } from 'node:test';
import S3 from '../S3.ts';

it('validates credentials', (t) => {
  const validateCreds = t.mock.method(S3, 'validateCreds', () => true);

  assert.equal(S3.validateCreds(), true);
  assert.equal(validateCreds.mock.callCount(), 1);
});
```

Mocks created through `t.mock` are restored automatically after the test.
Whole-module ESM mocking is experimental in Node.js 26 and requires
`--experimental-test-module-mocks`; prefer method mocks or dependency injection
unless module replacement is necessary.

## Vitest setup

Vitest is a fully supported alternative runner and remains an optional peer
dependency. Install it in projects that choose it:

```bash
npm install --save-dev vitest
```

Configure the framework's public adapters instead of importing lifecycle
internals:

```ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globalSetup: [
      '@adaptivestone/framework/tests/globalSetupVitest.js',
    ],
    setupFiles: [
      './src/tests/setup.ts',
      '@adaptivestone/framework/tests/setupVitest.js',
      './src/tests/setupHooks.ts',
    ],
  },
});
```

`./src/tests/setup.ts` loads before the framework adapter here too, so
[`configureTestServer`](#production-http-wiring-boothttp) belongs in the same
place for both runners.

Vitest hooks and mocks should use Vitest's APIs. Do not load the Node.js and
Vitest adapters in the same test run. The framework does not require a Vitest
coverage provider; projects that enable Vitest coverage should install and
configure their preferred provider separately.


---


# Document 14: 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

Your project's CLI entry must forward the command result to the process exit
code. This makes failed migrations, code generation, and other commands fail CI
and deployment steps instead of appearing successful:

```ts
import Cli from "@adaptivestone/framework/Cli.js";
import folderConfig from "./folderConfig.ts";

const cli = new Cli(folderConfig);
const result = await cli.run();

process.exit(result ? 0 : 1);
```

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, args) {
    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;
```

:::note Model commands wait for the database connection

`static isShouldInitModels = true` (the default) makes the framework load and initialize your models before the command runs **and wait for the MongoDB connection to be ready** — so your command's first query never races a not-yet-established connection (the cause of intermittent "buffering timed out" errors). You don't need to add your own connection-readiness check.

Set it to `false` for commands that don't touch the database (e.g. code generation, crypto helpers) so they start instantly without connecting. `static isShouldGetModelPaths` loads model _paths_ only (for type/path resolution) — it does not initialize models or open a connection.

:::

:::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` and its `AppModelTypes` map so `getConfig('foo')`, `getModel('Bar')`, and known-name `getModelOrThrow('Bar')` calls are typed. Config inference widens homogeneous arrays to `T[]`, preserves heterogeneous tuples, and keeps finite object keys exact; see [Typed config](02-configs.md#typed-config).
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

:::note Requires `oxc-parser`
Code generation parses your controller sources with [`oxc-parser`](https://www.npmjs.com/package/oxc-parser), an **optional peer dependency**. Install it as a devDependency — `npm i -D oxc-parser`. It is never loaded at runtime, so it stays out of production installs; the command fails with that instruction if it is missing.
:::

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

#### Recommended setup

Add to `package.json`:

```json
"gen": "node src/cli.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.

#### When to run codegen

Codegen only affects **types**, never runtime — so you run it whenever something that changes a handler's or `IApp`'s type shape changes, then type-check. In practice you wire it into `check:types` and forget it's there. The decision matrix:

| You changed… | Run `gen`? | Why |
|---|---|---|
| A route's `request:` / `query:` schema | **Yes** | `req.appInfo.request` / `.query` types change |
| A route path (added `:param` / `{*splat}`, renamed) | **Yes** | `req.params` and the `<Method>Request` alias change |
| A controller's `static get middleware()` chain | **Yes** | which `provides` fields land on `req.appInfo` changes |
| A middleware's `static get provides()` | **Yes** | downstream `req.appInfo` types change |
| Added / renamed a model or config | **Yes** | `getModel('X')` / `getConfig('Y')` typings change |
| Only handler body logic (no signature/schema change) | No | the generated types are unchanged |
| Prose, comments, formatting | No | nothing type-bearing changed |

When in doubt, just run it — it's idempotent and cheap. The standard wiring (`"check:types": "npm run gen && tsc --noEmit"`) regenerates before every type-check, so you never run it by hand in CI. See [Routes → Typed handler signatures](06-Controllers/02-routes.md#typed-handler-signatures-codegen).

### Generate OpenAPI

Generates an OpenAPI 3.1 document from your controllers (paths, parameters, request bodies, security, tags) — derived from the route definitions you already write. Opens no database/network connection and binds no port, so it's safe in CI.

```bash
node src/cli.ts openapi                       # print to stdout
node src/cli.ts openapi --output openapi.json # write to a file
```

See the [OpenAPI chapter](17-openapi.md) for what's documented, how schemas are introspected, and how middlewares contribute security schemes.

### List Routes

Prints your project's route tree — every mounted method, full path, path/splat parameters, and the middleware chain that runs for each route — by walking the same route registry the server logs at boot. Like `openapi`, it opens no database/network connection and binds no port.

```bash
node src/cli.ts routes
```

Example output:

```text
Registered routes:
/
├── GET     /
└── auth  (mw: GetUserByToken, RateLimiter)
    ├── login
    │   └── POST    /auth/login  [request]
    └── verify
        └── POST    /auth/verify  [request]

3 route(s) across 5 node(s) in the tree.
```

Reading the annotations:

- **`(mw: A, B)`** — middleware newly attached at this node. Inherited middleware from parent nodes is shown as `pmw:` so each route line is self-contained about what runs before it.
- **`[request]` / `[query]`** — this route **validates** a request body / query string. They are presence flags, not the schema itself: a route either declares a validator or it doesn't. To see the actual field shapes, run `openapi` — it resolves each schema to JSON Schema (where the validator supports introspection). Imperative validators such as `defineSchema` can supply an explicit `jsonSchema`; without one they appear here as `[request]` and degrade to a placeholder in the OpenAPI document.

Useful for answering "what's actually mounted in my app?" without starting the server. For the full request/response contract, use [`openapi`](#generate-openapi).


---


# Document 15: 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. A store time of `0` means **"don't cache"** — the callback runs on every call and nothing is written.

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.

:::

## Drivers

The cache is built on a small `CacheDriver` interface (`get` / `set` / `del`), with two first-party drivers:

- **`memory`** — **the default.** A per-process `Map` with per-key TTL. Needs no external service, so a plain install works out of the box and never loads `@redis/client`. Because it is per-process, each clustered worker has its own cache — fine for development and single-node deployments.
- **`redis`** — a shared cache backed by Redis. Use this for **multi-node deployments** (or anywhere multiple processes must see the same cached values). It lazy-loads `@redis/client`, which is an **optional peer dependency** — install it yourself (`npm i @redis/client`) when you select this driver.

The orchestration around the driver — namespacing, single-flight request dedup, serialization, and fail-soft degradation (a cache outage degrades to running your callback, it never fails the request) — is identical across drivers.

## Configuration

The driver is selected in `config/cache.ts`:

```ts title="config/cache.ts"
export default {
  // 'memory' (default) or 'redis'. Overridable via the CACHE_DRIVER env var.
  driver: (process.env.CACHE_DRIVER || "memory") as "memory" | "redis",
};
```

Two related settings live in `config/redis.ts` rather than here, because they are **shared with the rate limiter**:

- **`namespace`** — a key prefix applied to **every** cache (and rate-limiter) key, regardless of driver. Despite living in `redis.ts`, it is _not_ redis-specific: the in-memory driver prefixes its keys with it too. Think of it as a keyspace/tenant label (e.g. per environment). Keeping it in one place means the cache and rate limiter never drift apart, and the test helper `setTestRedisNamespace` can isolate both with a single switch.
- **`url`** — the Redis connection string, used only by the `redis` backends (this cache and the rate limiter's redis driver share one client). Irrelevant when both run on non-redis drivers.

:::tip Custom driver

You can supply your own backend by setting `driver` to an object implementing `CacheDriver` (`get`, `set`, `del`) instead of a string.

:::


---


# Document 16: 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

A template is a folder of files; each file's extension selects the engine that renders it. For each email you provide an HTML version, a subject, and (optionally) a text version, 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.

By default the module ships only plain-text engines — `html`, `text` and `css` (files are read as-is):

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

To use a real template language such as Pug, register its engine first (see [Template engines](#template-engines)); then your files can be `html.pug`, `subject.pug`, and so on.

## Template engines

:::warning Breaking change in v2

Before v2, Pug was bundled and `.pug` templates worked out of the box. As of **v2 the module ships no template-engine dependency** — only the plain-text `html`, `text` and `css` engines. To keep using `.pug` (or any other language) you must install that engine and register it yourself.

:::

Register an engine by mapping a file extension to a render function. The function receives the absolute path to the template file and the render data, and returns the rendered string (sync or async):

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

// Pug — was bundled by default before v2; now opt-in
Mailer.registerTemplateEngine("pug", (fullPath, data) =>
  pug.compileFile(fullPath)(data),
);

// any engine works the same way
Mailer.registerTemplateEngine("ejs", (fullPath, data) =>
  ejs.renderFile(fullPath, data),
);
```

### Where to register

Engines live in a **single process-wide registry** shared by every `Mailer` instance, so register them **once at process startup, before any email is sent** — not per request and not per `Mailer` instance.

The natural place is the worker bootstrap (`src/server.ts`), the file each worker process runs. Register before `startServer()`:

```js
// src/server.ts
import Server from "@adaptivestone/framework/server.js";
import Mailer from "@adaptivestone/framework-module-email";
import pug from "pug";
import folderConfig from "./folderConfig.ts";

Mailer.registerTemplateEngine("pug", (fullPath, data) =>
  pug.compileFile(fullPath)(data),
);

const server = new Server(folderConfig);
await server.startServer();
```

:::note

The registry is per **process**. If your app uses the cluster manager (`src/index.ts` forking workers), register in `src/server.ts` (which every worker runs), not in the master `src/index.ts` (which never sends mail).

:::

### Registering more than once

`registerTemplateEngine` can be called as many times as you like:

- **Different extensions accumulate** — call it once per engine you want (`pug`, `ejs`, `mjml`, …).
- **The same extension overrides** — the last registration for a given extension wins, so you can replace a built-in or re-register safely. There is no error on re-registration.
- Extensions are normalized, so `"pug"`, `".pug"` and `".PUG"` all target the same engine.

### Helpers

- `Mailer.registerTemplateEngine(extension, engine)` — register/override an engine for a file extension (leading dot optional, case-insensitive).
- `Mailer.unregisterTemplateEngine(extension)` — remove an engine; returns `true` if one was removed.
- `Mailer.hasTemplateEngine(extension)` — check whether an engine is registered.

### 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 17: 12-resize

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

# Image Resizing

Lazy image resizing for the framework, shipped as a separate package: [@adaptivestone/framework-module-resize](https://www.npmjs.com/package/@adaptivestone/framework-module-resize). Upload only the **original**; generate resized variants on demand with [`sharp`](https://sharp.pixelplumbing.com).

The **read path** decides — per requested size + format + filters — whether a preview is ready or missing. Ready ones return immediately; missing ones are **enqueued** and generated by a separate **worker**. Everything the module touches (queue transport, storage, media store, lock provider) is a **swappable driver** wired in one constructor literal.

## Installation

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

Requires Node `>=24` and the framework/mongoose peers (mandatory). The AWS SDKs are **optional peers** — install only the driver you use. Each is resolved **only** when you import its driver subpath, so the main entry never loads the AWS SDKs, and a missing peer fails loudly at your own import line at bootstrap, not at first I/O.

| You use… | Also install |
|---|---|
| **S3 storage** (`/storage/s3.js`) | `@aws-sdk/client-s3` `@aws-sdk/s3-request-presigner` |
| **SQS transport** (`/transports/sqs.js`) | `@aws-sdk/client-sqs` `sqs-consumer` |
| Mongo transport / framework media store / framework locks | nothing |

### Scaffold the integration files

The framework discovers models and commands by scanning your `src/` folder, so a few thin files must live in your app. Generate them once:

```bash
npx @adaptivestone/framework-module-resize resize-scaffold
```

It emits (into `process.cwd()`, or `--out <dir>`), **never overwriting** without `--force`:

| File | What it is |
|---|---|
| `src/resizer.ts` | the construction site — `new Resizer({ … })` |
| `src/models/ResizeTask.ts` | thin `class ResizeTask extends ResizeTaskModel {}` shim (Mongo transport) |
| `src/commands/ResizeWorker.ts` | one-line re-export of the module's worker command |
| `src/config/resize.ts` | editable config that spreads the module defaults |

The shims are **not vendored copies** — schema and behavior stay in the npm package (auto-updates, no drift). Other flags: `--check` (CI drift check), `--eject` (full editable model), `--eager` (eager-mode hosts), `--force`, `--out <dir>`.

## How it works

```
upload ─▶ store the ORIGINAL only (no previews baked at upload)
  read ─▶ resolve({ media, sizes }) ─┬─ ready?   → return the URL now
                                     └─ missing? → enqueue + return a placeholder/original
worker ─▶ download original → beforeSteps → per-variant resize + variantSteps + encode → upload
       ─▶ append preview to the media doc
  next read ─▶ ready
```

Generated **previews** live as metadata on the host's media document (`previews[]`) — the source of truth for what is ready. `resolve()` returns a **decision** (`ready[]` + `missing[]`); missing variants are enqueued while the read returns without ever blocking on `sharp`. This keeps `sharp` + storage I/O off your HTTP create/update handlers.

## Quick start (lazy mode, Mongo + S3)

**1. Wire the Resizer** in the scaffolded `src/resizer.ts`. All drivers are injected in one visible literal and fixed at construction — one Resizer per process (a second `new Resizer()` throws).

```ts
// src/resizer.ts — imported by src/server.ts so it runs in EVERY process (API + worker)
import { Resizer } from '@adaptivestone/framework-module-resize';
import { MongoTransport } from '@adaptivestone/framework-module-resize/transports/mongo.js';
import { S3Storage } from '@adaptivestone/framework-module-resize/storage/s3.js'; // optional AWS peers resolved only here

export const resizer = new Resizer({
  transport: new MongoTransport(),           // or new SqsTransport({ queueUrl, region }); omit for eager-only
  storage: new S3Storage({                   // REQUIRED — shipped driver or any custom ResizeStorage
    bucketPublic: 'my-cdn',
    bucketPrivate: 'my-originals',
    publicUrl: 'https://cdn.example.com',
  }),
  // mediaStore / lockProvider omitted → framework-backed defaults
  pipelines: {
    default: {},
    listing: { beforeSteps: [blurPlates] },  // async detector, applied once to the source
  },
  hooks: {
    resolveSizes:     (sizes, ctx) => ctx.entity === 'event' ? [...sizes, { fit: true }] : sizes,
    formatPublicUrls: (decision, ctx) => toHostDto(decision, ctx),   // your response shape + placeholders
  },
});
```

**2. Import it once from `src/server.ts`** so it runs in both the API and worker processes:

```ts
import './resizer.ts';
```

**3. Set your media model name** in `src/config/resize.ts` — the one required field:

```ts
import defaultResizeConfig from '@adaptivestone/framework-module-resize/config/resize.js';

export default {
  ...defaultResizeConfig,
  mediaModelName: 'File', // your host media model, e.g. 'File' or 'Media'
};
```

Your media model must carry `original` (incl. `width`/`height`) and `previews[]` (incl. `filters`/`fit`). That schema is host-owned; to avoid drift the module exports an **opt-in** `as const` fragment you can spread in:

```ts
import { resizeMediaSchemaFragment } from '@adaptivestone/framework-module-resize';
class File extends BaseModel {
  static get modelSchema() { return { ...existingFields, ...resizeMediaSchemaFragment } as const; }
}
```

**4. Run the worker** as a separate process (gated by `worker.enabled`):

```bash
npm run cli ResizeWorker
```

**5. Read** from your DTO builders. No `app` argument — the module reads the ambient app instance. `resolve` returns both the raw `decision` and the `output` of your `formatPublicUrls` hook:

```ts
import { resizer } from '../resizer.ts';   // or: getResizer()

const { output } = await resizer.resolve({
  media: fileDoc,
  pipeline: 'listing',
  sizes: [
    { width: 1760, height: 990 },
    { width: 620 },
    { fit: true },
    { width: 300, height: 300, filters: { blur: 40 } },
  ],
  ctx: { entity: 'event', isOwner },
});
return output; // your own shape, produced by formatPublicUrls
```

## Modes: lazy vs pre-warm vs eager

All three modes drive the **same resize core** and write the same `previews[]` shape, so you can switch later with no data migration, or mix them.

| | **Lazy** (default) | **Pre-warm** | **Eager** |
|---|---|---|---|
| Generate | on first read; `resolve()` enqueues missing | at upload; `prewarm()` enqueues the catalog | inline at upload via `resizer.generate(...)` |
| Needs | transport + `ResizeWorker` + `ResizeTask` + locks | same as lazy | storage + media model only — **no** queue/worker |
| Best for | high volume, fast uploads, large catalogs | fast uploads **and** a warm cache by first read | low/bursty volume, small fully-used catalogs |

:::tip Default to lazy

It keeps uploads fast and only does work that's actually needed. **Choose eager** when your app is low-volume, your size catalog is small and fully used, and you'd rather every image be ready the instant an upload finishes. The stored shape is identical, so you can switch later or mix the three.

:::

**Pre-warm** keeps the lazy wiring (transport + worker) but pushes the catalog into the queue at upload, so previews are usually ready by the first read. It never blocks and never throws:

```ts
// upload handler, after the media doc is created:
await resizer.prewarm({ media: fileDoc, sizes: getListingSizes(), pipeline: 'listing' });
// → { enqueued } = how many variants were handed to the queue
```

**Eager** constructs the Resizer **without** a `transport` and generates synchronously from the upload handler (`ctx` reaches pipeline steps here, unlike the queued worker):

```ts
const { previews } = await resizer.generate({
  media: fileDoc,
  sizes: getEventMediaSizes(),   // your catalog
  pipeline: 'listing',
  // persist: true (default) → $push previews + backfill dims; false → returns them for you to store
});
```

## Drivers & seams

Four seams, each a single active strategy fixed at construction. Two ship drivers; two default to framework-backed drivers when omitted, so a standard host wires only `transport` + `storage`. Every driver lives behind its own package subpath (the core entry never loads driver deps).

| Seam | Option | Shipped | Subpath import |
|---|---|---|---|
| Queue transport | `transport?` | `MongoTransport`, `SqsTransport` | `…/transports/mongo.js`, `…/transports/sqs.js` |
| Storage | `storage` **(required)** | `S3Storage` | `…/storage/s3.js` |
| Media store | `mediaStore?` | `FrameworkMediaStore` (default) | `…/mediaStore/framework.js` |
| Lock provider | `lockProvider?` | `FrameworkLockProvider` (default) | `…/locks/framework.js` |

Reach the process-wide instance anywhere via `getResizer()` (throws a clear error if none was constructed).

**Custom driver = implement the interface.** Any seam takes a plain object (or class) that satisfies its contract — no `app` parameter; it closes over its own client:

```ts
new Resizer({ /* … */, storage: {
  download: (ref) => s3.getObject(ref.bucket!, ref.key),
  upload: async ({ key, body, contentType, visibility }) => {
    const bucket = visibility === 'public' ? 'my-cdn' : 'my-originals';
    await s3.putObject(bucket, key, body, contentType);
    return { bucket, key };               // ← persisted onto the preview/original
  },
  publicUrl: (ref) => `https://cdn.example.com/${ref.key}`,   // pure; no I/O
  signedUrl: (ref, ttl) => s3.getSignedUrl(ref.bucket!, ref.key, ttl),
}});
```

Contract types (`QueueTransport`, `ResizeStorage`, `MediaStore`, `LockProvider`, …) are exported from the main entry. The shipped `S3Storage` / `SqsTransport` options are listed in the [README](https://github.com/adaptivestone/framework-module-resize#drivers--seams).

## Pipelines & hooks

**Pipelines** are named per-media-type pixel work, selected per read call by name. The worker runs in a separate process, so the task carries only the pipeline **name** — the worker resolves the functions from its own registry.

```ts
pipelines: {
  photo: {
    beforeSteps:  [detectAndBlurPlates, detectAndBlurFaces],  // run ONCE on the source, before any resize
    variantSteps: [(img, { variant }) => variant.filters?.blur ? img.blur(Number(variant.filters.blur)) : img],
  },
  avatar: {},                                                 // no special processing
}
// later / from another module: getResizer().registerPipeline('premium', { … })  (last-wins per name)
```

- **`beforeSteps`** — ordered, awaited, once per task on the source buffer. The home for detection metadata and pixel redaction (plate/face blur) that must apply to every variant. A throwing step fails the task.
- **`variantSteps`** — ordered per-variant chain, after resize, before encode. The home for keyed `filters` and anything sized relative to the output.

:::warning Watermark in variantSteps

Put a watermark in `variantSteps`, **not** `beforeSteps`. Baked onto the original once, a watermark scales down with each variant and becomes unreadable on small sizes.

:::

:::note ctx does NOT cross the queue

In the lazy worker `ctx === {}` — the task carries only `{ mediaId, pipeline, previews }`. Durable per-media data a step needs must be read from the loaded `media` doc. The full caller `ctx` reaches steps **only** in eager mode (`generate`, same process).

:::

**Hooks** are the cross-cutting seams. Taps run in registration order, awaited sequentially, and are error-isolated (a throwing tap is logged, never breaks the read/worker flow).

| Hook | Kind | Runs where |
|---|---|---|
| `resolveSizes` | waterfall | read path (real `ctx`) |
| `beforeEnqueue` | waterfall | read path (real `ctx`) |
| `formatPublicUrls` | waterfall | read path (real `ctx`) |
| `onPreviewGenerated` | observer | worker (`ctx === {}`) |
| `afterTaskComplete` | observer | worker (`ctx === {}`) |
| `onTaskFailed` | observer | per failed attempt (will retry) |
| `onTaskDeadLettered` | observer | task exhausted `maxAttempts` (host can alert/page) |

Register at construction (`hooks:`) or later via `getResizer().hook(name, fn)`. Taps are **typed** (`HookSignatures`): each name infers its exact signature, so a wrong argument or return shape is a compile error instead of a silent `any`. In every observer the `task` argument is the transport-agnostic `LeasedTask` (`{ taskId, mediaId, pipeline, previews }`) on **both** transports — never a raw driver document — so a host tap is portable. Every observer is **also** mirrored on the framework event bus as `resize:<name>` (fire-and-forget) for ecosystem subscribers.

## Sizes & identity

A size becomes a canonical **size key** via `getSizeKey`, and the full lookup/lock **identity** is `sizeKey:format:filterSig`. Filters are part of identity (empty → `none`), so a blurred variant is a distinct object.

| Size input | Size key | Meaning |
|---|---|---|
| `{ width: 300, height: 300 }` | `300x300` | cropped (cover) |
| `{ width: 620 }` | `620w` | width-only (banner/strip) |
| `{ height: 400 }` | `400h` | height-only |
| `{ fit: true }` | `fit` | uncropped ("contain"), bounded by `config.maxSize` |
| `{ width: 300, height: 300, filters: { blur: 40 } }` | `300x300` + `blur:40` in identity | keyed alternate rendering |

The **host owns the size catalogs** per entity, injected via `resolveSizes` + per-call `sizes`.

:::warning Security: the catalog is an allowlist

Never pass raw client-supplied dimensions into `sizes` — resolve them against a fixed per-entity catalog first, or you invite arbitrary-resize resource abuse. The module owns the identity key; the host owns which sizes are permitted.

:::

## Configuration

`src/config/resize.ts` (scaffolded, editable) spreads the module defaults and is deep-merged over them by `getResizeConfig()` — override any knob at any depth. **Arrays REPLACE**; nested objects merge field-by-field. The most-touched knobs:

| Key | Default | Notes |
|---|---|---|
| `mediaModelName` | — (**required**) | your host media model name (`'File'`/`'Media'`) |
| `formats` | `['jpeg','webp','avif']` | generated formats |
| `maxSize` | `{ width: 2000, height: 1200 }` | the `fit` cap |
| `encode.quality` | `{ jpeg: 80, webp: 82, avif: 64 }` | per-format — never reuse one int across codecs |
| `worker.enabled` | `false` | gate the worker process (env-driven in host) |
| `queue.maxAttempts` | `5` | delivery count before dead-letter (like SQS `maxReceiveCount`) |
| `queue.taskTimeoutMs` | `600000` | `handleTask` is raced against this; on timeout the task is failed and the slot freed (Mongo transport) |

Storage buckets/URLs and the SQS queue URL are **not** config — they are driver options passed to `new S3Storage({...})` / `new SqsTransport({...})`. See the [full config reference](https://github.com/adaptivestone/framework-module-resize#config-reference) for every knob (encode, limits, queue lease/backoff, worker concurrency).

## Operations

**`ResizeTask` lifecycle** (Mongo transport): `pending → processing → completed | dead`. Retries are capped at `queue.maxAttempts`, then the task is **dead-lettered** (`status:'dead'`) — the lease never reclaims a task past the cap, so no crash-loop runs forever. (SQS uses its native DLQ instead.)

**Dead-letter replay** is a host op — reset the row:

```ts
ResizeTask.updateOne({ _id }, { $set: { status: 'pending', attempts: 0, leaseExpiresAt: null } });
```

**Delivery is at-least-once** (both transports); the worker is **idempotent** — re-running a task for an already-generated identity skips via the existing-preview check, never duplicates.

:::warning SVG sanitization is host-owned

**SVG originals are pass-through** — when `original.contentType === 'image/svg+xml'` the read path serves the original at every requested size/format and never resizes or enqueues. Sanitize SVGs at upload, before storing.

:::

## Host responsibilities

The module owns the resize core; the host owns everything domain-specific:

- The public **response DTO shape** (via `formatPublicUrls`).
- **Which domain models** attach media and the **size catalogs** per entity (via `resolveSizes` + per-call `sizes` — treat catalogs as allowlists).
- **Data migration** from any legacy preview schema.
- **Domain image analysis** — NSFW/object detection, plate/face blur, watermark, masking (inject via pipeline `beforeSteps`/`variantSteps`).
- **Permissions** — who may delete/replace media; the host may opt a read into a signed-original URL via `ctx`.
- **SVG sanitization** and **deleting media / storage cleanup** (the module appends previews but never deletes them).

For the exhaustive tables (every driver option, config knob, and hook signature) see the [README](https://github.com/adaptivestone/framework-module-resize#readme).


---


# Document 18: 13-deploy

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

# Deployment and process lifecycle

Run each application process through exactly one supervisor. Docker,
Kubernetes, systemd, and PM2 already manage replica count and restarts, so they
should normally run the framework's single-process server entry. Use the
framework cluster runner only when one Node primary should supervise multiple
workers on the same host.

Do not nest supervisors. For example, do not run `runCluster()` inside PM2
cluster mode or start several clustered processes in one Kubernetes pod.

:::note HTTP/1.1 only — terminate TLS and HTTP/2 at your proxy
The framework serves **plain HTTP/1.1** (`http.createServer`); it does not implement TLS or HTTP/2.
That is the normal shape for a Node service: your load balancer, ingress, or CDN speaks HTTPS and
HTTP/2 to clients and forwards HTTP/1.1 to the app. Configure certificates there, not here.
:::

## Single process under an external supervisor (recommended)

Keep server construction in a dedicated module:

```ts title="src/server.ts"
import Server from '@adaptivestone/framework/server.js';
import folderConfig from './folderConfig.ts';

const server = new Server(folderConfig);
await server.startServer();
```

Run that entry directly:

```bash
NODE_ENV=production node src/server.ts
```

### PM2

Install PM2 and configure it to start with the host:

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

Start one application process. PM2 owns its restart lifecycle:

```bash
NODE_ENV=production pm2 start src/server.ts --name YOUR_APP_NAME
pm2 save
```

If you choose PM2's own cluster mode, continue to run `src/server.ts`; do not
also use the framework cluster runner.

## Framework cluster runner

For a standalone Node deployment on one host, use the public Node-only
`runCluster()` helper:

```ts title="src/index.ts"
import { runCluster } from '@adaptivestone/framework/cluster.js';

await runCluster(
  async () => {
    // Dynamic import is important: this callback runs only in workers, so the
    // primary never constructs a Server or opens application connections.
    await import('./server.ts');
  },
  {
    workers: 'auto',
    shutdownTimeoutMs: 30_000,
  },
);
```

Start it without file watching:

```bash
NODE_ENV=production node src/index.ts
```

The primary forks the configured workers and the callback executes only in
those workers. An abnormal worker exit is restarted after a fixed safety delay;
exceeding the framework's fixed rolling crash limit shuts down the cluster with
a non-zero exit status. A clean worker exit is not restarted. Backoff, jitter,
and rolling-deployment policy belong to an external process supervisor and are
deliberately not part of this API.

On `SIGTERM` or `SIGINT`, the primary stops scheduling restarts and forwards the
signal to every worker. Each framework server stops accepting requests and
drains its open connections. Workers still alive after `shutdownTimeoutMs` are
force-terminated and the primary exits non-zero.

`workers: 'auto'` uses Node's available parallelism. A positive integer pins
the worker count. Early lifecycle messages use `console` by default. Pass an
`onEvent` callback to send structured primary, worker-exit, shutdown, and error
events to an observability provider before the application itself exists.

### Which entry should I run?

| Deployment | Entry | Process-count owner |
|---|---|---|
| Docker/Kubernetes | `src/server.ts` | Orchestrator |
| systemd | `src/server.ts` | systemd/unit replicas |
| PM2 fork or cluster mode | `src/server.ts` | PM2 |
| Standalone multi-core host | `src/index.ts` with `runCluster()` | Framework cluster primary |

## Nginx

The Node process listens on localhost port 3300 in the default configuration.
Proxy requests to it from Nginx:

```nginx
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.

Run one Node process per container and let the orchestrator scale replicas. The
Dockerfile can look like this:

```dockerfile
FROM node:24
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
# npm install tolerates optional dependency records omitted on another CPU architecture.
RUN npm install
COPY --chown=node:node src/ ./src/
EXPOSE 3300
CMD ["node", "src/server.ts"]
```

To build it, use:

```bash
docker build --platform linux/amd64 -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 19: 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
```

### `getAppInstance()` (recommended for early code)

The `Server` constructor is what sets the `appInstance` singleton. Code that reads the raw `appInstance` binding **before** the `Server` is constructed — module scope, lazy imports, external modules — sees `undefined` and fails later with an opaque `TypeError: cannot read properties of undefined` that gives no hint at the cause.

Prefer the `getAppInstance()` getter in that code: it returns the same singleton, or throws a guided error when nothing is set yet.

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

const app = getAppInstance();
const Model = app.getModel("ModelName");
```

If the singleton is not set, the getter throws:

```text
App instance is not initialized yet — construct the Server first (its constructor sets the singleton). In tests, use setAppInstance() to inject one and resetAppInstance() to clear it.
```

:::note Test hooks

The same module exports `setAppInstance(app)` and `resetAppInstance()` for tests that need to run app-instance-dependent code without a full `Server`. `setAppInstance` throws if an instance is already set (only one `Server` per process is supported), and `resetAppInstance()` clears it. Prefer the per-file isolation provided by the shipped Node.js or Vitest test adapters, and reach for `resetAppInstance()` only when a runner can't isolate per file (it does **not** clean up mongoose-registered models, Redis client state, or environment variables).

:::

## Redis connection

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

:::note

`@redis/client` is an **optional peer dependency**. Install it (`npm i @redis/client`) before using this helper or the redis cache / rate-limiter driver. A project on the default in-memory cache never loads it.

:::

```ts
import { getRedisClient, getRedisClientSync } from '@adaptivestone/framework/helpers/redis/redisConnection.js';
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.

## Validation schema (`defineSchema`)

A zero-dependency way to build a [Standard Schema](https://standardschema.dev/) validator from a plain function — for simple `request:` / `query:` schemas, without pulling in a validator library. The framework itself uses it, so it ships validator-free.

```ts
import { defineSchema } from "@adaptivestone/framework/services/validate/defineSchema.js";

const loginSchema = defineSchema<{ email: string }>((value) => {
  const v = (value ?? {}) as Record<string, unknown>;
  if (typeof v.email !== "string" || !v.email.includes("@")) {
    return { issues: [{ message: "validation.email", path: ["email"] }] };
  }
  // Return only known keys — unknown input is stripped by construction.
  return { value: { email: v.email } };
});
```

The `Output` generic (`{ email: string }`) feeds the typed handler signature — codegen reads `StandardSchemaV1.InferOutput`. Return `{ value }` on success or `{ issues }` on failure; each message is an i18n key or literal text. For richer or deeply-nested validation, bring zod / valibot / arktype / yup as the route schema instead — `defineSchema` has no combinators on purpose. See [Routes → Validation](./06-Controllers/02-routes.md#validation).

## Uploaded-file type (`File`)

A vendor-neutral type for files uploaded via `multipart/form-data`. It aliases the parser's file class today and re-points at the web-standard `File` after the transport-neutral parser swap, so your validation code stays stable across that change.

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

request: z.object({
  avatar: z.array(z.instanceof(File)).length(1).transform(([f]) => f), // one file  -> File
  avatars: z.array(z.instanceof(File)),                                // many files -> File[]
});
```

`File` is exported as both a value (for `instanceof`) and a type. Every multipart field arrives as an array, so declare cardinality with your validator's array support. See [Routes → File Validation](./06-Controllers/02-routes.md#file-validation).

:::warning Deprecated: `YupFile`
The older yup-specific `YupFile` helper (`@adaptivestone/framework/helpers/yup.js`) is **deprecated and will be removed in v6** — it now emits a runtime `DeprecationWarning` (escalate it to a thrown error with Node's `--throw-deprecation`). Migrate to the `File` export above; it works with any validator and needs no yup.
:::


---


# Document 20: 15-recipes

<!-- Source: 15-recipes.md -->

# Recipes

A task-oriented cookbook for the things you build most often. Each recipe is a minimal, copy-paste starting point — follow the links into [Controllers](06-Controllers/01-intro.md) and [Routes](06-Controllers/02-routes.md) for the full reference.

:::note
Framework **package** imports use the published `.js` extension (the package ships compiled `.js`). Your own project files can be `.ts`.
:::

## Where the handler types come from

The recipes below type handlers with two things — here's where each comes from:

- **`FrameworkRequest`** — imported from `@adaptivestone/framework/services/http/HttpServer.js`. It's the Express `Request` plus the base `req.appInfo` (`app`, `ip`, `request`, `query`, `i18n`). Use it for handlers that only read `req.params` / `req.body` and don't need validated input or middleware-provided fields. `Response` and `NextFunction` come from `express`.
- **Generated `<Method>Request` aliases** — emitted by `npm run gen` into `<Controller>.routes.gen.ts`, one per handler method (`postCreate` → `PostCreateRequest`). Each one extends the base context with what *that route* actually carries: typed `req.params`, your `request:` / `query:` schema output on `req.appInfo`, and every field the route's middleware chain declares via `static get provides()` (`req.appInfo.user`, `req.appInfo.pagination`, …).

Rule of thumb: if the handler reads `req.appInfo.request` / `.query` / `.user` / `.pagination` or precise `req.params`, use the generated alias; otherwise `FrameworkRequest` is enough. See [Typed handler signatures](06-Controllers/02-routes.md#typed-handler-signatures-codegen).

## Add a controller

Drop a file in `src/controllers/`. The framework auto-loads it, but the filename does **not**
define the route. The default mount is the controller folder prefix plus the lowercased **class
name** (`src/controllers/admin/Article.ts` exporting `class Article` → `/admin/article`). Name the
file after the class for clarity, and never export the same controller class from two files—the
autoloader registers both. Every method listed in `routes` becomes an endpoint with no manual
registration step.

```ts
// src/controllers/Article.ts
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
import type { Response } from "express";

class Article extends AbstractController {
  get routes() {
    return {
      get: {
        "/": { handler: this.list },
        "/:id": { handler: this.getOne },
      },
    };
  }

  async list(req: FrameworkRequest, res: Response) {
    const Article = this.app.getModel("Article");
    return res.status(200).json({ data: await Article.find() });
  }

  async getOne(req: FrameworkRequest, res: Response) {
    const Article = this.app.getModel("Article");
    return res.status(200).json({ data: await Article.findById(req.params.id) });
  }
}

export default Article;
```

These handlers read only `req.params` and the model, so the base `FrameworkRequest` is enough. To customize the prefix, override `getHttpPath()`. See [Controllers](06-Controllers/01-intro.md).

:::warning
Raw `req.params.id` is never validated. Declare a route [`params:` schema](06-Controllers/02-routes.md#params) and read `req.appInfo.params` instead — see [Validate an ObjectId](#validate-an-objectid).
:::

## Add a route with a body schema

Declare a `request:` schema inline. The framework validates and casts the body **before** your handler runs, strips unknown keys, and exposes the typed result on `req.appInfo.request`. Run `npm run gen` to emit `Article.routes.gen.ts` (where `PostCreateRequest` lives) and type the handler with it.

```ts
// src/controllers/Article.ts
import type { PostCreateRequest } from "./Article.routes.gen.ts";
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
import type { Response } from "express";
import { object, string } from "yup";

class Article extends AbstractController {
  get routes() {
    return {
      post: {
        "/": {
          handler: this.postCreate,
          request: object().shape({
            title: string().trim().min(3).max(300).required(),
            body: string().trim().required(),
          }),
        },
      },
    };
  }

  async postCreate(req: PostCreateRequest, res: Response) {
    // req.appInfo.request.title / .body are typed (from the schema) and already validated
    const Article = this.app.getModel("Article");
    const created = await Article.create(req.appInfo.request);
    return res.status(201).json({ data: created });
  }
}
```

The handler type `PostCreateRequest` is generated from the method name (`postCreate` → `PostCreateRequest`). Any [Standard Schema](https://standardschema.dev/) validator works (yup, zod, valibot, arktype), or the zero-dependency [`defineSchema`](14-helpers.md). See [Routes → Validation](06-Controllers/02-routes.md#validation).

## Validate an ObjectId

The framework validates `request:` (body), `query:` (query string) and `params:` (path params) before your handler runs. Ids are worth an explicit pattern in whichever of the three carries them — a raw 24-hex string is the one input shape Mongoose will reject at the driver level rather than at yours.

**Body / query fields** — add the check to the schema you already declare. A strict 24-hex pattern is enough:

```ts
import { object, string } from "yup";

// in a route:
query: object().shape({
  authorId: string().matches(/^[0-9a-fA-F]{24}$/, "must be a valid id"),
}),
// zod equivalent: z.object({ authorId: z.string().regex(/^[0-9a-fA-F]{24}$/) })
```

A bad value is rejected with the framework's standard `{ errors: { authorId: [...] } }` 400 — it never reaches your handler.

**Path params (`:id`)** — declare a `params:` schema on the route. The check runs before your handler, so the model only ever sees a well-formed id:

```ts
import { object, string } from "yup";
import type { GetOneRequest } from "./Article.routes.gen.ts";
import type { Response } from "express";

// in the routes getter:
"/:id": {
  handler: this.getOne,
  params: object().shape({
    id: string().matches(/^[0-9a-fA-F]{24}$/, "must be a valid id"),
  }),
},

// the handler no longer guards anything:
async getOne(req: GetOneRequest, res: Response) {
  const Article = this.app.getModel("Article");
  const { id } = req.appInfo.params; // validated, and typed by codegen
  return res.status(200).json({ data: await Article.findById(id) });
}
```

A malformed id is rejected with the same `{ errors: { id: [...] } }` 400 as any body or query failure.

:::note Strict vs loose
`mongoose.isValidObjectId` — the guard this recipe used to recommend — is lenient: it also accepts any 12-character string (and some numbers). The 24-hex regex above is the tighter check, and works without importing mongoose into your controller.
:::

:::tip
Forgot the schema on some route? The framework still answers **400** rather than 500 when an unvalidated client value fails to cast — see [Error handling → the Mongoose cast safety net](06-Controllers/04-error-handling.md#built-in-the-mongoose-cast-safety-net). Treat that as a floor, not a replacement: only a `params:` schema gives you your own message, i18n wording, coercion, and a typed path parameter in your OpenAPI document.
:::

## Wire pagination

Add the built-in `Pagination` middleware to the route. It reads `page` / `limit` from the query and puts `{ page, limit, skip }` on `req.appInfo.pagination`. Because that field is middleware-provided, type the handler with its generated alias (`ListRequest`) — `Pagination`'s `provides` flows into it.

```ts
import Pagination from "@adaptivestone/framework/services/http/middleware/Pagination.js";
import type { ListRequest } from "./Article.routes.gen.ts";
import type { Response } from "express";

get routes() {
  return {
    get: {
      "/": {
        handler: this.list,
        middleware: [[Pagination, { limit: 20, maxLimit: 100 }]],
      },
    },
  };
}

async list(req: ListRequest, res: Response) {
  const { skip, limit, page } = req.appInfo.pagination; // typed via Pagination's `provides`
  const Article = this.app.getModel("Article");
  const [items, total] = await Promise.all([
    Article.find().skip(skip).limit(limit),
    Article.countDocuments(),
  ]);
  return res.status(200).json({ data: { items, page, limit, total } });
}
```

`[Pagination, { limit, maxLimit }]` is the tuple form — a middleware class plus its params. See [Middleware → Pagination](06-Controllers/03-middleware.md#pagination).

## Require authentication

Put `GetUserByToken` (parses the token → `req.appInfo.user`) and `Auth` (rejects unauthenticated requests) in the chain. After `Auth`, `req.appInfo.user` is typed as required in the generated alias — no `if (!user)` guard needed.

```ts
import GetUserByToken from "@adaptivestone/framework/services/http/middleware/GetUserByToken.js";
import Auth from "@adaptivestone/framework/services/http/middleware/Auth.js";
import type { ListMineRequest } from "./Article.routes.gen.ts";
import type { Response } from "express";

get routes() {
  return {
    get: {
      "/mine": {
        handler: this.listMine,
        middleware: [GetUserByToken, Auth],
      },
    },
  };
}

async listMine(req: ListMineRequest, res: Response) {
  // req.appInfo.user is required here (Auth's `provides`)
  const Article = this.app.getModel("Article");
  return res.status(200).json({
    data: await Article.find({ owner: req.appInfo.user._id }),
  });
}
```

To apply one chain to **every** route in the controller, use the middleware Map instead:

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

See [Middleware](06-Controllers/03-middleware.md).

## Write a middleware that contributes to `req.appInfo`

Extend `AbstractMiddleware`, set your field on `req.appInfo`, and declare `static get provides()` so handlers downstream get it typed. Type the middleware's own `req` with the field it writes (an inline intersection on the base `FrameworkRequest`).

```ts
// src/middleware/WithArticleCount.ts
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";
import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
import type { Response, NextFunction } from "express";

class WithArticleCount extends AbstractMiddleware {
  static get description() {
    return "Adds the total article count to req.appInfo";
  }

  static get provides() {
    return {} as { articleCount: number };
  }

  async middleware(
    req: FrameworkRequest & { appInfo: { articleCount?: number } },
    res: Response,
    next: NextFunction,
  ) {
    const Article = this.app.getModel("Article");
    req.appInfo.articleCount = await Article.countDocuments();
    return next();
  }
}

export default WithArticleCount;
```

The object `provides` returns is always `{}` — only its cast type matters (codegen reads it; the runtime ignores it). Any route with `WithArticleCount` in its chain now has `req.appInfo.articleCount: number` in its generated alias. See [Routes → Middleware-provided types](06-Controllers/02-routes.md#middleware-provided-types).

## Override a framework controller

Create a controller with the same filename as a built-in one and extend it — your version wins via the [file-inheritance](03-files-inheritance.md) mechanism. Override only the handlers you want to change.

```ts
// src/controllers/Auth.ts
import OriginalAuth from "@adaptivestone/framework/controllers/Auth.js";
import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
import type { Response } from "express";

class Auth extends OriginalAuth {
  async postLogin(req: FrameworkRequest, res: Response) {
    // your custom login — call super for the default behavior, or replace it
    return super.postLogin(req, res);
  }
}

export default Auth;
```

The same approach works for models and configs. See [File Inheritance](03-files-inheritance.md).

## Test a controller with the framework helpers

The framework boots a real test server and an in-memory Mongo. Use `getTestServerURL()` for the base URL and `appInstance.getModel()` to seed and reset data.

```ts
// src/controllers/Article.test.ts
import assert from "node:assert/strict";
import { beforeEach, describe, it } from "node:test";
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
import { getTestServerURL } from "@adaptivestone/framework/tests/testHelpers.js";

describe("Article controller", () => {
  beforeEach(async () => {
    await appInstance.getModel("Article").deleteMany({});
  });

  it("creates an article", async () => {
    const res = await fetch(getTestServerURL("/article"), {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title: "Hello world", body: "..." }),
    });
    assert.equal(res.status, 201);
  });
});
```

For authenticated requests, create a user and send its token in the `Authorization` header. See the [Testing](09-testsing.md) chapter for the full setup, helpers, and CI examples.


---


# Document 21: 16-anti-patterns

<!-- Source: 16-anti-patterns.md -->

# Anti-patterns

Common mistakes when working with the framework, and what to do instead. Most of them map to a feature that already solves the problem — reach for the seam, not the workaround.

## Don't hand-edit `.routes.gen.ts`

❌ Editing a generated `<File>.routes.gen.ts` file.
✅ Edit the `routes` getter / `request:` schema, then run `npm run gen`.

Gen files are overwritten on every `npm run gen` (and every `check:types`) — your edits vanish. They're derived from the `routes` getter and the resolved middleware chain, and they're gitignored for that reason. Change the source and regenerate. See [When to run codegen](10-cli.md#when-to-run-codegen).

## Don't `new` a middleware yourself

❌ `middleware: [new Pagination(app, { limit: 20 })]`
✅ `middleware: [[Pagination, { limit: 20 }]]`

Declare middleware as a class, or a `[Class, params]` tuple. The framework instantiates and caches them for you, and reads their static metadata (`provides`, schemas) **without** constructing them. A manual `new` runs constructor side effects at the wrong time and bypasses that caching. See [Middleware](06-Controllers/03-middleware.md).

## Don't mutate `req.body` to "validate"

❌ Reading and normalizing `req.body` by hand inside the handler.
✅ Declare a `request:` (or `query:`) schema; read the validated, cast result from `req.appInfo.request`.

The route schemas validate and coerce before the handler runs, produce a typed `req.appInfo.request` / `req.appInfo.query`, strip unknown keys, and feed codegen (and later OpenAPI). Hand-rolled parsing in the handler gets none of that. See [Validation](06-Controllers/02-routes.md#validation).

## Don't put state in the `routes` getter

❌ A `routes` getter that reads `this.something` assigned in the constructor.
✅ Keep `routes` declarative — list handler methods and inline schemas only.

`npm run gen` reads `routes` **statically from the source AST** — it never imports or constructs your controller, so type generation stays free of constructor side effects. The cost: if the `routes` getter isn't a plain object literal (it reads `this.something`, loops, or otherwise computes the route shape), codegen can't analyze it and **the run fails** — there is no constructor fallback. Move the dynamic part into handlers or a module-level constant so the returned shape stays literal. See [Keep `routes` declarative](06-Controllers/02-routes.md#keep-routes-declarative-codegen-reads-the-source-never-your-constructor).

## Don't reach into raw Express when `req.appInfo` already has it

❌ Re-parsing cookies, re-loading the user, or recomputing pagination in every handler.
✅ Read what middleware already put on `req.appInfo` — `user`, `request`, `query`, `pagination`, `i18n`, plus any field your own middleware declares via `provides`.

`req.appInfo` is the framework's typed request context. Middleware populates it once; handlers and codegen consume it. Reaching around it duplicates work and loses the types. See [Middleware-provided types](06-Controllers/02-routes.md#middleware-provided-types).

## Don't monkey-patch the framework — report it

❌ Copy-pasting a framework file to tweak it, overriding internals to route around a bug, or shimming a private method.
✅ Open an issue at [github.com/adaptivestone/framework/issues](https://github.com/adaptivestone/framework/issues) with a minimal repro.

Local workarounds rot, hide the bug from everyone else, and break on the next update. The framework is built to be extended through supported seams — [file inheritance](03-files-inheritance.md), middleware, `provides`, and config — not patched. If the seam you need is missing, that's worth an issue too.


---


# Document 22: 17-openapi

<!-- Source: 17-openapi.md -->

# OpenAPI

The framework generates an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0/) document from your controllers. Paths, parameters, request bodies, security, and tags are all derived from the route definitions you already write — point Swagger UI, Redoc, or a client-SDK generator at the output.

## Generate

```bash
node src/cli.ts openapi                       # print to stdout
node src/cli.ts openapi --output openapi.json # write to a file
```

Add a script to `package.json`:

```json
"openapi": "node src/cli.ts openapi"
```

```bash
npm run openapi -- --output openapi.json
```

:::note
The command loads your controllers but **opens no database or network connection and binds no port** — it walks the route registry and reads your schemas in-process. It's safe to run in CI. (Unlike codegen, which is on your hot path, this is a cold, occasional command, so it loads the real controller instances to read live schema objects.)
:::

## What gets documented

Everything comes from the route definitions you already have — there is nothing OpenAPI-specific to maintain separately:

| OpenAPI field | Source |
|---|---|
| `operationId` | handler method name |
| `tags` | controller class name |
| `summary` | route [`description`](06-Controllers/02-routes.md#route-third-level-route-object-level) |
| path `parameters` | `:name` path segments; typed by the route [`params:`](06-Controllers/02-routes.md#params) schema when declared, otherwise `string` |
| query `parameters` | route [`query:`](06-Controllers/02-routes.md#query) schema (+ middleware query schemas) |
| `requestBody` | route [`request:`](06-Controllers/02-routes.md#request) schema or [content-type map](06-Controllers/02-routes.md#different-schemas-per-content-type) (+ middleware request schemas) |
| `security` | middleware [`static get usedAuthParameters()`](#documenting-auth-security-schemes) |
| `info` / `servers` | your `package.json` + the `http` config (`port`, `myDomain`) |

Output is **OpenAPI 3.1** (JSON Schema 2020-12) only.

## Request bodies come from your schemas

Body, query and path-param shapes are produced by introspecting the **same validation schemas** you already use at runtime — through the validator driver's `toJsonSchema`. How much detail you get depends on the validator:

| Validator | OpenAPI body |
|---|---|
| [Zod](https://zod.dev/) | full JSON Schema via native `z.toJSONSchema` — types, formats, `min`/`max`, patterns, enums, defaults |
| [Yup](https://github.com/jquense/yup) | JSON Schema from `.describe()` — types, `required`, enums, nullable, arrays, `date` → `date-time` |
| [ArkType](https://arktype.io/) / any schema exposing a `.toJsonSchema()` method | its native output |
| [`defineSchema`](06-Controllers/02-routes.md#zero-dependency-schemas-defineschema) | optional explicit `jsonSchema`; otherwise a placeholder schema + a warning |
| A hand-rolled `~standard` function | **not introspectable** unless it exposes `.toJsonSchema()` — a placeholder schema + a warning |

### Zod request-input semantics

OpenAPI describes what a client sends, not the value after Zod transforms it.
For example, `z.string().transform(Number)` is documented as a string.
`z.coerce.date()` is documented as a `{ "type": "string", "format":
"date-time" }` request value. Other Zod values that cannot be represented in
JSON Schema, such as custom `instanceof` checks, safely degrade to `{}` rather
than aborting the whole document.

For file uploads, use a content-type map with an explicit
`multipart/form-data` entry. A custom runtime `instanceof` check cannot by
itself tell OpenAPI that a field contains binary data.

:::note Describe imperative schemas explicitly
`defineSchema` callbacks are imperative, so their code cannot be inferred as a shape. Pass its optional `jsonSchema` option when the endpoint should be documented, or use a declarative validator such as Zod or Yup. A custom `~standard` schema can likewise expose a `.toJsonSchema()` method. Without either, the generator emits a placeholder object and prints a warning, e.g.:

```
OpenAPI: 1 schema(s) could not be fully introspected:
  POST /auth/login body: schema introspection unavailable.
```
:::

Schema conversion is contained per route and middleware schema. Warnings name
the HTTP method, route, and schema position. An unavailable body schema gets a
placeholder object, an unavailable query schema is omitted, and other healthy
routes remain fully documented. Genuine command boot, generator, and file-write
failures still exit nonzero.

The built-in `Pagination` middleware already supplies its explicit schema, so
every route using it documents optional numeric `page` and `limit` query
parameters without application-specific OpenAPI annotations.

This is why the generator must load your controllers at runtime rather than read the generated types: JSON Schema can only be produced from the live schema object (`z.toJSONSchema(...)`, `schema.describe()`), not from a TypeScript type.

## Documenting auth (security schemes)

A middleware advertises the security scheme(s) it enforces with a `static get usedAuthParameters()` getter. The generator reads it **off the class — no instantiation** — adds each entry to `components.securitySchemes`, and attaches a `security` requirement to every operation whose middleware chain includes that middleware.

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

class TokenAuth extends AbstractMiddleware {
  static get usedAuthParameters() {
    return [
      // http bearer scheme
      { name: "bearerAuth", type: "http", scheme: "bearer", description: "Bearer token" },
      // or an apiKey header
      { name: "X-Api-Key", type: "apiKey", in: "header", description: "API key" },
    ];
  }

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

| Field | Meaning |
|---|---|
| `name` | scheme key in `components.securitySchemes` (for `apiKey`, also the header/query name) |
| `type` | `'apiKey'` or `'http'` |
| `in` | for `apiKey`: `'header'` (default) / `'query'` / `'cookie'` |
| `scheme` | for `http`: `'bearer'`, `'basic'`, … |
| `description` | shown in the docs UI |

The built-in [`GetUserByToken`](06-Controllers/03-middleware.md) already declares its `Authorization` header + bearer schemes, so any route behind it is documented as secured automatically.

## Current limitations

- **Response bodies are not yet schema-documented.** Every operation carries a generic `200`/`400`/`401`/`404` response with a text description but no body schema. (Documenting response shapes is on the roadmap — it needs a declared `response:` schema, since a response has no runtime schema object to introspect.)
- **Catch-all (`{*splat}`) routes** are approximated as a single `{splat}` path parameter, because OpenAPI has no catch-all; the command warns when it does this.
- The route `description` becomes the operation `summary`; there is no separate long description, `deprecated` flag, or per-tag description yet.


---


<!-- End of Documentation -->
