✨ New Features - Advanced data table with TanStack Table v8 + Server Actions - Server-side pagination (10/25/50/100 rows per page) - Multi-column sorting with visual indicators - Column management (show/hide, resize) persisted to localStorage - URL state synchronization for shareable filtered views - Sticky header with compact/comfortable density modes 📦 Components Added - PolicyTableV2.tsx - Main table with TanStack integration - PolicyTableColumns.tsx - 7 column definitions with sorting - PolicyTablePagination.tsx - Pagination controls - PolicyTableToolbar.tsx - Density toggle + column visibility menu - ColumnVisibilityMenu.tsx - Show/hide columns dropdown 🔧 Hooks Added - usePolicyTable.ts - TanStack Table initialization - useURLState.ts - URL query param sync with nuqs - useTablePreferences.ts - localStorage persistence 🎨 Server Actions Updated - getPolicySettingsV2 - Pagination + sorting + filtering + Zod validation - exportPolicySettingsCSV - Server-side CSV generation (max 5000 rows) 📚 Documentation Added - Intune Migration Guide (1400+ lines) - Reverse engineering strategy - Intune Reference Version tracking - Tasks completed: 22/62 (Phase 1-3) ✅ Zero TypeScript compilation errors ✅ All MVP success criteria met (pagination, sorting, column management) ✅ Ready for Phase 4-7 (filtering, export, detail view, polish) Refs: specs/004-policy-explorer-v2/tasks.md
33 KiB
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
- Overview
- PowerShell Reference Location
- Step-by-Step Implementation Process
- Data Points to Extract
- PowerShell to TypeScript Pattern Mapping
- Concrete Examples
- Troubleshooting API Discrepancies
- Understanding Existing Implementation Patterns
- Common Questions (FAQ)
- Edge Cases & Advanced Topics
- Complete End-to-End Example
- 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 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 typeConfigurationPolicies.psm1- Device configuration policiesApplications.psm1- App managementDeviceCompliance.psm1- Compliance policiesWindowsUpdateRings.psm1- Windows Update for BusinessSettingsCatalog.psm1- Settings Catalog policies
-
Extensions/: Utility modules for authentication, API calls, documentationMSALAuthentication.psm1- Authentication patternsIntuneTools.psm1- Common API utilitiesEndpointManagerInfo.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
- Identify the Intune resource type you need to implement (e.g., "Compliance Policies")
- Search
Modules/directory for related.psm1file (e.g.,DeviceCompliance.psm1) - 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
- Identify the resource type from the feature request (e.g., "App Protection Policies")
- Search for the PowerShell module:
find reference/IntuneManagement-master/Modules -name "*Protection*.psm1" # or search by keyword grep -r "AppProtection" reference/IntuneManagement-master/Modules/ - Open the corresponding
.psm1file in your editor
Phase 2: Extract API Endpoint Pattern
-
Find the Graph API call - Look for
Invoke-MSGraphRequestorInvoke-GraphRequest:$policies = Invoke-MSGraphRequest -Url "/beta/deviceAppManagement/managedAppPolicies" -
Document the endpoint:
- Full path:
/beta/deviceAppManagement/managedAppPolicies - API version:
beta(notv1.0) - Base segment:
deviceAppManagement
- Full path:
-
Check for query parameters:
$policies = Invoke-MSGraphRequest -Url "/beta/deviceManagement/windowsUpdateForBusinessConfigurations?`$expand=assignments"- Note the
$expand=assignmentsparameter
- Note the
Phase 3: Identify Data Transformation Logic
-
Look for property deletions:
$policy.PSObject.Properties.Remove('createdDateTime') $policy.PSObject.Properties.Remove('lastModifiedDateTime')→ These properties must be deleted before saving (read-only)
-
Look for type conversions:
$policy.scheduledInstallDay = [int]$policy.scheduledInstallDay→ Type must be coerced
-
Look for nested object flattening:
if($policy.assignments) { $policy | Add-Member -NotePropertyName assignmentIds -NotePropertyValue ($policy.assignments.id) }→ Extract IDs from nested structure
Phase 4: Document Undocumented Behaviors
-
Check for beta API requirements:
# Uses /beta because v1.0 doesn't support this resource yet -
Note pagination patterns:
do { $response = Invoke-MSGraphRequest -Url $url $policies += $response.value $url = $response.'@odata.nextLink' } while($url) -
Document error handling quirks:
# 404 is expected when no policies exist - don't treat as error if($response.StatusCode -eq 404) { return @() }
Phase 5: Map to TypeScript Implementation
- Create the sync job file:
worker/jobs/sync<ResourceType>.ts - Translate the endpoint: PowerShell URL → TypeScript Graph client call
- Implement data transformation: Match PowerShell cleanup logic
- Add pagination: Match PowerShell pagination pattern
- Handle errors: Replicate PowerShell error handling
Phase 6: Validate
- Compare API calls: Use Graph Explorer to verify endpoint and parameters match
- Compare data shape: Ensure TypeScript returns same structure as PowerShell
- Test edge cases: Empty result sets, large result sets, missing properties
- 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 (
betaorv1.0)
- Full URL path (e.g.,
-
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)
- Read-only properties to delete (e.g.,
-
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
- How to handle
-
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:
$policies = Invoke-MSGraphRequest -Url "/beta/deviceManagement/configurationPolicies"
TypeScript:
const response = await graphClient
.api('/deviceManagement/configurationPolicies')
.version('beta')
.get();
const policies = response.value;
Pattern 2: Query Parameters
PowerShell:
$url = "/beta/deviceManagement/configurationPolicies?`$expand=assignments&`$filter=isof('microsoft.graph.configurationPolicy')"
$policies = Invoke-MSGraphRequest -Url $url
TypeScript:
const response = await graphClient
.api('/deviceManagement/configurationPolicies')
.version('beta')
.expand('assignments')
.filter("isof('microsoft.graph.configurationPolicy')")
.get();
Pattern 3: Pagination
PowerShell:
$allPolicies = @()
$url = "/beta/deviceManagement/configurationPolicies"
do {
$response = Invoke-MSGraphRequest -Url $url
$allPolicies += $response.value
$url = $response.'@odata.nextLink'
} while($url)
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:
$policy.PSObject.Properties.Remove('createdDateTime')
$policy.PSObject.Properties.Remove('lastModifiedDateTime')
$policy.PSObject.Properties.Remove('@odata.context')
TypeScript:
delete policy.createdDateTime;
delete policy.lastModifiedDateTime;
delete policy['@odata.context'];
Pattern 5: Type Conversion
PowerShell:
$setting.value = [int]$setting.value
$policy.isAssigned = [bool]$policy.isAssigned
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
$ find reference/IntuneManagement-master -name "*Update*.psm1"
reference/IntuneManagement-master/Modules/WindowsUpdateRings.psm1
Step 2: Extract API Pattern from 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,lastModifiedDateTimemust be removed
Step 3: TypeScript Implementation
// 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
# 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
// 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:
$requestParams = @{
Url = "/beta/deviceManagement/deviceCompliancePolicies"
Method = "GET"
Headers = @{
"Accept" = "application/json"
}
}
$policies = Invoke-MSGraphRequest @requestParams
TypeScript Equivalent:
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:
# 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:
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:
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,$selectmatch - 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
// Current TypeScript (WRONG)
const response = await graphClient
.api('/deviceManagement/configurationPolicies')
.version('beta')
.get();
// Result: Policies have no assignments property
Step 2: Check PowerShell Reference
# 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
// 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
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 uses /beta, not /v1.0
$data = Invoke-MSGraphRequest -Url "/beta/deviceManagement/someEndpoint"
Root Cause: Endpoint only exists in beta API
Step 3: Fix
await graphClient.api('/deviceManagement/someEndpoint').version('beta').get(); // Changed to beta
Understanding Existing Implementation Patterns
Why We Delete Properties
Pattern You See:
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:
$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:
.version('beta')
Decision Process:
- Check PowerShell reference first: If PowerShell uses
/beta, you must use beta - Don't "upgrade" to v1.0 without verifying in PowerShell reference
- 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:
# Uses /beta because v1.0 doesn't support this yet
$policies = Invoke-MSGraphRequest -Url "/beta/deviceManagement/configurationPolicies"
TypeScript Equivalent:
// 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:
- Use official Graph API docs
- Test against at least 2 different tenants
- Validate with 5+ resource instances
- Document any undocumented behaviors discovered
- 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:
-
When implementing a new sync job, note the current PowerShell reference commit
-
Add a comment in the TypeScript file:
/** * Windows Update Rings Sync * * PowerShell Reference: commit 2eaf3257 (2025-12-09) * Module: WindowsUpdateRings.psm1 * Last Verified: 2025-12-09 */ -
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:
- Check Official Docs: Start with Microsoft Graph API documentation
- Use Graph Explorer: Test endpoints interactively at https://developer.microsoft.com/en-us/graph/graph-explorer
- 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
- Document Assumptions:
/** * [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 */ - Monitor for PowerShell Update: Check IntuneManagement repo periodically for new modules
Handling PowerShell Updates
Scenario: PowerShell reference is updated with breaking changes.
Detection:
- Monitor IntuneManagement GitHub repository for commits
- Review release notes in
reference/IntuneManagement-master/ReleaseNotes.md - Check commit messages for "breaking change" or "BREAKING"
Update Process:
-
Pull Latest Changes:
cd reference/IntuneManagement-master git fetch origin git log HEAD..origin/master --oneline # Review changes git merge origin/master -
Document New Version: Update
docs/architecture/intune-reference-version.mdwith new commit hash -
Test Critical Sync Jobs:
- Run sync jobs against test tenant
- Compare output before/after update
- Verify no data loss or corruption
-
Update TypeScript Implementations:
- Review changed PowerShell modules
- Update corresponding TypeScript sync jobs
- Update version comments in code
-
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:
-
Mark as Reference Only: Add comment to 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 */ -
Don't Remove Code: Keep for reference, but disable in job queue
-
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
$expandparameters - 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-specific: Escape backtick for line continuation
$url = "/beta/deviceManagement/configurationPolicies?``$expand=assignments"
TypeScript Should NOT Replicate:
// DO NOT include backtick - that's PowerShell syntax
const url = '/deviceManagement/configurationPolicies?$expand=assignments';
Marking Quirks:
// [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
$ find reference/IntuneManagement-master -name "*Compliance*.psm1"
reference/IntuneManagement-master/Modules/DeviceCompliance.psm1
Step 2: Analyze PowerShell Implementation
# 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
// 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<void> {
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
// 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
// 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
-
Run PowerShell sync:
Import-Module ./reference/IntuneManagement-master/DeviceCompliance.psm1 $policies = Get-CompliancePolicies $policies | ConvertTo-Json -Depth 10 > powershell-output.json -
Run TypeScript sync and export results:
const policies = await syncCompliancePolicies('test-tenant'); fs.writeFileSync('typescript-output.json', JSON.stringify(policies, null, 2)); -
Compare outputs:
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
- Demonstrates
-
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:
- Create a feature branch
- Update this guide with new findings
- Submit PR with rationale for changes
- Link to PowerShell reference commit showing the pattern
Guide Version: 1.0.0
Last Reviewed: 2025-12-09
PowerShell Reference: 2eaf3257
Maintainers: See docs/architecture/intune-reference-version.md for update process