Skip to content

Commit 35c78b7

Browse files
fix(authority-source-files): Fix persisting createdBy and updatedBy user id for shadow copies in ECS (#363)
* fix(authority-source-files): Fix persisting user's id when creating and updating shadow copies for member tenants in ECS Closes: MODELINKS-244
1 parent a0c440f commit 35c78b7

17 files changed

+533
-95
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
### Bug fixes
1616
* Fix context mix-up on data propagation ([MODELINKS-273](https://folio-org.atlassian.net/browse/MODELINKS-273))
1717
* Close input stream on s3 file read ([MODELINKS-278](https://folio-org.atlassian.net/browse/MODELINKS-278))
18+
* Fix persisting createdBy and updatedBy user id for shadow copies in ECS ([MODELINKS-244](https://folio-org.atlassian.net/browse/MODELINKS-244))
1819

1920
### Tech Dept
2021
* Add missing interface `source-storage-batch` dependency in module descriptor ([MODELINKS-275](https://folio-org.atlassian.net/browse/MODELINKS-275))

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
8-
<version>3.3.5</version>
8+
<version>3.4.0</version>
99
<relativePath /> <!-- lookup parent from repository -->
1010
</parent>
1111

src/main/java/org/folio/entlinks/controller/delegate/AuthoritySourceFileServiceDelegate.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.folio.spring.service.SystemUserScopedExecutionService;
3434
import org.folio.tenant.domain.dto.Parameter;
3535
import org.jetbrains.annotations.NotNull;
36+
import org.springframework.beans.factory.annotation.Qualifier;
3637
import org.springframework.stereotype.Service;
3738

3839
@Log4j2
@@ -44,7 +45,7 @@ public class AuthoritySourceFileServiceDelegate {
4445
private static final String AUTHORITY_TABLE_NAME = "authority";
4546
private static final String AUTHORITY_ARCHIVE_TABLE_NAME = "authority_archive";
4647

47-
private final AuthoritySourceFileService service;
48+
private final @Qualifier("authoritySourceFileService") AuthoritySourceFileService service;
4849
private final AuthoritySourceFileMapper mapper;
4950
private final UserTenantsService tenantsService;
5051
private final AuthoritySourceFileDomainEventPublisher eventPublisher;
@@ -72,7 +73,7 @@ public AuthoritySourceFileDto createAuthoritySourceFile(AuthoritySourceFilePostD
7273

7374
service.createSequence(created.getSequenceName(), created.getHridStartNumber());
7475

75-
propagationService.propagate(getPropagationData(entity, null), CREATE, context.getTenantId());
76+
propagationService.propagate(getPropagationData(created, null), CREATE, context.getTenantId());
7677
return mapper.toDto(created);
7778
}
7879

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package org.folio.entlinks.domain.repository;
2+
3+
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
4+
import static org.folio.entlinks.utils.JdbcUtils.getFullPath;
5+
import static org.folio.entlinks.utils.JdbcUtils.getParamPlaceholder;
6+
import static org.folio.entlinks.utils.JdbcUtils.getSchemaName;
7+
8+
import java.util.UUID;
9+
import org.folio.entlinks.domain.entity.AuthoritySourceFile;
10+
import org.folio.spring.FolioExecutionContext;
11+
import org.springframework.jdbc.core.JdbcTemplate;
12+
import org.springframework.stereotype.Repository;
13+
14+
15+
@Repository
16+
public class AuthoritySourceFileJdbcRepository {
17+
18+
private static final String AUTHORITY_SOURCE_FILE_CODE_TABLE = "authority_source_file_code";
19+
private static final String AUTHORITY_SOURCE_FILE_TABLE = "authority_source_file";
20+
21+
private final JdbcTemplate jdbcTemplate;
22+
private final FolioExecutionContext folioExecutionContext;
23+
24+
public AuthoritySourceFileJdbcRepository(JdbcTemplate jdbcTemplate, FolioExecutionContext folioExecutionContext) {
25+
this.jdbcTemplate = jdbcTemplate;
26+
this.folioExecutionContext = folioExecutionContext;
27+
}
28+
29+
public void insert(AuthoritySourceFile entity) {
30+
var sourceType = getFullPath(folioExecutionContext, "authority_source_file_source");
31+
var sqlValues = "%s::%s,%s".formatted(getParamPlaceholder(3), sourceType, getParamPlaceholder(9));
32+
33+
var sql = """
34+
INSERT INTO %s (id, name, source, type, base_url_protocol, base_url, hrid_start_number, _version,
35+
created_date, updated_date, created_by_user_id, updated_by_user_id)
36+
VALUES (%s);
37+
""";
38+
jdbcTemplate.update(sql.formatted(getFullPath(folioExecutionContext, AUTHORITY_SOURCE_FILE_TABLE), sqlValues),
39+
entity.getId(), entity.getName(),
40+
entity.getSource().name(), entity.getType(), entity.getBaseUrlProtocol(), entity.getBaseUrl(),
41+
entity.getHridStartNumber(), 0, entity.getCreatedDate(), entity.getUpdatedDate(), entity.getCreatedByUserId(),
42+
entity.getUpdatedByUserId());
43+
44+
if (isNotEmpty(entity.getAuthoritySourceFileCodes())) {
45+
var sourceFileCode = entity.getAuthoritySourceFileCodes().iterator().next();
46+
sql = "INSERT INTO %s (authority_source_file_id, code) VALUES (?, ?);";
47+
jdbcTemplate.update(sql.formatted(getFullPath(folioExecutionContext, AUTHORITY_SOURCE_FILE_CODE_TABLE)),
48+
sourceFileCode.getAuthoritySourceFile().getId(), sourceFileCode.getCode());
49+
}
50+
}
51+
52+
public void update(AuthoritySourceFile entity, int version) {
53+
var sourceType = getFullPath(folioExecutionContext, "authority_source_file_source");
54+
var sql = """
55+
UPDATE %s
56+
SET name=?, source=?::%s, type=?, base_url_protocol=?, base_url=?, hrid_start_number=?,
57+
created_date=?, updated_date=?, created_by_user_id=?, updated_by_user_id=?, _version=?
58+
WHERE id = ? and _version = ?;
59+
""";
60+
61+
var id = entity.getId();
62+
jdbcTemplate.update(sql.formatted(getFullPath(folioExecutionContext, AUTHORITY_SOURCE_FILE_TABLE), sourceType),
63+
entity.getName(), entity.getSource().name(), entity.getType(),
64+
entity.getBaseUrlProtocol(), entity.getBaseUrl(), entity.getHridStartNumber(),
65+
entity.getCreatedDate(), entity.getUpdatedDate(), entity.getCreatedByUserId(),
66+
entity.getUpdatedByUserId(), entity.getVersion(), id, version);
67+
68+
if (isNotEmpty(entity.getAuthoritySourceFileCodes())) {
69+
var sourceFileCode = entity.getAuthoritySourceFileCodes().iterator().next().getCode();
70+
71+
jdbcTemplate.execute("DELETE FROM %s WHERE authority_source_file_id = '%s';"
72+
.formatted(getFullPath(folioExecutionContext, AUTHORITY_SOURCE_FILE_CODE_TABLE), id));
73+
74+
jdbcTemplate.update("INSERT INTO %s (authority_source_file_id, code) VALUES (?, ?);"
75+
.formatted(getFullPath(folioExecutionContext, AUTHORITY_SOURCE_FILE_CODE_TABLE)), id, sourceFileCode);
76+
}
77+
}
78+
79+
public void delete(UUID id) {
80+
jdbcTemplate.execute("DELETE FROM %s WHERE authority_source_file_id = '%s'"
81+
.formatted(getFullPath(folioExecutionContext, AUTHORITY_SOURCE_FILE_CODE_TABLE), id));
82+
jdbcTemplate.execute("DELETE FROM %s WHERE id = '%s'"
83+
.formatted(getFullPath(folioExecutionContext, AUTHORITY_SOURCE_FILE_TABLE), id));
84+
}
85+
86+
public void createSequence(String sequenceName, int startNumber) {
87+
var command = String.format("""
88+
CREATE SEQUENCE %s MINVALUE %d INCREMENT BY 1 OWNED BY %s.authority_source_file.sequence_name;
89+
""",
90+
sequenceName, startNumber, getSchemaName(folioExecutionContext));
91+
jdbcTemplate.execute(command);
92+
}
93+
94+
public void dropSequence(String sequenceName) {
95+
var command = String.format("DROP SEQUENCE IF EXISTS %s.%s;", getSchemaName(folioExecutionContext), sequenceName);
96+
jdbcTemplate.execute(command);
97+
}
98+
}

src/main/java/org/folio/entlinks/service/authority/AuthorityService.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import org.folio.entlinks.domain.entity.Authority;
1818
import org.folio.entlinks.domain.entity.AuthoritySourceFile;
1919
import org.folio.entlinks.domain.repository.AuthorityRepository;
20+
import org.folio.entlinks.domain.repository.AuthoritySourceFileRepository;
2021
import org.folio.entlinks.exception.AuthorityNotFoundException;
22+
import org.folio.entlinks.exception.AuthoritySourceFileNotFoundException;
2123
import org.folio.entlinks.exception.OptimisticLockingException;
2224
import org.folio.spring.data.OffsetRequest;
2325
import org.springframework.context.annotation.Primary;
@@ -32,6 +34,7 @@
3234
public class AuthorityService implements AuthorityServiceI<Authority> {
3335

3436
private final AuthorityRepository repository;
37+
private final AuthoritySourceFileRepository sourceFileRepository;
3538

3639
@Override
3740
public Page<Authority> getAll(Integer offset, Integer limit, String cql) {
@@ -138,10 +141,9 @@ protected Authority createInner(Authority entity) {
138141

139142
protected AuthorityUpdateResult updateInner(Authority modified, boolean forced) {
140143
log.debug("update:: Attempting to update Authority [authority: {}]", modified);
141-
var id = modified.getId();
142-
var existing = repository.findByIdAndDeletedFalse(id).orElseThrow(() -> new AuthorityNotFoundException(id));
144+
145+
var existing = validateOnUpdateAndGetExisting(modified.getId(), modified);
143146
var detachedExisting = new Authority(existing);
144-
olCheck(modified, existing, id);
145147

146148
copyModifiableFields(existing, modified);
147149

@@ -159,6 +161,20 @@ protected Authority deleteByIdInner(UUID id, boolean forced) {
159161
return repository.save(existed);
160162
}
161163

164+
private Authority validateOnUpdateAndGetExisting(UUID id, Authority modified) {
165+
var existing = repository.findByIdAndDeletedFalse(id).orElseThrow(() -> new AuthorityNotFoundException(id));
166+
167+
var sourceFileId = Optional.ofNullable(modified.getAuthoritySourceFile())
168+
.map(AuthoritySourceFile::getId)
169+
.orElse(null);
170+
if (sourceFileId != null && !sourceFileRepository.existsById(sourceFileId)) {
171+
throw new AuthoritySourceFileNotFoundException(sourceFileId);
172+
}
173+
174+
olCheck(modified, existing, id);
175+
return existing;
176+
}
177+
162178
private static void olCheck(Authority modified, Authority existing, UUID id) {
163179
if (modified.getVersion() < existing.getVersion()) {
164180
throw OptimisticLockingException.optimisticLockingOnUpdate(id, existing.getVersion(), modified.getVersion());

src/main/java/org/folio/entlinks/service/authority/AuthoritySourceFileService.java

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
import org.folio.entlinks.domain.entity.AuthoritySourceFile;
1616
import org.folio.entlinks.domain.entity.AuthoritySourceFileCode;
1717
import org.folio.entlinks.domain.repository.AuthorityRepository;
18+
import org.folio.entlinks.domain.repository.AuthoritySourceFileJdbcRepository;
1819
import org.folio.entlinks.domain.repository.AuthoritySourceFileRepository;
1920
import org.folio.entlinks.exception.AuthoritySourceFileHridException;
2021
import org.folio.entlinks.exception.AuthoritySourceFileNotFoundException;
2122
import org.folio.entlinks.exception.OptimisticLockingException;
2223
import org.folio.entlinks.exception.RequestBodyValidationException;
2324
import org.folio.spring.FolioExecutionContext;
24-
import org.folio.spring.FolioModuleMetadata;
2525
import org.folio.spring.data.OffsetRequest;
2626
import org.folio.tenant.domain.dto.Parameter;
27+
import org.springframework.context.annotation.Primary;
2728
import org.springframework.dao.DataAccessException;
2829
import org.springframework.data.domain.Page;
2930
import org.springframework.jdbc.core.JdbcTemplate;
@@ -33,19 +34,21 @@
3334
import org.springframework.transaction.annotation.Propagation;
3435
import org.springframework.transaction.annotation.Transactional;
3536

36-
@Service
37+
@Primary
38+
@Service("authoritySourceFileService")
3739
@AllArgsConstructor
3840
@Log4j2
39-
public class AuthoritySourceFileService {
41+
public class AuthoritySourceFileService implements AuthoritySourceFileServiceI {
4042

4143
private static final String AUTHORITY_SEQUENCE_NAME_TEMPLATE = "hrid_authority_local_file_%s_seq";
4244
private final AuthoritySourceFileRepository repository;
45+
private final AuthoritySourceFileJdbcRepository jdbcRepository;
4346
private final AuthorityRepository authorityRepository;
4447
private final AuthoritySourceFileMapper mapper;
4548
private final JdbcTemplate jdbcTemplate;
46-
private final FolioModuleMetadata moduleMetadata;
4749
private final FolioExecutionContext folioExecutionContext;
4850

51+
@Override
4952
public Page<AuthoritySourceFile> getAll(Integer offset, Integer limit, String cql) {
5053
log.debug("getAll:: Attempts to find all AuthoritySourceFile by [offset: {}, limit: {}, cql: {}]", offset, limit,
5154
cql);
@@ -57,27 +60,14 @@ public Page<AuthoritySourceFile> getAll(Integer offset, Integer limit, String cq
5760
return repository.findByCql(cql, new OffsetRequest(offset, limit));
5861
}
5962

60-
/**
61-
* Retrieves {@link AuthoritySourceFile} by the given id.
62-
*
63-
* @param id {@link UUID} ID of the authority source file being retrieved
64-
* @return retrieved {@link AuthoritySourceFile}
65-
*
66-
* Note: This method assumes the authority source file exists for the given ID and thus throws exception in case
67-
* no authority source file is found
68-
*/
63+
@Override
6964
public AuthoritySourceFile getById(UUID id) {
7065
log.debug("getById:: Loading AuthoritySourceFile by ID [id: {}]", id);
7166

7267
return repository.findById(id).orElseThrow(() -> new AuthoritySourceFileNotFoundException(id));
7368
}
7469

75-
/**
76-
* Searches for the Authority Source File for the given ID.
77-
*
78-
* @param id {@link UUID} ID of the authority source file being searched
79-
* @return found {@link AuthoritySourceFile} instance or null if it is not found
80-
*/
70+
@Override
8171
public AuthoritySourceFile findById(UUID id) {
8272
log.debug("findById:: Querying for AuthoritySourceFile by ID [id: {}]", id);
8373

@@ -88,12 +78,7 @@ public AuthoritySourceFile findById(UUID id) {
8878
return repository.findById(id).orElse(null);
8979
}
9080

91-
/**
92-
* Searches for the Authority Source File for the given name.
93-
*
94-
* @param name Name of the authority source file being searched
95-
* @return found {@link AuthoritySourceFile} instance or null if it is not found
96-
*/
81+
@Override
9782
public AuthoritySourceFile findByName(String name) {
9883
log.debug("findById:: Querying for AuthoritySourceFile by name [name: {}]", name);
9984

@@ -104,6 +89,7 @@ public AuthoritySourceFile findByName(String name) {
10489
return repository.findByName(name).orElse(null);
10590
}
10691

92+
@Override
10793
@Transactional
10894
public AuthoritySourceFile create(AuthoritySourceFile entity) {
10995
log.debug("create:: Attempting to create AuthoritySourceFile [entity: {}]", entity);
@@ -115,6 +101,7 @@ public AuthoritySourceFile create(AuthoritySourceFile entity) {
115101
return repository.save(entity);
116102
}
117103

104+
@Override
118105
@Transactional(propagation = Propagation.REQUIRES_NEW)
119106
@Retryable(
120107
retryFor = OptimisticLockingException.class,
@@ -124,6 +111,7 @@ public AuthoritySourceFile update(UUID id, AuthoritySourceFile modified) {
124111
return updateInner(id, modified, null);
125112
}
126113

114+
@Override
127115
@Transactional(propagation = Propagation.REQUIRES_NEW)
128116
@Retryable(
129117
retryFor = OptimisticLockingException.class,
@@ -134,18 +122,14 @@ public AuthoritySourceFile update(UUID id, AuthoritySourceFile modified,
134122
return updateInner(id, modified, publishConsumer);
135123
}
136124

125+
@Override
137126
public void deleteById(UUID id) {
138127
log.debug("deleteById:: Attempt to delete AuthoritySourceFile by [id: {}]", id);
139-
var authoritySourceFile = repository.findById(id)
140-
.orElseThrow(() -> new AuthoritySourceFileNotFoundException(id));
141-
if (!FOLIO.equals(authoritySourceFile.getSource())) {
142-
repository.deleteById(id);
143-
} else {
144-
throw new RequestBodyValidationException("Cannot delete Authority source file with source 'folio'",
145-
List.of(new Parameter("source").value(authoritySourceFile.getSource().name())));
146-
}
128+
validateOnDelete(id);
129+
repository.deleteById(id);
147130
}
148131

132+
@Override
149133
public String nextHrid(UUID id) {
150134
log.debug("nextHrid:: Attempting to get next AuthoritySourceFile HRID [id: {}]", id);
151135
var sourceFile = getById(id);
@@ -162,17 +146,19 @@ public String nextHrid(UUID id) {
162146
}
163147
}
164148

149+
@Override
165150
public boolean authoritiesExistForSourceFile(UUID sourceFileId) {
166151
return authorityRepository.existsAuthorityByAuthoritySourceFileId(sourceFileId);
167152
}
168153

154+
@Override
169155
public boolean authoritiesExistForSourceFile(UUID sourceFileId, String tenantId, String tableName) {
170156
if (sourceFileId == null || tenantId == null) {
171157
return false;
172158
}
173159

174160
var command = String.format("select exists (select true from %s.%s a where a.source_file_id='%s' limit 1)",
175-
moduleMetadata.getDBSchemaName(tenantId), tableName, sourceFileId);
161+
folioExecutionContext.getFolioModuleMetadata().getDBSchemaName(tenantId), tableName, sourceFileId);
176162
return Boolean.TRUE.equals(jdbcTemplate.queryForObject(command, Boolean.class));
177163
}
178164

@@ -268,21 +254,17 @@ private void copyModifiableFields(AuthoritySourceFile existingEntity, AuthorityS
268254
}
269255
}
270256

257+
@Override
271258
public void createSequence(String sequenceName, int startNumber) {
272-
var command = String.format("""
273-
CREATE SEQUENCE %s MINVALUE %d INCREMENT BY 1 OWNED BY %s.authority_source_file.sequence_name;
274-
""",
275-
sequenceName, startNumber, moduleMetadata.getDBSchemaName(folioExecutionContext.getTenantId()));
276-
jdbcTemplate.execute(command);
259+
jdbcRepository.createSequence(sequenceName, startNumber);
277260
}
278261

262+
@Override
279263
public void deleteSequence(String sequenceName) {
280-
var command = String.format("DROP SEQUENCE IF EXISTS %s.%s;",
281-
moduleMetadata.getDBSchemaName(folioExecutionContext.getTenantId()), sequenceName);
282-
jdbcTemplate.execute(command);
264+
jdbcRepository.dropSequence(sequenceName);
283265
}
284266

285-
private void updateSequenceStartNumber(AuthoritySourceFile existing, AuthoritySourceFile modified) {
267+
protected void updateSequenceStartNumber(AuthoritySourceFile existing, AuthoritySourceFile modified) {
286268
if (!Objects.equals(existing.getHridStartNumber(), modified.getHridStartNumber())
287269
&& existing.getHridStartNumber() != null && modified.getHridStartNumber() != null) {
288270
var sequenceName = existing.getSequenceName();
@@ -291,4 +273,12 @@ private void updateSequenceStartNumber(AuthoritySourceFile existing, AuthoritySo
291273
createSequence(sequenceName, modifiedStartNumber);
292274
}
293275
}
276+
277+
protected void validateOnDelete(UUID id) {
278+
var sourceFile = repository.findById(id).orElseThrow(() -> new AuthoritySourceFileNotFoundException(id));
279+
if (FOLIO.equals(sourceFile.getSource())) {
280+
throw new RequestBodyValidationException("Cannot delete Authority source file with source 'folio'",
281+
List.of(new Parameter("source").value(sourceFile.getSource().name())));
282+
}
283+
}
294284
}

0 commit comments

Comments
 (0)