npm package: @mysteryinfosolutions/api-core
Description: A comprehensive Angular library providing robust base services, state management, and utilities for building data-driven applications with RESTful APIs.
Latest version: 1.9.3

# @mysteryinfosolutions/api-core

A comprehensive Angular library providing robust base services, state management, and utilities for building data-driven applications with RESTful APIs.

[![Angular](https://img.shields.io/badge/Angular-20.0.0-red)](https://angular.io/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.8.2-blue)](https://www.typescriptlang.org/)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)

## 📦 Installation

```bash
npm install @mysteryinfosolutions/api-core
```

### Peer Dependencies

Ensure you have the following peer dependencies installed:

```bash
npm install @angular/common@^20.0.0 @angular/core@^20.0.0 rxjs@^7.0.0
```

## 🎯 Features

- ✅ **Generic CRUD Service** - Type-safe base service for REST operations
- ✅ **State Management** - Comprehensive reactive state management with RxJS
- ✅ **Query Builder** - Convert filters to query strings automatically
- ✅ **Pagination & Sorting** - Built-in support with metadata
- ✅ **Permission System** - Generate and manage resource permissions
- ✅ **Table Configuration** - Standardized table/grid configurations
- ✅ **Type-Safe Models** - Interfaces for API responses and filters
- ✅ **Loading States** - Context-aware loading state management
- ✅ **Tree-Shakable** - Optimized bundle size

---

## 🚀 Quick Start

### 1. Basic Service Example

Create a service for your resource using `BaseService`:

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BaseService } from '@mysteryinfosolutions/api-core';

// Define your model
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  createdAt: string;
}

// Define your filter (optional custom fields)
interface UserFilter {
  role?: string;
  active?: boolean;
  page?: number;
  pageLength?: number;
  search?: string;
}

@Injectable({ providedIn: 'root' })
export class UserService extends BaseService<User, UserFilter> {
  constructor(http: HttpClient) {
    super(http, '/api/users');
  }
}
```

### 2. Use the Service in a Component

```typescript
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-users',
  template: `
    <div *ngIf="users$ | async as users">
      <div *ngFor="let user of users.data?.records">
        {{ user.name }} - {{ user.email }}
      </div>
    </div>
  `
})
export class UsersComponent implements OnInit {
  users$ = this.userService.getAll({ page: 1, pageLength: 10 });

  constructor(private userService: UserService) {}

  ngOnInit() {
    // Load users on init
    this.loadUsers();
  }

  loadUsers() {
    this.userService.getAll({
      page: 1,
      pageLength: 25,
      role: 'admin',
      search: 'john'
    }).subscribe(response => {
      if (response.data) {
        console.log('Users:', response.data.records);
        console.log('Total:', response.data.pager.totalRecords);
      }
    });
  }

  createUser() {
    this.userService.create({
      name: 'John Doe',
      email: 'john@example.com',
      role: 'user'
    }).subscribe(response => {
      console.log('Created:', response.data);
    });
  }

  updateUser(id: number) {
    this.userService.update(id, {
      name: 'Jane Doe'
    }).subscribe(response => {
      console.log('Updated:', response.data);
    });
  }

  deleteUser(id: number) {
    // Soft delete (default)
    this.userService.delete(id).subscribe(() => {
      console.log('Deleted');
    });

    // Hard delete
    this.userService.delete(id, 'hard').subscribe(() => {
      console.log('Permanently deleted');
    });
  }
}
```

---

## 📚 Core Components

### BaseService<T, TFilter, TCreate, TUpdate>

Generic HTTP service providing CRUD operations.

#### Type Parameters

| Parameter | Description | Default |
|-----------|-------------|---------|
| `T` | The full model type | Required |
| `TFilter` | Filter type (extends `Partial<T> & Filter`) | `Partial<T> & Filter` |
| `TCreate` | DTO for creating records | `Partial<T>` |
| `TUpdate` | DTO for updating records | `Partial<T>` |

#### Methods

| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| `getAll()` | `filter?: TFilter` | `Observable<IResponse<IMultiresult<T>>>` | Fetch paginated list |
| `getDetails()` | `id: number` | `Observable<IResponse<T>>` | Fetch single record |
| `create()` | `data: TCreate` | `Observable<IResponse<T>>` | Create new record |
| `update()` | `id: number, data: TUpdate` | `Observable<IResponse<T>>` | Update existing record |
| `delete()` | `id: number, method?: 'soft' \| 'hard'` | `Observable<IResponse<any>>` | Delete record |

#### Example with Custom DTOs

```typescript
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  role: string;
}

interface CreateUserDto {
  name: string;
  email: string;
  password: string;
  role: string;
}

interface UpdateUserDto {
  name?: string;
  email?: string;
  role?: string;
  // Note: password excluded for updates
}

@Injectable({ providedIn: 'root' })
export class UserService extends BaseService<
  User,
  UserFilter,
  CreateUserDto,
  UpdateUserDto
> {
  constructor(http: HttpClient) {
    super(http, '/api/users');
  }
}
```

---

### BaseStateService<TRecord, TFilter>

Comprehensive reactive state management for data-driven features.

#### State Properties

| Observable | Type | Description |
|------------|------|-------------|
| `filter$` | `Observable<TFilter>` | Current filter state |
| `records$` | `Observable<TRecord[]>` | Current records |
| `pager$` | `Observable<IMultiresultMetaData \| null>` | Pagination metadata |
| `selected$` | `Observable<TRecord \| null>` | Selected record |
| `loading$` | `Observable<Record<string, boolean>>` | Loading states |
| `error$` | `Observable<string \| null>` | Error message |

#### Getters

| Getter | Type | Description |
|--------|------|-------------|
| `currentFilter` | `TFilter` | Current filter value |
| `currentRecords` | `TRecord[]` | Current records value |
| `selected` | `TRecord \| null` | Selected record value |

#### Key Methods

**Filter Management**
- `setFilter(update: Partial<TFilter>)` - Update filter
- `resetFilter(defaults?)` - Reset to defaults
- `setPage(page: number)` - Change page
- `setPageLength(pageLength: number)` - Change page size

**Sorting**
- `setSort(column: string, order?: 'ASC' | 'DESC')` - Add/update sort
- `removeSort(column: string)` - Remove specific sort
- `clearSort()` - Clear all sorting

**Records Management**
- `setRecords(records: TRecord[])` - Replace all records
- `appendRecords(records: TRecord[])` - Add records
- `removeRecordById(id, idKey?)` - Remove record
- `replaceRecord(updated: TRecord, idKey?)` - Update record

**Pagination**
- `setPager(pager: IMultiresultMetaData)` - Set pager
- `setApiResponse(response: IMultiresult<TRecord>)` - Set records + pager
- `hasMorePages()` - Check if more pages exist
- `hasPreviousPage()` - Check if previous page exists

**Selection**
- `select(record: TRecord)` - Select record
- `clearSelection()` - Clear selection
- `isSelected(record: TRecord, idKey?)` - Check if selected

**Loading States**
- `setLoading(key: string, value: boolean)` - Set loading state
- `isLoading$(key: string)` - Observable for specific loading key
- `clearLoading(key?)` - Clear loading state(s)

**Error Handling**
- `setError(error: string | null)` - Set error message

**Lifecycle**
- `reset()` - Reset all state
- `destroy()` - Reset and complete subscriptions
- `destroySubscriptions(subjects?)` - Complete specific subjects

#### Complete Example

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BaseStateService } from '@mysteryinfosolutions/api-core';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

interface ProductFilter {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  page?: number;
  pageLength?: number;
  search?: string;
}

@Component({
  selector: 'app-products',
  template: `
    <div class="filters">
      <input 
        [value]="(state.filter$ | async)?.search || ''"
        (input)="onSearch($event)"
        placeholder="Search products...">
      
      <select (change)="onCategoryChange($event)">
        <option value="">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
    </div>

    <div class="loading" *ngIf="state.isLoading$('list') | async">
      Loading...
    </div>

    <div class="error" *ngIf="state.error$ | async as error">
      {{ error }}
    </div>

    <table>
      <thead>
        <tr>
          <th (click)="state.setSort('name')">Name</th>
          <th (click)="state.setSort('price')">Price</th>
          <th>Category</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let product of state.records$ | async"
            [class.selected]="state.isSelected(product)">
          <td>{{ product.name }}</td>
          <td>{{ product.price | currency }}</td>
          <td>{{ product.category }}</td>
          <td>
            <button (click)="state.select(product)">Select</button>
            <button (click)="editProduct(product)">Edit</button>
            <button (click)="deleteProduct(product.id)">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>

    <div class="pagination" *ngIf="state.pager$ | async as pager">
      <button 
        (click)="previousPage()" 
        [disabled]="!state.hasPreviousPage()">
        Previous
      </button>
      
      <span>
        Page {{ pager.currentPage }} of {{ pager.lastPage }}
        ({{ pager.totalRecords }} total)
      </span>
      
      <button 
        (click)="nextPage()" 
        [disabled]="!state.hasMorePages()">
        Next
      </button>
    </div>
  `,
  providers: [BaseStateService]
})
export class ProductsComponent implements OnInit, OnDestroy {
  state = new BaseStateService<Product, ProductFilter>();

  constructor(private productService: ProductService) {}

  ngOnInit() {
    // Subscribe to filter changes and load data
    this.state.filter$.subscribe(filter => {
      this.loadProducts(filter);
    });

    // Set initial filter
    this.state.setFilter({
      page: 1,
      pageLength: 25,
      category: 'electronics'
    });
  }

  loadProducts(filter: ProductFilter) {
    this.state.setLoading('list', true);
    this.state.setError(null);

    this.productService.getAll(filter).subscribe({
      next: (response) => {
        if (response.data) {
          this.state.setApiResponse(response.data);
        }
        this.state.setLoading('list', false);
      },
      error: (err) => {
        this.state.setError('Failed to load products');
        this.state.setLoading('list', false);
      }
    });
  }

  onSearch(event: Event) {
    const search = (event.target as HTMLInputElement).value;
    this.state.setFilter({ search, page: 1 });
  }

  onCategoryChange(event: Event) {
    const category = (event.target as HTMLSelectElement).value;
    this.state.setFilter({ category, page: 1 });
  }

  nextPage() {
    const currentPage = this.state.currentFilter.page || 1;
    this.state.setPage(currentPage + 1);
  }

  previousPage() {
    const currentPage = this.state.currentFilter.page || 1;
    this.state.setPage(Math.max(1, currentPage - 1));
  }

  editProduct(product: Product) {
    this.state.select(product);
    // Open edit modal or navigate to edit page
  }

  deleteProduct(id: number) {
    this.state.setLoading('delete', true);
    
    this.productService.delete(id).subscribe({
      next: () => {
        this.state.removeRecordById(id);
        this.state.setLoading('delete', false);
      },
      error: (err) => {
        this.state.setError('Failed to delete product');
        this.state.setLoading('delete', false);
      }
    });
  }

  ngOnDestroy() {
    this.state.destroy();
  }
}
```

---

## 🔧 Utilities

### Query Builder

Convert filter objects to URL query strings.

```typescript
import { jsonToQueryString, isEmpty } from '@mysteryinfosolutions/api-core';

const filter = {
  page: 1,
  pageLength: 25,
  search: 'laptop',
  category: 'electronics',
  tags: ['new', 'sale'],
  sort: [
    { field: 'price', order: 'ASC' },
    { field: 'name', order: 'DESC' }
  ]
};

const queryString = jsonToQueryString(filter);
// Result: "?page=1&pageLength=25&search=laptop&category=electronics&tags=[new,sale]&sort=price:ASC,name:DESC"

// Check if object is empty
isEmpty({}); // true
isEmpty({ page: 1 }); // false
```

### Permission Generator

Generate type-safe permission maps for resources.

```typescript
import { generatePermissions } from '@mysteryinfosolutions/api-core';

// Standard permissions
const userPermissions = generatePermissions('user');
/* Result:
{
  '*': 'user.*',
  'superAdmin': 'user.superAdmin',
  'view': 'user.view',
  'list': 'user.list',
  'create': 'user.create',
  'update': 'user.update',
  'delete': 'user.delete',
  'restore': 'user.restore',
  'forceDelete': 'user.forceDelete',
  'export': 'user.export',
  'import': 'user.import',
  'approve': 'user.approve',
  'reject': 'user.reject',
  'archive': 'user.archive',
  'unarchive': 'user.unarchive',
  'duplicate': 'user.duplicate',
  'share': 'user.share',
  'assign': 'user.assign',
  'changeStatus': 'user.changeStatus',
  'print': 'user.print',
  'preview': 'user.preview',
  'publish': 'user.publish',
  'unpublish': 'user.unpublish',
  'sync': 'user.sync',
  'audit': 'user.audit',
  'comment': 'user.comment',
  'favorite': 'user.favorite',
  'reorder': 'user.reorder',
  'toggleVisibility': 'user.toggleVisibility',
  'managePermissions': 'user.managePermissions',
  'assignRole': 'user.assignRole',
  'configure': 'user.configure',
  // ... all 30+ standard permissions
}
*/

// With custom permissions
const productPermissions = generatePermissions('product', ['discount', 'featured']);
/* Adds:
{
  ...standardPermissions,
  'discount': 'product.discount',
  'featured': 'product.featured'
}
*/

// Usage in component
@Component({
  template: `
    <button *ngIf="hasPermission(permissions.create)">
      Create Product
    </button>
  `
})
export class ProductsComponent {
  permissions = generatePermissions('product');

  hasPermission(permission: string): boolean {
    // Check with your auth service
    return this.authService.hasPermission(permission);
  }
}
```

---

## 🎨 Resource Configuration

Standardize table/grid configurations across your app.

```typescript
import { BaseResourceConfig, generatePermissions } from '@mysteryinfosolutions/api-core';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
  createdAt: string;
  updatedAt: string;
}

class ProductConfig extends BaseResourceConfig<Product> {
  columns = [
    {
      key: 'id',
      label: 'ID',
      width: '80px',
      isSortable: true
    },
    {
      key: 'name',
      label: 'Product Name',
      isSortable: true,
      type: 'text'
    },
    {
      key: 'price',
      label: 'Price',
      isSortable: true,
      pipe: 'currency',
      width: '120px'
    },
    {
      key: 'category',
      label: 'Category',
      isSortable: true
    },
    {
      key: 'updatedAt',
      label: 'Last Updated',
      isSortable: true,
      pipe: 'date',
      hideOnMobile: true
    }
  ];

  searchColumns = ['name', 'category'];
  defaultSortColumn = 'updatedAt';
  defaultSortOrder = 'DESC';
  defaultPageLength = 25;
  pageLengthOptions = [10, 25, 50, 100];
  
  modifyModalSize = 'lg';
  summaryModalSize = 'md';
  defaultDetailView = 'summary';
  
  permissions = generatePermissions('product');
}

// Usage
const config = new ProductConfig();
```

---

## 📋 Models & Types

### IResponse<T>

Standard API response wrapper.

```typescript
interface IResponse<T> {
  status?: number;
  data?: T | null;
  error?: IMisError;
  infoDtls?: any;
}
```

### IMultiresult<T>

Paginated list response.

```typescript
interface IMultiresult<T> {
  records: T[];
  pager: IMultiresultMetaData;
}

interface IMultiresultMetaData {
  totalRecords: number;
  previous?: number;
  currentPage?: number;
  next?: number;
  perPage?: number;
  segment?: number;
  lastPage?: number;
}
```

### Filter

Base filter class with pagination and sorting.

```typescript
abstract class Filter {
  ids?: number[];
  dateRangeColumn?: string;
  dateRangeFrom?: string;
  dateRangeTo?: string;
  
  page?: number;
  pageLength?: number;
  
  sort?: SortItem[];
  
  search?: string;
  searchColumns?: string;
  selectColumns?: string;
  selectMode?: SELECT_MODE;
}
```

### SortItem

Sort configuration.

```typescript
class SortItem {
  constructor(
    public field: string,
    public order: 'ASC' | 'DESC'
  ) {}
}
```

### TableColumn<T>

Column configuration for tables.

```typescript
interface TableColumn<T = any> {
  key: keyof T | string;
  label: string;
  isSortable?: boolean;
  width?: string;
  type?: 'text' | 'checkbox' | 'action' | 'custom';
  valueGetter?: (row: T) => any;
  hideOnMobile?: boolean;
  pipe?: 'date' | 'currency' | 'uppercase' | 'lowercase' | string;
  cellClass?: string;
  headerClass?: string;
  visible?: boolean;
}
```

---

## 🔌 API Format Requirements

This library expects your backend API to follow this format:

### Response Format

**Success Response:**
```json
{
  "status": 200,
  "data": { /* your data */ },
  "infoDtls": null
}
```

**List Response:**
```json
{
  "status": 200,
  "data": {
    "records": [
      { "id": 1, "name": "Item 1" },
      { "id": 2, "name": "Item 2" }
    ],
    "pager": {
      "totalRecords": 100,
      "currentPage": 1,
      "lastPage": 10,
      "perPage": 10,
      "next": 2,
      "previous": null
    }
  }
}
```

**Error Response:**
```json
{
  "status": 400,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": "Email is required"
  }
}
```

### Query String Format

**Pagination:**
```
?page=1&pageLength=25
```

**Sorting:**
```
?sort=name:ASC,createdAt:DESC
```

**Filtering:**
```
?search=laptop&category=electronics&minPrice=100
```

**Combined:**
```
?page=1&pageLength=25&sort=price:ASC&search=laptop&category=electronics
```

---

## 🏗 Best Practices

### 1. Service Pattern

```typescript
// ✅ Good: Extend BaseService
@Injectable({ providedIn: 'root' })
export class UserService extends BaseService<User, UserFilter> {
  constructor(http: HttpClient) {
    super(http, '/api/users');
  }

  // Add custom methods
  activateUser(id: number): Observable<IResponse<User>> {
    return this.http.post<IResponse<User>>(`${this.baseUrl}/${id}/activate`, {});
  }
}

// ❌ Bad: Don't reimplement CRUD
@Injectable({ providedIn: 'root' })
export class UserService {
  getAll() { /* duplicate code */ }
  getDetails() { /* duplicate code */ }
  // ...
}
```

### 2. State Management

```typescript
// ✅ Good: Use BaseStateService for complex lists
@Component({
  providers: [BaseStateService] // Component-level
})
export class UsersComponent {
  state = new BaseStateService<User, UserFilter>();
  
  ngOnDestroy() {
    this.state.destroy(); // Clean up
  }
}

// ✅ Good: Direct observable for simple cases
@Component({})
export class UserDetailComponent {
  user$ = this.userService.getDetails(this.userId);
}
```

### 3. Error Handling

```typescript
// ✅ Good: Handle errors properly
loadUsers() {
  this.state.setLoading('list', true);
  this.state.setError(null);

  this.userService.getAll(this.state.currentFilter).subscribe({
    next: (response) => {
      if (response.data) {
        this.state.setApiResponse(response.data);
      } else if (response.error) {
        this.state.setError(response.error.message || 'Failed to load');
      }
      this.state.setLoading('list', false);
    },
    error: (err) => {
      this.state.setError('Network error occurred');
      this.state.setLoading('list', false);
      console.error('Error loading users:', err);
    }
  });
}
```

### 4. Filter Management

```typescript
// ✅ Good: Reset page when filter changes
onCategoryChange(category: string) {
  this.state.setFilter({ 
    category, 
    page: 1  // Reset to first page
  });
}

// ✅ Good: Debounce search input
searchInput$ = new Subject<string>();

ngOnInit() {
  this.searchInput$.pipe(
    debounceTime(300),
    distinctUntilChanged()
  ).subscribe(search => {
    this.state.setFilter({ search, page: 1 });
  });
}
```

### 5. Permissions

```typescript
// ✅ Good: Generate once, reuse everywhere
export class UserConfig extends BaseResourceConfig<User> {
  permissions = generatePermissions('user', ['resetPassword', 'sendInvite']);
}

// In component
if (this.authService.hasPermission(config.permissions.create)) {
  // Show create button
}
```

---

## 🔄 Migration from v1.x to v2.x

When updating to future versions, check the CHANGELOG.md for breaking changes.

---

## 🤝 Contributing

Contributions are welcome! Please follow these guidelines:

1. Fork the repository
2. Create a feature branch
3. Write tests for new features
4. Ensure all tests pass
5. Submit a pull request

---

## 📄 License

MIT License - see LICENSE file for details

---

## 🐛 Issues & Support

For issues, questions, or feature requests:
- GitHub Issues: [Create an issue](https://github.com/mysteryinfosolutions/angular-libraries/issues)
- Email: support@mysteryinfosolutions.com

---

## 🙏 Credits

Developed and maintained by **Mystery Info Solutions**.

---

## 📚 Additional Resources

- [Angular Documentation](https://angular.dev)
- [RxJS Documentation](https://rxjs.dev)
- [TypeScript Documentation](https://www.typescriptlang.org)

---

**Version:** 1.8.0  
**Last Updated:** November 2024