sqlite-hub 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -13
- package/database.sqlite +0 -0
- package/fill.js +526 -0
- package/frontend/js/api.js +70 -11
- package/frontend/js/app.js +104 -32
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +95 -0
- package/frontend/js/components/queryEditor.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +145 -6
- package/frontend/js/components/sidebar.js +5 -5
- package/frontend/js/store.js +303 -34
- package/frontend/js/views/data.js +94 -70
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +1 -1
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +1 -1
- package/server/routes/data.js +3 -0
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +25 -4
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +199 -1
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +76 -0
- package/tests/sql-identifier-safety.test.js +59 -0
|
@@ -64,6 +64,198 @@ function normalizeColumn(column, visibleSet) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function isIdentifierCharacter(character) {
|
|
68
|
+
return /[A-Za-z0-9_$]/.test(character);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function skipQuotedSql(text, index) {
|
|
72
|
+
const quote = text[index];
|
|
73
|
+
let cursor = index + 1;
|
|
74
|
+
|
|
75
|
+
while (cursor < text.length) {
|
|
76
|
+
if (text[cursor] === quote) {
|
|
77
|
+
if (text[cursor + 1] === quote) {
|
|
78
|
+
cursor += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return cursor + 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cursor += 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return text.length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findMatchingParenthesis(text, openIndex) {
|
|
92
|
+
let depth = 0;
|
|
93
|
+
|
|
94
|
+
for (let index = openIndex; index < text.length; index += 1) {
|
|
95
|
+
const character = text[index];
|
|
96
|
+
|
|
97
|
+
if (character === "'" || character === '"' || character === "`") {
|
|
98
|
+
index = skipQuotedSql(text, index) - 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (character === "[") {
|
|
103
|
+
const closeIndex = text.indexOf("]", index + 1);
|
|
104
|
+
index = closeIndex === -1 ? text.length : closeIndex;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (character === "(") {
|
|
109
|
+
depth += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (character === ")") {
|
|
114
|
+
depth -= 1;
|
|
115
|
+
|
|
116
|
+
if (depth === 0) {
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extractCheckExpressions(ddl = "") {
|
|
126
|
+
const expressions = [];
|
|
127
|
+
const checkPattern = /\bCHECK\s*\(/gi;
|
|
128
|
+
let match;
|
|
129
|
+
|
|
130
|
+
while ((match = checkPattern.exec(ddl))) {
|
|
131
|
+
const openIndex = ddl.indexOf("(", match.index);
|
|
132
|
+
const closeIndex = findMatchingParenthesis(ddl, openIndex);
|
|
133
|
+
|
|
134
|
+
if (closeIndex === -1) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expressions.push(ddl.slice(openIndex + 1, closeIndex));
|
|
139
|
+
checkPattern.lastIndex = closeIndex + 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return expressions;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeIdentifier(value) {
|
|
146
|
+
const text = String(value ?? "").trim();
|
|
147
|
+
|
|
148
|
+
if (text.startsWith('"') && text.endsWith('"')) {
|
|
149
|
+
return text.slice(1, -1).replace(/""/g, '"').toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (text.startsWith("`") && text.endsWith("`")) {
|
|
153
|
+
return text.slice(1, -1).replace(/``/g, "`").toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
157
|
+
return text.slice(1, -1).replace(/\]\]/g, "]").toLowerCase();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return text.toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseSqlStringList(text = "") {
|
|
164
|
+
const values = [];
|
|
165
|
+
let index = 0;
|
|
166
|
+
|
|
167
|
+
while (index < text.length) {
|
|
168
|
+
if (text[index] !== "'") {
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let value = "";
|
|
174
|
+
index += 1;
|
|
175
|
+
|
|
176
|
+
while (index < text.length) {
|
|
177
|
+
if (text[index] === "'") {
|
|
178
|
+
if (text[index + 1] === "'") {
|
|
179
|
+
value += "'";
|
|
180
|
+
index += 2;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
index += 1;
|
|
185
|
+
values.push(value);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
value += text[index];
|
|
190
|
+
index += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return values;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findColumnInListExpression(expression, columnName) {
|
|
198
|
+
const normalizedColumnName = normalizeIdentifier(columnName);
|
|
199
|
+
const identifierPattern = /(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_$]*)\s+IN\s*\(/gi;
|
|
200
|
+
let match;
|
|
201
|
+
|
|
202
|
+
while ((match = identifierPattern.exec(expression))) {
|
|
203
|
+
const matchedIdentifier = match[0].replace(/\s+IN\s*\($/i, "").trim();
|
|
204
|
+
|
|
205
|
+
if (normalizeIdentifier(matchedIdentifier) !== normalizedColumnName) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const before = expression[match.index - 1] ?? "";
|
|
210
|
+
|
|
211
|
+
if (before && isIdentifierCharacter(before)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const openIndex = expression.indexOf("(", match.index + matchedIdentifier.length);
|
|
216
|
+
const closeIndex = findMatchingParenthesis(expression, openIndex);
|
|
217
|
+
|
|
218
|
+
if (closeIndex === -1) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const values = parseSqlStringList(expression.slice(openIndex + 1, closeIndex));
|
|
223
|
+
|
|
224
|
+
if (values.length) {
|
|
225
|
+
return values;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseCheckAllowedValues(ddl = "", columns = []) {
|
|
233
|
+
const allowedValuesByColumn = new Map();
|
|
234
|
+
const expressions = extractCheckExpressions(ddl);
|
|
235
|
+
|
|
236
|
+
columns.forEach((column) => {
|
|
237
|
+
const values = [];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
|
|
240
|
+
expressions.forEach((expression) => {
|
|
241
|
+
findColumnInListExpression(expression, column.name).forEach((value) => {
|
|
242
|
+
if (seen.has(value)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
seen.add(value);
|
|
247
|
+
values.push(value);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (values.length) {
|
|
252
|
+
allowedValuesByColumn.set(column.name, values);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return allowedValuesByColumn;
|
|
257
|
+
}
|
|
258
|
+
|
|
67
259
|
function groupForeignKeys(rows) {
|
|
68
260
|
const grouped = new Map();
|
|
69
261
|
|
|
@@ -140,9 +332,15 @@ function getTableDetail(db, tableName, options = {}) {
|
|
|
140
332
|
.all();
|
|
141
333
|
const visibleSet = new Set(tableInfo.map((column) => column.name));
|
|
142
334
|
|
|
143
|
-
|
|
335
|
+
let columns = extendedInfo
|
|
144
336
|
.map((column) => normalizeColumn(column, visibleSet))
|
|
145
337
|
.sort((left, right) => left.cid - right.cid);
|
|
338
|
+
const allowedValuesByColumn = parseCheckAllowedValues(entry.sql, columns);
|
|
339
|
+
|
|
340
|
+
columns = columns.map((column) => ({
|
|
341
|
+
...column,
|
|
342
|
+
allowedValues: allowedValuesByColumn.get(column.name) ?? [],
|
|
343
|
+
}));
|
|
146
344
|
|
|
147
345
|
const foreignKeys = groupForeignKeys(
|
|
148
346
|
db.prepare(`PRAGMA foreign_key_list(${quoteIdentifier(tableName)})`).all()
|
|
@@ -223,6 +223,8 @@ function mapEditableColumns(tableDetail, columnDefinitions) {
|
|
|
223
223
|
visible: isRowId ? true : Boolean(columnMeta?.visible),
|
|
224
224
|
generated: Boolean(columnMeta?.generated),
|
|
225
225
|
identity: identityColumns.has(definition.column),
|
|
226
|
+
notNull: Boolean(columnMeta?.notNull),
|
|
227
|
+
allowedValues: columnMeta?.allowedValues ?? [],
|
|
226
228
|
};
|
|
227
229
|
});
|
|
228
230
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const { ValidationError } = require("../../utils/errors");
|
|
2
|
+
const { quoteIdentifier } = require("../../utils/identifier");
|
|
3
|
+
|
|
4
|
+
const FILTER_OPERATORS = new Set(["=", "!=", "<", ">", "<=", ">=", "equals"]);
|
|
5
|
+
|
|
6
|
+
function escapeLikePattern(value) {
|
|
7
|
+
return String(value).replace(/[\\%_]/g, (character) => `\\${character}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isTextColumn(column) {
|
|
11
|
+
return String(column?.affinity ?? "").toUpperCase() === "TEXT";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeTableFilter(tableDetail, options = {}) {
|
|
15
|
+
const columnName = String(options.filterColumn ?? "").trim();
|
|
16
|
+
const operator = String(options.filterOperator ?? "=").trim();
|
|
17
|
+
const value = options.filterValue;
|
|
18
|
+
|
|
19
|
+
if (!columnName || value === undefined || value === null || String(value).trim() === "") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!FILTER_OPERATORS.has(operator)) {
|
|
24
|
+
throw new ValidationError(
|
|
25
|
+
`filterOperator must be one of: ${Array.from(FILTER_OPERATORS).join(", ")}.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const filterColumn = tableDetail.columns.find(
|
|
30
|
+
(column) => column.visible && column.name === columnName
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!filterColumn) {
|
|
34
|
+
throw new ValidationError(`Unknown filter column: ${columnName}.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalizedValue = String(value);
|
|
38
|
+
const quotedColumn = quoteIdentifier(filterColumn.name);
|
|
39
|
+
|
|
40
|
+
if (operator === "equals") {
|
|
41
|
+
return {
|
|
42
|
+
column: filterColumn.name,
|
|
43
|
+
operator,
|
|
44
|
+
value: normalizedValue,
|
|
45
|
+
matchMode: "equals",
|
|
46
|
+
clause: `${quotedColumn}${isTextColumn(filterColumn) ? " COLLATE NOCASE" : ""} = ?`,
|
|
47
|
+
params: [normalizedValue],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isTextColumn(filterColumn) && (operator === "=" || operator === "!=")) {
|
|
52
|
+
return {
|
|
53
|
+
column: filterColumn.name,
|
|
54
|
+
operator,
|
|
55
|
+
value: normalizedValue,
|
|
56
|
+
matchMode: operator === "=" ? "contains" : "notContains",
|
|
57
|
+
clause: `${quotedColumn} COLLATE NOCASE ${operator === "=" ? "LIKE" : "NOT LIKE"} ? ESCAPE '\\'`,
|
|
58
|
+
params: [`%${escapeLikePattern(normalizedValue)}%`],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
column: filterColumn.name,
|
|
64
|
+
operator,
|
|
65
|
+
value: normalizedValue,
|
|
66
|
+
matchMode: "comparison",
|
|
67
|
+
clause: `${quotedColumn} ${operator} ?`,
|
|
68
|
+
params: [normalizedValue],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
FILTER_OPERATORS,
|
|
74
|
+
normalizeTableFilter,
|
|
75
|
+
};
|
package/server/utils/csv.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
function
|
|
1
|
+
function stringifyDelimitedCell(value) {
|
|
2
2
|
if (value === null || value === undefined) {
|
|
3
3
|
return "";
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function escapeCsvCell(value, delimiter) {
|
|
10
|
+
const stringValue = stringifyDelimitedCell(value);
|
|
8
11
|
|
|
9
12
|
if (
|
|
10
13
|
stringValue.includes('"') ||
|
|
@@ -18,7 +21,7 @@ function escapeCsvCell(value, delimiter) {
|
|
|
18
21
|
return stringValue;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
function
|
|
24
|
+
function rowsToDelimitedText({ columns, rows, delimiter = "," }) {
|
|
22
25
|
const header = columns.map((column) => escapeCsvCell(column, delimiter)).join(delimiter);
|
|
23
26
|
const body = rows.map((row) =>
|
|
24
27
|
columns
|
|
@@ -29,6 +32,29 @@ function rowsToCsv({ columns, rows, delimiter = "," }) {
|
|
|
29
32
|
return [header, ...body].join("\n");
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
function rowsToCsv({ columns, rows, delimiter = "," }) {
|
|
36
|
+
return rowsToDelimitedText({ columns, rows, delimiter });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeMarkdownCell(value) {
|
|
40
|
+
return stringifyDelimitedCell(value)
|
|
41
|
+
.replaceAll("\\", "\\\\")
|
|
42
|
+
.replaceAll("|", "\\|")
|
|
43
|
+
.replace(/\r\n|\r|\n/g, "<br>");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rowsToMarkdownTable({ columns, rows }) {
|
|
47
|
+
const header = `| ${columns.map(escapeMarkdownCell).join(" | ")} |`;
|
|
48
|
+
const separator = `| ${columns.map(() => "---").join(" | ")} |`;
|
|
49
|
+
const body = rows.map(
|
|
50
|
+
(row) => `| ${columns.map((column) => escapeMarkdownCell(row[column])).join(" | ")} |`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return [header, separator, ...body].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
module.exports = {
|
|
33
57
|
rowsToCsv,
|
|
58
|
+
rowsToDelimitedText,
|
|
59
|
+
rowsToMarkdownTable,
|
|
34
60
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const Database = require("better-sqlite3");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
const { getTableDetail } = require("../server/services/sqlite/introspection");
|
|
5
|
+
const { SqlExecutor } = require("../server/services/sqlite/sqlExecutor");
|
|
6
|
+
|
|
7
|
+
test("table detail exposes string options from simple CHECK IN constraints", () => {
|
|
8
|
+
const db = new Database(":memory:");
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
db.exec(`
|
|
12
|
+
CREATE TABLE stream_company_mentions (
|
|
13
|
+
id INTEGER PRIMARY KEY,
|
|
14
|
+
mention_type TEXT,
|
|
15
|
+
CHECK (
|
|
16
|
+
mention_type IS NULL
|
|
17
|
+
OR mention_type IN (
|
|
18
|
+
'company',
|
|
19
|
+
'stock_company',
|
|
20
|
+
'brand',
|
|
21
|
+
'product',
|
|
22
|
+
'person',
|
|
23
|
+
'organization',
|
|
24
|
+
'terrorists',
|
|
25
|
+
'events',
|
|
26
|
+
'collective_terms',
|
|
27
|
+
'countries',
|
|
28
|
+
'none',
|
|
29
|
+
'unknown'
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
INSERT INTO stream_company_mentions (mention_type) VALUES ('company');
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
const tableDetail = getTableDetail(db, "stream_company_mentions");
|
|
37
|
+
const mentionTypeColumn = tableDetail.columns.find(
|
|
38
|
+
(column) => column.name === "mention_type"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
assert.deepEqual(mentionTypeColumn.allowedValues, [
|
|
42
|
+
"company",
|
|
43
|
+
"stock_company",
|
|
44
|
+
"brand",
|
|
45
|
+
"product",
|
|
46
|
+
"person",
|
|
47
|
+
"organization",
|
|
48
|
+
"terrorists",
|
|
49
|
+
"events",
|
|
50
|
+
"collective_terms",
|
|
51
|
+
"countries",
|
|
52
|
+
"none",
|
|
53
|
+
"unknown",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const executor = new SqlExecutor({
|
|
57
|
+
connectionManager: {
|
|
58
|
+
getActiveDatabase: () => db,
|
|
59
|
+
getActiveConnection: () => ({ id: "test" }),
|
|
60
|
+
},
|
|
61
|
+
appStateStore: {
|
|
62
|
+
recordQueryExecution: () => 1,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const result = executor.execute(
|
|
66
|
+
"SELECT id, mention_type FROM stream_company_mentions"
|
|
67
|
+
);
|
|
68
|
+
const editableColumn = result.editing.columns.find(
|
|
69
|
+
(column) => column.sourceColumn === "mention_type"
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
assert.deepEqual(editableColumn.allowedValues, mentionTypeColumn.allowedValues);
|
|
73
|
+
} finally {
|
|
74
|
+
db.close();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
@@ -39,6 +39,65 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
|
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
41
|
const tableData = service.getTableData(tableName, { limit: 10, offset: 0 });
|
|
42
|
+
const filteredTableData = service.getTableData(tableName, {
|
|
43
|
+
limit: 10,
|
|
44
|
+
offset: 0,
|
|
45
|
+
filterColumn: valueColumn,
|
|
46
|
+
filterOperator: "=",
|
|
47
|
+
filterValue: "EFO",
|
|
48
|
+
});
|
|
49
|
+
const negativeFilteredTableData = service.getTableData(tableName, {
|
|
50
|
+
limit: 10,
|
|
51
|
+
offset: 0,
|
|
52
|
+
filterColumn: valueColumn,
|
|
53
|
+
filterOperator: "!=",
|
|
54
|
+
filterValue: "EFO",
|
|
55
|
+
});
|
|
56
|
+
const exactFilteredTableData = service.getTableData(tableName, {
|
|
57
|
+
limit: 10,
|
|
58
|
+
offset: 0,
|
|
59
|
+
filterColumn: valueColumn,
|
|
60
|
+
filterOperator: "equals",
|
|
61
|
+
filterValue: "BEFORE",
|
|
62
|
+
});
|
|
63
|
+
const exactSubstringMissTableData = service.getTableData(tableName, {
|
|
64
|
+
limit: 10,
|
|
65
|
+
offset: 0,
|
|
66
|
+
filterColumn: valueColumn,
|
|
67
|
+
filterOperator: "equals",
|
|
68
|
+
filterValue: "EFO",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.equal(filteredTableData.rowCount, 1);
|
|
72
|
+
assert.equal(filteredTableData.rows[0][valueColumn], "before");
|
|
73
|
+
assert.deepEqual(filteredTableData.filter, {
|
|
74
|
+
column: valueColumn,
|
|
75
|
+
operator: "=",
|
|
76
|
+
value: "EFO",
|
|
77
|
+
matchMode: "contains",
|
|
78
|
+
});
|
|
79
|
+
assert.equal(negativeFilteredTableData.rowCount, 0);
|
|
80
|
+
assert.equal(exactFilteredTableData.rowCount, 1);
|
|
81
|
+
assert.equal(exactFilteredTableData.rows[0][valueColumn], "before");
|
|
82
|
+
assert.deepEqual(exactFilteredTableData.filter, {
|
|
83
|
+
column: valueColumn,
|
|
84
|
+
operator: "equals",
|
|
85
|
+
value: "BEFORE",
|
|
86
|
+
matchMode: "equals",
|
|
87
|
+
});
|
|
88
|
+
assert.equal(exactSubstringMissTableData.rowCount, 0);
|
|
89
|
+
assert.throws(
|
|
90
|
+
() =>
|
|
91
|
+
service.getTableData(tableName, {
|
|
92
|
+
limit: 10,
|
|
93
|
+
offset: 0,
|
|
94
|
+
filterColumn: valueColumn,
|
|
95
|
+
filterOperator: "<>",
|
|
96
|
+
filterValue: "before",
|
|
97
|
+
}),
|
|
98
|
+
/filterOperator/
|
|
99
|
+
);
|
|
100
|
+
|
|
42
101
|
const identity = tableData.rows[0].__identity;
|
|
43
102
|
const preview = service.previewTableRowUpdate(tableName, {
|
|
44
103
|
identity,
|