feat: 🎸 Refactor session recording filters (#2981)
This commit has a few notable changes: 1. A simple attempt at establishing a pattern for using SELECT statements. This will be useful in the future for joins/subquery support. 2. Expanded usage of searching to be able to pass instead of just a string, an object that lets a consumer choose which columns to search on. 3. Decouple our session recording filters from being loaded from the route model hook which is a departure from our convention. This has the benefit of actually letting the page first load and having the filters load async so the page doesn't wait for the filters to finish. This is also much faster to query for each individual filter option from SQLite rather than returning all data to filter in application code.
This commit is contained in:
1 parent
72f7c8a92d
commit
fa18bfff01
18 files changed
+558
-250
No files matched your search
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'dummy/tests/helpers';
|
||||
|
||||
module('Unit | Service | db', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('query throws assertion error for unsupported resource type', function (assert) {
|
||||
let service = this.owner.lookup('service:db');
|
||||
|
||||
assert.throws(() => {
|
||||
service.query('unsupported-type', { query: {} });
|
||||
}, /Resource type is not supported/);
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,10 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'dummy/tests/helpers';
|
||||
import { setupSqlite } from 'api/test-support/helpers/sqlite';
|
||||
import { modelMapping, searchTables } from 'api/services/sqlite';
|
||||
import { modelMapping } from 'api/services/sqlite';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
const supportedModels = Object.keys(modelMapping);
|
||||
const supportedFtsTables = [...searchTables];
|
||||
|
||||
module('Unit | Service | sqlite', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -38,7 +37,7 @@ module('Unit | Service | sqlite', function (hooks) {
|
||||
|
||||
test.each(
|
||||
'Mapping matches fts table columns',
|
||||
supportedFtsTables,
|
||||
supportedModels,
|
||||
async function (assert, resource) {
|
||||
const service = this.owner.lookup('service:sqlite');
|
||||
|
||||
|
||||
@@ -20,21 +20,6 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
delete String.prototype.removeExtraWhiteSpace;
|
||||
});
|
||||
|
||||
// TODO: Add a normal LIKE search when we add a resource that uses it
|
||||
test('it generates search correctly with FTS5', function (assert) {
|
||||
const query = { search: 'favorite' };
|
||||
|
||||
const { sql, parameters } = generateSQLExpressions('target', query);
|
||||
assert.strictEqual(
|
||||
sql,
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
WHERE rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, ['"favorite"*']);
|
||||
});
|
||||
|
||||
test('it grabs resources not in the model mapping', function (assert) {
|
||||
const query = {
|
||||
filters: { id: [{ equals: 'tokenKey' }] },
|
||||
@@ -52,7 +37,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
|
||||
test('it executes count queries correctly', function (assert) {
|
||||
const select = {
|
||||
select: ['count(*) as total'],
|
||||
select: [{ field: '*', isCount: true, alias: 'total' }],
|
||||
};
|
||||
|
||||
const { sql, parameters } = generateSQLExpressions('target', {}, select);
|
||||
@@ -65,6 +50,123 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
assert.deepEqual(parameters, []);
|
||||
});
|
||||
|
||||
test.each(
|
||||
'it generates select clause with various options',
|
||||
{
|
||||
'single field': {
|
||||
select: [{ field: 'name' }],
|
||||
expectedSelect: 'name',
|
||||
},
|
||||
'multiple fields': {
|
||||
select: [{ field: 'name' }, { field: 'type' }, { field: 'id' }],
|
||||
expectedSelect: 'name, type, id',
|
||||
},
|
||||
'field with alias': {
|
||||
select: [{ field: 'name', alias: 'resource_name' }],
|
||||
expectedSelect: 'name as resource_name',
|
||||
},
|
||||
'multiple fields with aliases': {
|
||||
select: [
|
||||
{ field: 'name', alias: 'resource_name' },
|
||||
{ field: 'type', alias: 'resource_type' },
|
||||
],
|
||||
expectedSelect: 'name as resource_name, type as resource_type',
|
||||
},
|
||||
'count with alias': {
|
||||
select: [{ field: 'id', isCount: true, alias: 'total_count' }],
|
||||
expectedSelect: 'count(id) as total_count',
|
||||
},
|
||||
'count without alias': {
|
||||
select: [{ field: '*', isCount: true }],
|
||||
expectedSelect: 'count(*)',
|
||||
},
|
||||
'count distinct': {
|
||||
select: [{ field: 'status', isCount: true, isDistinct: true }],
|
||||
expectedSelect: 'count(DISTINCT status)',
|
||||
},
|
||||
'count distinct with alias': {
|
||||
select: [
|
||||
{
|
||||
field: 'type',
|
||||
isCount: true,
|
||||
isDistinct: true,
|
||||
alias: 'unique_types',
|
||||
},
|
||||
],
|
||||
expectedSelect: 'count(DISTINCT type) as unique_types',
|
||||
},
|
||||
'mixed regular and count fields': {
|
||||
select: [
|
||||
{ field: 'name' },
|
||||
{ field: 'id', isCount: true, alias: 'count' },
|
||||
],
|
||||
expectedSelect: 'name, count(id) as count',
|
||||
},
|
||||
'field with distinct': {
|
||||
select: [{ field: 'type', isDistinct: true }],
|
||||
expectedSelect: 'DISTINCT type',
|
||||
},
|
||||
'multiple distinct fields': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true },
|
||||
{ field: 'status', isDistinct: true },
|
||||
],
|
||||
expectedSelect: 'DISTINCT type, status',
|
||||
},
|
||||
'multiple distinct with count fields': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true, isCount: true },
|
||||
{ field: 'status', isDistinct: true, isCount: true },
|
||||
],
|
||||
expectedSelect: 'count(DISTINCT type), count(DISTINCT status)',
|
||||
},
|
||||
'distinct with alias': {
|
||||
select: [{ field: 'type', isDistinct: true, alias: 'unique_type' }],
|
||||
expectedSelect: 'DISTINCT type as unique_type',
|
||||
},
|
||||
'multiple distinct fields with aliases': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true, alias: 'unique_type' },
|
||||
{ field: 'status', isDistinct: true, alias: 'unique_status' },
|
||||
],
|
||||
expectedSelect: 'DISTINCT type as unique_type, status as unique_status',
|
||||
},
|
||||
'count distinct multiple fields': {
|
||||
select: [
|
||||
{
|
||||
field: 'type',
|
||||
isCount: true,
|
||||
isDistinct: true,
|
||||
alias: 'type_count',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
isCount: true,
|
||||
isDistinct: true,
|
||||
alias: 'status_count',
|
||||
},
|
||||
],
|
||||
expectedSelect:
|
||||
'count(DISTINCT type) as type_count, count(DISTINCT status) as status_count',
|
||||
},
|
||||
},
|
||||
function (assert, { select, expectedSelect }) {
|
||||
const { sql, parameters } = generateSQLExpressions(
|
||||
'target',
|
||||
{},
|
||||
{ select },
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
sql,
|
||||
`
|
||||
SELECT ${expectedSelect} FROM "target"
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, []);
|
||||
},
|
||||
);
|
||||
|
||||
test.each(
|
||||
'it generates filters correctly',
|
||||
{
|
||||
@@ -283,6 +385,31 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
]);
|
||||
});
|
||||
|
||||
test('it generates FTS5 search with object parameter', function (assert) {
|
||||
const query = {
|
||||
search: {
|
||||
text: 'favorite',
|
||||
fields: ['name', 'description'],
|
||||
},
|
||||
filters: {
|
||||
type: [{ equals: 'ssh' }],
|
||||
},
|
||||
};
|
||||
|
||||
const { sql, parameters } = generateSQLExpressions('target', query);
|
||||
assert.strictEqual(
|
||||
sql,
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
WHERE (type = ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, [
|
||||
'ssh',
|
||||
'name:"favorite"* OR description:"favorite"*',
|
||||
]);
|
||||
});
|
||||
|
||||
test('it generates SQL with all clauses combined', function (assert) {
|
||||
const query = {
|
||||
search: 'favorite',
|
||||
@@ -300,7 +427,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
const { sql, parameters } = generateSQLExpressions('target', query, {
|
||||
page: 2,
|
||||
pageSize: 15,
|
||||
select: ['data'],
|
||||
select: [{ field: 'data' }],
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
@@ -323,6 +450,35 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
]);
|
||||
});
|
||||
|
||||
test.each(
|
||||
'it throws assertion error for invalid select combinations',
|
||||
{
|
||||
'mixing distinct with non-distinct columns': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true },
|
||||
{ field: 'status', isDistinct: true },
|
||||
{ field: 'name' },
|
||||
],
|
||||
expectedError:
|
||||
/Can not combine non-distincts with multi column distincts/,
|
||||
},
|
||||
'mixing distinct with aggregate non-distinct columns': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true },
|
||||
{ field: 'status', isDistinct: true },
|
||||
{ field: 'name', isCount: true },
|
||||
],
|
||||
expectedError:
|
||||
/Can not combine non-distincts with multi column distincts/,
|
||||
},
|
||||
},
|
||||
function (assert, { select, expectedError }) {
|
||||
assert.throws(() => {
|
||||
generateSQLExpressions('target', {}, { select });
|
||||
}, expectedError);
|
||||
},
|
||||
);
|
||||
|
||||
test.each(
|
||||
'it handles empty and invalid fields',
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user