Skip to content

Commit 7e4040f

Browse files
Copilotmarkhuot
andcommitted
Add DeleteSite tool with controller, tests, and documentation
Co-authored-by: markhuot <48975+markhuot@users.noreply.github.com>
1 parent c50646e commit 7e4040f

7 files changed

Lines changed: 579 additions & 0 deletions

File tree

SKILLS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ All API endpoints:
8080
- **create_site** - `POST /api/sites` - Create site with name/URL/language/handle
8181
- **get_sites** - `GET /api/sites` - List all sites with IDs/handles/URLs
8282
- **update_site** - `PUT /api/sites/<id>` - Update site properties/settings
83+
- **delete_site** - `DELETE /api/sites/<id>` - Permanently delete site with impact analysis

SKILLS/delete_site.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# delete_site
2+
3+
Delete sites permanently with impact analysis and data protection.
4+
5+
## Route
6+
7+
`DELETE /api/sites/<id>`
8+
9+
## Description
10+
11+
Deletes a site from Craft CMS. This will remove the site and potentially affect related data. The tool analyzes impact and provides usage statistics before deletion.
12+
13+
**WARNING**: Deleting a site with existing entries causes permanent data loss. This action cannot be undone. Always get user approval before forcing deletion of sites with content.
14+
15+
**IMPORTANT**: You cannot delete the primary site. If you need to delete it, first set another site as primary using the UpdateSite tool.
16+
17+
## Parameters
18+
19+
### Required Parameters
20+
21+
- **siteId** (integer): The ID of the site to delete (passed in URL as `<id>`)
22+
23+
### Optional Parameters
24+
25+
- **force** (boolean, optional): Force deletion even if entries exist. Default: `false`. Requires user approval for sites with content.
26+
27+
## Return Value
28+
29+
Returns an object containing impact analysis:
30+
31+
- **id** (integer): Deleted site's ID
32+
- **name** (string): Site name
33+
- **handle** (string): Site handle
34+
- **language** (string): Language code
35+
- **baseUrl** (string): Base URL
36+
- **impact** (object): Impact assessment containing:
37+
- `hasContent` (boolean): Whether site contains data
38+
- `entryCount` (integer): Number of entries
39+
- `draftCount` (integer): Number of drafts
40+
- `revisionCount` (integer): Number of revisions
41+
42+
## Example Usage
43+
44+
### Delete Empty Site
45+
```json
46+
{
47+
"siteId": 5
48+
}
49+
```
50+
51+
### Force Delete Site with Content
52+
```json
53+
{
54+
"siteId": 3,
55+
"force": true
56+
}
57+
```
58+
59+
## Example Response
60+
61+
```json
62+
{
63+
"id": 3,
64+
"name": "Old German Site",
65+
"handle": "oldGerman",
66+
"language": "de-DE",
67+
"baseUrl": "https://example.com/de",
68+
"impact": {
69+
"hasContent": true,
70+
"entryCount": 45,
71+
"draftCount": 3,
72+
"revisionCount": 127
73+
}
74+
}
75+
```
76+
77+
## Error Behavior
78+
79+
### Site with Content (force=false)
80+
81+
If site contains content and `force=false`, the tool throws an error with detailed impact assessment:
82+
83+
```
84+
Site 'German Site' contains data and cannot be deleted without force=true.
85+
86+
Impact Assessment:
87+
- Entries: 45
88+
- Drafts: 3
89+
- Revisions: 127
90+
91+
Set force=true to proceed with deletion. This action cannot be undone.
92+
```
93+
94+
### Primary Site Deletion
95+
96+
If attempting to delete the primary site:
97+
98+
```
99+
Cannot delete the primary site. Set another site as primary first.
100+
```
101+
102+
## Notes
103+
104+
- Always review impact assessment before deletion
105+
- Sites with content require `force=true` to delete
106+
- Get explicit user approval before forcing deletion
107+
- Deleted sites cannot be recovered
108+
- Cannot delete the primary site - set another site as primary first using `update_site`
109+
- All entries, drafts, and revisions for the site are permanently deleted when forced
110+
- Use `get_sites` to identify the primary site and other sites
111+
112+
## See Also
113+
114+
- `create_site` - Create new sites
115+
- `update_site` - Update site properties (including setting primary status)
116+
- `get_sites` - List all sites

src/Plugin.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,6 @@ protected function registerSiteUrlRules(RegisterUrlRulesEvent $event): void
134134
$event->rules['POST ' . $apiPrefix . '/sites'] = 'skills/sites/create';
135135
$event->rules['GET ' . $apiPrefix . '/sites'] = 'skills/sites/list';
136136
$event->rules['PUT ' . $apiPrefix . '/sites/<id>'] = 'skills/sites/update';
137+
$event->rules['DELETE ' . $apiPrefix . '/sites/<id>'] = 'skills/sites/delete';
137138
}
138139
}

src/controllers/SitesController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace happycog\craftmcp\controllers;
44

55
use happycog\craftmcp\tools\CreateSite;
6+
use happycog\craftmcp\tools\DeleteSite;
67
use happycog\craftmcp\tools\GetSites;
78
use happycog\craftmcp\tools\UpdateSite;
89
use yii\web\Response;
@@ -26,4 +27,10 @@ public function actionUpdate(int $id): Response
2627
$tool = \Craft::$container->get(UpdateSite::class);
2728
return $this->callTool($tool->update(...), ['siteId' => $id]);
2829
}
30+
31+
public function actionDelete(int $id): Response
32+
{
33+
$tool = \Craft::$container->get(DeleteSite::class);
34+
return $this->callTool($tool->delete(...), ['siteId' => $id]);
35+
}
2936
}

src/tools/DeleteSite.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
namespace happycog\craftmcp\tools;
4+
5+
use Craft;
6+
use craft\elements\Entry;
7+
use happycog\craftmcp\exceptions\ModelSaveException;
8+
9+
class DeleteSite
10+
{
11+
/**
12+
* Delete a site from Craft CMS. This will remove the site and potentially affect related data.
13+
*
14+
* **WARNING**: Deleting a site that has existing entries will cause data loss. The tool will
15+
* provide usage statistics and require confirmation for sites with existing content.
16+
*
17+
* You _must_ get the user's approval to use the force parameter to delete sites that have existing
18+
* entries. This action cannot be undone.
19+
*
20+
* **IMPORTANT**: You cannot delete the primary site. If you need to delete it, first set another
21+
* site as primary using the UpdateSite tool.
22+
*
23+
* @return array<string, mixed>
24+
*/
25+
public function delete(
26+
/** The ID of the site to delete */
27+
int $siteId,
28+
29+
/** Force deletion even if entries exist (default: false) */
30+
bool $force = false
31+
): array
32+
{
33+
$sitesService = Craft::$app->getSites();
34+
35+
// Get the site
36+
$site = $sitesService->getSiteById($siteId);
37+
throw_unless($site, "Site with ID {$siteId} not found");
38+
39+
// Prevent deletion of primary site
40+
throw_if($site->primary, "Cannot delete the primary site. Set another site as primary first.");
41+
42+
// Analyze impact before deletion
43+
$impact = $this->analyzeImpact($site);
44+
45+
// Check if force is required
46+
if ($impact['hasContent'] && !$force) {
47+
// Type-safe access to impact data for string interpolation
48+
assert(is_int($impact['entryCount']) || is_string($impact['entryCount']));
49+
assert(is_int($impact['draftCount']) || is_string($impact['draftCount']));
50+
assert(is_int($impact['revisionCount']) || is_string($impact['revisionCount']));
51+
52+
$entryCount = (string)$impact['entryCount'];
53+
$draftCount = (string)$impact['draftCount'];
54+
$revisionCount = (string)$impact['revisionCount'];
55+
56+
throw new \RuntimeException(
57+
"Site '{$site->name}' contains data and cannot be deleted without force=true.\n\n" .
58+
"Impact Assessment:\n" .
59+
"- Entries: {$entryCount}\n" .
60+
"- Drafts: {$draftCount}\n" .
61+
"- Revisions: {$revisionCount}\n\n" .
62+
"Set force=true to proceed with deletion. This action cannot be undone."
63+
);
64+
}
65+
66+
// Store site info for response
67+
$siteInfo = [
68+
'id' => $site->id,
69+
'name' => $site->name,
70+
'handle' => $site->handle,
71+
'language' => $site->language,
72+
'baseUrl' => $site->getBaseUrl(),
73+
'impact' => $impact
74+
];
75+
76+
// Delete the site
77+
throw_unless($sitesService->deleteSite($site), ModelSaveException::class, $site);
78+
79+
return $siteInfo;
80+
}
81+
82+
/**
83+
* @return array<string, mixed>
84+
*/
85+
private function analyzeImpact(\craft\models\Site $site): array
86+
{
87+
// Count entries for this site
88+
$entryCount = Entry::find()
89+
->siteId($site->id)
90+
->count();
91+
92+
// Count drafts for this site
93+
$draftCount = Entry::find()
94+
->siteId($site->id)
95+
->drafts()
96+
->count();
97+
98+
// Count revisions for this site
99+
$revisionCount = Entry::find()
100+
->siteId($site->id)
101+
->revisions()
102+
->count();
103+
104+
// Check if there's any content
105+
$hasContent = $entryCount > 0 || $draftCount > 0 || $revisionCount > 0;
106+
107+
return [
108+
'hasContent' => $hasContent,
109+
'entryCount' => $entryCount,
110+
'draftCount' => $draftCount,
111+
'revisionCount' => $revisionCount,
112+
];
113+
}
114+
}

0 commit comments

Comments
 (0)