# Intune Reverse Engineering Migration Guide **Version**: 1.0.0 **Last Updated**: 2025-12-09 **PowerShell Reference**: Commit `2eaf3257` (2025-12-09) **Status**: Active ## Table of Contents 1. [Overview](#overview) 2. [PowerShell Reference Location](#powershell-reference-location) 3. [Step-by-Step Implementation Process](#step-by-step-implementation-process) 4. [Data Points to Extract](#data-points-to-extract) 5. [PowerShell to TypeScript Pattern Mapping](#powershell-to-typescript-pattern-mapping) 6. [Concrete Examples](#concrete-examples) - [Windows Update Rings](#example-1-windows-update-rings) - [Settings Catalog with $expand](#example-2-settings-catalog-with-expand) - [Invoke-MSGraphRequest Translation](#example-3-invoke-msgraphrequest-translation) - [Property Cleanup Patterns](#example-4-property-cleanup-patterns) 7. [Troubleshooting API Discrepancies](#troubleshooting-api-discrepancies) 8. [Understanding Existing Implementation Patterns](#understanding-existing-implementation-patterns) 9. [Common Questions (FAQ)](#common-questions-faq) 10. [Edge Cases & Advanced Topics](#edge-cases--advanced-topics) - [Versioning Strategy](#versioning-strategy) - [Fallback Process for Missing PowerShell Reference](#fallback-process-for-missing-powershell-reference) - [Handling PowerShell Updates](#handling-powershell-updates) - [Deprecated Features](#deprecated-features) - [PowerShell Quirks vs Intentional Patterns](#powershell-quirks-vs-intentional-patterns) 11. [Complete End-to-End Example](#complete-end-to-end-example) 12. [Reference Implementations](#reference-implementations) --- ## Overview This guide documents the **reverse engineering strategy** for implementing Intune sync functionality in TenantPilot. Instead of relying solely on Microsoft Graph API documentation (which is often incomplete or outdated), we analyze the proven PowerShell implementation from [IntuneManagement](https://github.com/Micke-K/IntuneManagement) by Mikael Karlsson to discover the actual API patterns, undocumented parameters, and data transformation logic. ### Why This Approach? Microsoft Graph API documentation frequently: - Omits required query parameters (`$expand`, `$filter`, `$select`) - Incorrectly documents API versions (beta vs v1.0) - Lacks examples of property cleanup/transformation - Doesn't specify pagination patterns - Misses edge cases in error handling The IntuneManagement PowerShell tool has been battle-tested in production environments and contains the **actual working implementation** of Intune API interactions. ### When to Use This Guide - **Before implementing any new Intune sync job**: Follow the step-by-step process - **When troubleshooting API discrepancies**: Compare TypeScript vs PowerShell - **When onboarding new team members**: Understand rationale for existing patterns - **When PowerShell reference is updated**: Review for breaking changes ### Scope This guide focuses on the **process** of reverse engineering PowerShell → TypeScript. It does NOT modify existing sync job implementations. Use this guide for **new implementations** and **troubleshooting existing ones**. --- ## PowerShell Reference Location **Repository Path**: `reference/IntuneManagement-master/` ### Key Directories - **`Modules/`**: Core PowerShell modules organized by resource type - `ConfigurationPolicies.psm1` - Device configuration policies - `Applications.psm1` - App management - `DeviceCompliance.psm1` - Compliance policies - `WindowsUpdateRings.psm1` - Windows Update for Business - `SettingsCatalog.psm1` - Settings Catalog policies - **`Extensions/`**: Utility modules for authentication, API calls, documentation - `MSALAuthentication.psm1` - Authentication patterns - `IntuneTools.psm1` - Common API utilities - `EndpointManagerInfo.psm1` - Endpoint metadata - **`Core.psm1`**: Base functionality for Graph API calls ### Version Tracking Current version: **Commit `2eaf3257`** (2025-12-09) See `docs/architecture/intune-reference-version.md` for version history and update process. ### Finding the Right Module 1. Identify the Intune resource type you need to implement (e.g., "Compliance Policies") 2. Search `Modules/` directory for related `.psm1` file (e.g., `DeviceCompliance.psm1`) 3. If not obvious, search by Graph API endpoint (e.g., `grep -r "deviceManagement/deviceCompliancePolicies" reference/IntuneManagement-master/`) --- ## Step-by-Step Implementation Process Follow this process **before writing any TypeScript code** for a new Intune sync job. ### Phase 1: Locate PowerShell Reference 1. **Identify the resource type** from the feature request (e.g., "App Protection Policies") 2. **Search for the PowerShell module**: ```bash find reference/IntuneManagement-master/Modules -name "*Protection*.psm1" # or search by keyword grep -r "AppProtection" reference/IntuneManagement-master/Modules/ ``` 3. **Open the corresponding `.psm1` file** in your editor ### Phase 2: Extract API Endpoint Pattern 1. **Find the Graph API call** - Look for `Invoke-MSGraphRequest` or `Invoke-GraphRequest`: ```powershell $policies = Invoke-MSGraphRequest -Url "/beta/deviceAppManagement/managedAppPolicies" ``` 2. **Document the endpoint**: - Full path: `/beta/deviceAppManagement/managedAppPolicies` - API version: `beta` (not `v1.0`) - Base segment: `deviceAppManagement` 3. **Check for query parameters**: ```powershell $policies = Invoke-MSGraphRequest -Url "/beta/deviceManagement/windowsUpdateForBusinessConfigurations?`$expand=assignments" ``` - Note the `$expand=assignments` parameter ### Phase 3: Identify Data Transformation Logic 1. **Look for property deletions**: ```powershell $policy.PSObject.Properties.Remove('createdDateTime') $policy.PSObject.Properties.Remove('lastModifiedDateTime') ``` → These properties must be deleted before saving (read-only) 2. **Look for type conversions**: ```powershell $policy.scheduledInstallDay = [int]$policy.scheduledInstallDay ``` → Type must be coerced 3. **Look for nested object flattening**: ```powershell if($policy.assignments) { $policy | Add-Member -NotePropertyName assignmentIds -NotePropertyValue ($policy.assignments.id) } ``` → Extract IDs from nested structure ### Phase 4: Document Undocumented Behaviors 1. **Check for beta API requirements**: ```powershell # Uses /beta because v1.0 doesn't support this resource yet ``` 2. **Note pagination patterns**: ```powershell do { $response = Invoke-MSGraphRequest -Url $url $policies += $response.value $url = $response.'@odata.nextLink' } while($url) ``` 3. **Document error handling quirks**: ```powershell # 404 is expected when no policies exist - don't treat as error if($response.StatusCode -eq 404) { return @() } ``` ### Phase 5: Map to TypeScript Implementation 1. **Create the sync job file**: `worker/jobs/sync.ts` 2. **Translate the endpoint**: PowerShell URL → TypeScript Graph client call 3. **Implement data transformation**: Match PowerShell cleanup logic 4. **Add pagination**: Match PowerShell pagination pattern 5. **Handle errors**: Replicate PowerShell error handling ### Phase 6: Validate 1. **Compare API calls**: Use Graph Explorer to verify endpoint and parameters match 2. **Compare data shape**: Ensure TypeScript returns same structure as PowerShell 3. **Test edge cases**: Empty result sets, large result sets, missing properties 4. **Document deviations**: If TypeScript differs from PowerShell, document why --- ## Data Points to Extract When analyzing PowerShell reference code, extract these critical data points: ### ✅ Required Extractions - [ ] **Graph API Endpoint** - Full URL path (e.g., `/beta/deviceManagement/configurationPolicies`) - API version (`beta` or `v1.0`) - [ ] **Query Parameters** - `$filter` - Filtering criteria (e.g., `$filter=isof('microsoft.graph.windows10GeneralConfiguration')`) - `$expand` - Related entities to include (e.g., `$expand=assignments,settings`) - `$select` - Specific properties to return (e.g., `$select=id,displayName,createdDateTime`) - `$top` - Page size limit (e.g., `$top=999`) - `$orderby` - Sorting (e.g., `$orderby=displayName`) - [ ] **Property Cleanup Logic** - Read-only properties to delete (e.g., `createdDateTime`, `lastModifiedDateTime`, `@odata.type`) - Computed properties to remove (e.g., `version`, `supportsScopeTags`) - [ ] **Type Transformations** - Type coercions (string → int, string → boolean) - Date formatting (ISO 8601 strings) - Enum value mappings - [ ] **Pagination Pattern** - How to handle `@odata.nextLink` - Maximum page size - Total count handling - [ ] **Error Handling** - Expected error codes (e.g., 404 for empty results) - Retry logic - Rate limiting handling ### 🔍 Optional Extractions (If Present) - [ ] **Nested Object Handling** - Assignment structures - Setting collections - Related entity expansions - [ ] **Conditional Logic** - Resource type-specific branching - Feature flag checks - Tenant capability checks - [ ] **Batch Operations** - Batch request patterns - Transaction boundaries --- ## PowerShell to TypeScript Pattern Mapping Common PowerShell patterns and their TypeScript equivalents: ### Pattern 1: Basic Graph API Call **PowerShell**: ```powershell $policies = Invoke-MSGraphRequest -Url "/beta/deviceManagement/configurationPolicies" ``` **TypeScript**: ```typescript const response = await graphClient .api('/deviceManagement/configurationPolicies') .version('beta') .get(); const policies = response.value; ``` ### Pattern 2: Query Parameters **PowerShell**: ```powershell $url = "/beta/deviceManagement/configurationPolicies?`$expand=assignments&`$filter=isof('microsoft.graph.configurationPolicy')" $policies = Invoke-MSGraphRequest -Url $url ``` **TypeScript**: ```typescript const response = await graphClient .api('/deviceManagement/configurationPolicies') .version('beta') .expand('assignments') .filter("isof('microsoft.graph.configurationPolicy')") .get(); ``` ### Pattern 3: Pagination **PowerShell**: ```powershell $allPolicies = @() $url = "/beta/deviceManagement/configurationPolicies" do { $response = Invoke-MSGraphRequest -Url $url $allPolicies += $response.value $url = $response.'@odata.nextLink' } while($url) ``` **TypeScript**: ```typescript let allPolicies = []; let url = '/deviceManagement/configurationPolicies'; while (url) { const response = await graphClient.api(url).version('beta').get(); allPolicies.push(...response.value); url = response['@odata.nextLink']; } ``` ### Pattern 4: Property Deletion **PowerShell**: ```powershell $policy.PSObject.Properties.Remove('createdDateTime') $policy.PSObject.Properties.Remove('lastModifiedDateTime') $policy.PSObject.Properties.Remove('@odata.context') ``` **TypeScript**: ```typescript delete policy.createdDateTime; delete policy.lastModifiedDateTime; delete policy['@odata.context']; ``` ### Pattern 5: Type Conversion **PowerShell**: ```powershell $setting.value = [int]$setting.value $policy.isAssigned = [bool]$policy.isAssigned ``` **TypeScript**: ```typescript setting.value = parseInt(setting.value, 10); policy.isAssigned = Boolean(policy.isAssigned); ``` --- ## Concrete Examples ### Example 1: Windows Update Rings **Feature Request**: Implement sync for Windows Update for Business configurations **Step 1: Locate PowerShell Module** ```bash $ find reference/IntuneManagement-master -name "*Update*.psm1" reference/IntuneManagement-master/Modules/WindowsUpdateRings.psm1 ``` **Step 2: Extract API Pattern from PowerShell** ```powershell # From WindowsUpdateRings.psm1 $rings = Invoke-MSGraphRequest -Url "/beta/deviceManagement/windowsUpdateForBusinessConfigurations" ``` **Extracted Data Points**: - **Endpoint**: `/beta/deviceManagement/windowsUpdateForBusinessConfigurations` - **API Version**: `beta` (not available in v1.0) - **Query Params**: None required for list operation - **Property Cleanup**: `createdDateTime`, `lastModifiedDateTime` must be removed **Step 3: TypeScript Implementation** ```typescript // worker/jobs/syncWindowsUpdateRings.ts import { graphClient } from '../utils/graphClient'; import { db } from '@/lib/db'; import { windowsUpdateRings } from '@/lib/db/schema'; export async function syncWindowsUpdateRings(tenantId: string) { const response = await graphClient .api('/deviceManagement/windowsUpdateForBusinessConfigurations') .version('beta') // PowerShell uses beta .get(); const rings = response.value; // Cleanup properties (matches PowerShell) for (const ring of rings) { delete ring.createdDateTime; delete ring.lastModifiedDateTime; delete ring['@odata.context']; } // Save to database await db.insert(windowsUpdateRings).values( rings.map(ring => ({ tenantId, intuneId: ring.id, displayName: ring.displayName, data: ring, lastSyncedAt: new Date() })) ).onConflictDoUpdate({ target: [windowsUpdateRings.tenantId, windowsUpdateRings.intuneId], set: { data: ring, lastSyncedAt: new Date() } }); } ``` --- ### Example 2: Settings Catalog with $expand **Feature Request**: Implement Settings Catalog policy sync **Step 1: PowerShell Analysis** ```powershell # From SettingsCatalog.psm1 $url = "/beta/deviceManagement/configurationPolicies?`$expand=settings" $policies = Invoke-MSGraphRequest -Url $url ``` **Key Discovery**: The `$expand=settings` parameter is **not documented** in Microsoft Graph API docs, but is **required** to get policy settings in a single call. Without it, you'd need a separate API call per policy to fetch settings. **Step 2: TypeScript Implementation** ```typescript // worker/jobs/syncSettingsCatalog.ts export async function syncSettingsCatalog(tenantId: string) { const response = await graphClient .api('/deviceManagement/configurationPolicies') .version('beta') .expand('settings') // CRITICAL: Discovered from PowerShell reference .get(); const policies = response.value; // Now policies include nested settings array for (const policy of policies) { console.log(`Policy ${policy.displayName} has ${policy.settings?.length || 0} settings`); } // Save to database... } ``` **Without PowerShell Reference**: You would make N+1 API calls (1 for policies list, N for each policy's settings), causing: - Slower sync performance - Higher API rate limit consumption - Unnecessary complexity --- ### Example 3: Invoke-MSGraphRequest Translation **PowerShell Pattern**: ```powershell $requestParams = @{ Url = "/beta/deviceManagement/deviceCompliancePolicies" Method = "GET" Headers = @{ "Accept" = "application/json" } } $policies = Invoke-MSGraphRequest @requestParams ``` **TypeScript Equivalent**: ```typescript const response = await graphClient .api('/deviceManagement/deviceCompliancePolicies') .version('beta') .header('Accept', 'application/json') .get(); const policies = response.value; ``` **Key Mappings**: - `Url` → `.api(path).version(version)` - `Method = "GET"` → `.get()` - `Method = "POST"` → `.post(body)` - `Method = "PATCH"` → `.patch(body)` - `Headers` → `.header(name, value)` --- ### Example 4: Property Cleanup Patterns **PowerShell Pattern**: ```powershell # From ConfigurationPolicies.psm1 function Remove-ReadOnlyProperties($policy) { $policy.PSObject.Properties.Remove('id') $policy.PSObject.Properties.Remove('createdDateTime') $policy.PSObject.Properties.Remove('lastModifiedDateTime') $policy.PSObject.Properties.Remove('settingCount') $policy.PSObject.Properties.Remove('creationSource') $policy.PSObject.Properties.Remove('roleScopeTagIds') # Remove OData metadata $policy.PSObject.Properties | Where-Object { $_.Name -like '@odata.*' } | ForEach-Object { $policy.PSObject.Properties.Remove($_.Name) } } ``` **Why This Matters**: These properties are: - **Read-only**: Graph API will reject POST/PATCH requests containing them - **Server-managed**: Values are computed by Intune, not settable by clients - **Metadata**: OData annotations are for response enrichment only **TypeScript Implementation**: ```typescript function removeReadOnlyProperties(policy: any): void { // Core read-only fields delete policy.id; delete policy.createdDateTime; delete policy.lastModifiedDateTime; delete policy.settingCount; delete policy.creationSource; delete policy.roleScopeTagIds; // Remove all OData metadata Object.keys(policy).forEach(key => { if (key.startsWith('@odata.')) { delete policy[key]; } }); } ``` **Usage**: ```typescript const policy = await graphClient.api(`/deviceManagement/configurationPolicies/${id}`).get(); removeReadOnlyProperties(policy); await graphClient.api('/deviceManagement/configurationPolicies').post(policy); // Now succeeds ``` --- ## Troubleshooting API Discrepancies When TypeScript implementation returns different data than PowerShell reference, follow this systematic process: ### Troubleshooting Checklist - [ ] **1. Compare Endpoints**: Verify exact URL path matches PowerShell - [ ] **2. Check API Version**: Confirm beta vs v1.0 matches - [ ] **3. Verify Query Parameters**: Ensure `$expand`, `$filter`, `$select` match - [ ] **4. Inspect Response Data**: Compare raw JSON from both implementations - [ ] **5. Check Property Cleanup**: Verify same properties are deleted - [ ] **6. Review Type Conversions**: Confirm same type coercions applied - [ ] **7. Test Pagination**: Ensure all pages are fetched correctly - [ ] **8. Validate Error Handling**: Check for missing error case handling ### Example Troubleshooting Session **Problem**: TypeScript sync returns incomplete assignment data for Configuration Policies **Step 1: Compare API Calls** ```typescript // Current TypeScript (WRONG) const response = await graphClient .api('/deviceManagement/configurationPolicies') .version('beta') .get(); // Result: Policies have no assignments property ``` **Step 2: Check PowerShell Reference** ```powershell # From PowerShell (CORRECT) $policies = Invoke-MSGraphRequest -Url "/beta/deviceManagement/configurationPolicies?`$expand=assignments" # Result: Policies include assignments array ``` **Root Cause**: Missing `$expand=assignments` parameter **Step 3: Fix TypeScript** ```typescript // Fixed TypeScript const response = await graphClient .api('/deviceManagement/configurationPolicies') .version('beta') .expand('assignments') // Added this .get(); // Result: Policies now include assignments ``` --- ### Example: 400 Bad Request Debugging **Problem**: Sync job fails with "400 Bad Request" error **Step 1: Capture Request Details** ```typescript try { await graphClient.api('/deviceManagement/someEndpoint').version('v1.0').get(); } catch (error) { console.error('Request URL:', error.requestUrl); console.error('Response:', error.body); } ``` **Step 2: Check PowerShell for API Version** ```powershell # PowerShell uses /beta, not /v1.0 $data = Invoke-MSGraphRequest -Url "/beta/deviceManagement/someEndpoint" ``` **Root Cause**: Endpoint only exists in beta API **Step 3: Fix** ```typescript await graphClient.api('/deviceManagement/someEndpoint').version('beta').get(); // Changed to beta ``` --- ## Understanding Existing Implementation Patterns ### Why We Delete Properties **Pattern You See**: ```typescript delete policy.createdDateTime; delete policy.lastModifiedDateTime; ``` **Rationale**: This matches PowerShell cleanup logic. These properties are: - **Read-only**: Set by Graph API, not modifiable by clients - **Validation failures**: Including them in POST/PATCH causes 400 errors - **Intentional**: Not a refactoring opportunity - DO NOT "clean up" by keeping them **PowerShell Reference**: ```powershell $policy.PSObject.Properties.Remove('createdDateTime') $policy.PSObject.Properties.Remove('lastModifiedDateTime') ``` **To New Developers**: If you see property deletions in sync jobs, check the PowerShell reference before "fixing" them. They're intentional. --- ### When to Use Beta vs V1.0 API **Pattern You See**: ```typescript .version('beta') ``` **Decision Process**: 1. **Check PowerShell reference first**: If PowerShell uses `/beta`, you must use beta 2. **Don't "upgrade" to v1.0** without verifying in PowerShell reference 3. **Beta is not always unstable**: Many Intune features are beta-only for years **Why Beta**: - Resource type doesn't exist in v1.0 yet (e.g., Settings Catalog) - Required properties only available in beta - Microsoft hasn't stabilized the API schema **PowerShell Example**: ```powershell # Uses /beta because v1.0 doesn't support this yet $policies = Invoke-MSGraphRequest -Url "/beta/deviceManagement/configurationPolicies" ``` **TypeScript Equivalent**: ```typescript // Must use beta to match PowerShell const response = await graphClient .api('/deviceManagement/configurationPolicies') .version('beta') // DO NOT change to 'v1.0' .get(); ``` --- ## Common Questions (FAQ) ### Q: Should I always use the PowerShell reference? **A**: Yes, for Intune-related features. The PowerShell tool has been battle-tested and contains the actual working patterns. Microsoft Graph docs are often incomplete. ### Q: What if the PowerShell reference is outdated? **A**: Check the version tracking in `docs/architecture/intune-reference-version.md`. If outdated, update the reference and review for breaking changes before implementing new features. ### Q: What if there's no PowerShell equivalent for my feature? **A**: Use the [Fallback Process](#fallback-process-for-missing-powershell-reference): 1. Use official Graph API docs 2. Test against at least 2 different tenants 3. Validate with 5+ resource instances 4. Document any undocumented behaviors discovered 5. Consider contributing findings back to IntuneManagement project ### Q: Can I skip property cleanup if Graph API doesn't complain? **A**: No. Match the PowerShell cleanup logic exactly. Graph API behavior can change, and undocumented acceptance of read-only properties shouldn't be relied upon. ### Q: Why not just use the PowerShell tool directly instead of TypeScript? **A**: Our architecture requires TypeScript for type safety, integration with Next.js, and consistency with the rest of the codebase (see Constitution in `.specify/memory/constitution.md`). We use PowerShell as a **reference**, not as the implementation. --- ## Edge Cases & Advanced Topics ### Versioning Strategy **Principle**: Document which PowerShell commit was used as reference for each feature implementation. **Process**: 1. When implementing a new sync job, note the current PowerShell reference commit 2. Add a comment in the TypeScript file: ```typescript /** * Windows Update Rings Sync * * PowerShell Reference: commit 2eaf3257 (2025-12-09) * Module: WindowsUpdateRings.psm1 * Last Verified: 2025-12-09 */ ``` 3. When PowerShell reference is updated, review all sync jobs and update version references **Why This Matters**: If a sync job breaks after PowerShell update, you can: - Diff between old and new PowerShell commit - Identify breaking changes - Update TypeScript to match new patterns --- ### Fallback Process for Missing PowerShell Reference **When to Use**: New Intune feature has no corresponding PowerShell module yet. **Process**: 1. **Check Official Docs**: Start with Microsoft Graph API documentation 2. **Use Graph Explorer**: Test endpoints interactively at https://developer.microsoft.com/en-us/graph/graph-explorer 3. **Extensive Testing**: - Test against at least 2 different test tenants - Create/read/update/delete at least 5 resource instances - Test with different property combinations - Test pagination with large result sets 4. **Document Assumptions**: ```typescript /** * [NO POWERSHELL REFERENCE] * * Implementation based on Graph API docs (2025-12-09) * Assumptions: * - Uses beta API (not documented if v1.0 exists) * - No $expand parameters documented * - Property cleanup based on trial-and-error * * TODO: Check IntuneManagement project for updates */ ``` 5. **Monitor for PowerShell Update**: Check IntuneManagement repo periodically for new modules --- ### Handling PowerShell Updates **Scenario**: PowerShell reference is updated with breaking changes. **Detection**: 1. Monitor IntuneManagement GitHub repository for commits 2. Review release notes in `reference/IntuneManagement-master/ReleaseNotes.md` 3. Check commit messages for "breaking change" or "BREAKING" **Update Process**: 1. **Pull Latest Changes**: ```bash cd reference/IntuneManagement-master git fetch origin git log HEAD..origin/master --oneline # Review changes git merge origin/master ``` 2. **Document New Version**: Update `docs/architecture/intune-reference-version.md` with new commit hash 3. **Test Critical Sync Jobs**: - Run sync jobs against test tenant - Compare output before/after update - Verify no data loss or corruption 4. **Update TypeScript Implementations**: - Review changed PowerShell modules - Update corresponding TypeScript sync jobs - Update version comments in code 5. **Document Breaking Changes**: Add to this guide's version history --- ### Deprecated Features **Scenario**: PowerShell module exists but Microsoft deprecated the Intune feature. **Identification**: - PowerShell comments mention "deprecated" - Graph API returns 404 or "feature not supported" - Microsoft docs show deprecation notice **Handling**: 1. **Mark as Reference Only**: Add comment to TypeScript: ```typescript /** * [DEPRECATED FEATURE] * * This sync job is based on deprecated PowerShell module. * DO NOT use for new implementations. * * PowerShell Reference: DeviceCompliance.psm1 (deprecated 2024-06-01) * Replacement: Use ComplianceSettings.psm1 instead */ ``` 2. **Don't Remove Code**: Keep for reference, but disable in job queue 3. **Document Migration Path**: If replacement exists, document how to migrate --- ### PowerShell Quirks vs Intentional Patterns **Problem**: How to distinguish between PowerShell bugs and intentional API patterns? **Decision Framework**: #### Replicate These (Intentional Patterns) - Property cleanup logic (read-only field removal) - Specific `$expand` parameters - Beta vs v1.0 API version choices - Pagination patterns - Error handling for expected errors (404 for empty results) #### Document But Don't Replicate (PowerShell Quirks) - PowerShell-specific syntax workarounds - Windows-specific file path handling - PowerShell module dependency quirks **Example of PowerShell Quirk**: ```powershell # PowerShell-specific: Escape backtick for line continuation $url = "/beta/deviceManagement/configurationPolicies?``$expand=assignments" ``` **TypeScript Should NOT Replicate**: ```typescript // DO NOT include backtick - that's PowerShell syntax const url = '/deviceManagement/configurationPolicies?$expand=assignments'; ``` **Marking Quirks**: ```typescript // [POWERSHELL QUIRK]: PowerShell uses [int] type cast here due to PS type system // In TypeScript, parseInt() achieves the same result but is more idiomatic const value = parseInt(setting.value, 10); ``` --- ## Complete End-to-End Example **Feature Request**: Implement sync for Compliance Policies with assignments ### Step 1: Locate PowerShell Module ```bash $ find reference/IntuneManagement-master -name "*Compliance*.psm1" reference/IntuneManagement-master/Modules/DeviceCompliance.psm1 ``` ### Step 2: Analyze PowerShell Implementation ```powershell # From DeviceCompliance.psm1 function Get-CompliancePolicies { $url = "/beta/deviceManagement/deviceCompliancePolicies?`$expand=assignments" $policies = Invoke-MSGraphRequest -Url $url foreach($policy in $policies.value) { # Remove read-only properties $policy.PSObject.Properties.Remove('createdDateTime') $policy.PSObject.Properties.Remove('lastModifiedDateTime') $policy.PSObject.Properties.Remove('version') # Remove OData metadata $policy.PSObject.Properties.Remove('@odata.type') } return $policies.value } ``` **Extracted Data Points**: - Endpoint: `/beta/deviceManagement/deviceCompliancePolicies` - API Version: `beta` - Query Params: `$expand=assignments` - Property Cleanup: `createdDateTime`, `lastModifiedDateTime`, `version`, `@odata.type` ### Step 3: Create TypeScript Implementation ```typescript // worker/jobs/syncCompliancePolicies.ts /** * Compliance Policies Sync * * PowerShell Reference: commit 2eaf3257 (2025-12-09) * Module: DeviceCompliance.psm1 * Last Verified: 2025-12-09 */ import { graphClient } from '../utils/graphClient'; import { db } from '@/lib/db'; import { compliancePolicies } from '@/lib/db/schema'; import { eq, and } from 'drizzle-orm'; interface CompliancePolicy { id: string; displayName: string; description?: string; createdDateTime?: string; // Will be deleted lastModifiedDateTime?: string; // Will be deleted version?: number; // Will be deleted assignments?: Array<{ id: string; target: any; }>; '@odata.type'?: string; // Will be deleted [key: string]: any; } export async function syncCompliancePolicies(tenantId: string): Promise { console.log(`[${tenantId}] Starting compliance policies sync`); try { // Step 1: Fetch policies from Graph API (matches PowerShell) const response = await graphClient .api('/deviceManagement/deviceCompliancePolicies') .version('beta') // PowerShell uses beta .expand('assignments') // PowerShell uses $expand=assignments .get(); const policies: CompliancePolicy[] = response.value; console.log(`[${tenantId}] Fetched ${policies.length} policies`); // Step 2: Clean up properties (matches PowerShell) for (const policy of policies) { delete policy.createdDateTime; delete policy.lastModifiedDateTime; delete policy.version; delete policy['@odata.type']; } // Step 3: Save to database for (const policy of policies) { await db .insert(compliancePolicies) .values({ tenantId, intuneId: policy.id, displayName: policy.displayName, description: policy.description, data: policy, // Full policy JSON lastSyncedAt: new Date(), }) .onConflictDoUpdate({ target: [compliancePolicies.tenantId, compliancePolicies.intuneId], set: { displayName: policy.displayName, description: policy.description, data: policy, lastSyncedAt: new Date(), }, }); } console.log(`[${tenantId}] Compliance policies sync completed`); } catch (error) { console.error(`[${tenantId}] Compliance policies sync failed:`, error); throw error; } } ``` ### Step 4: Register Job in Queue ```typescript // worker/index.ts import { syncCompliancePolicies } from './jobs/syncCompliancePolicies'; // Register job processor syncQueue.process('sync-compliance-policies', async (job) => { const { tenantId } = job.data; await syncCompliancePolicies(tenantId); }); ``` ### Step 5: Test Implementation ```typescript // scripts/test-compliance-sync.ts import { syncCompliancePolicies } from '../worker/jobs/syncCompliancePolicies'; async function test() { const testTenantId = 'test-tenant-123'; await syncCompliancePolicies(testTenantId); console.log('✓ Sync completed successfully'); } test().catch(console.error); ``` ### Step 6: Validate Against PowerShell 1. Run PowerShell sync: ```powershell Import-Module ./reference/IntuneManagement-master/DeviceCompliance.psm1 $policies = Get-CompliancePolicies $policies | ConvertTo-Json -Depth 10 > powershell-output.json ``` 2. Run TypeScript sync and export results: ```typescript const policies = await syncCompliancePolicies('test-tenant'); fs.writeFileSync('typescript-output.json', JSON.stringify(policies, null, 2)); ``` 3. Compare outputs: ```bash diff powershell-output.json typescript-output.json # Should show minimal differences (only timestamps, etc.) ``` --- ## Reference Implementations See these existing sync jobs for real-world examples: - **Configuration Policies**: `worker/jobs/syncConfigurationPolicies.ts` - Demonstrates `$expand=assignments,settings` - Complex property cleanup - Nested object handling - **Applications**: `worker/jobs/syncApplications.ts` - Demonstrates type-specific filtering - Binary data handling (app icons) - Conditional property cleanup - **Device Enrollment**: `worker/jobs/syncEnrollmentProfiles.ts` - Demonstrates beta-only endpoint - Multiple $expand parameters - Assignment group resolution Each implementation includes: - PowerShell reference version comment - Module source reference - Last verified date - Inline explanations of undocumented behaviors --- ## Updates & Maintenance **This Guide Is a Living Document** - **Add new examples** when implementing new sync jobs - **Update patterns** when PowerShell reference changes - **Document discoveries** of undocumented API behaviors - **Track PowerShell version** in header when examples are updated **To Propose Changes**: 1. Create a feature branch 2. Update this guide with new findings 3. Submit PR with rationale for changes 4. Link to PowerShell reference commit showing the pattern --- **Guide Version**: 1.0.0 **Last Reviewed**: 2025-12-09 **PowerShell Reference**: [2eaf3257](https://github.com/Micke-K/IntuneManagement/commit/2eaf3257) **Maintainers**: See `docs/architecture/intune-reference-version.md` for update process