Skip to content
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
node-version: '22'
- uses: browser-actions/setup-chrome@v2
with:
chrome-version: '142'
chrome-version: '144'
install-chromedriver: true
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down Expand Up @@ -138,7 +138,7 @@ jobs:
node-version: '22'
- uses: browser-actions/setup-chrome@v2
with:
chrome-version: '142'
chrome-version: '143'
install-chromedriver: true
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down Expand Up @@ -186,7 +186,7 @@ jobs:
node-version: '22'
- uses: browser-actions/setup-chrome@v2
with:
chrome-version: '142'
chrome-version: '144'
install-chromedriver: true
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
20 changes: 12 additions & 8 deletions core/src/main/java/tanin/backdoor/core/BackdoorCoreServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,9 @@ public FullSystem start() throws Exception {
var primaryKeyFilters = json.asObject().get("primaryKeys").asArray().values().stream().map(s -> {
var o = s.asObject();
var value = o.get("value");
var operator = Filter.Operator.valueOf(o.get("operator").asString());

return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
return new Filter(o.get("name").asString(), value.asString(), operator);
}).toArray(Filter[]::new);

try (var engine = makeEngine(database)) {
Expand Down Expand Up @@ -339,24 +340,25 @@ public FullSystem start() throws Exception {
var primaryKeyFilters = json.asObject().get("primaryKeys").asArray().values().stream().map(s -> {
var o = s.asObject();
var value = o.get("value");
var operator = Filter.Operator.valueOf(o.get("operator").asString());

return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
return new Filter(o.get("name").asString(), value.asString(), operator);
}).toArray(Filter[]::new);

try (var engine = makeEngine(database)) {
var column = Arrays.stream(engine.getColumns(tableName)).filter(c -> c.name.equals(columnName)).findFirst().orElse(null);

var newSantiziedValue = setToNull ? null : newValue;
engine.update(tableName, column, newSantiziedValue, primaryKeyFilters);
assert column != null;
var newSanitized = setToNull ? null : newValue;

engine.update(tableName, column, newSanitized, primaryKeyFilters);
AtomicReference<JsonValue> newFetchedValue = new AtomicReference<>(Json.NULL);
engine.select(
tableName,
column,
Arrays.stream(primaryKeyFilters)
.peek(p -> {
if (p.name.equals(columnName)) {
p.value = newSantiziedValue;
p.value = newSanitized;
}
})
.toArray(Filter[]::new),
Expand Down Expand Up @@ -434,8 +436,9 @@ public FullSystem start() throws Exception {
var filters = json.asObject().get("filters").asArray().values().stream().map(s -> {
var o = s.asObject();
var value = o.get("value");
var operator = Filter.Operator.valueOf(o.get("operator").asString());

return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
return new Filter(o.get("name").asString(), value.asString(), operator);
}).toArray(Filter[]::new);
var sorts = json.asObject().get("sorts").asArray().values().stream().map(s -> {
var o = s.asObject();
Expand Down Expand Up @@ -554,8 +557,9 @@ public FullSystem start() throws Exception {
var filters = json.asObject().get("filters").asArray().values().stream().map(s -> {
var o = s.asObject();
var value = o.get("value");
var operator = Filter.Operator.valueOf(o.get("operator").asString());

return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
return new Filter(o.get("name").asString(), value.asString(), operator);
}).toArray(Filter[]::new);
var sorts = json.asObject().get("sorts").asArray().values().stream().map(s -> {
var o = s.asObject();
Expand Down
13 changes: 11 additions & 2 deletions core/src/main/java/tanin/backdoor/core/Filter.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@
import com.eclipsesource.json.JsonValue;

public class Filter {
public enum Operator {
EQUAL,
IS_NULL,
IS_NOT_NULL
}


public String name;
public String value;
public Operator operator;

Filter(String name, String value) {
Filter(String name, String value, Operator operator) {
this.name = name;
this.value = value;
this.operator = operator;
}

public JsonValue toJson() {
return Json.object()
.add("name", name)
.add("value", value);
.add("value", value)
.add("operator", operator.toString());
}
}
17 changes: 13 additions & 4 deletions core/src/main/java/tanin/backdoor/core/engine/Engine.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,19 @@ public String makeWhereClause(Filter[] filters) {
var clauses = Arrays
.stream(filters)
.map(s -> {
var value = s.value == null ? "NULL" : makeSqlLiteral(s.value);
var op = s.value == null ? "IS" : "=";

return makeSqlName(s.name) + " " + op + " " + value;
String operatorAndValue;

if (s.operator == Filter.Operator.IS_NULL) {
operatorAndValue = " IS NULL";
} else if (s.operator == Filter.Operator.IS_NOT_NULL) {
operatorAndValue = " IS NOT NULL";
} else if (s.operator == Filter.Operator.EQUAL) {
operatorAndValue = " = " + makeSqlLiteral(s.value);
} else {
throw new RuntimeException("Unknown operator: " + s.operator);
}

return makeSqlName(s.name) + " " + operatorAndValue;
})
.toArray(String[]::new);
whereClause = " WHERE " + String.join(" AND ", clauses);
Expand Down
39 changes: 38 additions & 1 deletion core/src/test/java/tanin/backdoor/postgres/TableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ void deleteRow() throws InterruptedException {
}

@Test
void filterRow() throws InterruptedException {
void filterRowSpecificValue() throws InterruptedException {
go("/");
click(tid("database-item"));
waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status")));
Expand All @@ -323,6 +323,43 @@ void filterRow() throws InterruptedException {
waitUntil(() -> assertColumnValues("username", "test_user_2"));
}

@Test
void filterRowNullAndNotNull() throws Exception {
try (var engine = server.engineProvider.createEngine(postgresConfig, null)) {
engine.connection.createStatement().execute(
"""
INSERT INTO "user" (
id,
username,
password
) VALUES (
'100',
'null-user',
NULL
)
"""
);
}
go("/");
click(tid("database-item"));
waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status")));

click(tid("menu-items", "postgres", null, "menu-item-table", "user"));

waitUntil(() -> assertTrue(hasElem(tid("sheet-tab", "user"))));
waitUntil(() -> assertColumnValues("username", "test_user_1", "test_user_2", "test_user_3", "test_user_4", "null-user"));

click(tid("sheet-view-column-header", "password", null, "filter-button"));
click(tid("not-null-checkbox"));
click(tid("submit-button"));
waitUntil(() -> assertColumnValues("username", "test_user_1", "test_user_2", "test_user_3", "test_user_4"));

click(tid("sheet-view-column-header", "password", null, "filter-button"));
click(tid("null-checkbox"));
click(tid("submit-button"));
waitUntil(() -> assertColumnValues("username", "null-user"));
}

@Test
void sortRow() throws InterruptedException {
go("/");
Expand Down
20 changes: 20 additions & 0 deletions core/src/test/java/tanin/backdoor/ux/EditTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ void editToNullAndRevertField() throws InterruptedException {
waitUntil(() -> assertEquals("new-password", elem(tid("sheet-column-value", "password")).getText().trim()));
}

@Test
void trimValue() throws InterruptedException {
go("/");
click(tid("database-item"));
waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status")));

click(tid("menu-items", "postgres", null, "menu-item-table", "user"));

click(tid("sheet-column-value", "password", null, "edit-field-button"));
fill(tid("new-value"), "\n \nnew-password\n \n");
click(tid("trim-value-checkbox"));
click(tid("submit-button"));
waitUntil(() -> assertEquals(" \n \nnew-password\n ", elem(tid("sheet-column-value", "password")).getText()));

click(tid("sheet-column-value", "password", null, "edit-field-button"));
click(tid("trim-value-checkbox"));
click(tid("submit-button"));
waitUntil(() -> assertEquals(" new-password", elem(tid("sheet-column-value", "password")).getText()));
}

@Test
void useNowTimestamp() throws Exception {
try (var engine = server.engineProvider.createEngine(postgresConfig, null)) {
Expand Down
40 changes: 29 additions & 11 deletions frontend/svelte/_edit_modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let rowIndex_: number | null = null
let currentValue: any
let currentColumn: Column | null
let setToNull: boolean = false
let shouldTrim: boolean = true

let isLoading = false
let errors: string[] = []
Expand Down Expand Up @@ -45,7 +46,11 @@ export function open(value: any, column: Column, rowValues: any[], rowIndex: num
.map((column, index) => {
if (column.isPrimaryKey) {
const value = rowValues[index];
return {name: column.name, value: value === null ? null : ('' + value)}
return {
name: column.name,
value: ('' + value),
operator: value === null ? 'IS NULL' : 'EQUAL'
}
} else {
return null
}
Expand Down Expand Up @@ -86,7 +91,7 @@ async function submit() {
table: sheet.name,
primaryKeys,
column: currentColumn!.name,
value: currentValue,
value: shouldTrim ? currentValue.trim() : currentValue,
setToNull,
})

Expand Down Expand Up @@ -165,15 +170,28 @@ async function submit() {
autocorrect="off"
></textarea>
</div>
{#if currentColumn && currentColumn.type === 'TIMESTAMP' && !setToNull}
<div
class="text-xs underline cursor-pointer text-neutral-content"
data-test-id="timestamp-now-button"
onclick={() => {
currentValue = new Date().toISOString()
}}
>Use the current timestamp
</div>
{#if !setToNull && currentColumn}
{#if currentColumn.type === 'TIMESTAMP'}
<div
class="text-xs underline cursor-pointer text-neutral-content"
data-test-id="timestamp-now-button"
onclick={() => {
currentValue = new Date().toISOString()
}}
>Use the current timestamp
</div>
{:else if currentColumn.type === 'STRING'}
<label class="flex gap-2 items-center cursor-pointer">
<input
data-test-id="trim-value-checkbox"
type="checkbox"
class="checkbox checkbox-xs"
bind:checked={shouldTrim}
disabled={isLoading}
/>
<span class="text-xs">Trim whitespaces before updating</span>
</label>
{/if}
{/if}
<ErrorPanel {errors}/>
<div class="flex items-center justify-between mt-2">
Expand Down
Loading
Loading