Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/migration-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,65 @@ _For larger projects, migrations naturally use significant bandwidth. Limits hav

Refer to the Official Guide for details on installing the extension from the Marketplace or manually.

## Configuration

### Hybrid Configuration System

The migration module uses a two-tier configuration system combining environment variables and Directus presets for maximum flexibility.

### 1. Environment Variables (Admin Defaults)

Administrators can set default values that apply to all users via environment variables:

```bash
# Default destination URL
MIGRATION_BUNDLE_DEFAULT_URL=https://stage.example.com

# Default admin token (optional - can be left empty for security)
MIGRATION_BUNDLE_DEFAULT_TOKEN=your-admin-token

# Default selected options (comma-separated)
MIGRATION_BUNDLE_DEFAULT_OPTIONS=content,users,flows
```

Available options for `MIGRATION_BUNDLE_DEFAULT_OPTIONS`:
- `content` - Collections and their data
- `users` - Users, roles, and permissions
- `comments` - Activity comments
- `presets` - User bookmarks
- `dashboards` - Insights dashboards
- `extensions` - Installed extensions
- `flows` - Automation flows

### 2. Directus Presets (User Configurations)

Users can save their migration configurations as Directus presets, which are stored in the database and accessible across sessions and devices.

**Features:**
- **Named Configurations**: Save multiple configs like "Production Sync", "Content Only", "Full Migration"
- **Scope Control**: Save presets for yourself, your role, or globally (admin only)
- **Quick Access**: Load saved configurations from a dropdown menu
- **Override Defaults**: User presets take precedence over environment defaults

**How to use:**
1. Configure your migration settings (URL, token, options)
2. Click the bookmark icon (📑) to save as a preset
3. Choose a name and scope (personal/role/global)
4. Load saved presets from the dropdown above the form

### 3. Configuration Priority

The system follows this priority order:
1. **User-selected preset** (highest priority)
2. **Environment defaults** (if no preset selected)
3. **Empty form** (if neither configured)

This approach provides:
- **For Admins**: Environment-specific defaults across environments
- **For Users**: Personal saved configurations
- **For Teams**: Shared configurations via role-based presets
- **Best Practice**: Consistent setups with flexibility

## How to Customize the Migration

1. On the migration module, add the destination server and token and click **Check**
Expand Down
129 changes: 128 additions & 1 deletion packages/migration-bundle/src/migration-endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,108 @@ export default defineEndpoint({

const storage = toArray(env.STORAGE_LOCATIONS)[0];

router.get('/*', async (req, res) => res.sendStatus(400));
router.get('/*', async (req, res) => {
if (req.url === '/defaults') {
// Return ENV-based defaults
const defaults = {
baseURL: env.MIGRATION_BUNDLE_DEFAULT_URL || '',
token: env.MIGRATION_BUNDLE_DEFAULT_TOKEN || '',
options: env.MIGRATION_BUNDLE_DEFAULT_OPTIONS
? String(env.MIGRATION_BUNDLE_DEFAULT_OPTIONS).split(',').map((s: string) => s.trim())
: []
};
res.json(defaults);
} else if (req.url === '/presets') {
// Get migration presets for current user
const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null;

if (!accountability?.admin) {
res.sendStatus(401);
return;
}

try {
// Query presets with proper priority (user > role > global)
const presets = await database('directus_presets')
.where({
collection: 'migration_bundle',
user: accountability.user
})
.orWhere(function() {
this.where({
collection: 'migration_bundle',
user: null,
role: accountability.role
});
})
.orWhere(function() {
this.where({
collection: 'migration_bundle',
user: null,
role: null
});
})
.whereNotNull('bookmark')
.orderBy([
{ column: 'user', order: 'desc', nulls: 'last' },
{ column: 'role', order: 'desc', nulls: 'last' },
{ column: 'bookmark', order: 'asc' }
]);

res.json({ data: presets });
} catch (error) {
console.error('Failed to load presets:', error);
res.json({ data: [] });
}
} else {
res.sendStatus(400);
}
});

router.post('/*', async (req, res) => {
if (req.url === '/presets') {
// Create or update migration preset
const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null;

if (!accountability?.admin) {
res.sendStatus(401);
return;
}

try {
const preset = {
collection: 'migration_bundle',
bookmark: req.body.name || `Migration Config ${new Date().toLocaleDateString()}`,
user: req.body.scope === 'user' ? accountability.user :
req.body.scope === 'global' ? null : accountability.user,
role: req.body.scope === 'role' ? accountability.role : null,
icon: 'sync',
color: '#2ECDA7',
layout: 'custom',
layout_options: JSON.stringify({
baseURL: req.body.baseURL,
token: req.body.token,
selectedOptions: req.body.options
})
};

// Check if preset exists for update
if (req.body.id) {
await database('directus_presets')
.where({ id: req.body.id })
.update(preset);
res.json({ data: { ...preset, id: req.body.id } });
} else {
const [id] = await database('directus_presets').insert(preset);
res.json({ data: { ...preset, id } });
}
} catch (error) {
console.error('Failed to save preset:', error);
res.status(500).json({ error: 'Failed to save preset' });
}
return;
}

if (!['/run', '/dry-run', '/check'].includes(req.url)) {
res.sendStatus(400);
throw new Error('Bad Request');
Expand Down Expand Up @@ -344,5 +443,33 @@ export default defineEndpoint({
}
}
});

// DELETE endpoint for presets
router.delete('/presets/:id', async (req, res) => {
const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null;

if (!accountability?.admin) {
res.sendStatus(401);
return;
}

try {
const presetId = parseInt(req.params.id);

// Only allow deletion of user's own presets or if they have proper permissions
await database('directus_presets')
.where({
id: presetId,
collection: 'migration_bundle',
user: accountability.user
})
.delete();

res.json({ success: true });
} catch (error) {
console.error('Failed to delete preset:', error);
res.status(500).json({ error: 'Failed to delete preset' });
}
});
},
});
Loading