Skip to content

Commit 1328ffa

Browse files
committed
Trim and not null
1 parent f290b76 commit 1328ffa

9 files changed

Lines changed: 196 additions & 44 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
node-version: '22'
5050
- uses: browser-actions/setup-chrome@v2
5151
with:
52-
chrome-version: '142'
52+
chrome-version: '144'
5353
install-chromedriver: true
5454
- name: Setup Gradle
5555
uses: gradle/actions/setup-gradle@v3
@@ -138,7 +138,7 @@ jobs:
138138
node-version: '22'
139139
- uses: browser-actions/setup-chrome@v2
140140
with:
141-
chrome-version: '142'
141+
chrome-version: '143'
142142
install-chromedriver: true
143143
- name: Setup Gradle
144144
uses: gradle/actions/setup-gradle@v3
@@ -186,7 +186,7 @@ jobs:
186186
node-version: '22'
187187
- uses: browser-actions/setup-chrome@v2
188188
with:
189-
chrome-version: '142'
189+
chrome-version: '144'
190190
install-chromedriver: true
191191
- name: Setup Gradle
192192
uses: gradle/actions/setup-gradle@v3

core/src/main/java/tanin/backdoor/core/BackdoorCoreServer.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,9 @@ public FullSystem start() throws Exception {
274274
var primaryKeyFilters = json.asObject().get("primaryKeys").asArray().values().stream().map(s -> {
275275
var o = s.asObject();
276276
var value = o.get("value");
277+
var operator = Filter.Operator.valueOf(o.get("operator").asString());
277278

278-
return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
279+
return new Filter(o.get("name").asString(), value.asString(), operator);
279280
}).toArray(Filter[]::new);
280281

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

343-
return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
345+
return new Filter(o.get("name").asString(), value.asString(), operator);
344346
}).toArray(Filter[]::new);
345347

346348
try (var engine = makeEngine(database)) {
347349
var column = Arrays.stream(engine.getColumns(tableName)).filter(c -> c.name.equals(columnName)).findFirst().orElse(null);
348-
349-
var newSantiziedValue = setToNull ? null : newValue;
350-
engine.update(tableName, column, newSantiziedValue, primaryKeyFilters);
351350
assert column != null;
351+
var newSanitized = setToNull ? null : newValue;
352+
353+
engine.update(tableName, column, newSanitized, primaryKeyFilters);
352354
AtomicReference<JsonValue> newFetchedValue = new AtomicReference<>(Json.NULL);
353355
engine.select(
354356
tableName,
355357
column,
356358
Arrays.stream(primaryKeyFilters)
357359
.peek(p -> {
358360
if (p.name.equals(columnName)) {
359-
p.value = newSantiziedValue;
361+
p.value = newSanitized;
360362
}
361363
})
362364
.toArray(Filter[]::new),
@@ -434,8 +436,9 @@ public FullSystem start() throws Exception {
434436
var filters = json.asObject().get("filters").asArray().values().stream().map(s -> {
435437
var o = s.asObject();
436438
var value = o.get("value");
439+
var operator = Filter.Operator.valueOf(o.get("operator").asString());
437440

438-
return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
441+
return new Filter(o.get("name").asString(), value.asString(), operator);
439442
}).toArray(Filter[]::new);
440443
var sorts = json.asObject().get("sorts").asArray().values().stream().map(s -> {
441444
var o = s.asObject();
@@ -554,8 +557,9 @@ public FullSystem start() throws Exception {
554557
var filters = json.asObject().get("filters").asArray().values().stream().map(s -> {
555558
var o = s.asObject();
556559
var value = o.get("value");
560+
var operator = Filter.Operator.valueOf(o.get("operator").asString());
557561

558-
return new Filter(o.get("name").asString(), value.isNull() ? null : value.asString());
562+
return new Filter(o.get("name").asString(), value.asString(), operator);
559563
}).toArray(Filter[]::new);
560564
var sorts = json.asObject().get("sorts").asArray().values().stream().map(s -> {
561565
var o = s.asObject();

core/src/main/java/tanin/backdoor/core/Filter.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,27 @@
44
import com.eclipsesource.json.JsonValue;
55

66
public class Filter {
7+
public enum Operator {
8+
EQUAL,
9+
IS_NULL,
10+
IS_NOT_NULL
11+
}
12+
713

814
public String name;
915
public String value;
16+
public Operator operator;
1017

11-
Filter(String name, String value) {
18+
Filter(String name, String value, Operator operator) {
1219
this.name = name;
1320
this.value = value;
21+
this.operator = operator;
1422
}
1523

1624
public JsonValue toJson() {
1725
return Json.object()
1826
.add("name", name)
19-
.add("value", value);
27+
.add("value", value)
28+
.add("operator", operator.toString());
2029
}
2130
}

core/src/main/java/tanin/backdoor/core/engine/Engine.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,19 @@ public String makeWhereClause(Filter[] filters) {
154154
var clauses = Arrays
155155
.stream(filters)
156156
.map(s -> {
157-
var value = s.value == null ? "NULL" : makeSqlLiteral(s.value);
158-
var op = s.value == null ? "IS" : "=";
159-
160-
return makeSqlName(s.name) + " " + op + " " + value;
157+
String operatorAndValue;
158+
159+
if (s.operator == Filter.Operator.IS_NULL) {
160+
operatorAndValue = " IS NULL";
161+
} else if (s.operator == Filter.Operator.IS_NOT_NULL) {
162+
operatorAndValue = " IS NOT NULL";
163+
} else if (s.operator == Filter.Operator.EQUAL) {
164+
operatorAndValue = " = " + makeSqlLiteral(s.value);
165+
} else {
166+
throw new RuntimeException("Unknown operator: " + s.operator);
167+
}
168+
169+
return makeSqlName(s.name) + " " + operatorAndValue;
161170
})
162171
.toArray(String[]::new);
163172
whereClause = " WHERE " + String.join(" AND ", clauses);

core/src/test/java/tanin/backdoor/postgres/TableTest.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ void deleteRow() throws InterruptedException {
305305
}
306306

307307
@Test
308-
void filterRow() throws InterruptedException {
308+
void filterRowSpecificValue() throws InterruptedException {
309309
go("/");
310310
click(tid("database-item"));
311311
waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status")));
@@ -323,6 +323,43 @@ void filterRow() throws InterruptedException {
323323
waitUntil(() -> assertColumnValues("username", "test_user_2"));
324324
}
325325

326+
@Test
327+
void filterRowNullAndNotNull() throws Exception {
328+
try (var engine = server.engineProvider.createEngine(postgresConfig, null)) {
329+
engine.connection.createStatement().execute(
330+
"""
331+
INSERT INTO "user" (
332+
id,
333+
username,
334+
password
335+
) VALUES (
336+
'100',
337+
'null-user',
338+
NULL
339+
)
340+
"""
341+
);
342+
}
343+
go("/");
344+
click(tid("database-item"));
345+
waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status")));
346+
347+
click(tid("menu-items", "postgres", null, "menu-item-table", "user"));
348+
349+
waitUntil(() -> assertTrue(hasElem(tid("sheet-tab", "user"))));
350+
waitUntil(() -> assertColumnValues("username", "test_user_1", "test_user_2", "test_user_3", "test_user_4", "null-user"));
351+
352+
click(tid("sheet-view-column-header", "password", null, "filter-button"));
353+
click(tid("not-null-checkbox"));
354+
click(tid("submit-button"));
355+
waitUntil(() -> assertColumnValues("username", "test_user_1", "test_user_2", "test_user_3", "test_user_4"));
356+
357+
click(tid("sheet-view-column-header", "password", null, "filter-button"));
358+
click(tid("null-checkbox"));
359+
click(tid("submit-button"));
360+
waitUntil(() -> assertColumnValues("username", "null-user"));
361+
}
362+
326363
@Test
327364
void sortRow() throws InterruptedException {
328365
go("/");

core/src/test/java/tanin/backdoor/ux/EditTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ void editToNullAndRevertField() throws InterruptedException {
3636
waitUntil(() -> assertEquals("new-password", elem(tid("sheet-column-value", "password")).getText().trim()));
3737
}
3838

39+
@Test
40+
void trimValue() throws InterruptedException {
41+
go("/");
42+
click(tid("database-item"));
43+
waitUntil(() -> assertEquals("loaded", elem(tid("database-item")).getDomAttribute("data-database-status")));
44+
45+
click(tid("menu-items", "postgres", null, "menu-item-table", "user"));
46+
47+
click(tid("sheet-column-value", "password", null, "edit-field-button"));
48+
fill(tid("new-value"), "\n \nnew-password\n \n");
49+
click(tid("trim-value-checkbox"));
50+
click(tid("submit-button"));
51+
waitUntil(() -> assertEquals(" \n \nnew-password\n ", elem(tid("sheet-column-value", "password")).getText()));
52+
53+
click(tid("sheet-column-value", "password", null, "edit-field-button"));
54+
click(tid("trim-value-checkbox"));
55+
click(tid("submit-button"));
56+
waitUntil(() -> assertEquals("new-password", elem(tid("sheet-column-value", "password")).getText()));
57+
}
58+
3959
@Test
4060
void useNowTimestamp() throws Exception {
4161
try (var engine = server.engineProvider.createEngine(postgresConfig, null)) {

frontend/svelte/_edit_modal.svelte

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let rowIndex_: number | null = null
1717
let currentValue: any
1818
let currentColumn: Column | null
1919
let setToNull: boolean = false
20+
let shouldTrim: boolean = true
2021
2122
let isLoading = false
2223
let errors: string[] = []
@@ -45,7 +46,11 @@ export function open(value: any, column: Column, rowValues: any[], rowIndex: num
4546
.map((column, index) => {
4647
if (column.isPrimaryKey) {
4748
const value = rowValues[index];
48-
return {name: column.name, value: value === null ? null : ('' + value)}
49+
return {
50+
name: column.name,
51+
value: ('' + value),
52+
operator: value === null ? 'IS NULL' : 'EQUAL'
53+
}
4954
} else {
5055
return null
5156
}
@@ -86,7 +91,7 @@ async function submit() {
8691
table: sheet.name,
8792
primaryKeys,
8893
column: currentColumn!.name,
89-
value: currentValue,
94+
value: shouldTrim ? currentValue.trim() : currentValue,
9095
setToNull,
9196
})
9297
@@ -165,15 +170,28 @@ async function submit() {
165170
autocorrect="off"
166171
></textarea>
167172
</div>
168-
{#if currentColumn && currentColumn.type === 'TIMESTAMP' && !setToNull}
169-
<div
170-
class="text-xs underline cursor-pointer text-neutral-content"
171-
data-test-id="timestamp-now-button"
172-
onclick={() => {
173-
currentValue = new Date().toISOString()
174-
}}
175-
>Use the current timestamp
176-
</div>
173+
{#if !setToNull && currentColumn}
174+
{#if currentColumn.type === 'TIMESTAMP'}
175+
<div
176+
class="text-xs underline cursor-pointer text-neutral-content"
177+
data-test-id="timestamp-now-button"
178+
onclick={() => {
179+
currentValue = new Date().toISOString()
180+
}}
181+
>Use the current timestamp
182+
</div>
183+
{:else if currentColumn.type === 'STRING'}
184+
<label class="flex gap-2 items-center cursor-pointer">
185+
<input
186+
data-test-id="trim-value-checkbox"
187+
type="checkbox"
188+
class="checkbox checkbox-xs"
189+
bind:checked={shouldTrim}
190+
disabled={isLoading}
191+
/>
192+
<span class="text-xs">Trim whitespaces before updating</span>
193+
</label>
194+
{/if}
177195
{/if}
178196
<ErrorPanel {errors}/>
179197
<div class="flex items-center justify-between mt-2">

0 commit comments

Comments
 (0)