Skip to content

Add Ability to Lock User Sequences #1641

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

Merged
merged 4 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package gov.nasa.jpl.aerie.database;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

@SuppressWarnings("SqlSourceToSinkFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserSequenceTests {
private final String sequenceDefinition = """
C BAKE_BREAD
C PEEL_BANANA "fromStem"
C GROW_BANANA 0 0
""";
private DatabaseTestHelper helper;
private MerlinDatabaseTestHelper merlinHelper;

private Connection connection;
private int commandDictionaryId;
private int parcelId;
private int workspaceId;
private int sequenceId;
private MerlinDatabaseTestHelper.User sequenceUser;

void setConnection(DatabaseTestHelper helper) {
connection = helper.connection();
}


@BeforeEach
void beforeEach() throws SQLException {
commandDictionaryId = createCommandDictionary("/path/to/dictionary.ts", "foo", "1.0.0", "{}");
parcelId = createParcel("Test Parcel");
workspaceId = createWorkspace("Test Workspace", sequenceUser.name());
sequenceId = createUserSequence("Test Sequence", parcelId, workspaceId, sequenceDefinition, sequenceUser.name());
}

@AfterEach
void afterEach() throws SQLException {
helper.clearSchema("merlin");
helper.clearSchema("sequencing");
}

@BeforeAll
void beforeAll() throws SQLException, IOException, InterruptedException {
helper = new DatabaseTestHelper("aerie_user_sequence_test", "User Sequence Tests");
setConnection(helper);
merlinHelper = new MerlinDatabaseTestHelper(connection);
sequenceUser = merlinHelper.insertUser("SequenceTest");
}

@AfterAll
void afterAll() throws SQLException, IOException, InterruptedException {
helper.clearSchema("merlin");
helper.clearSchema("sequencing");
helper.close();
}

// region Helper Functions
private int createCommandDictionary(String dictionaryPath, String mission, String version, String dictionaryJSON) throws SQLException {
try (final var statement = connection.createStatement()) {
final var res = statement.executeQuery(
//language=sql
"""
INSERT INTO sequencing.command_dictionary (dictionary_path, mission, version, parsed_json)
VALUES ('%s', '%s', '%s', '%s')
RETURNING id;
""".formatted(dictionaryPath, mission, version, dictionaryJSON));

res.next();
return res.getInt("id");
}
}

private int createParcel(String name) throws SQLException {
return createParcel(name, commandDictionaryId, sequenceUser.name());
}

private int createParcel(String name, int dictionaryId, String username) throws SQLException {
try (final var statement = connection.createStatement()) {
final var res = statement.executeQuery(
//language=sql
"""
INSERT INTO sequencing.parcel (name, command_dictionary_id, owner)
VALUES ('%s', '%d', '%s')
RETURNING id;
""".formatted(name, dictionaryId, username));

res.next();
return res.getInt("id");
}
}

private int createWorkspace(String name, String username) throws SQLException {
try (final var statement = connection.createStatement()) {
final var res = statement.executeQuery(
//language=sql
"""
INSERT INTO sequencing.workspace (name, owner)
VALUES ('%s', '%s')
RETURNING id;
""".formatted(name, username));

res.next();
return res.getInt("id");
}
}

private int createUserSequence(String name, int parcelId, int workspaceId, String definition, String username) throws SQLException {
try (final var statement = connection.createStatement()) {
final var res = statement.executeQuery(
//language=sql
"""
INSERT INTO sequencing.user_sequence (name, parcel_id, workspace_id, definition, owner)
VALUES ('%s', '%d', '%d', '%s', '%s')
RETURNING id;
""".formatted(name, parcelId, workspaceId, definition, username)
);

res.next();
return res.getInt("id");
}
}


private void assignParcelToSequence(int parcelId, int sequenceId) throws SQLException {
try (final var statement = connection.createStatement()) {
statement.execute(
//language=sql
"""
UPDATE sequencing.user_sequence
SET parcel_id = '%d'
WHERE id = %d
""".formatted(parcelId, sequenceId)
);
}
}

private void updateUserSequenceIsLocked(int sequenceId, boolean isLocked) throws SQLException {
try (final var statement = connection.createStatement()) {
statement.execute(
//language=sql
"""
UPDATE sequencing.user_sequence
SET is_locked = '%b'
WHERE id = %d
""".formatted(isLocked, sequenceId)
);
}
}

private void updateUserSequenceName(int sequenceId, String sequenceName) throws SQLException {
try (final var statement = connection.createStatement()) {
statement.execute(
//language=sql
"""
UPDATE sequencing.user_sequence
SET name = '%s'
WHERE id = %d
""".formatted(sequenceName, sequenceId)
);
}
}

private void deleteUserSequence(int sequenceId) throws SQLException {
try (final var statement = connection.createStatement()) {
statement.execute(
//language=sql
"""
DELETE FROM sequencing.user_sequence
WHERE id = %d;
""".formatted(sequenceId)
);
}
}

private ArrayList<UserSequence> getUserSequences(int sequenceId) throws SQLException {
try (final var statement = connection.createStatement()) {
final var sequences = new ArrayList<UserSequence>();
final var res = statement.executeQuery(
//language=sql
"""
SELECT id, name, definition, parcel_id, is_locked, workspace_id, owner
FROM sequencing.user_sequence
WHERE id = %d
""".formatted(sequenceId)
);

while (res.next()) {
sequences.add(new UserSequence(
res.getInt("id"),
res.getString("name"),
res.getString("definition"),
res.getInt("parcel_id"),
res.getBoolean("is_locked"),
res.getInt("workspace_id"),
res.getString("owner")));
}
return sequences;
}
}
// endregion

//region Records
private record UserSequence(int id, String name, String definition, int parcel_id, boolean is_locked, int workspace_id, String owner) {}
//endregion

@Test
void addUserSequence() throws SQLException {
final var userSequence = getUserSequences(sequenceId);

final var expectedUserSequence = new ArrayList<UserSequence>(1);
expectedUserSequence.add(new UserSequence(sequenceId, "Test Sequence", sequenceDefinition, parcelId, false, workspaceId, sequenceUser.name()));

assertEquals(expectedUserSequence, userSequence);
}

@Test
void updateUserSequenceParcel() throws SQLException {
final var testParcelId = createParcel("Test Parcel 2");
assertDoesNotThrow(()->assignParcelToSequence(testParcelId, sequenceId));

assignParcelToSequence(parcelId, sequenceId);
}

@Test
void lockUserSequence() throws SQLException {
updateUserSequenceIsLocked(sequenceId, true);

try {
updateUserSequenceName(sequenceId, "Foo Sequence");
} catch (SQLException ex) {
if (!ex.getMessage().contains("Cannot update locked user sequence.")) {
throw ex;
}
}
}

@Test
void lockDoesNotAffectOtherSequence() throws SQLException {
final var otherSequenceId = createUserSequence("Test Sequence 2", parcelId, workspaceId, sequenceDefinition, sequenceUser.name());

assertDoesNotThrow(()->updateUserSequenceName(otherSequenceId, "Bar Sequence"));
}

@Test
void unlockUserSequence() throws SQLException {
updateUserSequenceIsLocked(sequenceId, false);

assertDoesNotThrow(()->updateUserSequenceName(sequenceId, "Foo Sequence"));

final var userSequence = getUserSequences(sequenceId);
assertEquals(false, userSequence.get(0).is_locked);
}

@Test
void cannotDeleteLockedSequence() throws SQLException {
updateUserSequenceIsLocked(sequenceId, true);

try {
deleteUserSequence(sequenceId);
} catch (SQLException ex) {
if (!ex.getMessage().contains("Cannot delete locked user sequence.")) {
throw ex;
}
}
}

@Test
void canDeleteUnlockedSequence() throws SQLException {
final var otherSequenceId = createUserSequence("Test Sequence 3", parcelId, workspaceId, sequenceDefinition, sequenceUser.name());

assertDoesNotThrow(()->deleteUserSequence(otherSequenceId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,24 @@ select_permissions:
insert_permissions:
- role: aerie_admin
permission:
columns: [definition, seq_json, name, owner, parcel_id, workspace_id]
columns: [definition, seq_json, name, owner, is_locked, parcel_id, workspace_id]
check: {}
set:
owner: "x-hasura-user-id"
- role: user
permission:
columns: [definition, seq_json, name, owner, parcel_id, workspace_id]
columns: [definition, seq_json, name, owner, is_locked, parcel_id, workspace_id]
check: {}
set:
owner: "x-hasura-user-id"
update_permissions:
- role: aerie_admin
permission:
columns: [definition, seq_json, name, owner, parcel_id, workspace_id]
columns: [definition, seq_json, name, owner, is_locked, parcel_id, workspace_id]
filter: {}
- role: user
permission:
columns: [definition, seq_json, name, owner, parcel_id, workspace_id]
columns: [definition, seq_json, name, owner, is_locked, parcel_id, workspace_id]
filter: { "owner": { "_eq": "x-hasura-user-id" } }
delete_permissions:
- role: aerie_admin
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
drop trigger check_locked_delete on sequencing.user_sequence;
drop function sequencing.check_is_locked_delete();
drop trigger check_locked_update on sequencing.user_sequence;
drop function sequencing.check_is_locked_update();

create trigger set_timestamp
before update on sequencing.user_sequence
for each row
execute function util_functions.set_updated_at();

alter table sequencing.user_sequence
drop column is_locked;

call migrations.mark_migration_rolled_back('17');
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
alter table sequencing.user_sequence
add column is_locked boolean not null default false;

comment on column sequencing.user_sequence.is_locked is e''
'A boolean representing whether this user sequence is editable.';

-- Dropping trigger because check_locked_update includes updating the timestamp
drop trigger set_timestamp on sequencing.user_sequence;

create function sequencing.check_is_locked_update()
returns trigger
language plpgsql as $$
begin
if old.is_locked and new.is_locked then
raise exception 'Cannot update locked user sequence.';
end if;

-- Update the updated_at timestamp
new.updated_at = now();
return new;
end
$$;

create trigger check_locked_update
before update on sequencing.user_sequence
for each row
execute function sequencing.check_is_locked_update();

create function sequencing.check_is_locked_delete()
returns trigger
language plpgsql as $$
begin
if old.is_locked then
raise exception 'Cannot delete locked user sequence.';
end if;

return old;
end
$$;

create trigger check_locked_delete
before delete on sequencing.user_sequence
for each row
execute function sequencing.check_is_locked_delete();

call migrations.mark_migration_applied('17');
6 changes: 3 additions & 3 deletions deployment/postgres-init-db/sql/applied_migrations.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/*
This file denotes which migrations occur "before" this version of the schema.
*/

This file denotes which migrations occur "before" this version of the schema.
*/
call migrations.mark_migration_applied('0');
call migrations.mark_migration_applied('1');
call migrations.mark_migration_applied('2');
Expand All @@ -19,3 +18,4 @@ call migrations.mark_migration_applied('13');
call migrations.mark_migration_applied('14');
call migrations.mark_migration_applied('15');
call migrations.mark_migration_applied('16');
call migrations.mark_migration_applied('17');
Loading
Loading