npm package: @sftech/nestjs-db
Description: Database layer library providing TypeORM-based database access with CRUD patterns and OData support.
Latest version: 1.0.2

# @sftech/nestjs-db

Database layer library providing TypeORM-based database access with CRUD patterns and OData support.

## Installation

```bash
npm install @sftech/nestjs-db
```

## Peer Dependencies

- `@sftech/nestjs-core >= 1.0.0`
- `@nestjs/typeorm`
- `typeorm`

## Features

- CRUD use cases (get-all, get-by-id, insert, update, delete, bulk-save)
- CRUD bundles for rapid development
- TypeORM repository pattern implementation
- OData query support via `odata-v4-typeorm`
- Domain <-> Infrastructure entity mapping
- Base controller with standard REST endpoints

## Configuration

```typescript
import { NestjsDbModule, DatabaseOptionsMapper } from '@sftech/nestjs-db';
import { NestjsCoreModule } from '@sftech/nestjs-core';

@Module({
  imports: [
    NestjsCoreModule.register(),
    NestjsDbModule.register(DatabaseOptionsMapper.map()),
  ],
})
export class AppModule {}
```

## API Reference

### Application Layer

| Class | Purpose |
|-------|---------|
| `BaseCrudBundle<TDbModel>` | Bundle of all CRUD use cases |
| `BaseCrudIdentifierBundle<TDbModel>` | CRUD bundle with identifier-based lookup |
| `GetByIdUsecase` | Get single entity by ID |
| `GetAllUsecase` | Get all entities |
| `InsertUsecase` | Insert new entity |
| `UpdateUsecase` | Update existing entity |
| `DeleteUsecase` | Delete entity |
| `BulkSaveUsecase` | Save multiple entities |
| `OdataUsecase` | Query with OData syntax |

### Infrastructure Layer

| Class | Purpose |
|-------|---------|
| `BaseDbEntity` | Base TypeORM entity with id and createdAt |
| `BaseDbRepository<TDbModel, TDbEntity>` | Base repository implementation |
| `BaseDbMapper<TDbModel, TDbEntity>` | Base mapper for domain <-> entity conversion |

### Presentation Layer

| Class | Purpose |
|-------|---------|
| `BaseCrudController` | Base controller with CRUD endpoints |
| `BaseCrudIdentifierController` | Controller with identifier-based routes |
| `BaseDbCreateRequestDto` | Base DTO for create requests |
| `BaseDbUpdateRequestDto` | Base DTO for update requests |
| `BaseDtoDbModelConverter` | Convert DTOs to domain models |

---

## Base Classes & Interfaces

### BaseCrudBundle<TDbModel>

Bundle containing all CRUD use cases. **Instantiate this in your module to get ready-to-use CRUD operations.**

**Generic type parameters:**
- `TDbModel extends IBaseDbModel` - Your domain model

**Provides out of the box:**
- `repo` - The injected repository
- `getByIdUsecase` - Get entity by ID
- `getAllUsecase` - Get all entities
- `insertUsecase` - Insert new entity
- `updateUsecase` - Update entity
- `saveUsecase` - Insert or update
- `bulkSaveUsecase` - Save multiple entities
- `deleteUsecase` - Delete entity
- `odataUsecase` - OData queries

**Constructor parameters:**
- `_repo: IBaseDbRepository<TDbModel>` - Repository instance

**Usage example:**
```typescript
import { BaseCrudBundle, IBaseDbModel } from '@sftech/nestjs-db';
import { Injectable } from '@nestjs/common';

// 1. Define your domain model
export class User implements IBaseDbModel {
    id?: number;
    createdAt?: Date;
    email: string;
    name: string;
}

// 2. Create the bundle
@Injectable()
export class UserCrudBundle extends BaseCrudBundle<User> {
    constructor(userRepository: UserRepository) {
        super(userRepository);
    }
}

// 3. Register in module
@Module({
    imports: [TypeOrmModule.forFeature([UserEntity])],
    providers: [UserMapper, UserRepository, UserCrudBundle],
    exports: [UserCrudBundle],
})
export class UserModule {}

// 4. Use in controller
@Controller('users')
export class UserController {
    constructor(private readonly userBundle: UserCrudBundle) {}

    @Get()
    async getAll() {
        return this.userBundle.getAllUsecase.execute();
    }

    @Get(':id')
    async getById(@Param('id') id: number) {
        return this.userBundle.getByIdUsecase.execute({ id, detailed: false });
    }

    @Post()
    async create(@Body() dto: CreateUserDto) {
        const user = new User();
        user.email = dto.email;
        user.name = dto.name;
        return this.userBundle.insertUsecase.execute(user);
    }

    @Put(':id')
    async update(@Param('id') id: number, @Body() dto: UpdateUserDto) {
        const user = await this.userBundle.getByIdUsecase.execute({ id, detailed: false });
        user.name = dto.name;
        return this.userBundle.updateUsecase.execute(user);
    }

    @Delete(':id')
    async delete(@Param('id') id: number) {
        return this.userBundle.deleteUsecase.execute(id);
    }
}
```

---

### BaseDbRepository<TDbModel, TDbEntity>

Abstract base repository providing all standard database operations. **Extend this class for each entity.**

**Generic type parameters:**
- `TDbModel extends IBaseDbModel` - Your domain model
- `TDbEntity extends IBaseDbEntity` - Your TypeORM entity

**You must implement:**
- `_mapper: IBaseDbMapper<TDbModel, TDbEntity>` - Mapper for domain <-> entity conversion

**Provides out of the box:**
- `get(id, detailed?)` - Get by ID with optional relations
- `getAll()` - Get all entities
- `getByOdata(query)` - OData query support
- `findOneBy(options)` - Find single by options
- `findBy(options)` - Find multiple by options
- `insert(obj, saveRelations?)` - Insert new entity
- `update(obj, saveRelations?)` - Update existing entity
- `save(obj, saveRelations?)` - Insert or update
- `bulkSave(objs)` - Save multiple entities
- `delete(id)` - Delete by ID

**Usage example:**
```typescript
import { BaseDbRepository, IBaseDbMapper } from '@sftech/nestjs-db';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class UserRepository extends BaseDbRepository<User, UserEntity> {
    protected readonly _mapper: IBaseDbMapper<User, UserEntity>;

    constructor(
        @InjectRepository(UserEntity)
        repo: Repository<UserEntity>,
        mapper: UserMapper,
    ) {
        super(repo);
        this._mapper = mapper;
    }

    // Add custom methods if needed
    async findByEmail(email: string): Promise<User | null> {
        return this.findOneBy({ where: { email } });
    }
}
```

---

### BaseDbMapper<TDbModel, TDbEntity>

Abstract mapper for converting between domain models and TypeORM entities. **Implement this for each entity.**

**Generic type parameters:**
- `TDbModel extends IBaseDbModel` - Your domain model
- `TDbEntity extends IBaseDbEntity` - Your TypeORM entity

**You must implement:**
- `mapFromDb(from: TDbEntity, to?: TDbModel): TDbModel` - Entity to domain
- `mapToDb(from: TDbModel, to?: TDbEntity): TDbEntity` - Domain to entity

**Usage example:**
```typescript
import { BaseDbMapper } from '@sftech/nestjs-db';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserMapper extends BaseDbMapper<User, UserEntity> {
    mapFromDb(from: UserEntity, to?: User): User {
        const user = to ?? new User();
        user.id = from.id;
        user.createdAt = from.createdAt;
        user.email = from.email;
        user.name = from.name;
        return user;
    }

    mapToDb(from: User, to?: UserEntity): UserEntity {
        const entity = to ?? new UserEntity();
        entity.id = from.id;
        entity.email = from.email;
        entity.name = from.name;
        return entity;
    }
}
```

---

### BaseDbEntity

Base TypeORM entity with standard fields. **Extend this for all your entities.**

**Provides:**
- `id?: number` - Primary key (auto-generated)
- `createdAt?: Date` - Creation timestamp (auto-generated)

**Usage example:**
```typescript
import { BaseDbEntity } from '@sftech/nestjs-db';
import { Entity, Column } from 'typeorm';

@Entity('users')
export class UserEntity extends BaseDbEntity {
    @Column()
    email: string;

    @Column()
    name: string;

    @Column({ default: true })
    isActive: boolean;
}
```

---

### BaseCrudController<TDbModel, TResponseDto, TCreateDto, TUpdateDto>

Abstract controller providing standard CRUD REST endpoints. **Extend this to quickly create a full CRUD API.**

**Generic type parameters:**
- `TDbModel extends IBaseDbModel` - Domain model
- `TResponseDto extends IBaseDbPresenter` - Response DTO
- `TCreateDto extends BaseDbCreateRequestDto` - Create request DTO
- `TUpdateDto extends BaseDbUpdateRequestDto` - Update request DTO

**You must implement:**
- `_converter: IBaseDtoDbModelConverter<TCreateDto, TUpdateDto, TDbModel>` - DTO to model converter

**Provides out of the box (REST endpoints):**
- `GET /` - Get all entities
- `GET /odata` - OData query
- `GET /:id` - Get by ID
- `POST /` - Create entity
- `POST /bulk-save` - Bulk save entities
- `PUT /:id` - Update entity
- `DELETE /:id` - Delete entity

**Usage example:**
```typescript
import { BaseCrudController } from '@sftech/nestjs-db';
import { Controller } from '@nestjs/common';

@Controller('users')
export class UserController extends BaseCrudController<
    User,
    UserResponseDto,
    CreateUserDto,
    UpdateUserDto
> {
    protected readonly _converter: UserDtoConverter;

    constructor(
        userBundle: UserCrudBundle,
        converter: UserDtoConverter,
    ) {
        super(userBundle);
        this._converter = converter;
    }
}

// Now you have all CRUD endpoints automatically:
// GET    /users          -> getAll()
// GET    /users/odata    -> getByOdata()
// GET    /users/:id      -> getById()
// POST   /users          -> create()
// POST   /users/bulk-save -> bulkSave()
// PUT    /users/:id      -> update()
// DELETE /users/:id      -> delete()
```

---

### BaseDtoDbModelConverter<TCreateDto, TUpdateDto, TDbModel>

Abstract converter for transforming DTOs to domain models. **Implement this for DTO-to-model conversion.**

**You must implement:**
- `convertCreateDtoToDbModel(dto: TCreateDto): Promise<TDbModel>`
- `convertUpdateDtoToDbModel(dto: TUpdateDto, existing: TDbModel): Promise<TDbModel>`

**Usage example:**
```typescript
import { BaseDtoDbModelConverter } from '@sftech/nestjs-db';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserDtoConverter extends BaseDtoDbModelConverter<
    CreateUserDto,
    UpdateUserDto,
    User
> {
    async convertCreateDtoToDbModel(dto: CreateUserDto): Promise<User> {
        const user = new User();
        user.email = dto.email;
        user.name = dto.name;
        return user;
    }

    async convertUpdateDtoToDbModel(dto: UpdateUserDto, existing: User): Promise<User> {
        if (dto.name !== undefined) {
            existing.name = dto.name;
        }
        if (dto.email !== undefined) {
            existing.email = dto.email;
        }
        return existing;
    }
}
```

---

### IBaseDbModel

Interface for domain models. **Implement this for all your domain models.**

**Required properties:**
- `id?: number` - Entity ID
- `createdAt?: Date` - Creation timestamp

**Usage example:**
```typescript
import { IBaseDbModel } from '@sftech/nestjs-db';

export class Article implements IBaseDbModel {
    id?: number;
    createdAt?: Date;

    title: string;
    content: string;
    authorId: number;
    publishedAt?: Date;
}
```

---

## Complete Example

Here's a complete example of setting up a User entity with full CRUD:

```typescript
// === DOMAIN LAYER ===

// user.model.ts
import { IBaseDbModel } from '@sftech/nestjs-db';

export class User implements IBaseDbModel {
    id?: number;
    createdAt?: Date;
    email: string;
    name: string;
    isActive: boolean = true;
}

// === INFRASTRUCTURE LAYER ===

// user.entity.ts
import { BaseDbEntity } from '@sftech/nestjs-db';
import { Entity, Column } from 'typeorm';

@Entity('users')
export class UserEntity extends BaseDbEntity {
    @Column({ unique: true })
    email: string;

    @Column()
    name: string;

    @Column({ default: true })
    isActive: boolean;
}

// user.mapper.ts
import { BaseDbMapper } from '@sftech/nestjs-db';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserMapper extends BaseDbMapper<User, UserEntity> {
    mapFromDb(from: UserEntity, to?: User): User {
        const user = to ?? new User();
        user.id = from.id;
        user.createdAt = from.createdAt;
        user.email = from.email;
        user.name = from.name;
        user.isActive = from.isActive;
        return user;
    }

    mapToDb(from: User, to?: UserEntity): UserEntity {
        const entity = to ?? new UserEntity();
        if (from.id) entity.id = from.id;
        entity.email = from.email;
        entity.name = from.name;
        entity.isActive = from.isActive;
        return entity;
    }
}

// user.repository.ts
import { BaseDbRepository, IBaseDbMapper } from '@sftech/nestjs-db';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class UserRepository extends BaseDbRepository<User, UserEntity> {
    protected readonly _mapper: IBaseDbMapper<User, UserEntity>;

    constructor(
        @InjectRepository(UserEntity) repo: Repository<UserEntity>,
        mapper: UserMapper,
    ) {
        super(repo);
        this._mapper = mapper;
    }
}

// === APPLICATION LAYER ===

// user.bundle.ts
import { BaseCrudBundle } from '@sftech/nestjs-db';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserCrudBundle extends BaseCrudBundle<User> {
    constructor(repository: UserRepository) {
        super(repository);
    }
}

// === PRESENTATION LAYER ===

// user.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, Query } from '@nestjs/common';

@Controller('users')
export class UserController {
    constructor(private readonly userBundle: UserCrudBundle) {}

    @Get()
    async getAll() {
        return this.userBundle.getAllUsecase.execute();
    }

    @Get('odata')
    async getByOdata(@Query() query: OdataRequestQueryDto) {
        return this.userBundle.odataUsecase.execute(query);
    }

    @Get(':id')
    async getById(@Param('id') id: number) {
        return this.userBundle.getByIdUsecase.execute({ id, detailed: false });
    }

    @Post()
    async create(@Body() dto: CreateUserDto) {
        const user = new User();
        user.email = dto.email;
        user.name = dto.name;
        return this.userBundle.insertUsecase.execute(user);
    }

    @Put(':id')
    async update(@Param('id') id: number, @Body() dto: UpdateUserDto) {
        const user = await this.userBundle.getByIdUsecase.execute({ id, detailed: false });
        user.name = dto.name ?? user.name;
        return this.userBundle.updateUsecase.execute(user);
    }

    @Delete(':id')
    async delete(@Param('id') id: number) {
        return this.userBundle.deleteUsecase.execute(id);
    }
}

// === MODULE ===

// user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
    imports: [TypeOrmModule.forFeature([UserEntity])],
    providers: [UserMapper, UserRepository, UserCrudBundle],
    controllers: [UserController],
    exports: [UserCrudBundle],
})
export class UserModule {}
```

---

## OData Support

The library supports OData queries via the `odataUsecase`:

```typescript
// GET /users/odata?$top=10&$skip=0&$filter=name eq 'John'&$orderby=createdAt desc

@Get('odata')
async getByOdata(@Query() query: OdataRequestQueryDto) {
    return this.userBundle.odataUsecase.execute(query);
}
```

**Supported OData parameters:**
- `$top` - Limit results
- `$skip` - Skip results (pagination)
- `$filter` - Filter expression
- `$orderby` - Sort order
- `$expand` - Include relations
- `$count` - Include total count

---

## Development

```bash
# Build
npx nx build nestjs-db

# Format
npx biome format --write libs/nestjs-db/
```

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)

## License

MIT