@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.
📦 Installation
npm install @mysteryinfosolutions/api-corePeer 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 filterresetFilter(defaults?)- Reset to defaultssetPage(page: number)- Change pagesetPageLength(pageLength: number)- Change page size
Sorting
setSort(column: string, order?: 'ASC' | 'DESC')- Add/update sortremoveSort(column: string)- Remove specific sortclearSort()- Clear all sorting
Records Management
setRecords(records: TRecord[])- Replace all recordsappendRecords(records: TRecord[])- Add recordsremoveRecordById(id, idKey?)- Remove recordreplaceRecord(updated: TRecord, idKey?)- Update record
Pagination
setPager(pager: IMultiresultMetaData)- Set pagersetApiResponse(response: IMultiresult<TRecord>)- Set records + pagerhasMorePages()- Check if more pages existhasPreviousPage()- Check if previous page exists
Selection
select(record: TRecord)- Select recordclearSelection()- Clear selectionisSelected(record: TRecord, idKey?)- Check if selected
Loading States
setLoading(key: string, value: boolean)- Set loading stateisLoading$(key: string)- Observable for specific loading keyclearLoading(key?)- Clear loading state(s)
Error Handling
setError(error: string | null)- Set error message
Lifecycle
reset()- Reset all statedestroy()- Reset and complete subscriptionsdestroySubscriptions(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 }); // falsePermission 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=25Sorting:
?sort=name:ASC,createdAt:DESCFiltering:
?search=laptop&category=electronics&minPrice=100Combined:
?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:
- Fork the repository
- Create a feature branch
- Write tests for new features
- Ensure all tests pass
- 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
- Email: [email protected]
🙏 Credits
Developed and maintained by Mystery Info Solutions.
📚 Additional Resources
Version: 1.8.0
Last Updated: November 2024
