ProjectsParts Services
JSDoc Documentation Guide for Media Coverage API
This guide provides JSDoc comment templates and best practices for documenting the Media Coverage API codebase. Comprehensive inline documentation improves...
JSDoc Documentation Guide for Media Coverage API
Overview
This guide provides JSDoc comment templates and best practices for documenting the Media Coverage API codebase. Comprehensive inline documentation improves code maintainability, IDE autocomplete, and developer onboarding.
Key Benefits:
- IntelliSense/autocomplete in VS Code
- Type safety with TypeScript
- Automatic API documentation generation
- Better code readability
- Easier onboarding for new developers
Repository Methods
getCoverageSummary()
/**
* Get comprehensive media coverage summary for NHL parts
*
* Aggregates statistics across three media dimensions:
* - Gallery images (static photos in various angles)
* - 360° views (interactive spin views with 24-48 frames)
* - PDF documents (manuals, datasheets, certifications - Phase 2)
*
* **Performance:** ~150-300ms for 3,740 parts (MongoDB aggregation)
*
* **Caching:** Recommended 10-minute TTL in production
*
* @param {string} [environment='prod'] - Data environment (prod/dev/stage)
* @returns {Promise<MediaCoverageReport>} Coverage statistics with:
* - `summary`: Total parts, coverage percentages
* - `images`: Gallery and 360° breakdowns
* - `documents`: PDF statistics (currently zeros in Phase 1)
* - `qualityCorrelation`: Media impact on quality scores
*
* @throws {Error} If MongoDB connection fails
* @throws {Error} If environment parameter is invalid
*
* @example
* ```typescript
* const repo = new MediaRepository(db);
* const summary = await repo.getCoverageSummary('prod');
*
* console.log(`${summary.summary.coveragePercentage}% parts have media`);
* // Output: "68.02% parts have media"
*
* console.log(`${summary.images.view360.count} parts have 360° views`);
* // Output: "2507 parts have 360° views"
* ```
*
* @see {@link MediaCoverageReport} for full response schema
* @see {@link docs/MEDIA_COVERAGE_API.md} for API documentation
*/
async getCoverageSummary(environment: string = 'prod'): Promise<MediaCoverageReport> {
// Implementation...
}getImageTypeDistribution()
/**
* Get detailed distribution of image types and 360° view characteristics
*
* Analyzes gallery images by type (MARKETING, FRONT, BACK, etc.) and
* 360° views by frame count and quality levels.
*
* **Image Types:**
* - MARKETING: Lifestyle/promotional images (49.39% coverage)
* - FRONT: Front-facing product photo (59.73% coverage) - highest priority
* - BACK: Rear view for installation reference (48.74% coverage)
* - LEFT/RIGHT: Lateral views (4-5% coverage)
* - ANGLE1/ANGLE2: Contextual perspectives (2-3% coverage)
*
* **360° Quality Levels:**
* - Standard: <36 frames (basic rotation, 4×6 grid)
* - High: 36+ frames (smooth rotation, 6×6 or 8×6 grid)
*
* **Performance:** ~200-400ms (processes image arrays via aggregation)
*
* @param {string} [environment='prod'] - Data environment
* @param {'type' | 'quality' | 'frames'} [groupBy='type'] - Grouping strategy:
* - `type`: Group by image types (default)
* - `quality`: Group by 360° quality levels (high/standard)
* - `frames`: Group by 360° frame counts (24/36/48/other)
*
* @returns {Promise<MediaDistributionReport>} Distribution data with:
* - `imageTypes`: Stats for each image type (count, percentage, avgPerPart)
* - `view360Distribution`: Breakdowns by frame count, quality, and grid layout
* - `totalParts`: Total parts analyzed
*
* @throws {Error} If groupBy parameter is invalid
* @throws {Error} If MongoDB aggregation fails
*
* @example
* ```typescript
* // Get image type distribution (default)
* const distribution = await repo.getImageTypeDistribution('prod', 'type');
*
* console.log(`FRONT images: ${distribution.imageTypes.front.percentage}%`);
* // Output: "FRONT images: 59.73%"
*
* // Find least-covered image type
* const types = Object.entries(distribution.imageTypes)
* .sort((a, b) => a[1].percentage - b[1].percentage);
* const [leastCovered, stats] = types[0];
* console.log(`${leastCovered}: ${stats.percentage}%`);
* // Output: "angle2: 1.79%"
* ```
*
* @example
* ```typescript
* // Group by 360° quality levels
* const qualityDist = await repo.getImageTypeDistribution('prod', 'quality');
*
* const highQualityPercent = (qualityDist.view360Distribution.by_quality.high /
* (qualityDist.view360Distribution.by_quality.high +
* qualityDist.view360Distribution.by_quality.standard)) * 100;
*
* console.log(`${highQualityPercent.toFixed(1)}% of 360s are high quality`);
* // Output: "93.3% of 360s are high quality"
* ```
*
* @see {@link MediaDistributionReport} for response schema
* @see {@link ImageType} for valid image type values
*/
async getImageTypeDistribution(
environment: string = 'prod',
groupBy: 'type' | 'quality' | 'frames' = 'type'
): Promise<MediaDistributionReport> {
// Implementation...
}getMediaGaps()
/**
* Identify high-quality parts missing media (enrichment opportunities)
*
* Returns parts with quality scores above threshold but lacking specific media types.
* Useful for prioritizing content creation pipeline.
*
* **Enrichment Priority Calculation:**
* - `high`: Quality ≥ 85, critical media missing (e.g., no 360° for top-seller)
* - `medium`: Quality 70-84, some media missing
* - `low`: Quality < 70 or minimal impact
*
* **Use Cases:**
* - Generate photography shoot lists
* - Prioritize 360° view creation
* - Track enrichment progress over time
* - Identify PDF upload opportunities (Phase 2)
*
* **Performance:** ~300-600ms (quality score filtering + sorting)
*
* **Pagination:** Use `limit` and `offset` for large result sets
*
* @param {Object} options - Query options
* @param {number} [options.minQuality=70] - Minimum quality score threshold (0-100)
* @param {'all' | 'gallery' | 'view360' | 'documents'} [options.mediaType='all'] - Filter by specific media type
* @param {number} [options.limit=50] - Maximum results (1-500)
* @param {number} [options.offset=0] - Pagination offset
* @param {'quality' | 'partNumber' | 'sku'} [options.sortBy='quality'] - Sort field
* @param {'asc' | 'desc'} [options.sortOrder='desc'] - Sort direction
* @param {string} [options.environment='prod'] - Data environment
*
* @returns {Promise<MediaGapsReport>} Gaps data with:
* - `gaps`: Array of MediaGap objects (parts missing media)
* - `totalGaps`: Total matching parts (for pagination)
* - `showing`: Results returned in this response
* - `pagination`: Pagination metadata (hasMore flag)
* - `filters`: Applied filter values
*
* @throws {Error} If minQuality out of range (0-100)
* @throws {Error} If limit out of range (1-500)
* @throws {Error} If MongoDB query fails
*
* @example
* ```typescript
* // Get top 20 high-quality parts missing any media
* const gaps = await repo.getMediaGaps({
* minQuality: 80,
* limit: 20
* });
*
* console.log(`Found ${gaps.totalGaps} gaps`);
* // Output: "Found 297 gaps"
*
* // Generate shoot list for high-priority parts
* const shootList = gaps.gaps
* .filter(g => g.enrichmentPriority === 'high' && g.missingMedia.view360)
* .map(g => ({
* sku: g.sku,
* title: g.title,
* action: '360° photography needed'
* }));
* ```
*
* @example
* ```typescript
* // Find parts missing 360° views specifically
* const missing360 = await repo.getMediaGaps({
* mediaType: 'view360',
* minQuality: 75,
* limit: 50,
* sortBy: 'quality',
* sortOrder: 'desc'
* });
*
* // Export to CSV for content team
* const csv = missing360.gaps
* .map(g => `${g.sku},"${g.title}",${g.qualityScore}`)
* .join('\n');
* ```
*
* @example
* ```typescript
* // Paginated access (fetch page 2)
* const page2 = await repo.getMediaGaps({
* minQuality: 70,
* limit: 50,
* offset: 50 // Skip first 50 results
* });
*
* if (page2.pagination.hasMore) {
* console.log('More results available');
* }
* ```
*
* @see {@link MediaGapsReport} for response schema
* @see {@link MediaGap} for gap object structure
* @see {@link EnrichmentPriority} for priority levels
*/
async getMediaGaps(options: {
minQuality?: number;
mediaType?: 'all' | 'gallery' | 'view360' | 'documents';
limit?: number;
offset?: number;
sortBy?: 'quality' | 'partNumber' | 'sku';
sortOrder?: 'asc' | 'desc';
environment?: string;
}): Promise<MediaGapsReport> {
// Implementation...
}Route Handlers
GET /api/health/media/coverage
/**
* GET /api/health/media/coverage
*
* Get comprehensive media coverage summary for NHL parts catalog.
*
* **Authentication:** None (public endpoint)
*
* **Rate Limit:** 100 requests/minute per IP
*
* **Caching:** Response cached for 10 minutes (recommended)
*
* **Query Parameters:**
* - `environment` (optional): Data environment (prod/dev/stage). Default: prod
*
* **Response:** 200 OK
* ```json
* {
* "success": true,
* "data": {
* "summary": { "totalParts": 3740, ... },
* "images": { "coverage": {...}, "gallery": {...}, "view360": {...} },
* "documents": { "coverage": {...}, "byType": {...} },
* "qualityCorrelation": { ... }
* },
* "meta": {
* "timestamp": "2025-11-17T10:30:00Z",
* "environment": "prod"
* }
* }
* ```
*
* **Error Responses:**
* - 400 Bad Request: Invalid environment parameter
* - 500 Internal Server Error: MongoDB connection failed
* - 503 Service Unavailable: Database temporarily unavailable
*
* @param {Context} c - Hono context object
* @returns {Promise<Response>} JSON response with coverage data
*
* @example
* ```bash
* # Get production coverage
* curl http://localhost:3005/api/health/media/coverage
*
* # Get development coverage
* curl 'http://localhost:3005/api/health/media/coverage?environment=dev'
* ```
*
* @see {@link MediaCoverageReport} for response schema
* @see {@link docs/MEDIA_COVERAGE_API.md#get-apihealthmediacoverage} for full API docs
*/
app.get('/coverage', async (c) => {
try {
const environment = c.req.query('environment') || 'prod';
// Validate environment
if (!['prod', 'dev', 'stage'].includes(environment)) {
return c.json({
success: false,
error: {
message: 'Invalid environment parameter',
details: 'Environment must be one of: prod, dev, stage',
code: 'INVALID_ENVIRONMENT'
}
}, 400);
}
const repo = new MediaRepository(db);
const data = await repo.getCoverageSummary(environment);
return c.json({
success: true,
data,
meta: {
timestamp: new Date().toISOString(),
environment
}
});
} catch (error) {
console.error('Error fetching media coverage:', error);
return c.json({
success: false,
error: {
message: 'Failed to fetch media coverage',
details: error instanceof Error ? error.message : 'Unknown error'
}
}, 500);
}
});Type Definitions
MediaCoverageReport Interface
/**
* Media coverage statistics
*
* Tracks coverage across three media types:
* - **Images**: Gallery images and 360° views (primary focus - 67% of parts have 360°)
* - **Documents**: PDF manuals, datasheets, etc. (infrastructure ready, data pending)
* - **Quality correlation**: How media affects overall quality score
*
* **Data Source:** MongoDB `nh_unified` collection (3,740 NHL parts)
*
* **Update Frequency:** Data refreshes during nightly sync jobs
*
* **Cache Strategy:** Recommend 10-minute TTL in production
*
* @interface
* @property {CoverageSummary} summary - High-level coverage statistics
* @property {ImageCoverage} images - Image and 360° view breakdowns
* @property {DocumentCoverage} documents - PDF document statistics (Phase 1: zeros)
* @property {QualityCorrelation} qualityCorrelation - Media impact on quality scores
*
* @example
* ```typescript
* const report: MediaCoverageReport = {
* summary: {
* totalParts: 3740,
* withAnyMedia: 2544,
* withoutMedia: 1196,
* coveragePercentage: 68.02
* },
* images: {
* coverage: { count: 2501, percentage: 66.87 },
* gallery: { count: 2501, percentage: 66.87 },
* view360: {
* count: 2507,
* percentage: 67.03,
* withFrames: 2507,
* avgFrameCount: 24
* }
* },
* documents: {
* coverage: { count: 0, percentage: 0 },
* byType: { manuals: 0, datasheets: 0, certifications: 0 }
* },
* qualityCorrelation: {
* withMediaAvgQuality: 72.5,
* withoutMediaAvgQuality: 45.2,
* delta: 27.3
* }
* };
*
* // Interpret correlation
* if (report.qualityCorrelation.delta > 20) {
* console.log('Media significantly improves quality scores');
* }
* ```
*
* @see {@link CoverageSummary}
* @see {@link ImageCoverage}
* @see {@link DocumentCoverage}
* @see {@link QualityCorrelation}
*/
export interface MediaCoverageReport {
summary: CoverageSummary;
images: ImageCoverage;
documents: DocumentCoverage;
qualityCorrelation: QualityCorrelation;
}CoverageStats Interface
/**
* Coverage statistics with count and percentage
*
* Generic container for coverage metrics. Used throughout the API for
* consistent percentage calculation.
*
* **Formula:** `percentage = (count / totalParts) * 100`
*
* **Precision:** Percentages rounded to 2 decimal places
*
* @interface
* @property {number} count - Absolute count of parts
* @property {number} percentage - Percentage of total (0-100, rounded to 2 decimals)
*
* @example
* ```typescript
* const stats: CoverageStats = {
* count: 297, // 297 out of 3,740 parts
* percentage: 7.94 // 7.94% coverage
* };
*
* // Display in UI
* console.log(`${stats.count.toLocaleString()} parts (${stats.percentage}%)`);
* // Output: "297 parts (7.94%)"
* ```
*
* @example
* ```typescript
* // Create CoverageStats helper
* function createCoverageStats(count: number, total: number): CoverageStats {
* return {
* count,
* percentage: Math.round((count / total) * 100 * 100) / 100
* };
* }
*
* const galleryStats = createCoverageStats(2501, 3740);
* // Result: { count: 2501, percentage: 66.87 }
* ```
*/
export interface CoverageStats {
count: number;
percentage: number;
}Utility Functions
calculateEnrichmentPriority()
/**
* Calculate enrichment priority for a part with missing media
*
* Priority levels:
* - **high**: Quality ≥ 85 AND (no 360° view OR <2 gallery images)
* - **medium**: Quality 70-84 AND any media missing
* - **low**: Quality < 70 OR media has minimal impact
*
* **Business Logic:**
* High-quality parts benefit most from media enrichment because:
* - They already have good titles and descriptions
* - Adding media pushes them to "catalog-ready" status (80+ score)
* - Higher conversion rates on e-commerce
*
* @param {number} qualityScore - Overall quality score (0-100)
* @param {Object} missingMedia - Which media types are missing
* @param {boolean} missingMedia.gallery - Missing gallery images
* @param {boolean} missingMedia.view360 - Missing 360° view
* @param {boolean} missingMedia.documents - Missing PDFs
* @param {number} existingGalleryCount - Number of existing gallery images
* @returns {EnrichmentPriority} Priority level: 'high' | 'medium' | 'low'
*
* @example
* ```typescript
* // High-quality part missing 360° view
* const priority1 = calculateEnrichmentPriority(
* 85,
* { gallery: false, view360: true, documents: true },
* 3
* );
* console.log(priority1); // 'high'
*
* // Medium-quality part missing gallery images
* const priority2 = calculateEnrichmentPriority(
* 75,
* { gallery: true, view360: false, documents: true },
* 0
* );
* console.log(priority2); // 'medium'
*
* // Low-quality part (media won't help much)
* const priority3 = calculateEnrichmentPriority(
* 45,
* { gallery: true, view360: true, documents: true },
* 0
* );
* console.log(priority3); // 'low'
* ```
*
* @see {@link EnrichmentPriority} for priority type definition
* @see {@link estimateQualityImpact} for impact calculation
*/
export function calculateEnrichmentPriority(
qualityScore: number,
missingMedia: {
gallery: boolean;
view360: boolean;
documents: boolean;
},
existingGalleryCount: number
): EnrichmentPriority {
// High priority: Excellent quality, critical media missing
if (qualityScore >= 85) {
if (missingMedia.view360 || existingGalleryCount < 2) {
return 'high';
}
}
// Medium priority: Good quality, some media missing
if (qualityScore >= 70) {
if (missingMedia.gallery || missingMedia.view360) {
return 'medium';
}
}
// Low priority: Lower quality or minimal media impact
return 'low';
}estimateQualityImpact()
/**
* Estimate potential quality score increase if media added
*
* Uses quality score formula weights:
* - Images: 20 points (up to 4 images @ 5 points each)
* - 360° view: 20 points (binary: have it or not)
* - PDFs: 10 points (up to 2 docs @ 5 points each)
*
* **Assumptions:**
* - Adding media closes the gap in media-related score components
* - Actual impact may vary based on other quality factors
* - Conservative estimate (doesn't assume perfect media quality)
*
* @param {Object} missingMedia - Which media types are missing
* @param {boolean} missingMedia.gallery - Missing gallery images
* @param {boolean} missingMedia.view360 - Missing 360° view
* @param {boolean} missingMedia.documents - Missing PDFs
* @param {number} existingGalleryCount - Current gallery image count
* @returns {number} Estimated quality score increase (0-50 points)
*
* @example
* ```typescript
* // Part with no media at all
* const impact1 = estimateQualityImpact(
* { gallery: true, view360: true, documents: true },
* 0
* );
* console.log(impact1); // 50 (20 + 20 + 10)
*
* // Part with 1 image, missing 360° and PDFs
* const impact2 = estimateQualityImpact(
* { gallery: false, view360: true, documents: true },
* 1
* );
* console.log(impact2); // 45 (15 + 20 + 10)
*
* // Part with gallery and 360°, just missing PDFs
* const impact3 = estimateQualityImpact(
* { gallery: false, view360: false, documents: true },
* 4
* );
* console.log(impact3); // 10 (just PDFs)
* ```
*
* @see {@link calculateEnrichmentPriority} for priority calculation
* @see {@link docs/MEDIA_COVERAGE_API.md#quality-score-calculation} for scoring details
*/
export function estimateQualityImpact(
missingMedia: {
gallery: boolean;
view360: boolean;
documents: boolean;
},
existingGalleryCount: number
): number {
let impact = 0;
// Gallery images: up to 20 points (4 images max @ 5 points each)
if (missingMedia.gallery) {
const imagesToAdd = Math.max(0, 4 - existingGalleryCount);
impact += imagesToAdd * 5;
}
// 360° view: 20 points
if (missingMedia.view360) {
impact += 20;
}
// PDFs: up to 10 points (2 docs @ 5 points each)
if (missingMedia.documents) {
impact += 10;
}
return impact;
}MongoDB Aggregation Pipelines
Coverage Summary Aggregation
/**
* MongoDB aggregation pipeline for media coverage summary
*
* Uses `$facet` operator to calculate multiple aggregations in a single pass,
* optimizing performance for large collections.
*
* **Pipeline Stages:**
* 1. `summary`: Total counts and overall coverage
* 2. `galleryStats`: Gallery image statistics
* 3. `view360Stats`: 360° view statistics
* 4. `documentStats`: PDF document statistics (Phase 2)
* 5. `qualityCorrelation`: Quality score comparisons
*
* **Performance:** ~150-300ms for 3,740 parts
*
* **Indexes Required:**
* - `media.images` (multikey)
* - `media.view360.status`
* - `documents` (sparse)
* - `qualityScore.total`
*
* @returns {AggregationPipeline} MongoDB aggregation pipeline array
*
* @example
* ```typescript
* const pipeline = getCoverageSummaryPipeline();
* const [result] = await db.collection('nh_unified').aggregate(pipeline).toArray();
*
* // Result structure:
* {
* summary: [{
* totalParts: 3740,
* withGallery: 2501,
* with360: 2507,
* withDocuments: 0
* }],
* galleryStats: [{
* avgGallerySize: 2.3,
* maxGallerySize: 7
* }],
* // ... other facets
* }
* ```
*
* @see {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/} for $facet docs
*/
function getCoverageSummaryPipeline(): any[] {
return [
{
$facet: {
summary: [
{
$group: {
_id: null,
totalParts: { $sum: 1 },
withGallery: {
$sum: {
$cond: [
{ $gt: [{ $size: { $ifNull: ['$media.images', []] } }, 0] },
1,
0
]
}
},
with360: {
$sum: {
$cond: [
{ $in: ['$media.view360.status', ['gcp', 'external']] },
1,
0
]
}
},
withDocuments: {
$sum: {
$cond: [
{ $gt: [{ $size: { $ifNull: ['$documents', []] } }, 0] },
1,
0
]
}
}
}
}
],
qualityCorrelation: [
{
$group: {
_id: {
$cond: [
{ $in: ['$media.view360.status', ['gcp', 'external']] },
'with360',
'without360'
]
},
avgQuality: { $avg: '$qualityScore.total' },
count: { $sum: 1 }
}
}
]
}
}
];
}Best Practices
1. Always Include Examples
/**
* @example
* ```typescript
* // Simple example
* const result = await fn();
* ```
*
* @example
* ```typescript
* // Complex example with error handling
* try {
* const result = await fn({ param: 'value' });
* console.log(result);
* } catch (error) {
* console.error('Failed:', error.message);
* }
* ```
*/2. Document Performance Characteristics
/**
* **Performance:** ~300ms for 1,000 parts
*
* **Optimization Notes:**
* - Requires index on `qualityScore.total`
* - Use pagination for large result sets
* - Consider caching with 5-minute TTL
*/3. Link to Related Documentation
/**
* @see {@link MediaCoverageReport} for response schema
* @see {@link docs/MEDIA_COVERAGE_API.md} for API documentation
* @see {@link https://www.mongodb.com/docs/manual/aggregation/} for MongoDB aggregation
*/4. Document Business Logic
/**
* Priority levels based on business impact:
*
* **High Priority (score ≥ 85):**
* - Already catalog-ready quality
* - Adding media maximizes conversion rate
* - ROI: ~27% increase in quality score
*
* **Medium Priority (score 70-84):**
* - Good quality, needs enrichment push
* - Can become catalog-ready with media
*
* **Low Priority (score < 70):**
* - Other quality issues more critical
* - Fix title/description first
*/5. Include Type Guards
/**
* Type guard to check if response is success
*
* @param {ApiResponse<T>} response - API response
* @returns {response is { success: true; data: T }} Type predicate
*
* @example
* ```typescript
* const response = await fetch('/api/health/media/coverage');
* const json = await response.json();
*
* if (isSuccessResponse(json)) {
* // TypeScript knows json.data exists
* console.log(json.data.summary.totalParts);
* } else {
* // TypeScript knows json.error exists
* console.error(json.error.message);
* }
* ```
*/
export function isSuccessResponse<T>(
response: ApiResponse<T>
): response is { success: true; data: T; meta: ResponseMeta } {
return response.success === true;
}Validation
Generate Documentation
# Install TypeDoc
bun add -D typedoc
# Generate HTML docs
typedoc --out docs/api src/index.ts
# Generate Markdown docs
typedoc --plugin typedoc-plugin-markdown --out docs/api-md src/index.tsValidate JSDoc Comments
// Use TypeScript compiler to validate JSDoc
// tsconfig.json
{
"compilerOptions": {
"checkJs": true,
"noImplicitAny": true
}
}IDE Integration
VS Code settings for JSDoc validation:
{
"typescript.suggest.jsdoc.generateReturns": true,
"javascript.suggest.completeJSDocs": true,
"editor.quickSuggestions": {
"comments": true
}
}Checklist
When adding new endpoints or methods:
- JSDoc comment with description
-
@paramtags for all parameters (with defaults) -
@returnstag with detailed schema description -
@throwstags for possible errors - At least one
@example(preferably 2-3) -
@seelinks to related docs - Performance notes (execution time, caching)
- Business logic explanations
- Type definitions with interface docs
- Validation logic documented
Resources
- JSDoc Official Documentation
- TypeScript JSDoc Reference
- TSDoc Standard
- TypeDoc - Generate HTML docs from JSDoc
Last Updated: 2025-11-17
Document Version: 1.0