From f8bf5af17f22e1a6e6ebd2443afb5eafb8854bca Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Fri, 5 Jun 2026 16:40:25 +0100 Subject: [PATCH] Project restore working --- ...94602_4c6e2a6223194ea5bded4dfc59fbf838.trn | 5 - ...94610_32720706721643a5b196bef114e5e58f.trn | 5 - ...94610_3ffeff8809724de59583dba2b1c7387f.trn | 5 - ...94610_92039b11719e4e539af091bfa33ef1b8.trn | 5 - ...94610_934c631cf1f04f17b5dfcbf39af5188d.trn | 5 - ...53645_aa4b8e37a31548f9bac816af1f948b35.trn | 5 + ...53659_b4e053cbbf234e0faa5c6b9bc800a68e.trn | 5 + ...53659_d53d9f6555114bf19b0c4d276113d1dc.trn | 5 + ...53659_f6feaec606184bb389541cce1cca0407.trn | 5 + .../ProjectRestorePointsController.cs | 23 +- .../Data/ProjectRestorePointRepository.cs | 17 + PlotLine/Models/ProjectRestorePointModels.cs | 7 + PlotLine/Program.cs | 1 + .../Services/ProjectRestorePointService.cs | 33 +- PlotLine/Services/ProjectRestoreService.cs | 123 +++++++ PlotLine/Sql/036_ProjectRestoreApply.sql | 306 ++++++++++++++++++ .../Views/ProjectRestorePoints/Index.cshtml | 28 ++ PlotLine/wwwroot/js/site.js | 2 +- 18 files changed, 549 insertions(+), 36 deletions(-) delete mode 100644 .appdata/.dotnet/TelemetryStorageService/20260604194602_4c6e2a6223194ea5bded4dfc59fbf838.trn delete mode 100644 .appdata/.dotnet/TelemetryStorageService/20260604194610_32720706721643a5b196bef114e5e58f.trn delete mode 100644 .appdata/.dotnet/TelemetryStorageService/20260604194610_3ffeff8809724de59583dba2b1c7387f.trn delete mode 100644 .appdata/.dotnet/TelemetryStorageService/20260604194610_92039b11719e4e539af091bfa33ef1b8.trn delete mode 100644 .appdata/.dotnet/TelemetryStorageService/20260604194610_934c631cf1f04f17b5dfcbf39af5188d.trn create mode 100644 .appdata/.dotnet/TelemetryStorageService/20260605153645_aa4b8e37a31548f9bac816af1f948b35.trn create mode 100644 .appdata/.dotnet/TelemetryStorageService/20260605153659_b4e053cbbf234e0faa5c6b9bc800a68e.trn create mode 100644 .appdata/.dotnet/TelemetryStorageService/20260605153659_d53d9f6555114bf19b0c4d276113d1dc.trn create mode 100644 .appdata/.dotnet/TelemetryStorageService/20260605153659_f6feaec606184bb389541cce1cca0407.trn create mode 100644 PlotLine/Services/ProjectRestoreService.cs create mode 100644 PlotLine/Sql/036_ProjectRestoreApply.sql diff --git a/.appdata/.dotnet/TelemetryStorageService/20260604194602_4c6e2a6223194ea5bded4dfc59fbf838.trn b/.appdata/.dotnet/TelemetryStorageService/20260604194602_4c6e2a6223194ea5bded4dfc59fbf838.trn deleted file mode 100644 index c2e7f54..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260604194602_4c6e2a6223194ea5bded4dfc59fbf838.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACoxUzW7bOBB+FYFnUaEoihIF7MFwWtRI0xiJu7vojSKHMRFZNCjKaRD43ReU5dopetibMCJnvr/hO+rlDlCDFvv9pwP0YUApCnYqUUI5JhwTtslFw3hDaJYLzhkVP1CK7B28oQZVTKlcCcAFcMBMkxK3hVFYaxA5yTWhJYs95fOAmnckbabhYBVkbvgb/GBdjxr0j+21e42zpc1U50adedfBqh+C7FUE8221vMNfF+vNw/p0aoAhXs6sRg0qZNFWihhcKOCYFUbiGpTBVEhWAi9pySMIaTPbB/C97LJBv1zmaxd6CKqhGS0ygqlAxxRpGWSE3MoBNm/7iGKS6DbW06k8fTbv6AAeNTQ9i3lqd6M6e7Mb2tF2+iZI/wzBeLmDV+df4CA7lKK9d3vwwcKkzcNTcoGUk4xklFNCUIrupdraHpLVbfLQRcJS1YSBLrAAozETucSCFQXWRSkryk1VUYNSdAe+h+6q671V3g3OhGSWPPkwZ+n6YPvRjUOy6gM8exlO9z7LbgCUovXYdnbYPoLUbxv3OMZ//dhFLsvRe+hDspZhm3yRwxY1SMha5VJIISVXykBJwBBNJCeVBq45aEZ1pZlgmjNBFGeyzZmWZd6aWEEpehjDfgxn/X9egdh4u9uBviDYTBqvOxmM87uVhj5YY6M15xNToLpuYpXMPZedhT58EBk1iORAdVEDNjTnmNVaYlkWAnNecAO5EhXNT4IZ+zz+0okYo1lhyrrmrWhzAtRUNSOVaYlWplBCly1UtCJVDbVQhLQly+tWKKIhL5mO7LzTowq/R6GYDHoc+7idySrSfrU9/jkFe1Zk7V1wynXRMNvB09sQYPcn15Zut3eDDZG/ma39PsC96+Pfef9nzebLCxcuNYh7kEyrJ2jBCsY1JqximDFTY1GWGjNatsKYNudFgVJ069QL+CQGTNp+8uScqYenZOHV1gZQYfRx8r8TqZOdn88rc9Ej+/Zps3QeFvt9Olf/OkSRTs3OAfjwrnwf4vGtG8IV5ZnaE3TmDOwqThoOp6fKxiLXugbNc0xZXmImRI5rUVa4repWtLyGUl3imjyCth5UmPpt/Binzcr+KZcLH6yRKgxxeb46NcVpzud85HeQV7b9un0afmXdHKW5U05iZJ9OD+eUoP/1bh6Px+N/AAAA//8DACFE8GcoBgAA \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260604194610_32720706721643a5b196bef114e5e58f.trn b/.appdata/.dotnet/TelemetryStorageService/20260604194610_32720706721643a5b196bef114e5e58f.trn deleted file mode 100644 index c15a51c..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260604194610_32720706721643a5b196bef114e5e58f.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACpSTTW/cOAyG/4rBs+Xo25ZvwaSLHWTbDNpBd7E3WaIbIY48kOW0RTD/vZBnijZ724sgECL5vg+pV4j2GaGH29Pp3QvGvEANOWwhTrkmVBMqj8z0UveMNqoTujP6X6gh3ON36KGVzjFnkAjUSKSnigxidMR7NIwyT7mSpab9skD/CjY0Hl+Cw2ZePmNawhyhh79D9PPX0tuGxk3z6ps0T7iPS7bRFTEf9rt78tft4fhwuLxacCnJTfDQg7BiaB0diXCoiRSjJR26kXBjpUKtuNJFhA1NiBlTtFOz+Kdf/f2cI2bX84aLhhJu4FyDt9kWyYNd8Pj9VFRsiO5KvN7C27V/hRdM0PP6J8xLuRs3hZvnZVjD5G8uZ7bLU3F5SvMJUw64MdlsTpPNYY7VtdNuChgz1PDwqTpMNo9zen4D6tPF//5/2D+W7u++oVsz+t28xgw9MEmhhvfWPYaI1f4OeqAMuRcdkpEzTWTnLbFKGKK10CMyZ1rOoIbdHHOI67wu1T5m/JI2A9DDH3Za8CL9NrnHkNHlNRVX/2w6Dmn2q8vVL/6MNrQRtCh5WPNpzdVH9CGhy1j8HdN6rfefFK75lrRbU8KYq4PNj9WfdnmEHoztHLPGGmu1cyMqiiP11GraetReo5fct14a6bU01GlpBya9VWwYSwRqwDLuatswL5VrR8mJ4iMSKbwgHSpBqOCqVa1sFdVvOFYPU8mzrqMSvSAGR0+kYZYYKQTxQtmW67Ft+fgbkuv0GS2APb5c/sqmQHvfodeMcMkUkcYw0hnVkqHtBjPoDpUrmu8xRZx+A/U+uDQv85ir6/JUb9B9XGP579W2SF9DJN+2Gd3N7glTVWZsQyzrfR3r+Xw+/wAAAP//AwD+oJloNgQAAA== \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260604194610_3ffeff8809724de59583dba2b1c7387f.trn b/.appdata/.dotnet/TelemetryStorageService/20260604194610_3ffeff8809724de59583dba2b1c7387f.trn deleted file mode 100644 index 1d126db..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260604194610_3ffeff8809724de59583dba2b1c7387f.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS5Som7FO0IWbepEs0iC3ETmKCcvUgqScGIb/e0HtFo5PRS8EMeLMvHzmpV5IgEckA7k+nT48YciJ1CT7LSSY0JRpytSRm0HpgbOm7TnTSnwnNfG3+EwG0ilruTVIJWqkyrGWjnKy1Dk0nHHHRKtKTfiRyPBCwDcOn7zFZklfMSa/BDKQv31wy8/SG3xj52V1TVxm3IeUIdgi5q/97pb+eX043h3OpxKmktx4RwYiQY6dZROVFjVVcgLao52oMKBa1K1odREBvvEhYwwwN8k9vPV3Sw6Y7SAaIRtGhSGvNXGQoUgeIeHx+VRUbIhuSrzewtt2eCFPGMkg6n9hnstd2dlfPaZx9bO7Oq8Z0kPa1glsXuIzqckpLieM2ePG5zolfBzn5yOkh4/nM2WbPvxCu2Z0u2UNmQyEy47UZAM0z5D9EqqLxt3sMeTtY44+JG//q5YkNbn7Uh1myNMSH9/N48sZ8/5/UP4E9t4HrPY3ZCCMo3CyRzoJrqnqHVBopaFaSz0ht6YTnNRkt4Tsw7qsqdqHjD/idiUykI8wJzzru4723me0eY3lnt+2Zoe4uNXm6m2WnDWskYyVpDWf1lx9Rucj2ozlEse4Xup9fZ8itNiSdmuMGHJ1gHxf/QHpngzEQG85GDAA2toJW4YTcww06xxqp9Ep4TqnjHJaGWa1gpErBy0fpxIhNcFinWpz69RxJjTr6WR5R5XTnILkhgqpuetlxwwUg71xrO7mkge2ZwqdpAYnR5XhQI2SkjrZQif01HVi+g3JxQ+cFcAOn87vblOgneux9BWKt1QZw2lv2o6OXT+aUffY2qL5FmPA+TdQn7yNS1qmXF0cUr1D93kN5d9RbW756QP9tc3oZrEPGKsyY/ChPJXLWF9fX1//AQAA//8DAO7pQJCCBAAA \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260604194610_92039b11719e4e539af091bfa33ef1b8.trn b/.appdata/.dotnet/TelemetryStorageService/20260604194610_92039b11719e4e539af091bfa33ef1b8.trn deleted file mode 100644 index 9b669c4..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260604194610_92039b11719e4e539af091bfa33ef1b8.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACoxUXW/lJhD9KxavNV7AGIPf0iSrXm2zifZetVXfxjAkNA6+wji7qyj/vcI32U9V6otlDTDnzJkz80QiPCAZyNnxePmIMS+kJjlsIcGEokxRJg/cDFINnDWdVsIw8TepSXiHn8lAemkttwZpiwqpdKyjY+stdQ4NZ9wx0cmSE24XMjwRCI3Dx2CxmZc/MC1hjmQgf4bo5o8FG0Jjp3l1TZon3MUlQ7SFzPvd+Tv6+9nN4frmdGvBpTxugiMDaaEde8s8bS0qKlsPVKP1VBiQHapOdKqQgNCEmDFFmJrF3X/Fd3OOmO0gGtE2jApDnmviIEOhPMKCh8/HwmKT6KLE6y28/Q5P5BETGUT9KuYp3Rs7hTcPy7iGyb3ZvqQmxzQfMeWAmxrX++orCc4a1gglGCM1uQJ7FyJWu4vqeiolgtVMomupQe+oNByokW1LXdtBL5Tve+FJTd5hijh9k/Uq2DQvs8/Vi8jVdzj7s/PLCOOEBeMtTAuSmuyWfYboYJojXn5Cu+ZTrkNay/H5HHOI67wu1S5mvE3wcv76/nxNCWOubiDfVb/BckcGYkBbDgYMgLLWY8fQM8dAsd6hcgqdFK530kinpGFWSRi5dNDx0ZcIqcmvRcP9ai0uy1c2m0mmaeNQvfTpfAoY83cykoEwjsK1GqkXXFGpHVDoWkOVapVHbk0v+CvMAdItZjIQr8dxZNaOQhpuQRhonWetU94I0fcgfdv1UijVsR6s63vBpWt7tND6lmlBanKTZrfa/GOr260BN2n+B20uUpGBKNZ12sI4CiW5BqNRciW1Mtgb6YHJ1o7AOw3jyBl0zBotLHBlQI6u819Uuoy3IeJFWI4TfP4GVzeqaX+xAJp7kD9cf5vgAT/O6f79ycbN+8sDqcmHNZaNUO2KRT6GSD9tw3S1Tjkc7hKCQ3c1O/zZR1jGpdomVCjF9QgjZWpkVPrOU4Oqox57zk2vi6FJTS5me4+pKg6DEMtUfUl2va/Okr0LGW1eU+H318bjel/dTJD9nB6+2yTfFPajAI1oOSudcfh42kYbR+WcRqc4FZJ3VBrDqTZdT8dej2ZUGjtbFLte83HN1Qd0IaHNW8UvXtwwz+/Q3v8sxqsLXizKWfHa/rTFNmn/1xJ7rskDwrImfNi29fBEdjFi2pAv1tMo7uJVmKawoJ2jW8igdcubUs8Lw/++Z1SjtODPz8/P/wIAAP//AwA2Sn9NGwYAAA== \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260604194610_934c631cf1f04f17b5dfcbf39af5188d.trn b/.appdata/.dotnet/TelemetryStorageService/20260604194610_934c631cf1f04f17b5dfcbf39af5188d.trn deleted file mode 100644 index 0ccc912..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260604194610_934c631cf1f04f17b5dfcbf39af5188d.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIgtxaZCtDRCYHFOUkMObfF9RM4GRveyOK7O7qquILCfCEZCC3p9NfzxjyQmqS/QYJJjRlmjL1yM2g9MBZ0/a9Ml37hdTE3+NPMpBOWcutQSpRI1WOtXSUk6XOoeGMOyZaVXrC14UMLwR84/DZW2zi8hHT4mMgA/nkg4vfy2zwjZ3j6poUZ9yHJUOwhcw/+909fXt7eHw4XF4tuJTixjsyEAly7CybqLSoqZIT0B7tRIUB1aJuRasLCfCNDxlTgLlZ3LfX+S7mgNkOohGyYVQYcq6JgwyF8ggLPv48FRabRHcFrzd4Ow4v5BkTGUT9S8xLuxs7+xsbn54guJvJB78cSU1OKZ4wZY+bHNuG8wzZx1Bdh+xmjyGTmjx8qA4z5Cmmpz80+nBZff8/Nn8H9ugDVvs7MhDGUTjZI50E11T1Dii00lCtpZ6QW9MJTmqyiyH7sMZ1qfYh49e0sSQD+RvmBS/8bpM9+ow2r6lQ/7wNwx8+76IrACM1OaToVpurV7k5a1gjWbl8WPNpzdV7dD6hzVh2ekzrtf1/SoQWW9FuTQlDrg6Qj9UbWI5kIAZ6y8GAAdDWTtgynJhjoFnnUDuNTgnXOWWU08owqxWMXDlo+TgVpPAu7lZboJzknXYMqBXAqBIcqXFC0rGfhATTj1KWDLzKWj3MpQ5szxQ6SQ1OjirDgRolJXWyhU7oqevE9JskV8c5K3o7fL58jY2Bdq5HpzkVirdUGcNpb9qOjl0/mlH32NrC+R5TwPk3od55m+ISp1xdA1P9Id37NZTvXW3h+e4D/bFZdhftN0xVsRx8KGm+unw+n8//AgAA//8DAMd6qbMlBAAA \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260605153645_aa4b8e37a31548f9bac816af1f948b35.trn b/.appdata/.dotnet/TelemetryStorageService/20260605153645_aa4b8e37a31548f9bac816af1f948b35.trn new file mode 100644 index 0000000..cafc510 --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260605153645_aa4b8e37a31548f9bac816af1f948b35.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACoxUS2/bOBD+KwLPokJSFPUA9mA4LdZI0xiJu7vYG0UOYyKyaFCU06DIf19Qkmun6GFvwmhe32P4A/XyAKhBq+Px0wn6MKAUBTuFGGECE4FJsaNFk4uGFxnjlFQV+xelyN7BG2pQyZWiqgacgwDMNSlwmxuFtYaaEqoJK3jsKZ8H1PxA0mYaTlZB5oa/wA/W9ahBf9teu9c4W9pMdW7UmXcdbPohyF7FZb5u1nf4y2q7e9jOWQMMsTizGjWIaEoo1QxrSjjmOaW4FVBgnatCqoqJnJRzme0D+F522aBfLvO1Cz0E1bCM5RnBrEbvKdIyyLhyKwfYvR3jFhNFtzGeTuHps/mBTuBRw9IzmXO7G9XZm8PQjrbTN0H6ZwjGywO8Ov8CJ9mhFB29O4IPFiZuHp6Sy0qUZCRjghGCUnQv1d72kGxuk4cuApaqIhx0jmswGvOaSlzzPMc6L2TJhClLZlCK7sD30F11vbfKu8GZkCyUJx/mrF0fbD+6cUg2fYBnL8Nc91l2A6AUbce2s8P+EaR+27nHMf7rxy5iWY/eQx+SrQz75E857FGDalkpKmtZSymUMlAQMEQTKUipQWgBmjNdal5zLXhNlOCypVzLgrYmRlCKHsZwHMOZ/+9XS+y8PRxAXzbYTRxvOxmM84eNhj5YY6M054zJUF03oUqWnuvOQh8+kBwdRYHpvAJsGBWYV1piWeQ1FiIXBqiqS0Znwox9Hn/yRIzRPDdFVYm2bikBZsqKk9K0RCuTq1oXLZSsJGUFVa0IaQtOq7ZWRAMtuI7ovNOjCr9aIZ8Eehz7eJ3JJsJ+tT3+LviFka13wSnXRcFsB09vQ4DD71Rbu8PRDTZE/GaR9tsA966Pf5f7XzhbilcuXGIQ7yCZTk+DKouKaMxNLjBvKcdSCIrrqmV5wQmRJKK6deoFfBINJm0/aXL21MNTsvJqbwOoMPo4+Z8J1Czn5/PJXPjIvn7arZ2H1fGYLtE/TpGkudnZAB/elW9DTN+7IaIIfrzy0RN05rzXlZs0nOaXysag0LoCLShmnBaY1zXFVV2UuC2rtm5FBYW6uDV5BG09qDD1283TFmJ/Z8uVD9ZIFYZ4O1+cmty02HNJ+XXJK9V+Vs/Dr5RbnLR0oiQ69ml+NycD/a9n8/39/f0/AAAA//8DADRAO7QnBgAA \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260605153659_b4e053cbbf234e0faa5c6b9bc800a68e.trn b/.appdata/.dotnet/TelemetryStorageService/20260605153659_b4e053cbbf234e0faa5c6b9bc800a68e.trn new file mode 100644 index 0000000..5f5faaf --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260605153659_b4e053cbbf234e0faa5c6b9bc800a68e.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTTW/bOBD9KwLPpsIvUaJugbOLGmk3Rhu0i72NOaOaqEIZFJW2CPzfF5RdtOmtN+KRb+bNe8MXFuGJWM9uT6e/ninmmW1YDiukhLJcWC6aR9n02vaNq7Vx1mjzH9uwcE/fWc9a4730jrgmS9ygaPhBD54jkpNColCNKTXh88z6FwahRnoOnupp/khpDlNkPfsUIk5fS28ItR+nBes0jbSLc4boi5h/dtt7/vZ2//iwv7yaaS7kOiDrmUAppETFUQrDjZaSHyw1HLVvwHfKatFeaCFmShHGesYvP/vjlCNl36ta6Vpw5dh5wxAyFMkHmOnx+6moWC26K/hmhddj/8KeKbFebX6YeSl348dw46enJ4h4M4QY5iPbsFOaTpRyoNWOdcJxhBymWF2bbMdAMbMNe/hQ7UfIw5SeXnn04TL67g8mfwf+GCJVu7vCkaRQd8QHJS03HQKHRjturbYDSe9aJdmGbaeYQ1ymZa52MdPntKpkPfsbxpku+m6TP4ZMPi+pSP/XlqzpW8jbCQsg2Ibt04SLz9VPu6WoRa1FuXxY8mnJ1XvCkMhnKjM9puVa/jeKsmolbZeUKOZqD/lYvYH5yHrmoPMSHDgA6/1AjaBBoAArWiSLltAobNE4g9Y44a2BgzQIjTwMBSm6S7rVulAtEBljJTe+G7jRGrjrPHLTkIHOOeiwe2Vr9TAWHvhOGELNHQ3IjZPAndGao26gVXZoWzX8Ysk1cSmK30jPl6+xKrCIHaGVXBnZcOOc5J1rWn5ou4M72I4aXzTfU4o0/mLUu+DTNE9Drq4LU72y7v0Sy/eu1uX5GiL/tkZ2N/kvlKoSOYRYtvma8vl8Pv8PAAD//wMAz/peqCUEAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260605153659_d53d9f6555114bf19b0c4d276113d1dc.trn b/.appdata/.dotnet/TelemetryStorageService/20260605153659_d53d9f6555114bf19b0c4d276113d1dc.trn new file mode 100644 index 0000000..8c99ccc --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260605153659_d53d9f6555114bf19b0c4d276113d1dc.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRUy27cOBD8FYFnUeZLlKibMU6wA2/Wg2SQDXKj2K2YsEwNSMqJYfjfF9TMwvFpsReCaLGK1dVFvZBgH5EM5Pp0+vCEISdSk+y3kmBCU6Ypa4+8HaQeWtNIJZQS6jupib/FZzKQTjnHnUEqUSNVwFo6yslRADSccWCiVYXT/khkeCHWN4BP3mGzpK8Yk18CGcjfPsDys9xtfePmZYUmLjPuQ8o2uCLmr/3ulv55fTjeHc6nEqYCbjyQgTDgjHMQFDhTVEnO6aixpSBda10vtGTdGeZDxhjs3CR4eLsflhwwu0E0QjaMCkNeawI22yJ5tAmPz6eiYrPoptTrrbxthxfyhJEMov7XzDPdlZv91WMaVz/D1XnNNj2kbZ2sy0t8JjU5xeWEMXvc/LlOCR/H+flo08PH85myTR9+oVszwm5ZQyYD4dKQmmwGzbPNfgnVReNu9hjy9jFHH5J3/8UlSU3uvlSH2eZpiY/v5vHlbPP+f7j8ybp7H7Da3xQMRwGyRzoJrqnqwVLbSkO1lnpC7kwnOKnJbgnZh3VZU7UPGX/ErSUykI92TnjWdx3dvc/o8hpLn990ydUhLrC6XL3NkrOGNZKxAlrzac3VZwQf0WUsTRzjeuH7+h4itNhAuzVGDLk62Hxf/WHTPRmIsb3j1lhjrXZuwpbhxIBZzTpADRpBCehAGQVaGea0siNXYFs+TqVCaoIlOtWWVt6P0qLsKBN9S1VnOO07N1HdOt0rw1ppiydvPlZ3c8FZ1zOFIKnBCagy3FKjpKQgW9sJPXWdmH6z5JIHzgoZ4NP53W0KNECPoDkVirdUmaLAtB0du340o+6xdUXzLcaA829GffIuLmmZcnVJSPXOus9rKP+OakvLTx/or21GN4t7wFiVGVsfylO5jPX19fX1HwAAAP//AwD4jl43ggQAAA== \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260605153659_f6feaec606184bb389541cce1cca0407.trn b/.appdata/.dotnet/TelemetryStorageService/20260605153659_f6feaec606184bb389541cce1cca0407.trn new file mode 100644 index 0000000..020b37a --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260605153659_f6feaec606184bb389541cce1cca0407.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTS2/cNhD+KwLPosy3RN2MdYou3MSLZJEWvVGcUUxYphYU5SQw9r8H1G6QuLdeCGLAb+Z7DF9JdM9IenJ7Or17wZgXUpMctpJgwlBmKNNHrntpem0bqbRpefsvqUm4x++kJ63ynnuLVKJBqoBpOsjRUwC0nHFgQqvS031ZSP9KXGgAX4LHZl4+Y1rCHElP/g4R5q9ltguNn+YVmjRPuI9LdtEXMh/2u3v61+3h+HC4vFpwKeAmAOkJA844B0GBM0WV5JwOBjUF6bXznTCStRdYiBlTdFOzwNOv+TDniNn3ohGyYVRYcq4JuOwK5cEtePx+Kiw2i+5Kvd7K27V/JS+YSC/qn2Ze2t34Kdw8L8MaJri5nNktT0XlKc0nTDng5skmc5pcDnOsrpN2U8CYSU0ePlWHyeVxTs9vjPp00b//H/KPZfq7b+jXjLCb15hJT7gSpCbvnX8MEav9XWnHUYDskI6CG6o6cNRpaakx0ozIvW0FJzXZzTGHuM7rUu1jxi9pE0B68oebFrxQv03+MWT0eU1F1T+m7MIhzbD6XP3yn7OGNZKxAlrzac3VR4SQ0Gcs+o5pvfb7D0QYsYF2a0oYc3Vw+bH60y2PpCfWdZ4766xzxvsRNcORAXOGtYAGDIIS0IKyCoyyzBvlBq7AaT6MpUJqgiXuatswZ0SHWkuqBzZSpe1IreSOdpYJdNYh9/6Nj9XDtOF8xxSCpBZHoMpyR62SkoLUrhVmbFsx/mbJNX3OisGAL5e/sjEwAB2C4VQorqmyltPO6pYObTfYwXSofeF8jyni9JtR74NP8zKPubouT/XGuo9rLP+92hbpa4j025bR3eyfMFUlYxdiWe9rrOfz+fwDAAD//wMA+zzinzYEAAA= \ No newline at end of file diff --git a/PlotLine/Controllers/ProjectRestorePointsController.cs b/PlotLine/Controllers/ProjectRestorePointsController.cs index 9352600..8f3ac44 100644 --- a/PlotLine/Controllers/ProjectRestorePointsController.cs +++ b/PlotLine/Controllers/ProjectRestorePointsController.cs @@ -3,7 +3,10 @@ using PlotLine.Services; namespace PlotLine.Controllers; -public sealed class ProjectRestorePointsController(IProjectRestorePointService restorePoints, ILogger logger) : Controller +public sealed class ProjectRestorePointsController( + IProjectRestorePointService restorePoints, + IProjectRestoreService projectRestore, + ILogger logger) : Controller { public async Task Index(int projectId) { @@ -51,4 +54,22 @@ public sealed class ProjectRestorePointsController(IProjectRestorePointService r return RedirectToAction(nameof(Index), new { projectId }); } } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Restore(int projectId, int id) + { + try + { + var result = await projectRestore.RestoreAsync(projectId, id); + TempData["ProjectRestorePointMessage"] = result.Message; + } + catch (Exception ex) + { + logger.LogError(ex, "Project restore failed unexpectedly for restore point {ProjectRestorePointID} in project {ProjectID}.", id, projectId); + TempData["ProjectRestorePointMessage"] = "Restore failed. No project data was changed."; + } + + return RedirectToAction(nameof(Index), new { projectId }); + } } diff --git a/PlotLine/Data/ProjectRestorePointRepository.cs b/PlotLine/Data/ProjectRestorePointRepository.cs index 4e13f7a..1eddfbe 100644 --- a/PlotLine/Data/ProjectRestorePointRepository.cs +++ b/PlotLine/Data/ProjectRestorePointRepository.cs @@ -9,6 +9,7 @@ public interface IProjectRestorePointRepository Task> ListAsync(int projectId); Task GetAsync(int projectRestorePointId, int projectId); Task CreateAsync(int projectId, string restorePointName, string restorePointType, string jsonData, string? notes); + Task RestoreAsync(int projectId, int sourceRestorePointId, int safetyRestorePointId, string jsonData); } public sealed class ProjectRestorePointRepository(ISqlConnectionFactory connectionFactory) : IProjectRestorePointRepository @@ -47,4 +48,20 @@ public sealed class ProjectRestorePointRepository(ISqlConnectionFactory connecti }, commandType: CommandType.StoredProcedure); } + + public async Task RestoreAsync(int projectId, int sourceRestorePointId, int safetyRestorePointId, string jsonData) + { + using var connection = connectionFactory.CreateConnection(); + await connection.ExecuteAsync( + "dbo.ProjectRestore_ApplyFromJson", + new + { + ProjectID = projectId, + SourceRestorePointID = sourceRestorePointId, + SafetyRestorePointID = safetyRestorePointId, + JsonData = jsonData + }, + commandType: CommandType.StoredProcedure, + commandTimeout: 180); + } } diff --git a/PlotLine/Models/ProjectRestorePointModels.cs b/PlotLine/Models/ProjectRestorePointModels.cs index 8019f84..47ef46f 100644 --- a/PlotLine/Models/ProjectRestorePointModels.cs +++ b/PlotLine/Models/ProjectRestorePointModels.cs @@ -19,3 +19,10 @@ public sealed class ProjectRestorePointDownload public string ContentType { get; init; } = "application/json"; public string Json { get; init; } = string.Empty; } + +public sealed class ProjectRestoreResult +{ + public bool Succeeded { get; init; } + public string Message { get; init; } = string.Empty; + public ProjectRestorePoint? SafetyBackup { get; init; } +} diff --git a/PlotLine/Program.cs b/PlotLine/Program.cs index d86e712..69a665e 100644 --- a/PlotLine/Program.cs +++ b/PlotLine/Program.cs @@ -62,6 +62,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/PlotLine/Services/ProjectRestorePointService.cs b/PlotLine/Services/ProjectRestorePointService.cs index abb0aca..0c68a4d 100644 --- a/PlotLine/Services/ProjectRestorePointService.cs +++ b/PlotLine/Services/ProjectRestorePointService.cs @@ -8,6 +8,7 @@ public interface IProjectRestorePointService { Task GetAsync(int projectId); Task CreateManualAsync(int projectId, string? restorePointName, string? notes); + Task CreatePreRestoreAsync(int projectId, string sourceRestorePointName); Task DownloadAsync(int projectRestorePointId, int projectId); } @@ -18,6 +19,7 @@ public sealed class ProjectRestorePointService( ILogger logger) : IProjectRestorePointService { private const string ManualRestorePointType = "Manual"; + private const string PreRestorePointType = "PreRestore"; public async Task GetAsync(int projectId) { @@ -36,19 +38,32 @@ public sealed class ProjectRestorePointService( public async Task CreateManualAsync(int projectId, string? restorePointName, string? notes) { - var export = await projectExports.ExportAsync(projectId); - if (export is null) - { - logger.LogWarning("Manual restore point requested for missing project {ProjectID}.", projectId); - return null; - } - var name = string.IsNullOrWhiteSpace(restorePointName) ? $"Manual restore point - {DateTime.Now:yyyy-MM-dd HH:mm}" : restorePointName.Trim(); - var created = await restorePoints.CreateAsync(projectId, name, ManualRestorePointType, export.Json, Clean(notes)); - logger.LogInformation("Manual restore point {ProjectRestorePointID} created for project {ProjectID} with {JsonSizeBytes} JSON bytes.", + return await CreateAsync(projectId, name, ManualRestorePointType, Clean(notes)); + } + + public async Task CreatePreRestoreAsync(int projectId, string sourceRestorePointName) + { + var name = $"Automatic Pre-Restore Backup - {DateTime.Now:yyyy-MM-dd HH:mm}"; + var notes = $"Created automatically before restoring from \"{sourceRestorePointName}\"."; + return await CreateAsync(projectId, name, PreRestorePointType, notes); + } + + private async Task CreateAsync(int projectId, string restorePointName, string restorePointType, string? notes) + { + var export = await projectExports.ExportAsync(projectId); + if (export is null) + { + logger.LogWarning("{RestorePointType} restore point requested for missing project {ProjectID}.", restorePointType, projectId); + return null; + } + + var created = await restorePoints.CreateAsync(projectId, restorePointName, restorePointType, export.Json, notes); + logger.LogInformation("{RestorePointType} restore point {ProjectRestorePointID} created for project {ProjectID} with {JsonSizeBytes} JSON bytes.", + restorePointType, created.ProjectRestorePointID, projectId, created.JsonSizeBytes); diff --git a/PlotLine/Services/ProjectRestoreService.cs b/PlotLine/Services/ProjectRestoreService.cs new file mode 100644 index 0000000..16a569a --- /dev/null +++ b/PlotLine/Services/ProjectRestoreService.cs @@ -0,0 +1,123 @@ +using System.Text.Json; +using PlotLine.Data; +using PlotLine.Models; + +namespace PlotLine.Services; + +public interface IProjectRestoreService +{ + Task RestoreAsync(int projectId, int projectRestorePointId); +} + +public sealed class ProjectRestoreService( + IProjectRestorePointRepository restorePointRepository, + IProjectRestorePointService restorePointService, + ILogger logger) : IProjectRestoreService +{ + private static readonly JsonSerializerOptions ReadOptions = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + private static readonly JsonSerializerOptions RestoreSqlOptions = new() + { + WriteIndented = false + }; + + public async Task RestoreAsync(int projectId, int projectRestorePointId) + { + var restorePoint = await restorePointRepository.GetAsync(projectRestorePointId, projectId); + if (restorePoint?.JsonData is null) + { + return Failure("Restore point could not be found."); + } + + ProjectBackupExport? backup; + try + { + backup = JsonSerializer.Deserialize(restorePoint.JsonData, ReadOptions); + } + catch (JsonException ex) + { + logger.LogWarning(ex, "Restore point {ProjectRestorePointID} contains invalid JSON.", projectRestorePointId); + return Failure("Restore point JSON is invalid. No project data was changed."); + } + + var validationMessage = ValidateBackup(projectId, backup); + if (validationMessage is not null) + { + return Failure(validationMessage); + } + + var safetyBackup = await restorePointService.CreatePreRestoreAsync(projectId, restorePoint.RestorePointName); + if (safetyBackup is null) + { + logger.LogError("PreRestore backup creation failed for project {ProjectID}. Restore was aborted.", projectId); + return Failure("A safety backup could not be created. Restore was aborted and no project data was changed."); + } + + try + { + var restoreJson = JsonSerializer.Serialize(backup, RestoreSqlOptions); + await restorePointRepository.RestoreAsync(projectId, projectRestorePointId, safetyBackup.ProjectRestorePointID, restoreJson); + logger.LogInformation("Project {ProjectID} restored from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was created first.", + projectId, + projectRestorePointId, + safetyBackup.ProjectRestorePointID); + + return new ProjectRestoreResult + { + Succeeded = true, + Message = "Project restored. A safety backup of the previous state was created first.", + SafetyBackup = safetyBackup + }; + } + catch (Exception ex) + { + logger.LogError(ex, "Project restore failed for project {ProjectID} from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was retained.", + projectId, + projectRestorePointId, + safetyBackup.ProjectRestorePointID); + + return new ProjectRestoreResult + { + Succeeded = false, + Message = "Restore failed. The safety backup was created, and the project data was left unchanged.", + SafetyBackup = safetyBackup + }; + } + } + + private static string? ValidateBackup(int projectId, ProjectBackupExport? backup) + { + if (backup is null) + { + return "Restore point JSON is invalid. No project data was changed."; + } + + if (!string.Equals(backup.Format, "PlotWeaver.ProjectBackup", StringComparison.Ordinal)) + { + return "Restore point format is not recognised. No project data was changed."; + } + + if (!string.Equals(backup.FormatVersion, "1.0", StringComparison.Ordinal)) + { + return "Restore point format version is not supported. No project data was changed."; + } + + if (backup.Project?.ProjectID != projectId) + { + return "This restore point does not belong to the selected project. No project data was changed."; + } + + return null; + } + + private static ProjectRestoreResult Failure(string message) => new() + { + Succeeded = false, + Message = message + }; +} diff --git a/PlotLine/Sql/036_ProjectRestoreApply.sql b/PlotLine/Sql/036_ProjectRestoreApply.sql new file mode 100644 index 0000000..ff7870c --- /dev/null +++ b/PlotLine/Sql/036_ProjectRestoreApply.sql @@ -0,0 +1,306 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +IF OBJECT_ID(N'dbo.ProjectRestoreHistory', N'U') IS NULL +BEGIN + CREATE TABLE dbo.ProjectRestoreHistory + ( + ProjectRestoreHistoryID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ProjectRestoreHistory PRIMARY KEY, + ProjectID int NOT NULL, + SourceRestorePointID int NOT NULL, + SafetyRestorePointID int NOT NULL, + RestoredDateUtc datetime2 NOT NULL CONSTRAINT DF_ProjectRestoreHistory_RestoredDateUtc DEFAULT SYSUTCDATETIME(), + PerformedBy nvarchar(100) NULL, + Notes nvarchar(max) NULL, + CONSTRAINT FK_ProjectRestoreHistory_Projects FOREIGN KEY(ProjectID) REFERENCES dbo.Projects(ProjectID), + CONSTRAINT FK_ProjectRestoreHistory_SourceRestorePoint FOREIGN KEY(SourceRestorePointID) REFERENCES dbo.ProjectRestorePoints(ProjectRestorePointID), + CONSTRAINT FK_ProjectRestoreHistory_SafetyRestorePoint FOREIGN KEY(SafetyRestorePointID) REFERENCES dbo.ProjectRestorePoints(ProjectRestorePointID) + ); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ProjectRestoreHistory_ProjectDate' AND object_id = OBJECT_ID(N'dbo.ProjectRestoreHistory')) + CREATE INDEX IX_ProjectRestoreHistory_ProjectDate ON dbo.ProjectRestoreHistory(ProjectID, RestoredDateUtc DESC); +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectRestore_InsertJsonTable + @TableName sysname, + @JsonPath nvarchar(400), + @JsonData nvarchar(max), + @ProjectID int, + @ProjectScopedOnly bit = 0 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @FullTableName nvarchar(300) = N'dbo.' + QUOTENAME(@TableName); + DECLARE @ObjectID int = OBJECT_ID(@FullTableName, N'U'); + IF @ObjectID IS NULL + THROW 51000, 'Restore table not found.', 1; + + IF NOT EXISTS (SELECT 1 FROM OPENJSON(@JsonData, @JsonPath)) + RETURN; + + DECLARE @ColumnList nvarchar(max); + DECLARE @WithList nvarchar(max); + DECLARE @SelectList nvarchar(max); + DECLARE @ProjectFilter nvarchar(max) = N''; + DECLARE @HasIdentity bit = 0; + + DECLARE @Columns table + ( + Name sysname NOT NULL, + ColumnID int NOT NULL, + IsIdentity bit NOT NULL, + TypeName nvarchar(100) NOT NULL + ); + + INSERT INTO @Columns (Name, ColumnID, IsIdentity, TypeName) + SELECT + c.name, + c.column_id, + c.is_identity, + CASE + WHEN t.name IN (N'nvarchar', N'nchar') AND c.max_length = -1 THEN t.name + N'(max)' + WHEN t.name IN (N'nvarchar', N'nchar') THEN t.name + N'(' + CONVERT(nvarchar(20), c.max_length / 2) + N')' + WHEN t.name IN (N'varchar', N'char', N'varbinary', N'binary') AND c.max_length = -1 THEN t.name + N'(max)' + WHEN t.name IN (N'varchar', N'char', N'varbinary', N'binary') THEN t.name + N'(' + CONVERT(nvarchar(20), c.max_length) + N')' + WHEN t.name IN (N'decimal', N'numeric') THEN t.name + N'(' + CONVERT(nvarchar(20), c.precision) + N',' + CONVERT(nvarchar(20), c.scale) + N')' + WHEN t.name IN (N'datetime2', N'time', N'datetimeoffset') THEN t.name + N'(' + CONVERT(nvarchar(20), c.scale) + N')' + ELSE t.name + END + FROM sys.columns c + INNER JOIN sys.types t ON t.user_type_id = c.user_type_id + WHERE c.object_id = @ObjectID + AND c.is_computed = 0 + AND t.name <> N'timestamp' + AND EXISTS + ( + SELECT 1 + FROM OPENJSON(@JsonData, @JsonPath) rowJson + CROSS APPLY OPENJSON(rowJson.value) propertyJson + WHERE propertyJson.[key] COLLATE DATABASE_DEFAULT = c.name COLLATE DATABASE_DEFAULT + ); + + SELECT @ColumnList = STUFF((SELECT N', ' + QUOTENAME(Name) FROM @Columns ORDER BY ColumnID FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 2, N''); + SELECT @WithList = STUFF((SELECT N', ' + QUOTENAME(Name) + N' ' + TypeName + N' ''$.' + Name + N'''' FROM @Columns ORDER BY ColumnID FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 2, N''); + SELECT @SelectList = STUFF((SELECT N', src.' + QUOTENAME(Name) FROM @Columns ORDER BY ColumnID FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 2, N''); + SELECT @HasIdentity = CONVERT(bit, MAX(CONVERT(int, IsIdentity))) FROM @Columns; + + IF @ProjectScopedOnly = 1 + SET @ProjectFilter = N' WHERE src.ProjectID = @ProjectID'; + + DECLARE @Sql nvarchar(max) = N''; + + IF @HasIdentity = 1 + SET @Sql += N'SET IDENTITY_INSERT dbo.' + QUOTENAME(@TableName) + N' ON;'; + + SET @Sql += N' + INSERT INTO dbo.' + QUOTENAME(@TableName) + N' (' + @ColumnList + N') + SELECT ' + @SelectList + N' + FROM OPENJSON(@JsonData, @JsonPath) + WITH (' + @WithList + N') AS src' + @ProjectFilter + N';'; + + IF @HasIdentity = 1 + SET @Sql += N'SET IDENTITY_INSERT dbo.' + QUOTENAME(@TableName) + N' OFF;'; + + EXEC sp_executesql + @Sql, + N'@JsonData nvarchar(max), @JsonPath nvarchar(400), @ProjectID int', + @JsonData = @JsonData, + @JsonPath = @JsonPath, + @ProjectID = @ProjectID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectRestore_ApplyFromJson + @ProjectID int, + @SourceRestorePointID int, + @SafetyRestorePointID int, + @JsonData nvarchar(max) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF ISJSON(@JsonData) <> 1 + THROW 51001, 'Restore JSON is invalid.', 1; + + IF JSON_VALUE(@JsonData, '$.Format') <> N'PlotWeaver.ProjectBackup' + THROW 51002, 'Restore JSON format is not recognised.', 1; + + IF JSON_VALUE(@JsonData, '$.FormatVersion') <> N'1.0' + THROW 51003, 'Restore JSON format version is not supported.', 1; + + IF TRY_CONVERT(int, JSON_VALUE(@JsonData, '$.Project.ProjectID')) <> @ProjectID + THROW 51004, 'Restore point does not belong to this project.', 1; + + DECLARE @RestoreTables table (TableName sysname NOT NULL PRIMARY KEY); + INSERT INTO @RestoreTables (TableName) + VALUES + (N'ProjectTimelineSettings'), (N'ProjectTimelineMetricSettings'), (N'TimelineViewPresets'), + (N'SceneMetricTypes'), (N'SceneMetricValues'), (N'Books'), (N'Chapters'), (N'Scenes'), + (N'ScenePurposes'), (N'SceneOutcomes'), (N'PlotLines'), (N'PlotThreads'), (N'ThreadEvents'), + (N'AssetStates'), (N'StoryAssets'), (N'AssetEvents'), (N'AssetDependencies'), + (N'AssetCustodyEvents'), (N'AssetCustodyEventCharacters'), (N'Characters'), (N'SceneCharacters'), + (N'CharacterAttributeTypes'), (N'CharacterAttributeEvents'), (N'CharacterKnowledge'), + (N'CharacterRelationships'), (N'RelationshipEvents'), (N'Locations'), (N'LocationRelationships'), + (N'SceneAssetLocations'), (N'SceneDependencies'), (N'ValidationRuns'), (N'ContinuityWarnings'), + (N'SceneNotes'), (N'SceneWorkflow'), (N'SceneChecklistItems'), (N'SceneAttachments'), + (N'ChapterGoals'), (N'Scenarios'), (N'ScenarioSceneOrder'), (N'ScenarioValidationRuns'), + (N'ScenarioWarnings'); + + BEGIN TRANSACTION; + + DECLARE @TableName sysname; + DECLARE DisableCursor CURSOR LOCAL FAST_FORWARD FOR SELECT TableName FROM @RestoreTables; + OPEN DisableCursor; + FETCH NEXT FROM DisableCursor INTO @TableName; + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @DisableSql nvarchar(max) = N'ALTER TABLE dbo.' + QUOTENAME(@TableName) + N' NOCHECK CONSTRAINT ALL;'; + EXEC (@DisableSql); + FETCH NEXT FROM DisableCursor INTO @TableName; + END + CLOSE DisableCursor; + DEALLOCATE DisableCursor; + + DELETE sw FROM dbo.ScenarioWarnings sw INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sw.ScenarioID WHERE sc.ProjectID = @ProjectID; + DELETE svr FROM dbo.ScenarioValidationRuns svr INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = svr.ScenarioID WHERE sc.ProjectID = @ProjectID; + DELETE sso FROM dbo.ScenarioSceneOrder sso INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sso.ScenarioID WHERE sc.ProjectID = @ProjectID; + DELETE FROM dbo.Scenarios WHERE ProjectID = @ProjectID; + + DELETE FROM dbo.ContinuityWarnings WHERE ProjectID = @ProjectID; + DELETE FROM dbo.ValidationRuns WHERE ProjectID = @ProjectID; + + DELETE sa FROM dbo.SceneAttachments sa INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE sci FROM dbo.SceneChecklistItems sci INNER JOIN dbo.Scenes s ON s.SceneID = sci.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE sw FROM dbo.SceneWorkflow sw INNER JOIN dbo.Scenes s ON s.SceneID = sw.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE sn FROM dbo.SceneNotes sn INNER JOIN dbo.Scenes s ON s.SceneID = sn.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE cg FROM dbo.ChapterGoals cg INNER JOIN dbo.Chapters c ON c.ChapterID = cg.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + + DELETE sd FROM dbo.SceneDependencies sd + LEFT JOIN dbo.Scenes ss ON ss.SceneID = sd.SourceSceneID + LEFT JOIN dbo.Chapters sc ON sc.ChapterID = ss.ChapterID + LEFT JOIN dbo.Books sb ON sb.BookID = sc.BookID + LEFT JOIN dbo.Scenes ts ON ts.SceneID = sd.TargetSceneID + LEFT JOIN dbo.Chapters tc ON tc.ChapterID = ts.ChapterID + LEFT JOIN dbo.Books tb ON tb.BookID = tc.BookID + WHERE sd.ProjectID = @ProjectID OR sb.ProjectID = @ProjectID OR tb.ProjectID = @ProjectID; + + DELETE re FROM dbo.RelationshipEvents re INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID WHERE cr.ProjectID = @ProjectID; + DELETE FROM dbo.CharacterRelationships WHERE ProjectID = @ProjectID; + DELETE ck FROM dbo.CharacterKnowledge ck INNER JOIN dbo.Characters ch ON ch.CharacterID = ck.CharacterID WHERE ch.ProjectID = @ProjectID; + DELETE cae FROM dbo.CharacterAttributeEvents cae INNER JOIN dbo.Characters ch ON ch.CharacterID = cae.CharacterID WHERE ch.ProjectID = @ProjectID; + DELETE sc FROM dbo.SceneCharacters sc INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID WHERE ch.ProjectID = @ProjectID; + + DELETE acec FROM dbo.AssetCustodyEventCharacters acec INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE ace FROM dbo.AssetCustodyEvents ace INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE ad FROM dbo.AssetDependencies ad INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ad.SourceAssetID WHERE sa.ProjectID = @ProjectID; + DELETE ae FROM dbo.AssetEvents ae INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE sal FROM dbo.SceneAssetLocations sal INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE FROM dbo.StoryAssets WHERE ProjectID = @ProjectID; + + DELETE lr FROM dbo.LocationRelationships lr INNER JOIN dbo.Locations l ON l.LocationID = lr.FromLocationID WHERE l.ProjectID = @ProjectID; + DELETE FROM dbo.Locations WHERE ProjectID = @ProjectID; + + DELETE te FROM dbo.ThreadEvents te INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE pl.ProjectID = @ProjectID; + DELETE pt FROM dbo.PlotThreads pt INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE pl.ProjectID = @ProjectID; + DELETE FROM dbo.PlotLines WHERE ProjectID = @ProjectID; + + DELETE FROM dbo.TimelineViewPresets WHERE ProjectID = @ProjectID; + DELETE FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID; + DELETE FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID; + + DELETE smv FROM dbo.SceneMetricValues smv INNER JOIN dbo.Scenes s ON s.SceneID = smv.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE FROM dbo.SceneMetricTypes WHERE ProjectID = @ProjectID; + DELETE FROM dbo.CharacterAttributeTypes WHERE ProjectID = @ProjectID; + DELETE FROM dbo.AssetStates WHERE ProjectID = @ProjectID; + + DELETE sp FROM dbo.ScenePurposes sp INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE so FROM dbo.SceneOutcomes so INNER JOIN dbo.Scenes s ON s.SceneID = so.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE s FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE c FROM dbo.Chapters c INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE FROM dbo.Books WHERE ProjectID = @ProjectID; + DELETE FROM dbo.Characters WHERE ProjectID = @ProjectID; + + UPDATE p + SET ProjectName = src.ProjectName, + Description = src.Description, + CreatedDate = src.CreatedDate, + UpdatedDate = SYSUTCDATETIME(), + IsArchived = src.IsArchived + FROM dbo.Projects p + CROSS APPLY OPENJSON(@JsonData, '$.Project') + WITH + ( + ProjectID int '$.ProjectID', + ProjectName nvarchar(200) '$.ProjectName', + Description nvarchar(max) '$.Description', + CreatedDate datetime2 '$.CreatedDate', + IsArchived bit '$.IsArchived' + ) src + WHERE p.ProjectID = @ProjectID AND src.ProjectID = @ProjectID; + + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneMetricTypes', N'$.Data.SceneMetricTypes', @JsonData, @ProjectID, 1; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetStates', N'$.Data.AssetStates', @JsonData, @ProjectID, 1; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterAttributeTypes', N'$.Data.CharacterAttributeTypes', @JsonData, @ProjectID, 1; + EXEC dbo.ProjectRestore_InsertJsonTable N'Books', N'$.Data.Books', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Chapters', N'$.Data.Chapters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Characters', N'$.Data.Characters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Locations', N'$.Data.Locations', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Scenes', N'$.Data.Scenes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenePurposes', N'$.Data.ScenePurposes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneOutcomes', N'$.Data.SceneOutcomes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneMetricValues', N'$.Data.SceneMetricValues', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ProjectTimelineSettings', N'$.Data.TimelineSettings', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ProjectTimelineMetricSettings', N'$.Data.TimelineMetricSettings', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'TimelineViewPresets', N'$.Data.TimelineViewPresets', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'PlotLines', N'$.Data.PlotLines', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'PlotThreads', N'$.Data.PlotThreads', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ThreadEvents', N'$.Data.ThreadEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'StoryAssets', N'$.Data.StoryAssets', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetEvents', N'$.Data.AssetEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetDependencies', N'$.Data.AssetDependencies', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetCustodyEvents', N'$.Data.AssetCustodyEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetCustodyEventCharacters', N'$.Data.AssetCustodyCharacters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneCharacters', N'$.Data.SceneCharacters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterAttributeEvents', N'$.Data.CharacterAttributeEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterKnowledge', N'$.Data.CharacterKnowledge', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterRelationships', N'$.Data.CharacterRelationships', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'RelationshipEvents', N'$.Data.RelationshipEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'LocationRelationships', N'$.Data.LocationRelationships', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneAssetLocations', N'$.Data.SceneAssetLocations', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneDependencies', N'$.Data.SceneDependencies', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ValidationRuns', N'$.Data.ValidationRuns', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ContinuityWarnings', N'$.Data.ContinuityWarnings', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneNotes', N'$.Data.SceneNotes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneWorkflow', N'$.Data.SceneWorkflow', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneChecklistItems', N'$.Data.SceneChecklistItems', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneAttachments', N'$.Data.SceneAttachments', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ChapterGoals', N'$.Data.ChapterGoals', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Scenarios', N'$.Data.Scenarios', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioSceneOrder', N'$.Data.ScenarioSceneOrder', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioValidationRuns', N'$.Data.ScenarioValidationRuns', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioWarnings', N'$.Data.ScenarioWarnings', @JsonData, @ProjectID, 0; + + DECLARE EnableCursor CURSOR LOCAL FAST_FORWARD FOR SELECT TableName FROM @RestoreTables; + OPEN EnableCursor; + FETCH NEXT FROM EnableCursor INTO @TableName; + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @EnableSql nvarchar(max) = N'ALTER TABLE dbo.' + QUOTENAME(@TableName) + N' WITH CHECK CHECK CONSTRAINT ALL;'; + EXEC (@EnableSql); + FETCH NEXT FROM EnableCursor INTO @TableName; + END + CLOSE EnableCursor; + DEALLOCATE EnableCursor; + + INSERT INTO dbo.ProjectRestoreHistory (ProjectID, SourceRestorePointID, SafetyRestorePointID, PerformedBy, Notes) + VALUES (@ProjectID, @SourceRestorePointID, @SafetyRestorePointID, SUSER_SNAME(), N'Project restored from saved restore point.'); + + COMMIT TRANSACTION; +END; +GO diff --git a/PlotLine/Views/ProjectRestorePoints/Index.cshtml b/PlotLine/Views/ProjectRestorePoints/Index.cshtml index 0b4f53d..e6631f8 100644 --- a/PlotLine/Views/ProjectRestorePoints/Index.cshtml +++ b/PlotLine/Views/ProjectRestorePoints/Index.cshtml @@ -97,11 +97,39 @@ @restorePoint.Notes Download + } + + @foreach (var restorePoint in Model.RestorePoints) + { + + } } diff --git a/PlotLine/wwwroot/js/site.js b/PlotLine/wwwroot/js/site.js index 7260810..2d7260c 100644 --- a/PlotLine/wwwroot/js/site.js +++ b/PlotLine/wwwroot/js/site.js @@ -99,7 +99,7 @@ document.addEventListener("submit", (event) => { const form = event.target; - if (!(form instanceof HTMLFormElement) || form.dataset.confirmed === "true") { + if (!(form instanceof HTMLFormElement) || form.dataset.confirmed === "true" || form.dataset.confirmSkip === "true") { return; }