Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:24:57 +08:00
commit 1ca48c6d54
8 changed files with 2724 additions and 0 deletions

427
commands/create-service.md Normal file
View File

@@ -0,0 +1,427 @@
---
name: create-service
description: Generate Angular service with HTTP methods, error handling, caching, and proper dependency injection
---
Generate a production-ready Angular service with HTTP communication, state management, and error handling.
## Service Types
Ask the user which type:
1. **Data Service** - HTTP API communication
2. **State Service** - Global state management
3. **Utility Service** - Helper functions and utilities
4. **Facade Service** - Simplifies complex subsystems
## Information Needed
1. **Service name** - kebab-case (e.g., `user-service`)
2. **Service type** - Data, State, Utility, or Facade
3. **API endpoint** - Base URL for data services
4. **Data model** - What type of data does it handle?
5. **Operations needed** - CRUD? Search? Filter?
6. **Caching?** - Should responses be cached?
## Service Structure
### Data Service Template
```typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, shareReplay } from 'rxjs/operators';
import { environment } from '@environments/environment';
export interface DataModel {
id: string;
// Add properties
}
export interface QueryParams {
page?: number;
limit?: number;
search?: string;
}
@Injectable({
providedIn: 'root' // Singleton service
})
export class DataService {
private http = inject(HttpClient);
private baseUrl = `${environment.apiUrl}/data`;
// GET all with pagination
getAll(params?: QueryParams): Observable<DataModel[]> {
let httpParams = new HttpParams();
if (params) {
Object.keys(params).forEach(key => {
if (params[key as keyof QueryParams] !== undefined) {
httpParams = httpParams.set(key, params[key as keyof QueryParams]!.toString());
}
});
}
return this.http.get<DataModel[]>(this.baseUrl, { params: httpParams }).pipe(
retry(2),
catchError(this.handleError)
);
}
// GET by ID with caching
getById(id: string): Observable<DataModel> {
return this.http.get<DataModel>(`${this.baseUrl}/${id}`).pipe(
shareReplay(1), // Cache for multiple subscribers
catchError(this.handleError)
);
}
// POST - Create
create(data: Omit<DataModel, 'id'>): Observable<DataModel> {
return this.http.post<DataModel>(this.baseUrl, data).pipe(
catchError(this.handleError)
);
}
// PUT - Update
update(id: string, data: Partial<DataModel>): Observable<DataModel> {
return this.http.put<DataModel>(`${this.baseUrl}/${id}`, data).pipe(
catchError(this.handleError)
);
}
// PATCH - Partial update
patch(id: string, data: Partial<DataModel>): Observable<DataModel> {
return this.http.patch<DataModel>(`${this.baseUrl}/${id}`, data).pipe(
catchError(this.handleError)
);
}
// DELETE
delete(id: string): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
// Search
search(query: string): Observable<DataModel[]> {
return this.http.get<DataModel[]>(`${this.baseUrl}/search`, {
params: { q: query }
}).pipe(
catchError(this.handleError)
);
}
// Error handling
private handleError(error: any): Observable<never> {
console.error('Service error:', error);
let errorMessage = 'An error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
return throwError(() => new Error(errorMessage));
}
}
```
### State Service Template
```typescript
import { Injectable, signal, computed } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface AppState {
loading: boolean;
data: DataModel[];
selectedId: string | null;
error: string | null;
}
@Injectable({
providedIn: 'root'
})
export class StateService {
// Using Signals (Modern approach)
private dataSignal = signal<DataModel[]>([]);
private loadingSignal = signal(false);
private errorSignal = signal<string | null>(null);
// Public readonly signals
readonly data = this.dataSignal.asReadonly();
readonly loading = this.loadingSignal.asReadonly();
readonly error = this.errorSignal.asReadonly();
// Computed signals
readonly itemCount = computed(() => this.data().length);
readonly hasData = computed(() => this.data().length > 0);
// OR using BehaviorSubject (Traditional approach)
private stateSubject = new BehaviorSubject<AppState>({
loading: false,
data: [],
selectedId: null,
error: null
});
state$ = this.stateSubject.asObservable();
// Setters
setData(data: DataModel[]) {
this.dataSignal.set(data);
}
addItem(item: DataModel) {
this.dataSignal.update(current => [...current, item]);
}
updateItem(id: string, updates: Partial<DataModel>) {
this.dataSignal.update(current =>
current.map(item => item.id === id ? { ...item, ...updates } : item)
);
}
removeItem(id: string) {
this.dataSignal.update(current => current.filter(item => item.id !== id));
}
setLoading(loading: boolean) {
this.loadingSignal.set(loading);
}
setError(error: string | null) {
this.errorSignal.set(error);
}
clearState() {
this.dataSignal.set([]);
this.errorSignal.set(null);
this.loadingSignal.set(false);
}
}
```
### Facade Service Template
```typescript
import { Injectable, inject } from '@angular/core';
import { Observable, forkJoin, combineLatest } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class FacadeService {
private dataService = inject(DataService);
private stateService = inject(StateService);
private cacheService = inject(CacheService);
// Simplified API for components
readonly data$ = this.stateService.data;
readonly loading$ = this.stateService.loading;
// Complex operation simplified
loadData(): Observable<DataModel[]> {
this.stateService.setLoading(true);
return this.dataService.getAll().pipe(
tap(data => {
this.stateService.setData(data);
this.cacheService.set('data', data);
this.stateService.setLoading(false);
}),
catchError(error => {
this.stateService.setError(error.message);
this.stateService.setLoading(false);
return throwError(() => error);
})
);
}
// Orchestrate multiple services
initialize(): Observable<any> {
return forkJoin({
config: this.dataService.getConfig(),
user: this.dataService.getUser(),
permissions: this.dataService.getPermissions()
}).pipe(
tap(({ config, user, permissions }) => {
this.stateService.setConfig(config);
this.stateService.setUser(user);
this.stateService.setPermissions(permissions);
})
);
}
}
```
### Utility Service Template
```typescript
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UtilityService {
// Date utilities
formatDate(date: Date | string): string {
return new Date(date).toLocaleDateString('en-US');
}
// String utilities
capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
slugify(str: string): string {
return str
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
// Array utilities
groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
return array.reduce((result, item) => {
const groupKey = String(item[key]);
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
return result;
}, {} as Record<string, T[]>);
}
// Number utilities
formatCurrency(amount: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
}
// Validation utilities
isValidEmail(email: string): boolean {
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return regex.test(email);
}
}
```
## Service with Caching
```typescript
import { Injectable, inject } from '@angular/core';
import { Observable, of, timer } from 'rxjs';
import { tap, switchMap, shareReplay } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class CachedDataService {
private http = inject(HttpClient);
private cache = new Map<string, { data: any; timestamp: number }>();
private TTL = 5 * 60 * 1000; // 5 minutes
getData(key: string, forceRefresh = false): Observable<DataModel[]> {
const cached = this.cache.get(key);
// Return cached if valid
if (!forceRefresh && cached && Date.now() - cached.timestamp < this.TTL) {
return of(cached.data);
}
// Fetch fresh data
return this.http.get<DataModel[]>(`/api/${key}`).pipe(
tap(data => {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}),
shareReplay(1)
);
}
invalidateCache(key?: string) {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
}
```
## Test Template
```typescript
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch data', () => {
const mockData = [{ id: '1', name: 'Test' }];
service.getAll().subscribe(data => {
expect(data).toEqual(mockData);
});
const req = httpMock.expectOne(`${service['baseUrl']}`);
expect(req.request.method).toBe('GET');
req.flush(mockData);
});
});
```
## Best Practices
1. **Use `providedIn: 'root'`** for singleton services
2. **Inject dependencies with `inject()`** function
3. **Handle errors properly** with catchError
4. **Add retry logic** for network requests
5. **Cache responses** when appropriate
6. **Use interfaces** for data models
7. **Add JSDoc comments** for complex methods
8. **Write tests** for all public methods
9. **Use environment variables** for URLs
10. **Use HttpParams** for query parameters
## Usage
```bash
/angular-development:create-service
# Natural language
"Create a data service for products with CRUD operations"
"Generate a state service for shopping cart"
```