fix(backend): rename users table to app_users (closes #418) #419
@@ -104,7 +104,7 @@ public interface AuditLogQueryRepository extends JpaRepository<AuditLog, UUID> {
|
||||
ag.happened_at_until AS happenedAtUntil,
|
||||
(ag.payload->>'commentId')::uuid AS commentId
|
||||
FROM aggregated ag
|
||||
LEFT JOIN users u ON u.id = ag.actor_id
|
||||
LEFT JOIN app_users u ON u.id = ag.actor_id
|
||||
ORDER BY ag.happened_at DESC
|
||||
LIMIT :limit
|
||||
""", nativeQuery = true)
|
||||
@@ -157,7 +157,7 @@ public interface AuditLogQueryRepository extends JpaRepository<AuditLog, UUID> {
|
||||
COALESCE(u.color, '') AS actorColor,
|
||||
CONCAT_WS(' ', u.first_name, u.last_name) AS actorName
|
||||
FROM audit_log a
|
||||
LEFT JOIN users u ON u.id = a.actor_id
|
||||
LEFT JOIN app_users u ON u.id = a.actor_id
|
||||
WHERE a.kind IN ('ANNOTATION_CREATED', 'TEXT_SAVED', 'BLOCK_REVIEWED')
|
||||
AND a.document_id IN :documentIds
|
||||
AND a.actor_id IS NOT NULL
|
||||
@@ -189,7 +189,7 @@ public interface AuditLogQueryRepository extends JpaRepository<AuditLog, UUID> {
|
||||
ORDER BY MAX(a.happened_at) DESC
|
||||
) AS rn
|
||||
FROM audit_log a
|
||||
LEFT JOIN users u ON u.id = a.actor_id
|
||||
LEFT JOIN app_users u ON u.id = a.actor_id
|
||||
WHERE a.kind IN ('ANNOTATION_CREATED', 'TEXT_SAVED', 'BLOCK_REVIEWED')
|
||||
AND a.document_id IN :documentIds
|
||||
AND a.actor_id IS NOT NULL
|
||||
|
||||
@@ -24,7 +24,7 @@ import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@Table(name = "app_users")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@@ -69,7 +69,7 @@ public class AppUser {
|
||||
private boolean notifyOnMention = false;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(name = "users_groups", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "group_id"))
|
||||
@JoinTable(name = "app_users_groups", joinColumns = @JoinColumn(name = "app_user_id"), inverseJoinColumns = @JoinColumn(name = "group_id"))
|
||||
@Builder.Default
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Set<UserGroup> groups = new HashSet<>();
|
||||
|
||||
@@ -71,7 +71,7 @@ public class DocumentComment {
|
||||
@JoinTable(
|
||||
name = "comment_mentions",
|
||||
joinColumns = @JoinColumn(name = "comment_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "user_id")
|
||||
inverseJoinColumns = @JoinColumn(name = "app_user_id")
|
||||
)
|
||||
@JsonIgnore
|
||||
@Builder.Default
|
||||
|
||||
@@ -30,7 +30,7 @@ public class PasswordResetToken {
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
@JoinColumn(name = "app_user_id", nullable = false)
|
||||
private AppUser user;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 64)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Align the auth-account table name with the AppUser entity (issue #418).
|
||||
-- The historical mismatch (table 'users' alongside table 'persons') misleads
|
||||
-- schema-first readers into assuming the two are related. Renaming the table to
|
||||
-- 'app_users' makes the deliberate split between auth accounts and historical
|
||||
-- persons explicit at the schema layer.
|
||||
--
|
||||
-- Scope: the table itself, the users_groups join table, and the three FK
|
||||
-- columns whose name is literally 'user_id'. Semantic FK columns
|
||||
-- (audit_log.actor_id, notifications.recipient_id, document_versions.editor_id,
|
||||
-- document_comments.author_id, transcription_blocks.created_by/updated_by,
|
||||
-- transcription_block_versions.changed_by, document_annotations.created_by,
|
||||
-- ocr_training_runs.triggered_by, invite_tokens.created_by, geschichten.author_id)
|
||||
-- keep their names — the role they describe is the documentation, not the type.
|
||||
|
||||
ALTER TABLE users RENAME TO app_users;
|
||||
|
||||
ALTER TABLE users_groups RENAME TO app_users_groups;
|
||||
ALTER TABLE app_users_groups RENAME COLUMN user_id TO app_user_id;
|
||||
|
||||
ALTER TABLE comment_mentions RENAME COLUMN user_id TO app_user_id;
|
||||
ALTER TABLE password_reset_tokens RENAME COLUMN user_id TO app_user_id;
|
||||
@@ -32,7 +32,7 @@ class AuditLogQueryRepositoryContributorsTest {
|
||||
|
||||
@Test
|
||||
@Sql(statements = {
|
||||
"INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#f00')",
|
||||
"INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#f00')",
|
||||
"INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test', 'test.pdf', 'PLACEHOLDER')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-000000000001', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')"
|
||||
})
|
||||
@@ -47,11 +47,11 @@ class AuditLogQueryRepositoryContributorsTest {
|
||||
|
||||
@Test
|
||||
@Sql(statements = {
|
||||
"INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#aaa')",
|
||||
"INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000002', true, 'b@test.com', 'pw', 'Ben', 'Wolf', '#bbb')",
|
||||
"INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000003', true, 'c@test.com', 'pw', 'Clara', 'Zorn', '#ccc')",
|
||||
"INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000004', true, 'd@test.com', 'pw', 'Dirk', 'Ott', '#ddd')",
|
||||
"INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000005', true, 'e@test.com', 'pw', 'Eva', 'Kern', '#eee')",
|
||||
"INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000001', true, 'a@test.com', 'pw', 'Anna', 'Meier', '#aaa')",
|
||||
"INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000002', true, 'b@test.com', 'pw', 'Ben', 'Wolf', '#bbb')",
|
||||
"INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000003', true, 'c@test.com', 'pw', 'Clara', 'Zorn', '#ccc')",
|
||||
"INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000004', true, 'd@test.com', 'pw', 'Dirk', 'Ott', '#ddd')",
|
||||
"INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-000000000005', true, 'e@test.com', 'pw', 'Eva', 'Kern', '#eee')",
|
||||
"INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test', 'test.pdf', 'PLACEHOLDER')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id, happened_at) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-000000000001', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', now() - interval '5 hours')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id, happened_at) VALUES ('TEXT_SAVED', 'aaaaaaaa-aaaa-aaaa-aaaa-000000000002', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', now() - interval '4 hours')",
|
||||
|
||||
@@ -33,7 +33,7 @@ class AuditLogQueryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
@Sql(statements = {
|
||||
"INSERT INTO users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')",
|
||||
"INSERT INTO app_users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')",
|
||||
"INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')"
|
||||
})
|
||||
@@ -45,7 +45,7 @@ class AuditLogQueryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
@Sql(statements = {
|
||||
"INSERT INTO users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')",
|
||||
"INSERT INTO app_users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')",
|
||||
"INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"pageNumber\":1}')"
|
||||
})
|
||||
@@ -63,7 +63,7 @@ class AuditLogQueryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
@Sql(statements = {
|
||||
"INSERT INTO users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')",
|
||||
"INSERT INTO app_users (id, enabled, email, password) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw')",
|
||||
"INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"pageNumber\":1}')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id, payload) VALUES ('TEXT_SAVED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '{\"blockId\":\"ccc\",\"pageNumber\":1}')",
|
||||
@@ -82,7 +82,7 @@ class AuditLogQueryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
@Sql(statements = {
|
||||
"INSERT INTO users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw', 'Anna', 'Meier', '#f00')",
|
||||
"INSERT INTO app_users (id, enabled, email, password, first_name, last_name, color) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true, 'testuser@test.com', 'pw', 'Anna', 'Meier', '#f00')",
|
||||
"INSERT INTO documents (id, title, original_filename, status) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Test Doc', 'test.pdf', 'PLACEHOLDER')",
|
||||
"INSERT INTO audit_log (kind, actor_id, document_id) VALUES ('ANNOTATION_CREATED', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb')"
|
||||
})
|
||||
|
||||
@@ -51,10 +51,10 @@ class AuditLogQueryRepositoryRolledUpTest {
|
||||
|
||||
private void insertUserAndDocs() {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (id, enabled, email, password) VALUES (?, true, ?, 'pw')",
|
||||
"INSERT INTO app_users (id, enabled, email, password) VALUES (?, true, ?, 'pw')",
|
||||
USER_ID, "rollup-" + USER_ID + "@test.com");
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO users (id, enabled, email, password) VALUES (?, true, ?, 'pw')",
|
||||
"INSERT INTO app_users (id, enabled, email, password) VALUES (?, true, ?, 'pw')",
|
||||
OTHER_USER_ID, "rollup-" + OTHER_USER_ID + "@test.com");
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO documents (id, title, original_filename, status) VALUES (?, 'Brief A', 'a.pdf', 'PLACEHOLDER')",
|
||||
|
||||
@@ -280,7 +280,7 @@ class MigrationIntegrationTest {
|
||||
void v44_emailNotNullConstraint_rejectsInsertWithNullEmail() {
|
||||
assertThatThrownBy(() ->
|
||||
jdbc.update("""
|
||||
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
VALUES (gen_random_uuid(), NULL, 'hash', true, false, false)
|
||||
""")
|
||||
).isInstanceOf(DataIntegrityViolationException.class);
|
||||
@@ -290,13 +290,13 @@ class MigrationIntegrationTest {
|
||||
void v44_emailUniqueConstraint_rejectsDuplicateEmail() {
|
||||
String email = "unique-test-" + UUID.randomUUID() + "@example.com";
|
||||
jdbc.update("""
|
||||
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
VALUES (gen_random_uuid(), ?, 'hash', true, false, false)
|
||||
""", email);
|
||||
|
||||
assertThatThrownBy(() ->
|
||||
jdbc.update("""
|
||||
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
VALUES (gen_random_uuid(), ?, 'hash', true, false, false)
|
||||
""", email)
|
||||
).isInstanceOf(DataIntegrityViolationException.class);
|
||||
@@ -446,7 +446,7 @@ class MigrationIntegrationTest {
|
||||
private UUID insertUser(String email) {
|
||||
UUID id = UUID.randomUUID();
|
||||
jdbc.update("""
|
||||
INSERT INTO users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
INSERT INTO app_users (id, email, password, enabled, notify_on_reply, notify_on_mention)
|
||||
VALUES (?, ?, 'hash', true, false, false)
|
||||
""", id, email);
|
||||
return id;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.raddatz.familienarchiv.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Verifies the V60 rename: {@code users → app_users} and the three literal
|
||||
* {@code user_id} FK columns. Semantic FK columns (e.g. {@code audit_log.actor_id})
|
||||
* are deliberately untouched and asserted to remain. See issue #418.
|
||||
*/
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import({PostgresContainerConfig.class, FlywayConfig.class})
|
||||
class RenameUsersToAppUsersMigrationTest {
|
||||
|
||||
@Autowired JdbcTemplate jdbc;
|
||||
|
||||
@Test
|
||||
void v60_appUsersTableExists_andUsersTableIsGone() {
|
||||
assertThat(tableExists("app_users")).isTrue();
|
||||
assertThat(tableExists("users")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void v60_appUsersGroupsJoinTableExists_withRenamedColumn() {
|
||||
assertThat(tableExists("app_users_groups")).isTrue();
|
||||
assertThat(tableExists("users_groups")).isFalse();
|
||||
assertThat(columnExists("app_users_groups", "app_user_id")).isTrue();
|
||||
assertThat(columnExists("app_users_groups", "user_id")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void v60_commentMentionsColumnRenamed() {
|
||||
assertThat(columnExists("comment_mentions", "app_user_id")).isTrue();
|
||||
assertThat(columnExists("comment_mentions", "user_id")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void v60_passwordResetTokensColumnRenamed() {
|
||||
assertThat(columnExists("password_reset_tokens", "app_user_id")).isTrue();
|
||||
assertThat(columnExists("password_reset_tokens", "user_id")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void v60_auditLogActorIdRemains() {
|
||||
assertThat(columnExists("audit_log", "actor_id")).isTrue();
|
||||
assertThat(columnExists("audit_log", "app_user_id")).isFalse();
|
||||
}
|
||||
|
||||
private boolean tableExists(String tableName) {
|
||||
Integer count = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM information_schema.tables " +
|
||||
"WHERE table_schema = 'public' AND table_name = ?",
|
||||
Integer.class, tableName);
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private boolean columnExists(String tableName, String columnName) {
|
||||
Integer count = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM information_schema.columns " +
|
||||
"WHERE table_schema = 'public' AND table_name = ? AND column_name = ?",
|
||||
Integer.class, tableName, columnName);
|
||||
return count != null && count > 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user