schema-drift-detector
Detect ComparePower API schema changes by comparing current API responses with expected schemas, identifying new/removed/changed fields, and auto-generating Payload field definitions. Use when monitoring API schemas, detecting field changes, updating collection schemas after API changes, generating Payload field configs, or maintaining schema synchronization.
- 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: schema-drift-detector
description: Detect ComparePower API schema changes by comparing current API responses with expected schemas, identifying new/removed/changed fields, and auto-generating Payload field definitions. Use when monitoring API schemas, detecting field changes, updating collection schemas after API changes, generating Payload field configs, or maintaining schema synchronization.
allowed-tools: Read, Write, Bash, WebFetch
---
# Schema Drift Detector Skill
Monitor external API schemas and detect changes that require collection updates.
## What This Skill Does
This skill monitors ComparePower API endpoints for schema changes:
- Fetch current API responses
- Compare with expected schema definitions
- Detect new/removed/changed fields
- Auto-generate Payload field definitions
- Update collection schemas
- Generate migration reports
## When to Use This Skill
Use this skill when:
- Monitoring external API schemas
- Detecting field additions/removals
- Validating schema assumptions
- Generating Payload field configs
- Updating collections after API changes
- Maintaining schema documentation
- Creating migration scripts for schema changes
## Input/Output Contract
### Input
```typescript
interface DriftDetectionInput {
apiEndpoint: string // API URL to check
expectedSchema: SchemaDefinition // Current schema definition
collectionName: string // Target Payload collection
}
```
### Output
```typescript
interface DriftDetectionOutput {
hasDrift: boolean // True if changes detected
newFields: FieldChange[] // Added fields
removedFields: FieldChange[] // Removed fields
changedFields: FieldChange[] // Type/structure changes
payloadFieldDefs: string // Generated Payload field code
migrationScript: string // Update script
report: string // Human-readable report
}
```
## Drift Detection Pipeline
### Phase 1: Fetch Current API Response
**Purpose**: Get latest API data to analyze
**Process**:
```typescript
import fetch from 'node-fetch'
async function fetchApiResponse(endpoint: string): Promise<any> {
try {
const response = await fetch(endpoint, {
headers: {
'Accept': 'application/json',
'User-Agent': 'Schema-Drift-Detector/1.0'
}
})
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`)
}
const data = await response.json()
return data
} catch (error) {
throw new Error(`Failed to fetch API: ${error.message}`)
}
}
```
**Validation**:
- [ ] API returns 200 OK
- [ ] Response is valid JSON
- [ ] Data structure is object/array
### Phase 2: Analyze Response Structure
**Purpose**: Extract field names, types, and structure
**Process**:
```typescript
interface FieldAnalysis {
name: string
type: string
isArray: boolean
isNullable: boolean
nested?: FieldAnalysis[]
sampleValue: any
}
function analyzeStructure(
data: any,
prefix = ''
): FieldAnalysis[] {
const fields: FieldAnalysis[] = []
for (const [key, value] of Object.entries(data)) {
const fieldName = prefix ? `${prefix}.${key}` : key
if (value === null) {
fields.push({
name: fieldName,
type: 'null',
isArray: false,
isNullable: true,
sampleValue: null
})
} else if (Array.isArray(value)) {
const itemType = value.length > 0 ? typeof value[0] : 'unknown'
fields.push({
name: fieldName,
type: itemType,
isArray: true,
isNullable: false,
sampleValue: value[0]
})
// Analyze nested array items
if (value.length > 0 && typeof value[0] === 'object') {
const nested = analyzeStructure(value[0], fieldName)
fields.push(...nested)
}
} else if (typeof value === 'object') {
// Nested object
fields.push({
name: fieldName,
type: 'object',
isArray: false,
isNullable: false,
nested: analyzeStructure(value, fieldName),
sampleValue: value
})
} else {
// Primitive value
fields.push({
name: fieldName,
type: typeof value,
isArray: false,
isNullable: false,
sampleValue: value
})
}
}
return fields
}
```
**Output**: Complete field inventory
**Validation**:
- [ ] All fields extracted
- [ ] Types detected correctly
- [ ] Nested structures analyzed
### Phase 3: Compare with Expected Schema
**Purpose**: Detect differences between current and expected
**Expected Schema Format**:
```typescript
interface SchemaDefinition {
collectionName: string
version: string
lastUpdated: string
fields: ExpectedField[]
}
interface ExpectedField {
name: string
type: string
isArray: boolean
required: boolean
payloadType: string
}
```
**Comparison Logic**:
```typescript
interface SchemaDrift {
newFields: FieldChange[]
removedFields: FieldChange[]
changedFields: FieldChange[]
}
function compareSchemas(
current: FieldAnalysis[],
expected: ExpectedField[]
): SchemaDrift {
const currentMap = new Map(current.map(f => [f.name, f]))
const expectedMap = new Map(expected.map(f => [f.name, f]))
const newFields: FieldChange[] = []
const removedFields: FieldChange[] = []
const changedFields: FieldChange[] = []
// Detect new fields
for (const field of current) {
if (!expectedMap.has(field.name)) {
newFields.push({
name: field.name,
action: 'added',
currentType: field.type,
isArray: field.isArray,
sampleValue: field.sampleValue
})
}
}
// Detect removed fields
for (const field of expected) {
if (!currentMap.has(field.name)) {
removedFields.push({
name: field.name,
action: 'removed',
expectedType: field.type,
payloadType: field.payloadType
})
}
}
// Detect changed fields
for (const field of current) {
const expectedField = expectedMap.get(field.name)
if (expectedField) {
if (field.type !== expectedField.type) {
changedFields.push({
name: field.name,
action: 'type_changed',
expectedType: expectedField.type,
currentType: field.type,
sampleValue: field.sampleValue
})
}
if (field.isArray !== expectedField.isArray) {
changedFields.push({
name: field.name,
action: 'array_changed',
wasArray: expectedField.isArray,
isArray: field.isArray
})
}
}
}
return { newFields, removedFields, changedFields }
}
```
**Validation**:
- [ ] All new fields detected
- [ ] All removed fields detected
- [ ] Type changes detected
- [ ] Array/scalar changes detected
### Phase 4: Generate Payload Field Definitions
**Purpose**: Auto-generate Payload field configurations for new fields
**Type Mapping**:
| API Type | Payload Type | Notes |
|----------|-------------|-------|
| `string` | `text` | Default |
| `string` (URL) | `text` | With validation |
| `string` (email) | `email` | Email field |
| `number` | `number` | Direct mapping |
| `boolean` | `checkbox` | Direct mapping |
| `object` | `group` | Nested fields |
| `array<string>` | `array` | Text subfield |
| `array<object>` | `array` | Group subfields |
**Code Generation**:
```typescript
function generatePayloadField(
field: FieldChange
): string {
const { name, currentType, isArray, sampleValue } = field
// Detect special types
if (typeof sampleValue === 'string') {
if (sampleValue.startsWith('http')) {
return `{
name: '${name}',
type: 'text',
required: false,
validate: validateUrl
}`
}
if (sampleValue.includes('@')) {
return `{
name: '${name}',
type: 'email',
required: false
}`
}
}
// Array field
if (isArray) {
const itemType = currentType
if (itemType === 'object') {
return `{
name: '${name}',
type: 'array',
fields: [
// TODO: Define nested fields
]
}`
}
return `{
name: '${name}',
type: 'array',
fields: [
{
name: 'value',
type: '${mapType(itemType)}'
}
]
}`
}
// Object field (group)
if (currentType === 'object') {
return `{
name: '${name}',
type: 'group',
fields: [
// TODO: Define nested fields
]
}`
}
// Primitive field
return `{
name: '${name}',
type: '${mapType(currentType)}',
required: false
}`
}
function mapType(jsType: string): string {
switch (jsType) {
case 'string': return 'text'
case 'number': return 'number'
case 'boolean': return 'checkbox'
default: return 'text'
}
}
```
**Output**: TypeScript field definitions
**Validation**:
- [ ] Valid Payload field types
- [ ] Proper field structure
- [ ] Required TODO comments for nested fields
### Phase 5: Generate Migration Script
**Purpose**: Create script to update collection schema
**Script Template**:
```typescript
// Generated on ${date} by schema-drift-detector
import type { CollectionConfig } from 'payload'
/**
* Schema Drift Report
*
* New Fields (${newFieldCount}):
${newFieldsList}
*
* Removed Fields (${removedFieldCount}):
${removedFieldsList}
*
* Changed Fields (${changedFieldCount}):
${changedFieldsList}
*/
// Add these fields to src/collections/${CollectionName}/index.ts:
const newFields = [
${generatedFieldDefs}
]
// Instructions:
// 1. Review generated field definitions
// 2. Update required/validation as needed
// 3. Add fields to collection config
// 4. Run dev server to verify
// 5. Update expected-schema.json
```
**Validation**:
- [ ] Valid TypeScript syntax
- [ ] Clear instructions
- [ ] All changes documented
### Phase 6: Update Expected Schema
**Purpose**: Update schema definition file for future comparisons
**Process**:
```typescript
async function updateExpectedSchema(
schemaPath: string,
drift: SchemaDrift
): Promise<void> {
const schema: SchemaDefinition = JSON.parse(
fs.readFileSync(schemaPath, 'utf-8')
)
// Add new fields
for (const field of drift.newFields) {
schema.fields.push({
name: field.name,
type: field.currentType!,
isArray: field.isArray!,
required: false, // Default to optional
payloadType: mapType(field.currentType!)
})
}
// Remove removed fields
schema.fields = schema.fields.filter(
f => !drift.removedFields.some(rf => rf.name === f.name)
)
// Update changed fields
for (const change of drift.changedFields) {
const field = schema.fields.find(f => f.name === change.name)
if (field && change.currentType) {
field.type = change.currentType
field.payloadType = mapType(change.currentType)
}
}
// Update metadata
schema.version = incrementVersion(schema.version)
schema.lastUpdated = new Date().toISOString()
// Write back
fs.writeFileSync(
schemaPath,
JSON.stringify(schema, null, 2),
'utf-8'
)
}
```
**Validation**:
- [ ] Schema file updated
- [ ] Version incremented
- [ ] Timestamp updated
## Example Drift Detection
### API Endpoint
```
https://api.comparepower.com/v1/providers/4change-energy
```
### Current API Response
```json
{
"id": "4change",
"name": "4Change Energy",
"phone": "866-420-4567",
"website": "https://www.4changeenergy.com",
"rating": 4.5,
"reviewCount": 1234,
"plans": [
{
"id": "plan-123",
"name": "Fixed 12",
"rate": 0.095
}
],
"serviceAreas": ["Houston", "Dallas"],
"established": 2012,
"isGreen": true,
"email": "support@4changeenergy.com" // NEW FIELD
}
```
### Expected Schema (Before)
```json
{
"collectionName": "providers",
"version": "1.0.0",
"lastUpdated": "2025-10-01T00:00:00Z",
"fields": [
{ "name": "id", "type": "string", "isArray": false, "required": true, "payloadType": "text" },
{ "name": "name", "type": "string", "isArray": false, "required": true, "payloadType": "text" },
{ "name": "phone", "type": "string", "isArray": false, "required": true, "payloadType": "text" },
{ "name": "website", "type": "string", "isArray": false, "required": false, "payloadType": "text" },
{ "name": "rating", "type": "number", "isArray": false, "required": false, "payloadType": "number" },
{ "name": "reviewCount", "type": "number", "isArray": false, "required": false, "payloadType": "number" }
]
}
```
### Detected Drift
**New Fields**:
- `email` (string) → Payload `email` field
- `established` (number) → Payload `number` field
- `isGreen` (boolean) → Payload `checkbox` field
- `serviceAreas` (array<string>) → Payload `array` field
- `plans` (array<object>) → Payload `array` with nested fields
**Generated Payload Fields**:
```typescript
{
name: 'email',
type: 'email',
required: false
},
{
name: 'established',
type: 'number',
required: false
},
{
name: 'isGreen',
type: 'checkbox',
required: false
},
{
name: 'serviceAreas',
type: 'array',
fields: [
{
name: 'value',
type: 'text'
}
]
},
{
name: 'plans',
type: 'array',
fields: [
{
name: 'id',
type: 'text'
},
{
name: 'name',
type: 'text'
},
{
name: 'rate',
type: 'number'
}
]
}
```
### Drift Report
```
===========================================
SCHEMA DRIFT DETECTION REPORT
===========================================
API Endpoint: https://api.comparepower.com/v1/providers/4change-energy
Collection: providers
Timestamp: 2025-10-24T12:30:00Z
-------------------------------------------
SUMMARY
-------------------------------------------
New Fields: 5
Removed Fields: 0
Changed Fields: 0
Status: ⚠️ DRIFT DETECTED
-------------------------------------------
NEW FIELDS (5)
-------------------------------------------
1. email
Type: string
Array: false
Sample: "support@4changeenergy.com"
Payload Type: email
2. established
Type: number
Array: false
Sample: 2012
Payload Type: number
3. isGreen
Type: boolean
Array: false
Sample: true
Payload Type: checkbox
4. serviceAreas
Type: string[]
Array: true
Sample: ["Houston", "Dallas"]
Payload Type: array
5. plans
Type: object[]
Array: true
Sample: { id: "plan-123", name: "Fixed 12", rate: 0.095 }
Payload Type: array (nested fields)
-------------------------------------------
REMOVED FIELDS (0)
-------------------------------------------
None
-------------------------------------------
CHANGED FIELDS (0)
-------------------------------------------
None
-------------------------------------------
ACTION REQUIRED
-------------------------------------------
1. Review generated field definitions in:
.claude/skills/schema-drift-detector/generated-fields.ts
2. Update collection schema:
src/collections/Providers/index.ts
3. Test changes:
pnpm dev
# Navigate to /admin/collections/providers
4. Update expected schema:
.claude/skills/schema-drift-detector/expected-schemas/providers.json
===========================================
```
## Scripts Reference
### `scripts/detect-changes.ts`
**Purpose**: Compare API response with expected schema
**Usage**:
```bash
pnpm tsx .claude/skills/schema-drift-detector/scripts/detect-changes.ts \
--api-endpoint "https://api.comparepower.com/v1/providers/4change-energy" \
--expected-schema expected-schemas/providers.json \
--collection providers
```
**Output**: Drift report + generated field definitions
### `scripts/update-collections.ts`
**Purpose**: Apply generated field definitions to collection
**Usage**:
```bash
pnpm tsx .claude/skills/schema-drift-detector/scripts/update-collections.ts \
--collection providers \
--fields generated-fields.ts
```
**Output**: Updated collection file
## Validation Checklist
Before marking drift detection complete, verify:
- [ ] API endpoint accessible
- [ ] Response parsed successfully
- [ ] All fields extracted
- [ ] Comparison with expected schema complete
- [ ] New fields identified
- [ ] Removed fields identified
- [ ] Changed fields identified
- [ ] Payload field definitions generated
- [ ] Migration script generated
- [ ] Drift report created
- [ ] Expected schema updated
## Troubleshooting
### Issue: API Unreachable
**Symptom**: Fetch fails with network error
**Causes**:
- API down
- Wrong endpoint URL
- Authentication required
**Fix**:
1. Verify API endpoint URL
2. Check API status
3. Add authentication headers if needed
4. Test with curl/Postman first
### Issue: Type Detection Incorrect
**Symptom**: Wrong Payload type generated
**Causes**:
- Sample value not representative
- Field has multiple types
- Null values
**Fix**:
1. Review sample values
2. Manually adjust generated field
3. Add validation logic
4. Update type mapping rules
### Issue: Nested Fields Not Detected
**Symptom**: Object/array structure not analyzed
**Causes**:
- Empty arrays
- Null nested objects
- Recursive structures
**Fix**:
1. Fetch multiple API responses
2. Analyze non-null samples
3. Manually define nested structure
4. Document known nested fields
## Best Practices
1. **Run Regularly**: Check for drift weekly or after API updates
2. **Version Expected Schemas**: Track schema evolution over time
3. **Review Generated Fields**: Don't blindly apply all changes
4. **Test Before Deploying**: Verify new fields work in dev
5. **Document API Changes**: Note why fields were added/removed
6. **Update Migration Scripts**: Keep historical migration records
## Resources
- **Payload Fields**: https://payloadcms.com/docs/fields/overview
- **JSON Schema**: https://json-schema.org/
- **API Versioning**: https://www.postman.com/api-platform/api-versioning/
---
**Created**: 2025-10-24
**Version**: 1.0
**Use**: Ask "Check if API schema changed"