npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mysteryinfosolutions/api-core

v1.9.3

Published

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

Readme

@mysteryinfosolutions/api-core

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

Angular TypeScript License

📦 Installation

npm install @mysteryinfosolutions/api-core

Peer Dependencies

Ensure you have the following peer dependencies installed:

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:

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

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: '[email protected]',
      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

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

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.

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.

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.

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

Standard API response wrapper.

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

IMultiresult

Paginated list response.

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.

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.

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

TableColumn

Column configuration for tables.

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:

{
  "status": 200,
  "data": { /* your data */ },
  "infoDtls": null
}

List Response:

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

{
  "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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

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


🙏 Credits

Developed and maintained by Mystery Info Solutions.


📚 Additional Resources


Version: 1.8.0
Last Updated: November 2024