# ADR 0001: Implement `data-access` API Requests

| Field | Value |
|-------|-------|
| Status | Draft |
| Date | 29.09.2025 |
| Owners | Lorenz, Nino |
| Participants | N/A |
| Related ADRs | [ADR-0002](./0002-models-schemas-dtos-architecture.md) |
| Tags | architecture, data-access, library, swagger |

---
## Summary (Decision in One Sentence)
Standardize all data-access libraries with a three-layer architecture: Zod schemas for validation, domain models extending generated DTOs, and services with consistent error handling and logging.

## Context & Problem Statement

Inconsistent patterns across data-access libraries (`catalogue`, `remission`, `crm`, `oms`) cause:
- High cognitive load when switching domains
- Duplicated validation and error handling code
- Mixed approaches to request cancellation and logging
- No standard for extending generated DTOs

**Goals:** Standardize structure, reduce boilerplate 40%, eliminate validation runtime errors, improve type safety.

**Constraints:** Must integrate with generated Swagger clients, support AbortSignal, align with `@isa/core/logging`.

**Scope:** Schema validation, model extensions, service patterns, standard exports.

## Decision

Implement a **three-layer architecture** for all data-access libraries:

1. **Schema Layer** (`schemas/`): Zod schemas for input validation and type inference
   - Pattern: `<Operation>Schema` with `<Operation>` and `<Operation>Input` types
   - Example: `SearchItemsSchema`, `SearchItems`, `SearchItemsInput`

2. **Model Layer** (`models/`): Domain-specific types based on generated DTOs
   - **Simple re-export pattern** (default): `export type Product = ProductDTO;`
   - **Extension pattern** (when domain enhancements needed): `interface MyModel extends GeneratedDTO { ... }`
   - Use `EntityContainer<T>` for lazy-loaded relationships
   - **Rule**: Generated DTOs MUST NOT be imported outside data-access libraries (see ADR-0002)

3. **Service Layer** (`services/`): Injectable services integrating Swagger clients
   - Pattern: Async methods with AbortSignal support
   - Standardized error handling with `ResponseArgsError`
   - Structured logging via `@isa/core/logging`

**Standard exports structure:**
```typescript
// src/index.ts
export * from './lib/models';
export * from './lib/schemas';
export * from './lib/services';
// Optional: stores, helpers
```

## Rationale

**Why this approach:**
- **Type Safety**: Zod provides runtime validation + compile-time types with zero manual type definitions
- **Separation of Concerns**: Clear boundaries between validation, domain logic, and API integration
- **Consistency**: Identical patterns across all domains reduce cognitive load
- **Maintainability**: Changes to generated clients don't break domain-specific enhancements
- **Developer Experience**: Auto-completion, type inference, and standardized error handling improve velocity

**Evidence supporting this decision:**
- Analysis of 4 existing data-access libraries shows these patterns emerging naturally
- `RemissionReturnReceiptService` demonstrates successful integration with logging
- `EntityContainer<T>` pattern proven effective for relationship management
- Zod validation catches input errors before API calls, reducing backend load

## Consequences

**Positive:** Consistent patterns, runtime + compile-time type safety, clear maintainability, reusable utilities, structured debugging, optimized performance.

**Negative:** Migration effort for existing libs, learning curve for Zod, ~1-2ms validation overhead, extra abstraction layer.

**Open Questions:** User-facing error message conventions, testing standards.

## Detailed Design Elements

### Schema Validation Pattern

**Two schema patterns coexist:**

**Pattern A: Operation-based schemas** (for query/search operations)
```typescript
// Input validation schema for search operation
export const SearchByTermSchema = z.object({
  searchTerm: z.string().min(1, 'Search term must not be empty'),
  skip: z.number().int().min(0).default(0),
  take: z.number().int().min(1).max(100).default(20),
});

// Type inference
export type SearchByTerm = z.infer<typeof SearchByTermSchema>;
export type SearchByTermInput = z.input<typeof SearchByTermSchema>;
```

**Pattern B: Entity-based schemas** (for CRUD operations, see ADR-0002)
```typescript
// Full entity schema defining all fields
export const ProductSchema = z.object({
  id: z.string().optional(),
  name: z.string().min(1).max(255),
  contributor: z.string().optional(),
  price: z.number().positive().optional(),
  // ... all fields
});

// Derived validation schemas for specific operations
export const CreateProductSchema = ProductSchema.pick({ name: true });
export const UpdateProductSchema = ProductSchema.pick({ id: true, name: true }).required();

// Validate requests only, not responses
```

### Model Pattern

**Pattern A: Simple Re-export** (default, recommended - see ADR-0002)
```typescript
import { ProductDTO } from '@generated/swagger/catalogue-api';

/**
 * Product model for catalogue domain.
 * Simple re-export of generated DTO.
 */
export type Product = ProductDTO;
```

**Pattern B: Extension** (when domain-specific enhancements needed)
```typescript
import { ProductDTO } from '@generated/swagger/cat-search-api';

/**
 * Enhanced product with computed/derived fields.
 */
export interface Product extends ProductDTO {
  name: string;
  contributors: string;
  catalogProductNumber: string;
  // Domain-specific computed fields
  displayName: string;
  formattedPrice: string;
}
```

**Entity Container Pattern** (for lazy-loaded relationships)
```typescript
import { ReturnDTO } from '@generated/swagger/remission-api';
import { EntityContainer } from '@isa/common/data-access';

export interface Return extends ReturnDTO {
  id: number;
  receipts: EntityContainer<Receipt>[]; // Lazy-loaded relationships
}
```

**Important:** Generated DTOs (`@generated/swagger/*`) MUST NOT be imported directly in feature/UI libraries. Always import models from data-access.

### Service Implementation Pattern
**Standard service structure:**
```typescript
@Injectable({ providedIn: 'root' })
export class DomainService {
  #apiService = inject(GeneratedApiService);
  #logger = logger(() => ({ service: 'DomainService' }));

  async fetchData(params: InputType, abortSignal?: AbortSignal): Promise<ResultType> {
    const validated = ValidationSchema.parse(params);
    
    let req$ = this.#apiService.operation(validated);
    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }
    
    const res = await firstValueFrom(req$);
    
    if (res.error) {
      this.#logger.error('Operation failed', new Error(res.message));
      throw new ResponseArgsError(res);
    }
    
    return res.result as ResultType;
  }
}
```

### Error Handling Strategy
1. **Input Validation**: Zod schemas validate and transform inputs
2. **API Error Handling**: Check `res.error` from generated clients
3. **Structured Logging**: Log errors with context via `@isa/core/logging`
4. **Error Propagation**: Throw `ResponseArgsError` for consistent handling

### Concurrency & Cancellation
- **AbortSignal Support**: All async operations accept optional AbortSignal
- **RxJS Integration**: Use `takeUntilAborted` operator for cancellation
- **Promise Pattern**: `firstValueFrom` prevents subscription memory leaks
- **Caching**: `@InFlight` decorator prevents duplicate concurrent requests

### Extension Points
- **Custom Decorators**: `@Cache`, `@InFlight`, `@CacheTimeToLive`
- **Schema Transformations**: Zod `.transform()` for data normalization
- **Model Inheritance**: Interface extension for domain-specific properties
- **Service Composition**: Services can depend on other domain services

## Code Examples

### Complete Data-Access Library Structure
See full examples in existing implementations:
- `libs/catalogue/data-access` - Basic patterns
- `libs/remission/data-access` - Advanced with EntityContainer
- `libs/crm/data-access` - Service examples
- `libs/oms/data-access` - Model extensions

**Quick Reference:**
```typescript
// libs/domain/data-access/src/lib/schemas/fetch-items.schema.ts
import { z } from 'zod';

export const FetchItemsSchema = z.object({
  categoryId: z.string().min(1),
  skip: z.number().int().min(0).default(0),
  take: z.number().int().min(1).max(100).default(20),
  filters: z.record(z.any()).default({}),
});

export type FetchItems = z.infer<typeof FetchItemsSchema>;
export type FetchItemsInput = z.input<typeof FetchItemsSchema>;

// libs/domain/data-access/src/lib/models/item.ts
import { ItemDTO } from '@generated/swagger/domain-api';
import { EntityContainer } from '@isa/common/data-access';
import { Category } from './category';

export interface Item extends ItemDTO {
  id: number;
  displayName: string;
  category: EntityContainer<Category>;
  // Domain-specific enhancements
  isAvailable: boolean;
  formattedPrice: string;
}

// Service
@Injectable({ providedIn: 'root' })
export class ItemService {
  #itemService = inject(GeneratedItemService);
  #logger = logger(() => ({ service: 'ItemService' }));

  async fetchItems(
    params: FetchItemsInput,
    abortSignal?: AbortSignal
  ): Promise<Item[]> {
    this.#logger.debug('Fetching items', () => ({ params }));
    
    const { categoryId, skip, take, filters } = FetchItemsSchema.parse(params);
    
    let req$ = this.#itemService.getItems({
      categoryId,
      queryToken: { skip, take, filter: filters }
    });
    
    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }
    
    const res = await firstValueFrom(req$);
    
    if (res.error) {
      this.#logger.error('Failed to fetch items', new Error(res.message));
      throw new ResponseArgsError(res);
    }
    
    this.#logger.info('Successfully fetched items', () => ({ 
      count: res.result?.length || 0 
    }));
    
    return res.result as Item[];
  }
}

// libs/domain/data-access/src/index.ts
export * from './lib/models';
export * from './lib/schemas';
export * from './lib/services';
```

### Usage in Feature Components
```typescript
// feature component using the data-access library
import { Component, inject, signal } from '@angular/core';
import { ItemService, Item, FetchItemsInput } from '@isa/domain/data-access';

@Component({
  selector: 'app-item-list',
  template: `
    @if (loading()) {
      <div>Loading...</div>
    } @else {
      @for (item of items(); track item.id) {
        <div>{{ item.displayName }}</div>
      }
    }
  `
})
export class ItemListComponent {
  #itemService = inject(ItemService);
  
  items = signal<Item[]>([]);
  loading = signal(false);
  
  async loadItems(categoryId: string) {
    this.loading.set(true);
    
    try {
      const params: FetchItemsInput = {
        categoryId,
        take: 50,
        filters: { active: true }
      };
      
      const items = await this.#itemService.fetchItems(params);
      this.items.set(items);
    } catch (error) {
      console.error('Failed to load items', error);
    } finally {
      this.loading.set(false);
    }
  }
}
```

### Migration Pattern for Existing Services
```typescript
// Before: Direct HTTP client usage
@Injectable()
export class LegacyItemService {
  constructor(private http: HttpClient) {}
  
  getItems(categoryId: string): Observable<any> {
    return this.http.get(`/api/items?category=${categoryId}`);
  }
}

// After: Standardized data-access pattern
@Injectable({ providedIn: 'root' })
export class ItemService {
  #itemService = inject(GeneratedItemService);
  #logger = logger(() => ({ service: 'ItemService' }));
  
  async fetchItems(params: FetchItemsInput, abortSignal?: AbortSignal): Promise<Item[]> {
    const validated = FetchItemsSchema.parse(params);
    // ... standard implementation pattern
  }
}
```

## Open Questions / Follow-Ups
- Unresolved design clarifications
- Dependent ADRs required
- External approvals needed

## Decision Review & Revalidation
When and how this ADR will be re-evaluated (date, trigger conditions, metrics thresholds).

## Status Log
| Date | Change | Author |
|------|--------|--------|
| 2025-11-03 | Updated model/schema patterns to align with ADR-0002, added entity-based schemas, clarified DTO encapsulation | System |
| 2025-10-02 | Condensed for readability | Lorenz, Nino |
| 2025-09-29 | Created (Draft) | Lorenz |
| 2025-09-25 | Analysis completed, comprehensive patterns documented | Lorenz, Nino |

## References
**Existing Implementation Examples:**
- `libs/catalogue/data-access` - Basic schema and service patterns
- `libs/remission/data-access` - Advanced patterns with EntityContainer and stores
- `libs/common/data-access` - Shared utilities and response types
- `generated/swagger/` - Generated API clients integration

**Key Dependencies:**
- [Zod](https://github.com/colinhacks/zod) - Schema validation library
- [ng-swagger-gen](https://github.com/cyclosproject/ng-swagger-gen) - OpenAPI client generation
- `@isa/core/logging` - Structured logging infrastructure
- `@isa/common/data-access` - Shared utilities and types

**Related ADRs:**
- [ADR-0002: Models, Schemas, and DTOs Architecture](./0002-models-schemas-dtos-architecture.md) - Detailed guidance on model patterns, DTO encapsulation, and validation strategies

**Related Documentation:**
- ISA Frontend Copilot Instructions - Data-access patterns
- Tech Stack Documentation - Architecture overview
- Code Style Guidelines - TypeScript and Angular patterns

---
> Document updates MUST reference this ADR number in commit messages: `ADR-NNNN:` prefix.
> Keep this document updated through all lifecycle stages.
