| name | producer-implementation |
| description | Producer implementation patterns for API operations and business logic |
Producer Implementation Patterns
🚨 CRITICAL RULE #1: ALL Business Logic in Producers
- Client: ONLY connection management (connect, isConnected, disconnect)
- Producers: ALL API operations (list, get, create, update, delete)
- Client provides HTTP client instance to producers, nothing more
🚨 CRITICAL RULE #8: NO Connection Context Parameters
- FORBIDDEN: Adding parameters that exist in ConnectionProfile or ConnectionState
- MANDATORY: Access connection context through client instance
- Operations receive ONLY operation-specific parameters
class UserProducer {
async getUser(
userId: string,
apiKey: string,
baseUrl: string,
orgId: string
): Promise<User> {
}
}
class UserProducer {
constructor(private client: GitHubClient) {}
async getUser(userId: string): Promise<User> {
const { data } = await this.client.apiClient
.request({
url: `/users/${userId}`,
method: 'get'
})
.catch(handleAxiosError);
return toUser(data);
}
}
RULES:
- ✅ Parameters in ConnectionProfile → Access via client
- ✅ Parameters in ConnectionState → Access via client
- ✅ Operation methods receive ONLY business parameters (IDs, filters, etc.)
- ❌ NEVER add apiKey, token, baseUrl, organizationId to method signatures
- ❌ NEVER duplicate what connect() already provides
WHY: Connection context is established once during connect() and managed by the client. Operations should focus on business logic, not connection details.
Producer Pattern - ALL Business Logic Here
Producer Class Structure
class UserProducer {
constructor(private client: GitHubClient) {}
async listUsers(
results: PagedResults<User>,
name?: string
): Promise<void> {
const { data } = await this.client.apiClient
.request({
url: '/users',
method: 'get',
params: {
limit: results.pageSize,
page: results.pageNumber,
name
}
})
.catch(handleAxiosError);
results.items = data.map(toUser) || [];
if (data.total !== undefined) {
results.count = data.total;
}
}
async getUser(userId: string): Promise<User> {
const { data } = ..
.({
: ,
:
})
.(handleAxiosError);
(data);
}
(: ): <> {
{ data } = ..
.({
: ,
: ,
: input
})
.(handleAxiosError);
(data);
}
(: ): <> {
..
.({
: ,
:
})
.(handleAxiosError);
}
}
🚨 CRITICAL: NEVER use Promise
- NEVER use
any in producer method signatures
- ALWAYS use generated interfaces from generated/api/
PagedResults Rules
- ALWAYS use manual assignment:
results.items = ...
- NEVER use
ingest() - Unreliable trimming behavior
- Update
results.count when API provides total
- Set
results.pageToken for token-based pagination
- NEVER modify
pageNumber, pageSize, or pageCount
- Sorting: ALWAYS done by API via params, never post-process
Mapper Pattern - MANDATORY
ALWAYS use const output: Type pattern
export function toUser(data: any): User {
if (!data?.id || !data?.name) {
throw new InvalidInputError('user', 'Missing required fields');
}
const output: User = {
id: map(UUID, data.id),
name: data.name,
email: map(Email, data.email),
website: map(URL, data.website),
createdAt: map(DateTime, data.created_at)?.toDate()
};
return output;
}
CRITICAL:
- NEVER use
! (non-null assertion) - Design code to avoid it
- PREFER
map() over constructors - Use map() from @auditmation/util-hub-module-utils unless map() doesn't meet requirements
- Validate required fields - Throw if missing, then TypeScript knows they exist
- Let map() handle optionals - It returns
Type | undefined automatically
- Always declare const with type - Enables type checking
- DateTime special case - Use
map(DateTime, value)?.toDate() since interface expects Date not DateTime
- Constructor fallback - Only use constructors directly if map() doesn't provide needed functionality
- 🚨 Null to undefined - Convert null values to undefined (use
|| undefined or map() which handles this)
export function toUser(data: any): User {
const output: User = {
id: new UUID(data.id),
email: new Email(data.email),
createdAt: new DateTime(data.created_at).toDate()
};
return output;
}
Null to Undefined Conversion
🚨 CRITICAL: API spec never uses nullable: true, mappers convert null → undefined
RULE: If external API returns null values, convert them to undefined in mappers.
export function toUser(data: any): User {
const output: User = {
id: map(UUID, data.id),
name: data.name || undefined,
email: map(Email, data.email),
website: map(URL, data.website)
};
return output;
}
export function toOrganization(data: any): Organization {
const output: Organization = {
id: data.id !== null ? String(data.id) : undefined,
name: data. || ,
: data. ??
};
output;
}
(): {
: = {
: (, data.),
: data.,
: data.
};
output;
}
WHY:
- TypeScript uses
| undefined for optional fields, not | null
- API spec doesn't have
nullable: true (keeps it clean)
- Generated types use
Type | undefined, not Type | null
- Consistent with TypeScript conventions
map() function behavior:
map(Type, null) → returns undefined
map(Type, undefined) → returns undefined
map(Type, value) → returns Type instance or undefined
⚠️ WARNING: NEVER use || undefined on number fields
count: data.count || undefined
age: data.age || undefined
balance: data.balance || undefined
count: data.count ?? undefined
age: data.age !== null ? data.age : undefined
balance: data.balance !== undefined ? data.balance : undefined
WHY: 0 || undefined evaluates to undefined because 0 is falsy.
- Use
?? undefined (nullish coalescing) for numbers
- Or explicit
!== null / !== undefined checks
|| undefined is ONLY safe for strings, objects, and arrays
Extending Mappers
export function toUserInfo(data: any): UserInfo {
const output: UserInfo = {
...toUser(data),
lastLogin: map(DateTime, data.last_login)?.toDate(),
permissions: data.permissions || []
};
return output;
}
Field Validation Process
MANDATORY 3-STEP:
- Analyze interface (generated/api/index.ts)
- Check API schema (api.yml response)
- Map ALL fields (count must match)
interface UserInfo {
id: UUID;
name: string;
email?: Email;
createdAt: Date;
status?: StatusEnum;
}
export function toUserInfo(raw: any): UserInfo {
const output: UserInfo = {
id: map(UUID, raw.id),
name: raw.name,
createdAt: map(Date, raw.created_at),
email: map(Email, raw.email),
status: toEnum(StatusEnum, raw.status)
};
return output;
}
Required Field Validation
🚨 CRITICAL: Mappers MUST throw errors for missing required fields
export function toOrganization(data: any): Organization | undefined {
if (!data) return undefined;
if (!data.id && data.id !== 0) {
throw new InvalidInputError('organization', 'Missing required field: id');
}
return {
id: String(data.id),
name: data.name || undefined,
};
}
export function toOrganization(data: any): Organization | undefined {
if (!data) return undefined;
if (!data.id) return undefined;
return {
id: String(data.id),
name: data. || ,
};
}
Nested Object Handling
When objects appear in nested contexts and might have incomplete data, the parent mapper handles validation errors:
export function toTokenScope(data: any): TokenScope {
let org: Organization | undefined;
if (data.org) {
try {
org = toOrganization(data.org);
} catch (error) {
org = undefined;
}
}
return {
org,
user: data.user ? toUser(data.user) : undefined,
scope: data.scope || undefined,
};
}
RULE:
- Child mappers: Always validate required fields, throw errors
- Parent mappers: Catch validation errors when nesting allows incomplete objects
- Direct API responses: Let errors propagate (invalid response = error)
- Nested contexts: Catch and convert to undefined (incomplete nested = skip)
Enum Mapping
🚨 CRITICAL: Enum values MUST be snake_case
STANDARD: All enum values in this codebase use snake_case format.
- NEVER instantiate EnumValue directly
- ALWAYS use
toEnum(EnumClass, value)
- DEFAULT:
toEnum automatically converts input to snake_case (matches our standard)
- RARELY NEEDED: Pass third parameter to override transform only if API truly doesn't use snake_case
const status = toEnum(StatusEnum, data.status);
const status = toEnum(StatusEnum, data.status, (val) => val);
if (!data?.status) {
throw new InvalidInputError('resource', 'Missing status');
}
const status = toEnum(StatusEnum, data.status);
Why snake_case is our standard:
- Consistent across all modules
- Matches OpenAPI spec conventions
- Most REST APIs return enum values in snake_case
- Generated enums from OpenAPI use snake_case values
toEnum default behavior aligns with this standard
When custom transform might be needed (rare):
- Legacy API uses ONLY PascalCase enum values (e.g., "ActiveUser" not "active_user")
- Legacy API uses ONLY UPPERCASE values (e.g., "ACTIVE" not "active")
- API documentation explicitly shows non-snake_case enum format
IMPORTANT: Before using custom transform, verify the API truly doesn't accept snake_case. Most APIs accept multiple formats but prefer snake_case.
Complex Mapper Patterns
Nested objects
function toAddress(data: any): Address | undefined {
if (!data) return undefined;
const output: Address = {
street: data.street,
city: data.city,
country: toCountry(data.country)
};
return output;
}
Arrays
function toUsers(data: any[]): User[] {
if (!Array.isArray(data)) return [];
return data.map(toUser);
}
Recursive structures
function toTreeNode(data: any): TreeNode {
const output: TreeNode = {
id: data.id,
name: data.name,
children: data.children?.map(toTreeNode)
};
return output;
}
Validation Scripts
Validate Producer Implementation
grep -E "async (get|list|create|update|delete).*\(.*[,\s](apiKey|token|baseUrl|organizationId)" src/*Producer.ts && echo "❌ Producer has connection params!" || echo "✅ Producers clean"
grep -E "\.catch\(handleAxiosError\)" src/*Producer.ts && echo "✅ Error handling present" || echo "⚠️ Missing error handler"
grep -E "Promise<any>" src/*Producer.ts && echo "❌ Found Promise<any>!" || echo "✅ No Promise<any>"
Validate Mapper Patterns
grep -E "const output: [A-Z]" src/Mappers.ts && echo "✅ Using output pattern" || echo "⚠️ Check mapper pattern"
grep -E "throw new InvalidInputError.*Missing required" src/Mappers.ts && echo "✅ Required field validation present" || echo "⚠️ Check required field validation"
grep "!" src/Mappers.ts | grep -v "//" | grep -v "!=" && echo "❌ Found non-null assertions!" || echo "✅ No non-null assertions"
Validate PagedResults Usage
grep -E "results\.items\s*=" src/*Producer.ts && echo "✅ Manual assignment" || echo "⚠️ Check PagedResults pattern"
grep "ingest(" src/*Producer.ts && echo "❌ Found ingest()!" || echo "✅ No ingest()"
Comment Style for Producers
See code-comment-style skill for complete guidelines
Key Rules for Producer Code
NEVER comment standard patterns:
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
}
if (results.pageNumber && results.pageSize) {
params.offset = (results.pageNumber - 1) * results.pageSize;
params.limit = Math.min(Math.max(results.pageSize, 1), 1000);
}
results.items = response.data.data.map(toUser);
results.count = response.data.totalCount || 0;
results.items = response.data.data.map(toUser);
results.count = response.data.totalCount || 0;
DO comment non-obvious API behavior:
const rawData = response.data.data || response.data;
Reference: See code-comment-style.md for full guidelines