> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Repository

> Decorator-based data access layer with a paginated query contract and dependency injection.

The `@ooneex/repository` component is a thin data access layer. You register a repository class with a decorator, implement the `IRepository` contract, and resolve it from the container. The contract standardizes how repositories open and close their data source and return paginated, searchable results — so every repository in your app reads and writes the same way, regardless of the database underneath.

## Why this component

* **One contract for every repository.** `IRepository<T, TCriteria>` defines `open`, `close`, and `find` — implement it the same way for any data source.
* **Paginated by default.** `find` returns a `FilterResultType<T>` with `resources`, `total`, `totalPages`, `page`, and `limit`.
* **Search built in.** Every `find` accepts an optional `q` query string alongside your own criteria fields.
* **Type-safe.** Generics carry your entity type `T` and criteria type `TCriteria` through the whole API.
* **Container-managed.** Register a repository with a decorator and resolve (or inject) it anywhere, with the scope you choose.

## How it works

A repository implements the `IRepository` interface and is registered with `@decorator.repository()`. The decorator adds the class to the `@ooneex/container`, so you resolve it instead of constructing it by hand. The contract is intentionally small:

| Method           | Purpose                                                                           |
| ---------------- | --------------------------------------------------------------------------------- |
| `open()`         | Open or initialize the data source connection; returns the underlying connection. |
| `close()`        | Close the data source connection.                                                 |
| `find(criteria)` | Return entities matching `criteria` as a paginated `FilterResultType<T>`.         |

`find` always merges your own filter fields with three reserved keys:

| Field   | Default | Purpose                     |
| ------- | ------- | --------------------------- |
| `page`  | `1`     | Page number for pagination. |
| `limit` | `10`    | Items per page.             |
| `q`     | —       | Free-text search query.     |

The result is always shaped the same way:

```typescript theme={null}
type FilterResultType<T> = {
  resources: T[];
  total: number;
  totalPages: number;
  page: number;
  limit: number;
};
```

## Decorator and usage

### `@decorator.repository(scope?)`

Registers a repository class with the container. It accepts an optional scope (defaults to `EContainerScope.Singleton`).

| Scope       | Behavior                                              |
| ----------- | ----------------------------------------------------- |
| `Singleton` | One instance shared across all resolutions (default). |
| `Transient` | A new instance is created on every resolution.        |
| `Request`   | A new instance per request context.                   |

Implement the `IRepository<T, TCriteria>` contract on your class:

```typescript theme={null}
import { decorator, type IRepository } from "@ooneex/repository";
import type { FilterResultType } from "@ooneex/types";

interface User {
  id: string;
  email: string;
  name: string;
}

interface UserCriteria {
  email?: string;
  name?: string;
  isActive?: boolean;
}

@decorator.repository()
class UserRepository implements IRepository<User, UserCriteria> {
  private connection: unknown = null;

  public async open(): Promise<unknown> {
    this.connection = await this.createConnection();
    return this.connection;
  }

  public async close(): Promise<void> {
    this.connection = null;
  }

  public async find(
    criteria: UserCriteria & { page?: number; limit?: number; q?: string },
  ): Promise<FilterResultType<User>> {
    const { page = 1, limit = 10, q, ...filters } = criteria;

    const users = await this.queryUsers(filters, q);
    const total = await this.countUsers(filters, q);

    return {
      resources: users,
      total,
      totalPages: Math.ceil(total / limit),
      page,
      limit,
    };
  }
}
```

Resolve it from the container and run a paginated, filtered query:

```typescript theme={null}
import { container } from "@ooneex/container";

const userRepository = container.get(UserRepository);

await userRepository.open();

const result = await userRepository.find({
  isActive: true,
  page: 1,
  limit: 20,
  q: "john",
});

result.resources; // User[]
result.total; // total matching rows
result.totalPages; // number of pages

await userRepository.close();
```

Pass a scope when you need a fresh instance per resolution:

```typescript theme={null}
import { decorator, type IRepository } from "@ooneex/repository";
import { EContainerScope } from "@ooneex/container";

@decorator.repository(EContainerScope.Transient)
class TransientRepository implements IRepository {
  // each container.get(...) returns a new instance
}
```

## Best practices

* **Implement the full contract.** Always provide `open`, `close`, and `find` so callers can treat every repository identically.
* **Keep pagination defaults sane.** Default `page` to `1` and `limit` to a bounded value; never return an unbounded result set.
* **Always compute `totalPages` from `total`.** Use `Math.ceil(total / limit)` so callers can paginate reliably.
* **Spread reserved keys out of your filters.** Destructure `page`, `limit`, and `q` before building the query, then apply the rest as filter fields.
* **Honor `q` consistently.** Apply the same search predicate to both the result query and the count, so totals match the page.
* **Resolve, don't construct.** Pull repositories from the container (or inject them) instead of using `new`, so scope and dependencies are wired correctly.
* **Pick the scope deliberately.** Use `Singleton` for shared, stateless repositories; `Transient` or `Request` when each consumer needs isolated state.

## CLI command

Scaffold a repository class and its test file with the generator. It writes the class under `modules/<module>/src/repositories/<Name>Repository.ts`, a spec under `modules/<module>/tests/repositories/`, and installs `@ooneex/repository` if it is missing.

```bash theme={null}
# Interactive: prompts for the name
ooneex repository:create

# Provide the name
ooneex repository:create --name=User

# Target a module and overwrite
ooneex repository:create --name=User --module=auth --override
```

| Option       | Description                                                               | Default             |
| ------------ | ------------------------------------------------------------------------- | ------------------- |
| `--name`     | Repository class name. The `Repository` suffix is appended automatically. | Prompted if omitted |
| `--module`   | Target module the class is generated into.                                | `shared`            |
| `--override` | Overwrite an existing class without prompting.                            | `false`             |

The generated class is a TypeORM-backed repository with `open`, `close`, `find` (paginated + searchable), and full CRUD methods, ready for you to wire up to your entity:

```typescript theme={null}
import { inject } from "@ooneex/container";
import type { ITypeormDatabase } from "@ooneex/database";
import { decorator } from "@ooneex/repository";
import type { FilterResultType } from "@ooneex/types";
import type { FindManyOptions, FindOptionsWhere, Repository, SaveOptions, UpdateResult } from "typeorm";
import { UserEntity } from "../entities/UserEntity";

@decorator.repository()
export class UserRepository {
  constructor(
    @inject("database")
    private readonly database: ITypeormDatabase,
  ) {}

  public async open(): Promise<Repository<UserEntity>> {
    return await this.database.open(UserEntity);
  }

  public async close(): Promise<void> {
    await this.database.close();
  }

  public async find(
    criteria: FindManyOptions<UserEntity> & { page?: number; limit?: number; q?: string },
  ): Promise<FilterResultType<UserEntity>> {
    const repository = await this.open();

    const { page = 1, limit = 100, q, ...rest } = criteria;

    let skip: number | undefined;
    const take = limit === 0 ? 100 : limit;

    if (page && page > 0 && limit && limit > 0) {
      skip = (page - 1) * take;
    }

    let findOptions = { ...rest, take, ...(skip !== undefined && { skip }) };
    if (q) {
      findOptions = {
        ...findOptions,
        where: {
          ...rest.where,
          // name: ILike(`%${q}%`),
        },
      };
    }

    const result = await repository.find(findOptions);

    let countWhere = rest.where;
    if (q) {
      countWhere = {
        ...rest.where,
        // name: ILike(`%${q}%`),
      };
    }

    const total = await this.count(countWhere);
    const totalPages = Math.ceil(total / limit);

    return { resources: result, total, totalPages, page, limit };
  }

  public async findOne(id: string): Promise<UserEntity | null> {
    const repository = await this.open();
    return await repository.findOne({ where: { id } });
  }

  public async findOneBy(criteria: FindOptionsWhere<UserEntity>): Promise<UserEntity | null> {
    const repository = await this.open();
    return await repository.findOne({ where: criteria });
  }

  public async create(entity: UserEntity, options?: SaveOptions): Promise<UserEntity> {
    const repository = await this.open();
    return await repository.save(entity, options);
  }

  public async createMany(entities: UserEntity[], options?: SaveOptions): Promise<UserEntity[]> {
    const repository = await this.open();
    return await repository.save(entities, options);
  }

  public async update(entity: Partial<UserEntity> & { id: string }): Promise<UpdateResult> {
    const repository = await this.open();
    return await repository.update(entity.id, entity);
  }

  public async updateMany(entities: (Partial<UserEntity> & { id: string })[]): Promise<UpdateResult[]> {
    const repository = await this.open();
    return await Promise.all(entities.map((entity) => repository.update(entity.id, entity)));
  }

  public async delete(
    criteria: FindOptionsWhere<UserEntity> | FindOptionsWhere<UserEntity>[],
  ): Promise<UpdateResult> {
    const repository = await this.open();
    return await repository.softDelete(criteria);
  }

  public async count(criteria?: FindOptionsWhere<UserEntity> | FindOptionsWhere<UserEntity>[]): Promise<number> {
    const repository = await this.open();
    return await repository.count(criteria ? { where: criteria } : {});
  }
}
```

See [repository:create](/cli/commands/repository-create) for the full command reference.

## Use with Claude and Codex

The generator ships a matching `repository:create` skill. It runs the scaffold and then guides your AI agent through completing the repository — verifying the entity import path, adjusting the `find` search fields, and trimming or adding CRUD methods to fit the entity. Initialize the skills once for your agent:

<Tabs>
  <Tab title="Claude">
    ```bash theme={null}
    ooneex claude:init
    ```

    Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a repository for the User entity.
    ```
  </Tab>

  <Tab title="Codex">
    ```bash theme={null}
    ooneex codex:init
    ```

    Then ask Codex in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a repository for the User entity.
    ```
  </Tab>
</Tabs>

For example, the prompt above maps to `repository:create --name=User`, then completes the CRUD methods against the `User` entity.
