migration-validator
Verify migration results at all layers - database queries, field presence checks, relationship integrity, admin UI testing with Playwright, and comprehensive validation reporting. Use when validating migrations, verifying database seeding, checking data integrity, testing admin UI functionality, generating validation reports, or confirming migration success.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: migration-validator
description: Verify migration results at all layers - database queries, field presence checks, relationship integrity, admin UI testing with Playwright, and comprehensive validation reporting. Use when validating migrations, verifying database seeding, checking data integrity, testing admin UI functionality, generating validation reports, or confirming migration success.
allowed-tools: Read, Write, Bash
---
# Migration Validator Skill
Comprehensive validation of migration results across all system layers.
## What This Skill Does
This skill verifies migration success through multiple validation layers:
- Database count queries
- Field presence verification
- Relationship integrity checks
- Soft-delete configuration validation
- Admin UI testing (Playwright)
- Comprehensive validation reporting
## When to Use This Skill
Use this skill when:
- Completing a collection migration
- Verifying database seeding
- Checking data integrity
- Testing admin UI functionality
- Generating validation reports
- Debugging migration failures
- Confirming 100% success rate
## Input/Output Contract
### Input
```typescript
interface ValidationInput {
collection: string // Collection to validate
expectedCount: number // Expected record count
sampleIds?: string[] // Specific records to test
checkRelationships?: boolean // Validate relationships
testAdminUI?: boolean // Run Playwright tests
}
```
### Output
```typescript
interface ValidationOutput {
passed: boolean // Overall pass/fail
layers: LayerResult[] // Results per layer
errors: ValidationError[] // All errors found
warnings: string[] // Non-critical issues
report: string // Human-readable report
}
```
## Validation Layers
### Layer 1: Database Count Validation
**Purpose**: Verify expected number of records exist
**Query**:
```typescript
import { getPayload } from 'payload'
async function validateCount(
collection: string,
expectedCount: number
): Promise<ValidationResult> {
const payload = await getPayload({ config })
const { totalDocs } = await payload.find({
collection,
limit: 1,
where: {
deletedAt: { equals: null } // Active records only
}
})
const passed = totalDocs === expectedCount
return {
layer: 'Database Count',
passed,
expected: expectedCount,
actual: totalDocs,
message: passed
? `✓ Found ${totalDocs} records (expected ${expectedCount})`
: `✗ Found ${totalDocs} records (expected ${expectedCount})`
}
}
```
**Validation Criteria**:
- [ ] Total docs equals expected count
- [ ] Query executes without errors
- [ ] Only active records counted (deletedAt: null)
### Layer 2: Required Fields Validation
**Purpose**: Verify all required fields are present and populated
**Query**:
```typescript
async function validateRequiredFields(
collection: string,
requiredFields: string[]
): Promise<ValidationResult> {
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection,
limit: 10, // Sample check
where: {
deletedAt: { equals: null }
}
})
const errors: string[] = []
for (const doc of docs) {
for (const field of requiredFields) {
const value = getNestedValue(doc, field)
if (value === null || value === undefined || value === '') {
errors.push(`Record ${doc.id}: Missing required field "${field}"`)
}
}
}
return {
layer: 'Required Fields',
passed: errors.length === 0,
checked: docs.length,
errors
}
}
function getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((acc, part) => acc?.[part], obj)
}
```
**Required Fields Checklist**:
- [ ] `createdAt` present (ISO timestamp)
- [ ] `updatedAt` present (ISO timestamp)
- [ ] `deletedAt` present (null for active records)
- [ ] `title` present (for most collections)
- [ ] `slug` present and unique
- [ ] `publishedAt` present (if applicable)
**Validation Criteria**:
- [ ] All required fields exist
- [ ] No null/undefined/empty values
- [ ] Nested fields accessible (e.g., `seo.title`)
### Layer 3: Soft-Delete Configuration
**Purpose**: Verify soft-delete is properly configured
**Query**:
```typescript
async function validateSoftDelete(
collection: string
): Promise<ValidationResult> {
const payload = await getPayload({ config })
// Check if deletedAt field exists
const { docs } = await payload.find({
collection,
limit: 1,
where: {
deletedAt: { equals: null }
}
})
if (docs.length === 0) {
return {
layer: 'Soft-Delete',
passed: false,
message: 'No records found - cannot verify soft-delete'
}
}
const doc = docs[0]
// Verify deletedAt field exists
const hasDeletedAtField = 'deletedAt' in doc
// Verify it's null for active records
const isNull = doc.deletedAt === null
const passed = hasDeletedAtField && isNull
return {
layer: 'Soft-Delete',
passed,
hasDeletedAtField,
isNull,
message: passed
? '✓ Soft-delete configured correctly'
: '✗ Soft-delete misconfigured'
}
}
```
**Validation Criteria**:
- [ ] `deletedAt` field exists
- [ ] `deletedAt` is `null` for active records
- [ ] Collection has `trash: true` in config
- [ ] Queries filter on `deletedAt` by default
### Layer 4: Relationship Integrity
**Purpose**: Verify all relationship references resolve
**Query**:
```typescript
async function validateRelationships(
collection: string,
relationships: RelationshipConfig[]
): Promise<ValidationResult> {
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection,
limit: 100, // Sample check
where: {
deletedAt: { equals: null }
}
})
const errors: string[] = []
for (const doc of docs) {
for (const rel of relationships) {
const value = getNestedValue(doc, rel.field)
if (!value) continue // Optional relationship
// Single relationship
if (rel.hasMany === false) {
const exists = await checkReferenceExists(
payload,
rel.collection,
value
)
if (!exists) {
errors.push(
`Record ${doc.id}: Invalid ${rel.field} reference "${value}"`
)
}
}
// Multiple relationships
if (rel.hasMany === true && Array.isArray(value)) {
for (const refId of value) {
const exists = await checkReferenceExists(
payload,
rel.collection,
refId
)
if (!exists) {
errors.push(
`Record ${doc.id}: Invalid ${rel.field} reference "${refId}"`
)
}
}
}
}
}
return {
layer: 'Relationship Integrity',
passed: errors.length === 0,
checked: docs.length,
errors
}
}
async function checkReferenceExists(
payload: Payload,
collection: string,
id: string
): Promise<boolean> {
try {
const doc = await payload.findByID({
collection,
id
})
return !!doc
} catch {
return false
}
}
```
**Validation Criteria**:
- [ ] All relationship IDs exist in target collection
- [ ] No orphaned references
- [ ] `hasMany` relationships are arrays
- [ ] Single relationships are strings
### Layer 5: Admin UI Testing (Playwright)
**Purpose**: Verify records display correctly in admin UI
**Test Script**:
```typescript
import { test, expect } from '@playwright/test'
test('Collection loads in admin UI', async ({ page }) => {
// Navigate to admin
await page.goto('http://localhost:3002/admin')
// Login
await page.fill('input[name="email"]', 'admin@example.com')
await page.fill('input[name="password"]', 'password')
await page.click('button[type="submit"]')
// Wait for dashboard
await expect(page.locator('h1')).toContainText('Dashboard')
// Navigate to collection
await page.click(`a[href="/admin/collections/providers"]`)
// Wait for collection list
await expect(page.locator('h1')).toContainText('Providers')
// Verify records appear
const rows = page.locator('table tbody tr')
await expect(rows).toHaveCount.greaterThan(0)
// Click first record
await rows.first().click()
// Verify detail view loads
await expect(page.locator('h1')).toContainText('Provider')
// Check for "Document not found" error
const errorMessage = page.locator('text=Document not found')
await expect(errorMessage).not.toBeVisible()
})
```
**Validation Criteria**:
- [ ] Collection list loads
- [ ] Records appear in table
- [ ] Individual record detail view loads
- [ ] No "Document not found" errors
- [ ] All fields render without errors
### Layer 6: Field Type Validation
**Purpose**: Verify field values match expected types
**Query**:
```typescript
async function validateFieldTypes(
collection: string,
fieldTypes: Record<string, string>
): Promise<ValidationResult> {
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection,
limit: 10,
where: {
deletedAt: { equals: null }
}
})
const errors: string[] = []
for (const doc of docs) {
for (const [field, expectedType] of Object.entries(fieldTypes)) {
const value = getNestedValue(doc, field)
if (value === null || value === undefined) continue // Optional field
const actualType = typeof value
if (actualType !== expectedType) {
errors.push(
`Record ${doc.id}: Field "${field}" has type ${actualType} (expected ${expectedType})`
)
}
}
}
return {
layer: 'Field Types',
passed: errors.length === 0,
checked: docs.length,
errors
}
}
```
**Validation Criteria**:
- [ ] String fields are strings
- [ ] Number fields are numbers
- [ ] Date fields are valid ISO timestamps
- [ ] Boolean fields are booleans
- [ ] Array fields are arrays
## Example Validation
### Input Configuration
```typescript
const validationConfig = {
collection: 'providers',
expectedCount: 157,
requiredFields: [
'title',
'slug',
'status',
'publishedAt',
'createdAt',
'updatedAt',
'deletedAt'
],
relationships: [
{
field: 'providerMetadata',
collection: 'providerMetadatas',
hasMany: false
}
],
fieldTypes: {
'title': 'string',
'slug': 'string',
'publishedAt': 'string',
'wpPostId': 'number'
},
testAdminUI: true
}
```
### Validation Report
```
===========================================
MIGRATION VALIDATION REPORT
===========================================
Collection: providers
Timestamp: 2025-10-24T12:30:00Z
-------------------------------------------
LAYER 1: Database Count
-------------------------------------------
Status: ✓ PASSED
Expected: 157
Actual: 157
Message: Found 157 active records
-------------------------------------------
LAYER 2: Required Fields
-------------------------------------------
Status: ✓ PASSED
Checked: 10 sample records
Missing: 0
Required fields verified:
✓ title
✓ slug
✓ status
✓ publishedAt
✓ createdAt
✓ updatedAt
✓ deletedAt
-------------------------------------------
LAYER 3: Soft-Delete Configuration
-------------------------------------------
Status: ✓ PASSED
deletedAt field: Present
Value for active records: null
Config setting (trash): true
-------------------------------------------
LAYER 4: Relationship Integrity
-------------------------------------------
Status: ✓ PASSED
Checked: 100 records
Relationships verified:
✓ providerMetadata → providerMetadatas
Orphaned references: 0
-------------------------------------------
LAYER 5: Admin UI Testing
-------------------------------------------
Status: ✓ PASSED
Tests executed:
✓ Collection list loads
✓ Records appear in table (157 rows)
✓ Individual record loads
✓ No "Document not found" errors
✓ All fields render
-------------------------------------------
LAYER 6: Field Types
-------------------------------------------
Status: ✓ PASSED
Checked: 10 sample records
Field types verified:
✓ title: string
✓ slug: string
✓ publishedAt: string (ISO date)
✓ wpPostId: number
-------------------------------------------
OVERALL RESULT: ✓ PASSED
-------------------------------------------
All 6 validation layers passed successfully.
Migration is complete and verified.
===========================================
```
## Scripts Reference
### `scripts/verify-database.mjs`
**Purpose**: Run database validation layers
**Usage**:
```bash
./scripts/doppler-run.sh dev node \
.claude/skills/migration-validator/scripts/verify-database.mjs \
--collection providers \
--expected-count 157
```
**Output**: Database validation report
### `scripts/verify-admin-ui.mjs`
**Purpose**: Run Playwright admin UI tests
**Usage**:
```bash
pnpm playwright test \
.claude/skills/migration-validator/scripts/verify-admin-ui.mjs
```
**Output**: Playwright test results
## Validation Checklist
Before marking validation complete, verify:
- [ ] Database count matches expected
- [ ] All required fields present
- [ ] Soft-delete configured (deletedAt field)
- [ ] All relationships resolve
- [ ] Field types correct
- [ ] Admin UI loads collection
- [ ] Individual records load in admin UI
- [ ] No "Document not found" errors
- [ ] Validation report generated
- [ ] All layers passed
## Troubleshooting
### Issue: Count Mismatch
**Symptom**: Total docs ≠ expected count
**Causes**:
- Some records failed to seed
- Duplicate slugs prevented insertion
- Soft-deleted records not excluded
**Fix**:
1. Query for failed records
2. Check seed script logs
3. Verify slug uniqueness
4. Re-seed missing records
### Issue: Missing Fields
**Symptom**: Required fields missing or null
**Causes**:
- Field not mapped in seed script
- Frontmatter missing in source
- Field name mismatch
**Fix**:
1. Check frontmatter in source files
2. Verify field mapping in seed script
3. Update schema if needed
4. Re-seed affected records
### Issue: Orphaned References
**Symptom**: Relationship IDs don't exist
**Causes**:
- Related collection not seeded yet
- Incorrect ID mapping
- Deleted referenced records
**Fix**:
1. Seed related collection first
2. Verify ID resolution logic
3. Check for deleted references
4. Update references or re-seed
### Issue: Admin UI Errors
**Symptom**: "Document not found" when clicking records
**Causes**:
- Missing `deletedAt` field
- MongoDB index missing
- Payload query filtering incorrectly
**Fix**:
1. Check `deletedAt` field exists and is null
2. Create MongoDB indexes
3. Verify collection config has `trash: true`
4. Check Payload access control
## Best Practices
1. **Run After Every Migration**: Don't skip validation
2. **Test Sample First**: Start with 10 records before full validation
3. **Check All Layers**: Don't stop at database queries
4. **Use Playwright**: UI testing catches real issues
5. **Generate Reports**: Document validation results
6. **Fix Before Proceeding**: Don't continue with failed validation
7. **Automate**: Include validation in CI/CD pipeline
## Resources
- **Playwright Documentation**: https://playwright.dev/docs/intro
- **Payload Find API**: https://payloadcms.com/docs/queries/overview
- **MongoDB Indexes**: https://www.mongodb.com/docs/manual/indexes/
---
**Created**: 2025-10-24
**Version**: 1.0
**Use**: Ask "Validate the provider migration"