Skip to content

Commit d977027

Browse files
Merge branch 'dev' into feature/submission-send
2 parents dffda88 + c997ef3 commit d977027

95 files changed

Lines changed: 2592 additions & 167 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/main.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111

1212
env:
1313
REGISTRY: ghcr.io
14+
FRONTEND_REF: dev
1415

1516
jobs:
1617
build:
@@ -73,9 +74,22 @@ jobs:
7374
uses: actions/checkout@v4
7475
with:
7576
repository: ita-social-projects/oitClient
76-
ref: dev
77+
ref: ${{ env.FRONTEND_REF }}
7778
path: frontend
7879

80+
- name: Capture frontend build metadata
81+
run: |
82+
COMMIT_ID=$(git -C frontend rev-parse HEAD)
83+
COMMIT_TIME=$(date -u -d "$(git -C frontend log -1 --format=%cI)" +%Y-%m-%dT%H:%M:%SZ)
84+
VERSION=$(node -p "require('./frontend/package.json').version || ''")
85+
{
86+
echo "oit.frontend.commit-id=$COMMIT_ID"
87+
echo "oit.frontend.short-commit-id=${COMMIT_ID:0:7}"
88+
echo "oit.frontend.commit-time=$COMMIT_TIME"
89+
echo "oit.frontend.branch=$FRONTEND_REF"
90+
echo "oit.frontend.version=$VERSION"
91+
} > src/main/resources/frontend-info.properties
92+
7993
- name: Build frontend
8094
run: |
8195
cd frontend

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ build/
4141
.vscode/
4242

4343
myenv-example
44-
/.env
44+
/.env
45+
46+
# Generated by CI during the backend image build
47+
src/main/resources/frontend-info.properties

pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,14 @@
281281
<plugin>
282282
<groupId>org.springframework.boot</groupId>
283283
<artifactId>spring-boot-maven-plugin</artifactId>
284+
<executions>
285+
<execution>
286+
<id>build-info</id>
287+
<goals>
288+
<goal>build-info</goal>
289+
</goals>
290+
</execution>
291+
</executions>
284292
<configuration>
285293
<excludes>
286294
<exclude>
@@ -290,6 +298,23 @@
290298
</excludes>
291299
</configuration>
292300
</plugin>
301+
<plugin>
302+
<groupId>io.github.git-commit-id</groupId>
303+
<artifactId>git-commit-id-maven-plugin</artifactId>
304+
<configuration>
305+
<failOnNoGitDirectory>false</failOnNoGitDirectory>
306+
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
307+
<verbose>false</verbose>
308+
<dateFormat>yyyy-MM-dd'T'HH:mm:ssXXX</dateFormat>
309+
<dateFormatTimeZone>UTC</dateFormatTimeZone>
310+
<includeOnlyProperties>
311+
<includeOnlyProperty>^git.branch$</includeOnlyProperty>
312+
<includeOnlyProperty>^git.commit.id$</includeOnlyProperty>
313+
<includeOnlyProperty>^git.commit.id.abbrev$</includeOnlyProperty>
314+
<includeOnlyProperty>^git.commit.time$</includeOnlyProperty>
315+
</includeOnlyProperties>
316+
</configuration>
317+
</plugin>
293318
</plugins>
294319
</build>
295320
</project>

src/main/java/com/itasocialacademy/oitassist/competition/api/CompetitionFacade.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,15 @@ public interface CompetitionFacade {
5959
* @return the competition tree, or empty if no competition with this ID exists
6060
*/
6161
Optional<CompetitionTreeDetail> findCompetitionTreeByCompetitionId(Long competitionId);
62+
63+
/**
64+
* Retrieves tours by their IDs.
65+
*
66+
* @param tourIds Tour IDs, must not be {@code null} (an empty list yields an
67+
* empty result)
68+
* @return the tours found for the given IDs, in unspecified order; IDs with no
69+
* matching tour are silently omitted, so the result may be smaller than
70+
* {@code tourIds}
71+
*/
72+
List<TourDetail> findToursByIds(List<Long> tourIds);
6273
}

src/main/java/com/itasocialacademy/oitassist/competition/controller/CompetitionController.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,17 @@ public ResponseEntity<CompetitionTreeResponse> getCompetitionTree(@PathVariable
143143
@ApiResponse(responseCode = "403", description = "Access denied (requires ADMIN or ORG role)",
144144
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
145145
@ApiResponse(responseCode = "404", description = "Competition not found",
146+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
147+
@ApiResponse(responseCode = "409",
148+
description = "Conflict — the entity was modified by another request since it was last read "
149+
+ "(stale version)",
146150
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
147151
})
148152
@PatchMapping("/{competitionId}/status")
149153
@PreAuthorize("hasAnyRole('ADMIN', 'ORG')")
150154
public ResponseEntity<CompetitionResponse> changeStatus(
151155
@PathVariable Long competitionId,
152156
@Valid @RequestBody ChangeCompetitionStatusRequest request) {
153-
return ResponseEntity.ok(competitionService.changeStatus(competitionId, request.status()));
157+
return ResponseEntity.ok(competitionService.changeStatus(competitionId, request));
154158
}
155159
}

src/main/java/com/itasocialacademy/oitassist/competition/controller/StageController.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ public ResponseEntity<List<StageResponse>> getAllStages(@PathVariable Long compe
8484
@ApiResponse(responseCode = "403", description = "Access denied",
8585
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
8686
@ApiResponse(responseCode = "404", description = "Stage not found",
87+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
88+
@ApiResponse(responseCode = "409",
89+
description = "Conflict — the entity was modified by another request since it was last read "
90+
+ "(stale version)",
8791
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
8892
})
8993
@PutMapping("/competitions/{competitionId}/stages/{stageId}")
@@ -105,6 +109,10 @@ public ResponseEntity<StageResponse> updateStage(
105109
@ApiResponse(responseCode = "403", description = "Access denied",
106110
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
107111
@ApiResponse(responseCode = "404", description = "Stage not found",
112+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
113+
@ApiResponse(responseCode = "409",
114+
description = "Conflict — the entity was modified by another request since it was last read "
115+
+ "(stale version)",
108116
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
109117
})
110118
@PreAuthorize("hasAnyRole('ADMIN', 'ORG')")

src/main/java/com/itasocialacademy/oitassist/competition/controller/TourController.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ public ResponseEntity<List<TourResponse>> getAllTours(@PathVariable Long stageId
7979
@ApiResponse(responseCode = "403", description = "Access denied",
8080
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
8181
@ApiResponse(responseCode = "404", description = "Tour not found",
82-
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
82+
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
83+
@ApiResponse(responseCode = "409",
84+
description = "Conflict — the entity was modified by another request since it was last read "
85+
+ "(stale version)",
86+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
8387
})
8488
@PutMapping("/stages/{stageId}/tours/{tourId}")
8589
@PreAuthorize("hasAnyRole('ADMIN', 'ORG')")
@@ -98,7 +102,11 @@ public ResponseEntity<TourResponse> updateTour(
98102
@ApiResponse(responseCode = "403", description = "Access denied",
99103
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
100104
@ApiResponse(responseCode = "404", description = "Tour not found",
101-
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
105+
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
106+
@ApiResponse(responseCode = "409",
107+
description = "Conflict — the entity was modified by another request since it was last read "
108+
+ "(stale version)",
109+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
102110
})
103111
@PatchMapping("/stages/{stageId}/tours/{tourId}/status")
104112
@PreAuthorize("hasAnyRole('ADMIN', 'ORG')")

src/main/java/com/itasocialacademy/oitassist/competition/dao/model/CompetitionEvent.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import jakarta.persistence.GenerationType;
77
import jakarta.persistence.Id;
88
import jakarta.persistence.MappedSuperclass;
9+
import jakarta.persistence.Version;
910
import java.time.Instant;
1011
import java.time.ZonedDateTime;
1112
import lombok.AllArgsConstructor;
@@ -58,4 +59,8 @@ public abstract class CompetitionEvent {
5859
@LastModifiedDate
5960
@Column(name = "updated_at")
6061
private Instant updatedAt;
62+
63+
@Version
64+
@Column(name = "version", nullable = false)
65+
private Long version;
6166
}

src/main/java/com/itasocialacademy/oitassist/competition/dao/repository/CompetitionRepository.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,28 @@
22

33
import com.itasocialacademy.oitassist.competition.dao.enums.CompetitionStatus;
44
import com.itasocialacademy.oitassist.competition.dao.model.Competition;
5+
import jakarta.persistence.LockModeType;
6+
import java.util.Optional;
57
import org.springframework.data.domain.Page;
68
import org.springframework.data.domain.Pageable;
79
import org.springframework.data.jpa.repository.JpaRepository;
810
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
11+
import org.springframework.data.jpa.repository.Lock;
12+
import org.springframework.data.jpa.repository.Query;
13+
import org.springframework.data.repository.query.Param;
914
import org.springframework.stereotype.Repository;
1015

1116
@Repository
1217
public interface CompetitionRepository extends JpaRepository<Competition, Long>, JpaSpecificationExecutor<Competition> {
1318
Page<Competition> findAllByCompetitionStatus(CompetitionStatus status, Pageable pageable);
19+
20+
/**
21+
* Fetches a Competition with a pessimistic write lock (SELECT ... FOR UPDATE),
22+
* used as the entry point for any structural hierarchy mutation or status
23+
* transition, to serialize concurrent changes on the same competition and close
24+
* the write-skew window between publish/finish and delete of a Stage/Tour.
25+
*/
26+
@Lock(LockModeType.PESSIMISTIC_WRITE)
27+
@Query("SELECT c from Competition c where c.id = :id")
28+
Optional<Competition> findByIdForUpdate(@Param("id") Long id);
1429
}

src/main/java/com/itasocialacademy/oitassist/competition/dto/request/ChangeCompetitionStatusRequest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.itasocialacademy.oitassist.competition.dao.enums.CompetitionStatus;
44
import io.swagger.v3.oas.annotations.media.Schema;
5+
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
56
import jakarta.validation.constraints.NotNull;
67

78
@Schema(description = "DTO for changing the lifecycle status of a Competition")
@@ -10,5 +11,8 @@ public record ChangeCompetitionStatusRequest(
1011
description = "The target status to transition the competition into (e.g., PUBLISHED, FINISHED, ARCHIVED)",
1112
example = "PUBLISHED",
1213
requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(
13-
message = "New status cannot be null") CompetitionStatus status) {
14+
message = "New status cannot be null") CompetitionStatus status,
15+
16+
@Schema(description = "Optimistic locking version; must be echoed back on updates",
17+
requiredMode = RequiredMode.REQUIRED) @NotNull Long version) {
1418
}

0 commit comments

Comments
 (0)