npm package: @nicolaselge/ng-store
Description: Angular Signals-based state management library with CRUD operations and DTO mapping
Latest version: 0.0.5

# 🧠 Angular Signal CRUD
### Signal Store infrastructure for Angular 16+

> A headless, opinionated CRUD infrastructure built on Angular Signals.
> Designed for long-lived applications, clean architectures, and real-world backend constraints.

---

## ✨ Features

- **Angular Signals-based stores** - Reactive state management with Angular Signals
- **Class-based stores** - No function-based defineStore, uses familiar class syntax
- **Full CRUD abstraction** - Complete CRUD operations (single & bulk) out of the box
- **Optimistic updates** - UI updates immediately
- **Soft delete, restore, hard delete** - Complete deletion workflow support
- **Policy / permission layer** - Frontend-side permission checking before operations
- **Event bus** - Decouple stores and enable cross-store communication
- **Type-safe HTTP client** - Full TypeScript support with comprehensive options
- **Automatic HTTP request logging** - Dev mode only, configurable
- **DTO Mapping** - Automatic transformation between backend DTOs and frontend entities
- **Headless & UI-agnostic** - Works with any UI framework or library
- **Designed for Angular 16+** - Uses stable Angular Signals and functional DI

---

## 📦 Installation

```bash
npm install @nicolaselge/ng-store
```

---

## 🧩 Requirements

### Peer dependencies

- Angular >= 16.0.0
- RxJS >= 7.5.0
- TypeScript >= 5.0

This library relies on Angular Signals and functional dependency injection,
which are officially stable starting from Angular 16.

---

## 🚀 Quick Start

### 1️⃣ Define your entity

```ts
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
  updatedAt: string;
  deletedAt?: string | null;
}
```

---

### 2️⃣ Create a Store

```ts
import { BaseCrudStore } from '@nicolaselge/ng-store';

@Injectable({ providedIn: 'root' })
export class UserStore extends BaseCrudStore<User> {
  protected override storeName = 'users';
  protected override endpoint = '/api/users';
}
```

---

### 3️⃣ Use it in a component

```ts
@Component({...})
export class UserListComponent {

  readonly users = this.userStore.entities;
  readonly loading = this.userStore.loading;

  private userStore: UserStore = inject(UserStore);

  constructor() {
    this.userStore.getAll();
  }

  delete(user: User) {
    this.userStore.deleteOne(user.id);
  }
}
```

---

## 📚 Complete API Reference

### BaseCrudStore<T, TDTO>

The main store class that provides all CRUD operations.

**Type Parameters:**
- `T` - Entity type (must have an `id` property)
- `TDTO` - DTO type (defaults to `T` if no mapper is used)

**Properties:**

#### `entities: Signal<T[]>`
Reactive Signal containing all entities in the store.

**Type:** `Signal<T[]>`

**Usage:**
```ts
readonly users = this.userStore.entities;

// In template
<div *ngFor="let user of users()">{{ user.name }}</div>
```

---

#### `loading: Signal<boolean>`
Reactive Signal indicating if an operation is in progress.

**Type:** `Signal<boolean>`

**Usage:**
```ts
readonly loading = this.userStore.loading;

// In template
<div *ngIf="loading()">Loading...</div>
```

---

#### `selected: Signal<T | null>`
Reactive Signal containing the currently selected entity.

**Type:** `Signal<T | null>`

**Usage:**
```ts
readonly selectedUser = this.userStore.selected;

// In template
<div *ngIf="selectedUser()">
  Details: {{ selectedUser()!.name }}
</div>
```

---

### Read Operations

#### `getOne(id: T['id']): Promise<T>`

Retrieves a single entity by its ID.

**Parameters:**
- `id` - ID of the entity to retrieve

**Returns:** Promise resolved with the retrieved entity

**Features:**
- Checks permissions via `PolicyEngine`
- Updates `selected` with the retrieved entity
- Emits event `{storeName}:get:one`
- Automatic DTO mapper support

**Endpoint:** `GET {endpoint}/:id`

**Example:**
```ts
const user = await this.userStore.getOne(123);
console.log(user.name);

// Entity is automatically set in selected
const selected = this.userStore.selected(); // Signal containing the user
```

---

#### `getAll(): Promise<T[]>`

Retrieves all entities.

**Returns:** Promise resolved with the array of entities

**Features:**
- Checks permissions via `PolicyEngine`
- Completely replaces the `items` collection
- Emits event `{storeName}:get:all`
- Automatic DTO mapper support

**Endpoint:** `GET {endpoint}`

**Example:**
```ts
const users = await this.userStore.getAll();

// Collection is automatically updated
const allUsers = this.userStore.entities(); // Signal containing all users
```

---

#### `getMany(ids: T['id'][]): Promise<T[]>`

Retrieves multiple entities by their IDs.

**Parameters:**
- `ids` - Array of IDs of entities to retrieve

**Returns:** Promise resolved with the array of retrieved entities

**Features:**
- Checks permissions via `PolicyEngine`
- Updates the `items` collection with retrieved entities (upsert)
- Emits event `{storeName}:get:many`
- Automatic DTO mapper support

**Endpoint:** `POST {endpoint}/many`
**Body:** Array of entity IDs

**Example:**
```ts
const users = await this.userStore.getMany([1, 2, 3]);

// Entities are automatically added/updated in the collection
```

---

### Create Operations

#### `createOne(entity: T): Promise<T>`

Creates a new entity.

**Parameters:**
- `entity` - Entity to create

**Returns:** Promise resolved with the created entity (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Adds entity immediately to store
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:create:one`
- Automatic DTO mapper support

**Endpoint:** `POST {endpoint}`
**Body:** Entity DTO (or entity if no mapper)

**Example:**
```ts
const newUser: User = {
  id: 0,
  name: 'John Doe',
  email: 'john@example.com',
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),
  deletedAt: null
};

try {
  const created = await this.userStore.createOne(newUser);
  console.log('User created:', created);
  // User is already visible in this.userStore.entities() (optimistic)
} catch (error) {
  console.error('Error:', error);
  // State has been automatically restored (rollback)
}
```

---

#### `createMany(entities: T[]): Promise<T[]>`

Creates multiple new entities.

**Parameters:**
- `entities` - Array of entities to create

**Returns:** Promise resolved with the created entities (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Adds entities immediately to store
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:create:many`
- Automatic DTO mapper support

**Endpoint:** `POST {endpoint}/bulk`
**Body:** Array of entity DTOs (or entities if no mapper)

**Example:**
```ts
const newUsers: User[] = [
  { id: 0, name: 'John', email: 'john@example.com', ... },
  { id: 0, name: 'Jane', email: 'jane@example.com', ... }
];

const created = await this.userStore.createMany(newUsers);
```

---

### Update Operations

#### `updateOne(entity: T): Promise<T>`

Updates an existing entity (full replacement).

**Parameters:**
- `entity` - Complete entity with updated values

**Returns:** Promise resolved with the updated entity (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Updates entity immediately in store
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:update:one`
- Automatic DTO mapper support

**Endpoint:** `PUT {endpoint}/:id`
**Body:** Complete entity DTO (or entity if no mapper)

**Example:**
```ts
const updatedUser: User = {
  ...existingUser,
  name: 'John Updated',
  email: 'john.updated@example.com'
};

const result = await this.userStore.updateOne(updatedUser);
```

---

#### `updateMany(entities: T[]): Promise<T[]>`

Updates multiple existing entities (full replacement).

**Parameters:**
- `entities` - Array of complete entities with updated values

**Returns:** Promise resolved with the updated entities (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Updates entities immediately in store
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:update:many`
- Automatic DTO mapper support

**Endpoint:** `PUT {endpoint}/bulk`
**Body:** Array of complete entity DTOs

---

#### `patchOne(entity: T): Promise<T>`

Partially updates an existing entity (PATCH operation).

**Parameters:**
- `entity` - Entity with only the fields to update (other fields can be omitted)

**Returns:** Promise resolved with the patched entity (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Updates entity immediately in store
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:patch:one`
- Automatic DTO mapper support

**Endpoint:** `PATCH {endpoint}/:id`
**Body:** Partial entity DTO (only fields to update)

**Example:**
```ts
// Update only the name
const partial: User = {
  id: 123,
  name: 'New Name'
  // email and other fields not included
};

const result = await this.userStore.patchOne(partial);
```

---

#### `patchMany(entities: T[]): Promise<T[]>`

Partially updates multiple existing entities (PATCH operation).

**Parameters:**
- `entities` - Array of entities with only fields to update

**Returns:** Promise resolved with the patched entities (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Updates entities immediately in store
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:patch:many`
- Automatic DTO mapper support

**Endpoint:** `PATCH {endpoint}/bulk`
**Body:** Array of partial entity DTOs

---

### Delete Operations

#### `deleteOne(id: T['id']): Promise<T>`

Soft deletes an entity (marks as deleted but keeps in database).

**Parameters:**
- `id` - ID of the entity to soft delete

**Returns:** Promise resolved with the deleted entity (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Marks entity as deleted immediately (sets `deletedAt`)
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:delete:one`
- Automatic DTO mapper support

**Endpoint:** `PATCH {endpoint}/:id/delete`

**Example:**
```ts
// Soft delete a user
await this.userStore.deleteOne(123);
// Entity is marked as deleted (deletedAt is set)
// It can be restored with restoreOne()
```

---

#### `deleteMany(ids: T['id'][]): Promise<T[]>`

Soft deletes multiple entities (marks as deleted but keeps in database).

**Parameters:**
- `ids` - Array of IDs of entities to soft delete

**Returns:** Promise resolved with the deleted entities (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Marks entities as deleted immediately (sets `deletedAt`)
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:delete:many`
- Automatic DTO mapper support

**Endpoint:** `PATCH {endpoint}/delete`
**Body:** Array of entity IDs

---

#### `restoreOne(id: T['id']): Promise<T>`

Restores a soft-deleted entity (removes the deleted flag).

**Parameters:**
- `id` - ID of the entity to restore

**Returns:** Promise resolved with the restored entity (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Removes deleted flag immediately (sets `deletedAt` to null)
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:restore:one`
- Automatic DTO mapper support

**Endpoint:** `PATCH {endpoint}/:id/restore`

**Example:**
```ts
// Restore a previously deleted user
await this.userStore.restoreOne(123);
// Entity is now active (deletedAt is null)
```

---

#### `restoreMany(ids: T['id'][]): Promise<T[]>`

Restores multiple soft-deleted entities (removes the deleted flag).

**Parameters:**
- `ids` - Array of IDs of entities to restore

**Returns:** Promise resolved with the restored entities (returned by server)

**Features:**
- Checks permissions via `PolicyEngine`
- **Optimistic update**: Removes deleted flag immediately (sets `deletedAt` to null)
- **Automatic rollback**: Restores state on error
- Emits event `{storeName}:restore:many`
- Automatic DTO mapper support

**Endpoint:** `PATCH {endpoint}/restore`
**Body:** Array of entity IDs

---

#### `destroyOne(id: T['id']): Promise<void>`

Permanently deletes an entity from the database (hard delete).

**Parameters:**
- `id` - ID of the entity to permanently delete

**Returns:** Promise that resolves when deletion is complete

**Features:**
- Checks permissions via `PolicyEngine` (requires 'destroy' permission)
- **Optimistic update**: Removes entity immediately from store
- **Automatic rollback**: Restores state on error (except network errors)
- Emits event `{storeName}:destroy:one`

**Warning:** This operation is irreversible. The entity is permanently deleted.

**Endpoint:** `DELETE {endpoint}/:id`

**Example:**
```ts
// Permanently delete a user
await this.userStore.destroyOne(123);
// Entity is removed from store and deleted from database
```

---

#### `destroyMany(ids: T['id'][]): Promise<void>`

Permanently deletes multiple entities from the database (hard delete).

**Parameters:**
- `ids` - Array of IDs of entities to permanently delete

**Returns:** Promise that resolves when deletion is complete

**Features:**
- Checks permissions via `PolicyEngine` (requires 'destroy' permission)
- **Optimistic update**: Removes entities immediately from store
- **Automatic rollback**: Restores state on error (except network errors)
- Emits event `{storeName}:destroy:many`

**Warning:** This operation is irreversible. The entities are permanently deleted.

**Endpoint:** `DELETE {endpoint}`
**Body:** Array of entity IDs

---

---

## 🔐 Policy / Permission Layer

### Overview

The `PolicyEngine` service provides a centralized way to check permissions before CRUD operations. By default, all operations are allowed.

### Custom Implementation

Override the `PolicyEngine` to implement your own permission logic:

```ts
import { Injectable, inject } from '@angular/core';
import { PolicyEngine, CrudAction } from '@nicolaselge/ng-store';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class CustomPolicyEngine extends PolicyEngine {
  private auth = inject(AuthService);

  override can(action: CrudAction, entity?: any): boolean {
    const user = this.auth.currentUser();
    if (!user) return false;

    switch (action) {
      case 'destroy':
        // Only admins can permanently delete
        return user.role === 'admin';
      
      case 'update':
        // Users can update their own entities, admins can update any
        return user.id === entity?.ownerId || user.role === 'admin';
      
      case 'create':
        // Only authenticated users can create
        return !!user;
      
      default:
        return true;
    }
  }
}
```

### Provide the Custom Engine

```ts
// In app.config.ts
import { provideHttpClient } from '@angular/common/http';
import { PolicyEngine } from '@nicolaselge/ng-store';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    { provide: PolicyEngine, useClass: CustomPolicyEngine }
  ]
};
```

### How it works

- Before each CRUD operation, `BaseCrudStore` calls `policy.can(action, entity)`
- If `false`, an error is thrown: `Permission denied: cannot {action} entity in store '{storeName}'`
- The operation is blocked before any optimistic update or HTTP request

---

## 📡 Event Bus

### Overview

The `EventBus` service enables decoupled communication between stores and other parts of your application.

### How it works

Stores automatically emit events after successful CRUD operations:

- Format: `{storeName}:{action}:{type}`
- Examples: `users:create:one`, `users:update:many`, `users:delete:one`

### Listening to Events

```ts
import { EventBus } from '@nicolaselge/ng-store';
import { inject, OnInit, OnDestroy } from '@angular/core';

@Component({...})
export class UserNotificationsComponent implements OnInit, OnDestroy {
  private eventBus = inject(EventBus);
  private notificationService = inject(NotificationService);

  ngOnInit() {
    // Listen to user creation events
    this.eventBus.on('users:create:one', (user) => {
      this.notificationService.show(`User ${user.name} created`);
    });

    // Listen to bulk updates
    this.eventBus.on('users:update:many', (users) => {
      this.notificationService.show(`${users.length} users updated`);
    });

    // Listen to all user events
    this.eventBus.on('users:delete:one', (user) => {
      this.analytics.track('user_deleted', { userId: user.id });
    });
  }
}
```

### Available Events

For a store named `users`, the following events are emitted:

- `users:get:one` - After `getOne()` succeeds
- `users:get:all` - After `getAll()` succeeds
- `users:get:many` - After `getMany()` succeeds
- `users:create:one` - After `createOne()` succeeds
- `users:create:many` - After `createMany()` succeeds
- `users:update:one` - After `updateOne()` succeeds
- `users:update:many` - After `updateMany()` succeeds
- `users:patch:one` - After `patchOne()` succeeds
- `users:patch:many` - After `patchMany()` succeeds
- `users:delete:one` - After `deleteOne()` succeeds
- `users:delete:many` - After `deleteMany()` succeeds
- `users:restore:one` - After `restoreOne()` succeeds
- `users:restore:many` - After `restoreMany()` succeeds
- `users:destroy:one` - After `destroyOne()` succeeds
- `users:destroy:many` - After `destroyMany()` succeeds

### Use Cases

- **Analytics**: Track user actions
- **Notifications**: Show success/error messages
- **Cross-store updates**: Update related stores when data changes
- **Side effects**: Trigger additional operations
- **UI updates**: Refresh related components

---

## 🔁 DTO Mapping (snake_case ↔ camelCase)

### Why DTO Mapping?

This library works **internally with camelCase entities**, following TypeScript and Angular conventions.

If your backend API returns **snake_case JSON** (common with SQL-based or legacy backends), you must provide a **DTO ↔ Entity mapper**.

**Benefits:**
- Keep frontend code idiomatic and clean
- Decouple backend representation from frontend domain
- Avoid leaking API formats into components and stores
- Change backend format without touching components

---

### Entity Mapper Interface

```ts
export interface EntityMapper<DTO, Entity> {
  fromDto(dto: DTO): Entity;
  toDto(entity: Entity): DTO;
}
```

---

### Complete Example

#### 1. Define DTO (Backend Format)

```ts
export interface UserDto {
  id: number;
  name: string;
  email: string;
  created_at: string;      // snake_case
  updated_at: string;      // snake_case
  deleted_at: string | null;
}
```

#### 2. Define Entity (Frontend Format)

```ts
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;      // camelCase
  updatedAt: string;      // camelCase
  deletedAt: string | null;
}
```

#### 3. Create the Mapper

```ts
import { Injectable } from '@angular/core';
import { EntityMapper } from '@nicolaselge/ng-store';

@Injectable({ providedIn: 'root' })
export class UserMapper implements EntityMapper<UserDto, User> {
  fromDto(dto: UserDto): User {
    return {
      id: dto.id,
      name: dto.name,
      email: dto.email,
      createdAt: dto.created_at,      // snake_case → camelCase
      updatedAt: dto.updated_at,      // snake_case → camelCase
      deletedAt: dto.deleted_at
    };
  }

  toDto(entity: User): UserDto {
    return {
      id: entity.id,
      name: entity.name,
      email: entity.email,
      created_at: entity.createdAt,    // camelCase → snake_case
      updated_at: entity.updatedAt,    // camelCase → snake_case
      deleted_at: entity.deletedAt
    };
  }
}
```

#### 4. Create the Store with Mapper

```ts
import { Injectable, inject } from '@angular/core';
import { BaseCrudStore } from '@nicolaselge/ng-store';

@Injectable({ providedIn: 'root' })
export class UserStore extends BaseCrudStore<User, UserDto> {
  protected override storeName = 'users';
  protected override endpoint = '/api/users';
  protected mapper = inject(UserMapper);
}
```

#### 5. Use in Component

```ts
@Component({...})
export class UserComponent {
  private userStore = inject(UserStore);

  // The store returns User entities (camelCase), not UserDto
  readonly users = this.userStore.entities;

  async createUser() {
    // You work with User entities (camelCase)
    const newUser: User = {
      id: 0, // Will be set by the server
      name: 'John Doe',
      email: 'john@example.com',
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      deletedAt: null
    };

    // The mapper automatically converts User → UserDto before sending
    await this.userStore.createOne(newUser);
  }
}
```

### How it works

The transformation happens automatically in `BaseCrudStore`:

1. **On requests (POST, PUT, PATCH)**: 
   - The entity is converted to DTO using `mapper.toDto()` before sending to the backend
   - Location: `BaseCrudStore.toDto()` → `mapper.toDto(entity)`

2. **On responses (GET, POST, PUT, PATCH)**: 
   - The DTO from the backend is converted to entity using `mapper.fromDto()` 
   - This happens in the `onSuccess` callback and in the returned Promise
   - Location: `BaseCrudStore.fromDto()` → `mapper.fromDto(dto)`

3. **In your components**: 
   - You always work with clean camelCase entities
   - The DTO transformation is completely transparent

---

## 🌐 HTTP Effects

### Overview

`HttpEffects` is a fully typed HTTP client wrapper that extends Angular's `HttpClient` with additional features:

- Type-safe requests with full TypeScript support
- Automatic request/response logging (dev mode only)
- Loading and error state management via Signals
- Path parameter resolution (e.g., `:id` → actual value)
- Query parameter handling
- Callback support (onSuccess, onError)

### Basic Usage

```ts
import { HttpEffects } from '@nicolaselge/ng-store';
import { inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpEffects);

  // Simple GET request
  getUser(id: number) {
    return this.http.get<User>('/api/users/:id', {
      onSuccess: (user) => console.log('User loaded:', user),
      onError: (error) => console.error('Failed to load user:', error),
    }).run({ path: { id } });
  }

  // POST with body and query parameters
  searchUsers(query: string) {
    return this.http.post<User[], SearchParams>('/api/users/search', {
    }).run({
      body: { query },
      query: { limit: 10, offset: 0 }
    });
  }

  // PUT with path parameters
  updateUser(user: User) {
    return this.http.put<User, User>('/api/users/:id', {
      onSuccess: (updated) => console.log('User updated:', updated),
    }).run({
      path: { id: user.id },
      body: user
    });
  }
}
```

### HTTP Methods

All standard HTTP methods are supported:

- `get<TResult>(url, opts?)` - GET request
- `post<TResult, TBody>(url, opts?)` - POST request
- `put<TResult, TBody>(url, opts?)` - PUT request
- `patch<TResult, TBody>(url, opts?)` - PATCH request
- `delete<TResult, TBody>(url, opts?)` - DELETE request

### Request Options

```ts
interface HttpEffectsOptions<TResult, TBody> {
  // Custom options
  onSuccess?: (result: TResult, params?: HttpEffectsParams<TBody>) => void;
  onError?: (error: any) => void;

  // Standard Angular HTTP options
  headers?: HttpHeaders | Record<string, string | string[]>;
  observe?: 'body' | 'events' | 'response';
  responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';
  reportProgress?: boolean;
  withCredentials?: boolean;
  timeout?: number;
  // ... and more (all Angular HttpClient options are supported)
}
```

### Request Parameters

```ts
interface HttpEffectsParams<TBody> {
  path?: Record<string, string | number>;  // URL path parameters (e.g., :id)
  body?: TBody;                              // Request body
  query?: Record<string, any>;               // Query parameters
}
```

### Return Value

All HTTP methods return an object with:

```ts
interface HttpEffectsResult<TResult> {
  run: (params?: HttpEffectsParams<any>) => Promise<TResult>;
  loading: Signal<boolean>;
  error: Signal<any>;
}
```

**Example:**
```ts
const request = this.http.get<User>('/api/users/:id');

// Access loading state
const isLoading = request.loading(); // Signal<boolean>

// Access error state
const error = request.error(); // Signal<any>

// Execute the request
const user = await request.run({ path: { id: 123 } });
```

---

## 📊 HTTP Logging

### Overview

HTTP requests are automatically logged in development mode using `isDevMode()`. Logs are automatically disabled in production builds.

### Log Format

**Request:**
```
[HTTP] GET /api/users/:id { path: { id: 123 } }
```

**Success:**
```
[HTTP] GET /api/users/123 → SUCCESS (45ms) { statusCode: 200, result: {...} }
```

**Error:**
```
[HTTP] POST /api/users → ERROR [400] (12ms) { error: {...} }
```

### Customizing Logging Behavior

You can override the default behavior by providing a custom configuration:

```ts
import { STORE_LOGGER_CONFIG } from '@nicolaselge/ng-store';

// In your app.config.ts or module providers
providers: [
  {
    provide: STORE_LOGGER_CONFIG,
    useValue: {
      enableHttpLogs: true,   // Force enable (even in production)
      enableStoreLogs: false  // Disable store logs
    }
  }
]
```

### Configuration Options

```ts
interface StoreLoggerConfig {
  /**
   * Force enable/disable HTTP logs.
   * If undefined, uses isDevMode() by default.
   */
  enableHttpLogs?: boolean;

  /**
   * Force enable/disable store logs.
   * If undefined, uses isDevMode() by default.
   */
  enableStoreLogs?: boolean;
}
```

---

---

## 🌐 Expected Backend REST Contract

### Required Entity Fields (Logical Model)

Your entities should have these fields (names can vary, but concepts should exist):

```json
{
  "id": "uuid | number",
  "createdAt": "ISO string",
  "updatedAt": "ISO string",
  "deletedAt": null
}
```

### Endpoint Structure

The library expects the following REST endpoints:

**Read:**
- `GET {endpoint}` - Get all entities
- `GET {endpoint}/:id` - Get one entity
- `POST {endpoint}/many` - Get many entities (body: array of IDs)

**Create:**
- `POST {endpoint}` - Create one entity
- `POST {endpoint}/bulk` - Create many entities

**Update:**
- `PUT {endpoint}/:id` - Update one entity (full replacement)
- `PUT {endpoint}/bulk` - Update many entities
- `PATCH {endpoint}/:id` - Partially update one entity
- `PATCH {endpoint}/bulk` - Partially update many entities

**Delete:**
- `PATCH {endpoint}/:id/delete` - Soft delete one entity
- `PATCH {endpoint}/delete` - Soft delete many entities (body: array of IDs)
- `PATCH {endpoint}/:id/restore` - Restore one entity
- `PATCH {endpoint}/restore` - Restore many entities (body: array of IDs)
- `DELETE {endpoint}/:id` - Hard delete one entity
- `DELETE {endpoint}` - Hard delete many entities (body: array of IDs)

---

## 📦 Philosophy

This library is designed for:

- **Large Angular applications** - Scalable architecture for complex projects
- **Long-term maintainability** - Clean code, clear patterns, comprehensive documentation
- **Clean, layered architectures** - Separation of concerns, dependency injection
- **Real-world backend constraints** - Handles snake_case, soft deletes, etc.

---

## 💡 Advanced Patterns

### Custom Store with Additional Methods

You can extend `BaseCrudStore` with custom methods:

```ts
@Injectable({ providedIn: 'root' })
export class UserStore extends BaseCrudStore<User> {
  protected override storeName = 'users';
  protected override endpoint = '/api/users';

  // Custom method
  async searchUsers(query: string) {
    return this.http.post<User[]>(
      `${this.endpoint}/search`,
    ).run({ body: { query } });
  }

  // Custom computed property
  get activeUsers() {
    return computed(() => 
      this.entities().filter(u => !u.deletedAt)
    );
  }
}
```

### Listening to Store Events

```ts
@Component({...})
export class UserEffectsComponent implements OnInit {
  private eventBus = inject(EventBus);
  private analytics = inject(AnalyticsService);

  ngOnInit() {
    // Track user creation
    this.eventBus.on('users:create:one', (user) => {
      this.analytics.track('user_created', {
        userId: user.id,
        userName: user.name
      });
    });

    // Notify on bulk updates
    this.eventBus.on('users:update:many', (users) => {
      this.notificationService.show(
        `${users.length} users updated successfully`
      );
    });
  }
}
```

### Custom Permission Logic

```ts
@Injectable({ providedIn: 'root' })
export class CustomPolicyEngine extends PolicyEngine {
  private auth = inject(AuthService);
  private userRoles = inject(UserRolesService);

  override can(action: CrudAction, entity?: any): boolean {
    const user = this.auth.currentUser();
    if (!user) return false;

    // Admin can do everything
    if (user.role === 'admin') return true;

    // Check specific permissions
    switch (action) {
      case 'destroy':
        return false; // Only admins (checked above)
      
      case 'update':
      case 'delete':
        // Users can modify their own entities
        return entity?.ownerId === user.id;
      
      case 'create':
        return this.userRoles.hasPermission(user, 'create:users');
      
      default:
        return true;
    }
  }
}
```

---

## 🎯 Best Practices

### 1. Store Naming

Use descriptive, plural names for stores:

```ts
// ✅ Good
protected override storeName = 'users';
protected override storeName = 'products';
protected override storeName = 'orders';

// ❌ Bad
protected override storeName = 'user';
protected override storeName = 'data';
```

### 2. Entity Structure

Always include required fields for proper CRUD operations:

```ts
export interface User {
  id: number;              // Required: unique identifier
  createdAt: string;       // Required: creation timestamp
  updatedAt: string;       // Required: update timestamp
  deletedAt?: string | null; // Optional: for soft delete
  // ... your custom fields
}
```

### 3. Error Handling

Always handle errors in your components:

```ts
async createUser(user: User) {
  try {
    await this.userStore.createOne(user);
    this.notificationService.showSuccess('User created');
  } catch (error: any) {
    if (error.message?.includes('Permission denied')) {
      this.notificationService.showError('You do not have permission');
    } else {
      this.notificationService.showError('Failed to create user');
    }
  }
}
```

### 4. Using Signals in Templates

Always call Signals as functions in templates:

```ts
// ✅ Good
<div *ngFor="let user of users()">{{ user.name }}</div>
<div *ngIf="loading()">Loading...</div>

// ❌ Bad
<div *ngFor="let user of users">{{ user.name }}</div>
```

### 5. DTO Mapping

Use mappers when your backend uses different naming conventions:

```ts
// ✅ Use mapper for snake_case backend
export class UserStore extends BaseCrudStore<User, UserDto> {
  protected mapper = inject(UserMapper);
}

// ✅ Skip mapper if backend uses camelCase
export class UserStore extends BaseCrudStore<User> {
  // No mapper needed
}
```

---

## 🐛 Troubleshooting

### Events are not being received

**Problem:** Event listeners are not triggered.

**Solution:** 
- Ensure the event name matches exactly: `{storeName}:{action}:{type}`
- Check that the store's `storeName` is correctly set
- Verify the listener is registered before the event is emitted

### Permissions are always denied

**Problem:** All operations throw "Permission denied" errors.

**Solution:** 
- Check your `PolicyEngine` implementation
- Ensure `can()` method returns `true` for allowed operations
- Verify the custom `PolicyEngine` is properly provided in your app config

### DTO mapping not working

**Problem:** Entities are not transformed to/from DTOs.

**Solution:**
- Verify the mapper is injected: `protected mapper = inject(UserMapper);`
- Check that the store extends `BaseCrudStore<User, UserDto>` (with DTO type)
- Ensure mapper methods (`fromDto`, `toDto`) are correctly implemented

### Optimistic updates not visible

**Problem:** UI doesn't update immediately after mutations.

**Solution:**
- Use Signals in templates: `users()` not `users`
- Check that you're using the `entities` getter: `this.userStore.entities`
- Verify the component is using change detection (Signals trigger it automatically)

---

## 🗺️ Roadmap

- v1.0: Stable CRUD ✅
- v1.1: IndexedDB adapter
- v1.2: Devtools & inspection tools
- v2.0: Optional GraphQL adapter

---

## 📝 License

[Your License Here]