forked from usdot-jpo-ode/jpo-cvmanager
-
Notifications
You must be signed in to change notification settings - Fork 2
Adding Email Unsubscribe Page #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jacob6838
wants to merge
70
commits into
develop
Choose a base branch
from
iapi-email-unsubscription
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
70 commits
Select commit
Hold shift + click to select a range
45f35f8
Adding notif email generation and sending to the iapi
jacob6838 c8cbeeb
Adding notification email examples
jacob6838 3d068d4
Adding iapi email controller
jacob6838 169105e
Fixing EmailServiceTest
jacob6838 6987b60
Update EmailController.java
jacob6838 4d4fce5
Update EmailServiceTest.java
jacob6838 1cb6ae9
Fixing broken config property refs
jacob6838 7100784
Adding prototype unsubscription page and api endpoint
jacob6838 56e42ae
Adding descriptions and required roles to email subscriptions
jacob6838 2e21c67
Adding manage subscriptions endpoint
jacob6838 7efcf26
Working email unsubscription through API
jacob6838 7dd1294
Adding unit tests for email components
jacob6838 6e251a4
Adding EmailType default constructor
jacob6838 11f03f8
Removing KC authentication from unsubscribe page
jacob6838 8361e8e
Updating email generator comments, colors, and content
jacob6838 d4deef4
Updating default iapi env vars to avoid localhost
jacob6838 b5411d8
Update EmailRecipient.java
jacob6838 9eebbe7
Adding email frequency to tables and queries
jacob6838 4b79219
Update launch.json
jacob6838 648b367
Improving getCombinedResponseEntity to handle partial failures
jacob6838 9990c6f
Merge branch 'intersection-api-email-generation-1' into iapi-email-un…
jacob6838 d86d53e
Update UserControllerTest.java
jacob6838 f748fb6
Removing secure env vars from application.yaml
jacob6838 06338b1
Renaming email template to email_template.html
jacob6838 9c89f37
Removing duplicate dependency
jacob6838 c7676b9
Update services/intersection-api/api/src/main/java/us/dot/its/jpo/ode…
jacob6838 0a3330b
Handling unsubscribe URL generation errors
jacob6838 84c8cbe
Merge branch 'intersection-api-email-generation-1' of https://github.…
jacob6838 f1e79bc
Removing unused methods
jacob6838 f00d7f2
Cleaning up email generation code
jacob6838 6825d8d
Writing email provider tests
jacob6838 552cc70
Renaming unsubscribe controller
jacob6838 28f739e
Working SubscriptionController
jacob6838 aa92f59
Merge branch 'intersection-api-email-generation-1' into iapi-email-un…
jacob6838 bde2dcc
Adding unsubscriptionSlice
jacob6838 a6ee962
Implementing subscription management page in cvmanager
jacob6838 b2f0cd3
Fixing unit tests
jacob6838 791aa48
Merge branch 'develop' into intersection-api-email-generation-1
jacob6838 bf13505
Merge branch 'intersection-api-email-generation-1' into iapi-email-un…
jacob6838 85bf31a
Merge branch 'develop' into iapi-email-unsubscription
jacob6838 13da463
Migrating PostgresService to Repositories
jacob6838 a2c34fc
Updating EmailService tests
jacob6838 df83a0a
Merge branch 'develop' into iapi-email-unsubscription
jacob6838 9becb12
Resolving subscription ui typescript errors
jacob6838 ad82998
Syncing unsubscribe and manage subscriptions pages
jacob6838 dc32426
Delete EmailSubscriptionsTable.tsx
jacob6838 3ce05e4
Removing AdminNotification pages
jacob6838 ca464e4
Adding hikari to application-integration-test.yaml
jacob6838 18ff475
Removing adminNotification webapp references
jacob6838 907b9e3
Filtering email subscriptions by required role
jacob6838 baaa9e4
Enabling notif subscription page feedback
jacob6838 cf0639a
Adding BrowserRouter to notif sub tests
jacob6838 4850dc6
Update SubscriptionForm.test.tsx
jacob6838 a57dc52
Create SubscriptionForm.test.tsx.snap
jacob6838 343cce3
Cleaning up
jacob6838 f816b02
Removing client-side role filtering from notif sub page
jacob6838 7f46fde
Adding isModified detection to notif sub form
jacob6838 3a76b93
Rewriting email_type sql update script
jacob6838 cb22010
Handling notif sub form parent updates with prevSubscriptions
jacob6838 3457329
Create email_type_required_role.sql
jacob6838 572aa81
Writing EmailServiceTest without EqualsAndHashCode
jacob6838 08331da
Merge branch 'develop' into iapi-email-unsubscription
jacob6838 d65f8d8
Updating exception and return types in notif sub endpoints
jacob6838 e205db0
Validating notif api requests
jacob6838 d90db48
Updating notif sub controller tests
jacob6838 52843d3
Merge branch 'develop' into iapi-email-unsubscription
jacob6838 601f6ef
Merge branch 'develop' into iapi-email-unsubscription
jacob6838 930ba8f
Removing global exception handler 500 catch-all
jacob6838 285a29a
Converting SubscriptionController tests to MVC
jacob6838 66b12fc
Developing unsubscribe controller mvc tests
jacob6838 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,6 +127,7 @@ | |
| "timefield", | ||
| "toaddrs", | ||
| "txrxmsg", | ||
| "unsubscription", | ||
| "upgrader", | ||
| "usdot", | ||
| "usdotjpoode", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
resources/sql_scripts/update_scripts/email_type_required_role.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| -- Update public.email_type table definition | ||
| -- omit NOT NULL constraint on required_role for now to allow for smooth transition, will set to NOT NULL after backfilling data | ||
| ALTER TABLE public.email_type | ||
| ADD COLUMN IF NOT EXISTS required_role integer; | ||
|
|
||
| -- Add foreign key constraint | ||
| ALTER TABLE public.email_type | ||
| ADD CONSTRAINT IF NOT EXISTS fk_role_id FOREIGN KEY (required_role) | ||
| REFERENCES public.roles (role_id) MATCH SIMPLE | ||
| ON UPDATE NO ACTION | ||
| ON DELETE NO ACTION; | ||
|
|
||
| -- Set default value for all existing entries to role_id 1 (ADMIN) | ||
| UPDATE public.email_type | ||
| SET required_role = 1 | ||
| WHERE required_role IS NULL; | ||
|
|
||
| -- ADMIN roles | ||
| UPDATE public.email_type | ||
| SET required_role = 1 | ||
| WHERE email_type IN ('Support Requests', 'Access Requests'); | ||
|
|
||
| -- OPERATOR roles | ||
| UPDATE public.email_type | ||
| SET required_role = 2 | ||
| WHERE email_type IN ('Firmware Upgrade Failures', 'Critical Error Messages'); | ||
|
|
||
| -- USER roles | ||
| UPDATE public.email_type | ||
| SET required_role = 3 | ||
| WHERE email_type IN ('Daily Message Counts', 'Intersection Notification Summary'); | ||
|
|
||
| -- Make the column NOT NULL after setting all values | ||
| ALTER TABLE public.email_type | ||
| ALTER COLUMN required_role SET NOT NULL; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
...pi/api/src/main/java/us/dot/its/jpo/ode/api/controllers/users/SubscriptionController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package us.dot.its.jpo.ode.api.controllers.users; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestMethod; | ||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
| import jakarta.validation.Valid; | ||
| import us.dot.its.jpo.ode.api.models.emails.UserEmailNotificationDto; | ||
| import us.dot.its.jpo.ode.api.models.keycloak.CvManagerAuthToken; | ||
| import us.dot.its.jpo.ode.api.models.UserRole; | ||
| import us.dot.its.jpo.ode.api.models.emails.EmailSubscriptionGetResponse; | ||
| import us.dot.its.jpo.ode.api.services.EmailService; | ||
| import us.dot.its.jpo.ode.api.services.PermissionService; | ||
|
|
||
| import org.springframework.security.access.prepost.PreAuthorize; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
|
|
||
| @Slf4j | ||
| @RestController | ||
| @ConditionalOnProperty(name = "enable.api", havingValue = "true", matchIfMissing = false) | ||
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "401", description = "Unauthorized"), | ||
| @ApiResponse(responseCode = "500", description = "Internal Server Error") | ||
| }) | ||
| @RequestMapping("/users/subscriptions") | ||
| @RequiredArgsConstructor | ||
| public class SubscriptionController { | ||
| private final EmailService emailService; | ||
| private final PermissionService permissionService; | ||
|
|
||
| @RequestMapping(value = "/email-subscriptions", method = RequestMethod.GET, produces = "application/json") | ||
| @PreAuthorize("@PermissionService.isSuperUser() || @PermissionService.hasRole('USER')") | ||
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "200", description = "Success"), | ||
| @ApiResponse(responseCode = "400", description = "Invalid message body"), | ||
| }) | ||
| public EmailSubscriptionGetResponse getEmailSubscriptions() { | ||
|
|
||
| CvManagerAuthToken authToken = permissionService.getCvManagerAuthToken(); | ||
| boolean isOperator = !authToken.getQualifiedOrgList(UserRole.OPERATOR).isEmpty(); | ||
| boolean isAdmin = !authToken.getQualifiedOrgList(UserRole.ADMIN).isEmpty(); | ||
| List<UserEmailNotificationDto> subscriptions = emailService.getAllEmailSubscriptionOptionsForUser( | ||
| authToken.getEmail(), | ||
| isOperator, isAdmin); | ||
| return new EmailSubscriptionGetResponse(subscriptions, authToken.getEmail()); | ||
| } | ||
|
|
||
| @Operation(summary = "Update email subscription preferences", description = "Update the user's email subscription preferences") | ||
| @RequestMapping(value = "/email-subscriptions", method = RequestMethod.POST, produces = "application/json") | ||
| @PreAuthorize("@PermissionService.isSuperUser() || @PermissionService.hasRole('USER')") | ||
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "200", description = "Success"), | ||
| @ApiResponse(responseCode = "400", description = "Invalid message body"), | ||
| }) | ||
| @ResponseStatus(HttpStatus.OK) | ||
| public void updateEmailSubscriptions( | ||
| @Valid @RequestBody List<UserEmailNotificationDto> requestedSubscriptions) { | ||
|
|
||
| CvManagerAuthToken authToken = permissionService.getCvManagerAuthToken(); | ||
|
|
||
| emailService.updateEmailSubscriptions(authToken.getEmail(), requestedSubscriptions); | ||
|
|
||
| return; | ||
| } | ||
| } |
85 changes: 85 additions & 0 deletions
85
...api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/users/UnsubscribeController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| package us.dot.its.jpo.ode.api.controllers.users; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import javax.ws.rs.NotAuthorizedException; | ||
|
|
||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestMethod; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
| import jakarta.validation.Valid; | ||
| import us.dot.its.jpo.ode.api.emails.UnsubscribeTokenGenerator; | ||
| import us.dot.its.jpo.ode.api.models.emails.UserEmailNotificationDto; | ||
| import us.dot.its.jpo.ode.api.models.postgres.tables.UserOrganization; | ||
| import us.dot.its.jpo.ode.api.repositories.UserOrganizationRepository; | ||
| import us.dot.its.jpo.ode.api.models.UserRole; | ||
| import us.dot.its.jpo.ode.api.models.emails.EmailSubscriptionGetResponse; | ||
| import us.dot.its.jpo.ode.api.services.EmailService; | ||
|
|
||
| import org.springframework.web.bind.annotation.RequestBody; | ||
|
|
||
| @Slf4j | ||
| @RestController | ||
| @ConditionalOnProperty(name = "enable.api", havingValue = "true", matchIfMissing = false) | ||
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "401", description = "Unauthorized"), | ||
| @ApiResponse(responseCode = "500", description = "Internal Server Error") | ||
| }) | ||
| @RequestMapping("/users/unsubscribe") | ||
| @RequiredArgsConstructor | ||
| public class UnsubscribeController { | ||
| private final EmailService emailService; | ||
| private final UserOrganizationRepository userOrganizationRepository; | ||
| private final UnsubscribeTokenGenerator unsubscribeTokenGenerator; | ||
|
|
||
| @RequestMapping(value = "/email-subscriptions", method = RequestMethod.GET, produces = "application/json") | ||
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "200", description = "Success"), | ||
| @ApiResponse(responseCode = "400", description = "Invalid message body"), | ||
| }) | ||
| public EmailSubscriptionGetResponse getEmailSubscriptions( | ||
| @RequestParam(required = false) String token) { | ||
| String userEmail = unsubscribeTokenGenerator.parseAndValidateToken(token); | ||
| if (userEmail == null) { | ||
| throw new NotAuthorizedException("Invalid or expired token"); | ||
| } | ||
|
|
||
| List<UserOrganization> userOrganizations = userOrganizationRepository.findAllByEmail(userEmail); | ||
| boolean isOperator = userOrganizations.stream() | ||
| .anyMatch(org -> UserRole.OPERATOR.equals(UserRole.fromString(org.getRole().getName()))); | ||
| boolean isAdmin = userOrganizations.stream() | ||
| .anyMatch(org -> UserRole.ADMIN.equals(UserRole.fromString(org.getRole().getName()))); | ||
|
|
||
| List<UserEmailNotificationDto> subscriptions = emailService.getAllEmailSubscriptionOptionsForUser(userEmail, | ||
| isOperator, isAdmin); | ||
| return new EmailSubscriptionGetResponse(subscriptions, userEmail); | ||
| } | ||
|
|
||
| @Operation(summary = "Update email subscription preferences", description = "Update the user's email subscription preferences") | ||
| @RequestMapping(value = "/email-subscriptions", method = RequestMethod.POST, produces = "application/json") | ||
| @ApiResponses(value = { | ||
| @ApiResponse(responseCode = "200", description = "Success"), | ||
| @ApiResponse(responseCode = "400", description = "Invalid message body"), | ||
| }) | ||
| public void updateEmailSubscriptions( | ||
| @RequestParam(required = false) String token, | ||
| @Valid @RequestBody List<UserEmailNotificationDto> requestedSubscriptions) { | ||
| String userEmail = unsubscribeTokenGenerator.parseAndValidateToken(token); | ||
| if (userEmail == null) { | ||
| throw new NotAuthorizedException("Invalid or expired token"); | ||
| } | ||
|
|
||
| emailService.updateEmailSubscriptions(userEmail, requestedSubscriptions); | ||
|
|
||
| return; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
...ion-api/api/src/main/java/us/dot/its/jpo/ode/api/mappers/UserEmailNotificationMapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package us.dot.its.jpo.ode.api.mappers; | ||
|
|
||
| import org.mapstruct.Mapper; | ||
| import org.mapstruct.Mapping; | ||
| import org.mapstruct.MappingConstants; | ||
|
|
||
| import us.dot.its.jpo.ode.api.models.emails.UserEmailNotificationDto; | ||
| import us.dot.its.jpo.ode.api.models.postgres.tables.EmailType; | ||
| import us.dot.its.jpo.ode.api.models.postgres.tables.UserEmailNotification; | ||
|
|
||
| @Mapper(componentModel = MappingConstants.ComponentModel.SPRING) | ||
| public interface UserEmailNotificationMapper { | ||
|
|
||
| /** | ||
| * Convert UserEmailNotification entity to UserEmailNotificationDto | ||
| * MapStruct will automatically map fields with the same name | ||
| */ | ||
| @Mapping(source = "emailType.emailType", target = "category") | ||
| @Mapping(source = "emailType.description", target = "description") | ||
| @Mapping(source = "emailType.requiredRole.name", target = "requiredRole") | ||
| @Mapping(source = "emailType.supportsImmediate", target = "supportsImmediate") | ||
| @Mapping(source = "emailType.supportsHourly", target = "supportsHourly") | ||
| @Mapping(source = "emailType.supportsDaily", target = "supportsDaily") | ||
| @Mapping(source = "emailType.supportsWeekly", target = "supportsWeekly") | ||
| @Mapping(source = "emailType.supportsMonthly", target = "supportsMonthly") | ||
| UserEmailNotificationDto toDto(UserEmailNotification notification); | ||
|
|
||
| // category, immediate, hourly, daily, weekly, monthly | ||
| @Mapping(source = "emailType", target = "category") | ||
| @Mapping(source = "requiredRole.name", target = "requiredRole") | ||
| @Mapping(target = "immediate", constant = "false") | ||
| @Mapping(target = "hourly", constant = "false") | ||
| @Mapping(target = "daily", constant = "false") | ||
| @Mapping(target = "weekly", constant = "false") | ||
| @Mapping(target = "monthly", constant = "false") | ||
| UserEmailNotificationDto fromEmailType(EmailType emailType); | ||
|
|
||
| // id, user, emailType | ||
| @Mapping(target = "id", ignore = true) // Never set id when creating/updating - it's auto-generated | ||
| @Mapping(target = "user", ignore = true) // set in service layer | ||
| @Mapping(target = "emailType", ignore = true) // set in service layer | ||
| UserEmailNotification toEntity(UserEmailNotificationDto dto); | ||
| } |
15 changes: 15 additions & 0 deletions
15
.../api/src/main/java/us/dot/its/jpo/ode/api/models/emails/EmailSubscriptionGetResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package us.dot.its.jpo.ode.api.models.emails; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Data; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| @Data | ||
| public class EmailSubscriptionGetResponse { | ||
| private List<UserEmailNotificationDto> subscriptions; | ||
| private String email; | ||
| } |
15 changes: 15 additions & 0 deletions
15
...n-api/api/src/main/java/us/dot/its/jpo/ode/api/models/emails/ManageSubscriptionsBody.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package us.dot.its.jpo.ode.api.models.emails; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Data; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| @Data | ||
| public class ManageSubscriptionsBody { | ||
| private String email; | ||
| private List<EmailCategory> categoriesToSubscribe; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PostgreSQL supports
ADD COLUMN IF NOT EXISTS, but it does not supportALTER TABLE ... ADD CONSTRAINT IF NOT EXISTS ...in many commonly used Postgres versions. This migration may fail at runtime. Consider guarding constraint creation with aDO $$ ... IF NOT EXISTS (SELECT 1 FROM pg_constraint ...) THEN ... END IF; $$block, or create the constraint unconditionally in a fresh migration where you can ensure it doesn’t already exist.