feat: 🎸 Add custom sort support to sql parser (#2959)

* feat: 🎸 Add custom sort support to sql parser

* test: 💍 fix column numner

* refactor: 💡 apply pr feedback

* fix: 🐛 use every method for checking attrs

* refactor: 💡 PR feedback + ensure default sort

* refactor: 💡 Consolidate attributes field when sorting

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17441

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
This commit is contained in:
Lisbet AlvarezandZhihe Li authored and Zhihe Li committed 2025-09-03 11:41:41 -04:00
1 parent eab93cf6c5
commit d414df4ca4
29 files changed
+333 -260

No files matched your search

+7
View File
@@ -28,6 +28,13 @@ export const modelMapping = {
scope_id: 'scope.scope_id',
created_time: 'created_time',
},
group: {
id: 'id',
name: 'name',
description: 'description',
scope_id: 'scope.scope_id',
created_time: 'created_time',
},
};
// A list of tables that we support searching using FTS5 in SQLite.
+39 -10
View File
@@ -41,11 +41,11 @@ function getSortableValue(schema, record, attribute) {
export const sortResults = (results, { querySort, schema }) => {
querySort = querySort ?? {};
const sortAttribute = querySort.attribute || SORT_DEFAULT_ATTRIBUTE;
const sortAttribute = querySort.attributes?.[0] || SORT_DEFAULT_ATTRIBUTE;
// Default sort direction is ascending unless we are sorting by `created_time` (default sort attribute)
const defaultSortDirection =
sortAttribute === SORT_DEFAULT_ATTRIBUTE && !querySort.sortFunction
sortAttribute === SORT_DEFAULT_ATTRIBUTE
? SORT_DIRECTION_DESCENDING
: SORT_DIRECTION_ASCENDING;
const sortDirection = querySort.direction || defaultSortDirection;
@@ -58,10 +58,44 @@ export const sortResults = (results, { querySort, schema }) => {
throw new Error('Invalid sort direction');
}
// Execute custom sort function if one is provided.
if (querySort.sortFunction) {
const sortAttributeDataType = schema.attributes.get(sortAttribute)?.type;
const sortFunction =
sortFunctions[sortAttributeDataType] ?? sortFunctions.string;
// Execute custom sort for multiple attributes provided.
if (querySort.attributes?.length > 1) {
const { attributes } = querySort;
return results.toSorted((a, b) => {
const sortResult = querySort.sortFunction(a, b);
let sortValueA, sortValueB;
attributes.forEach((attr) => {
const valueA = getSortableValue(schema, a, attr);
const valueB = getSortableValue(schema, b, attr);
if (valueA && !sortValueA) {
sortValueA = valueA;
}
if (valueB && !sortValueB) {
sortValueB = valueB;
}
});
const sortResult = sortFunction(sortValueA, sortValueB);
return sortDirection === SORT_DIRECTION_ASCENDING
? sortResult
: -1 * sortResult;
});
}
// Execute custom sort for attribute map provided.
if (querySort.customSort) {
const { attributeMap } = querySort.customSort;
return results.toSorted((a, b) => {
const sortValueA = getSortableValue(schema, a, sortAttribute);
const sortValueB = getSortableValue(schema, b, sortAttribute);
const sortResult = sortFunction(
attributeMap[sortValueA],
attributeMap[sortValueB],
);
return sortDirection === SORT_DIRECTION_ASCENDING
? sortResult
@@ -69,11 +103,6 @@ export const sortResults = (results, { querySort, schema }) => {
});
}
const sortAttributeDataType = schema.attributes.get(sortAttribute)?.type;
const sortFunction =
sortFunctions[sortAttributeDataType] ?? sortFunctions.string;
return results.toSorted((a, b) => {
// Extract the values to sort and sort them.
const sortValueA = getSortableValue(schema, a, sortAttribute);
+46 -13
View File
@@ -33,18 +33,7 @@ export function generateSQLExpressions(
addFilterConditions(filters, parameters, conditions);
addSearchConditions(search, resource, parameters, conditions);
const { attribute, direction } = sort;
let orderByClause = '';
if (attribute) {
// We have to check if the attribute is valid for the resource
// as we can't use parameterized queries for ORDER BY
const validAttributes = Object.keys(modelMapping[resource]);
if (validAttributes.includes(attribute)) {
orderByClause = `ORDER BY ${attribute} ${direction === 'desc' ? 'DESC' : 'ASC'}`;
}
} else if (modelMapping[resource]?.created_time) {
orderByClause = `ORDER BY created_time DESC`;
}
const orderByClause = constructOrderByClause(resource, sort);
const whereClause = conditions.length ? `WHERE ${and(conditions)}` : '';
@@ -53,7 +42,7 @@ export function generateSQLExpressions(
parameters.push(pageSize, (page - 1) * pageSize);
}
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM ${resource}`;
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM "${resource}"`;
return {
// Replace any empty newlines or leading whitespace on each line to be consistent with formatting
@@ -145,6 +134,50 @@ function addSearchConditions(search, resource, parameters, conditions) {
conditions.push(searchConditions);
}
function constructOrderByClause(resource, sort) {
const defaultOrderByClause = 'ORDER BY created_time DESC';
const { attributes, customSort, direction, isCoalesced } = sort;
const sortDirection = direction === 'desc' ? 'DESC' : 'ASC';
// We have to check if the attributes are valid for the resource
// as we can't use parameterized queries for ORDER BY
const validAttributes = Object.keys(modelMapping[resource] ?? {});
if (attributes?.some((attr) => !validAttributes.includes(attr))) {
return modelMapping[resource]?.created_time ? defaultOrderByClause : '';
}
if (customSort?.attributeMap) {
const whenClauses = Object.keys(customSort.attributeMap).reduce(
(acc, key) => {
return acc + `WHEN '${key}' THEN '${customSort.attributeMap[key]}' `;
},
'',
);
return `ORDER BY CASE ${attributes.join(', ')} ${whenClauses}END ${sortDirection}`;
} else if (attributes?.length > 0) {
const commaSeparatedVals = attributes.join(', ');
// In places where `collate nocase` is used, it is to ensure case is ignored on the initial sort.
// Then, a sort on the same condition is performed to ensure upper-case strings are given preference in a tie.
if (isCoalesced) {
return `ORDER BY COALESCE(${attributes.join(', ')}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
}
const attributesWithNoCollate = attributes
.map((attr) => `${attr} COLLATE NOCASE ${sortDirection}`)
.join(', ');
const attributesWithDirection = attributes
.map((attr) => `${attr} ${sortDirection}`)
.join(', ');
return `ORDER BY ${attributesWithNoCollate}, ${attributesWithDirection}`;
} else if (modelMapping[resource]?.created_time) {
return defaultOrderByClause;
} else {
return '';
}
}
// Comparison Operators
const OPERATORS = {
equals: ' = ?',
+13 -3
View File
@@ -14,7 +14,7 @@ const createTargetTables = `
CREATE TABLE IF NOT EXISTS target (
id TEXT NOT NULL PRIMARY KEY,
type TEXT NOT NULL,
name TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT,
address TEXT,
scope_id TEXT NOT NULL,
@@ -105,6 +105,16 @@ CREATE TABLE IF NOT EXISTS token (
${createTargetTables}
${createAliasTables}
CREATE TABLE IF NOT EXISTS "group" (
id TEXT NOT NULL PRIMARY KEY,
name TEXT,
description TEXT,
scope_id TEXT NOT NULL,
created_time TEXT NOT NULL,
data TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_group_scope_id_created_time ON "group"(scope_id, created_time DESC);
COMMIT;`;
export const INSERT_STATEMENTS = (resource, items, modelMapping) => {
@@ -121,8 +131,8 @@ export const INSERT_STATEMENTS = (resource, items, modelMapping) => {
const numColumns = Array(columns.length + 1);
const placeholders = `(${numColumns.fill('?').join(', ')})`;
return `REPLACE INTO ${resource} (${columns.join(', ')}, data) VALUES ${items.map(() => placeholders).join(', ')};`;
return `REPLACE INTO "${resource}" (${columns.join(', ')}, data) VALUES ${items.map(() => placeholders).join(', ')};`;
};
export const DELETE_STATEMENT = (resource, ids) =>
`DELETE FROM ${resource} WHERE id IN (${ids.map(() => '?').join(',')})`;
`DELETE FROM "${resource}" WHERE id IN (${ids.map(() => '?').join(',')})`;
@@ -262,7 +262,7 @@ module('Unit | Handler | indexed-db-handler', function (hooks) {
);
const results = await store.query('target', {
query: { sort: { attribute: 'name' } },
query: { sort: { attributes: ['name'] } },
});
assert.deepEqual(
@@ -292,10 +292,10 @@ module('Unit | Handler | indexed-db-handler', function (hooks) {
);
const resultsDesc = await store.query('alias', {
query: { sort: { attribute: 'name', direction: 'desc' } },
query: { sort: { attributes: ['name'], direction: 'desc' } },
});
const resultsAsc = await store.query('alias', {
query: { sort: { attribute: 'name', direction: 'asc' } },
query: { sort: { attributes: ['name'], direction: 'asc' } },
});
assert.deepEqual(
resultsDesc.map(({ name }) => name),
@@ -313,16 +313,11 @@ module('Unit | Handler | indexed-db-handler', function (hooks) {
const alias3 = this.server.create('alias', { name: 'alias3' });
const alias4 = this.server.create('alias', { name: 'alias4' });
const sortName = (recordA, recordB) => {
const nameMap = {
alias3: 'Alpha',
alias4: 'Beta',
alias1: 'Delta',
alias2: 'Epsilon',
};
return String(nameMap[recordA.attributes.name]).localeCompare(
String(nameMap[recordB.attributes.name]),
);
const nameMap = {
alias3: 'Alpha',
alias4: 'Beta',
alias1: 'Delta',
alias2: 'Epsilon',
};
const shuffledAliases = faker.helpers.shuffle([
@@ -340,10 +335,22 @@ module('Unit | Handler | indexed-db-handler', function (hooks) {
);
const resultsDesc = await store.query('alias', {
query: { sort: { sortFunction: sortName, direction: 'desc' } },
query: {
sort: {
attributes: ['name'],
customSort: { attributeMap: nameMap },
direction: 'desc',
},
},
});
const resultsAsc = await store.query('alias', {
query: { sort: { sortFunction: sortName, direction: 'asc' } },
query: {
sort: {
attributes: ['name'],
customSort: { attributeMap: nameMap },
direction: 'asc',
},
},
});
assert.deepEqual(
resultsDesc.map(({ name }) => name),
@@ -303,7 +303,7 @@ module('Unit | Handler | sqlite-handler', function (hooks) {
);
const results = await store.query('target', {
query: { sort: { attribute: 'name' } },
query: { sort: { attributes: ['name'] } },
});
assert.deepEqual(
@@ -345,10 +345,10 @@ module('Unit | Handler | sqlite-handler', function (hooks) {
);
const resultsDesc = await store.query('target', {
query: { sort: { attribute: 'name', direction: 'desc' } },
query: { sort: { attributes: ['name'], direction: 'desc' } },
});
const resultsAsc = await store.query('target', {
query: { sort: { attribute: 'name', direction: 'asc' } },
query: { sort: { attributes: ['name'], direction: 'asc' } },
});
assert.deepEqual(
resultsDesc.map(({ name }) => name),
@@ -37,7 +37,7 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
]);
const querySort = { attribute: 'name' };
const querySort = { attributes: ['name'] };
// Note: `name` is not defined as a schema attribute but exists on the record
const schema = { attributes: new Map() };
@@ -55,7 +55,7 @@ module('Unit | Utility | sortResults', function (hooks) {
});
const querySort = {
attribute: 'name',
attributes: ['name'],
direction: 'invalid sort direction',
};
const schema = { attributes: new Map() };
@@ -119,7 +119,7 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySort = { attribute: 'id' };
const querySort = { attributes: ['id'] };
const schema = { attributes: new Map() };
schema.attributes.set('id', { type: 'string' });
const sortedResults = sortResults(shuffledResults, { querySort, schema });
@@ -152,7 +152,7 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySort = { attribute: 'name' };
const querySort = { attributes: ['name'] };
const schema = { attributes: new Map() };
schema.attributes.set('name', { type: 'string' });
const sortedResults = sortResults(shuffledResults, { querySort, schema });
@@ -182,8 +182,8 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySortAsc = { attribute: 'name', direction: 'asc' };
const querySortDesc = { attribute: 'name', direction: 'desc' };
const querySortAsc = { attributes: ['name'], direction: 'asc' };
const querySortDesc = { attributes: ['name'], direction: 'desc' };
const schema = { attributes: new Map() };
schema.attributes.set('name', { type: 'string' });
const sortedResultsAsc = sortResults(shuffledResults, {
@@ -227,8 +227,8 @@ module('Unit | Utility | sortResults', function (hooks) {
result04,
result05,
]);
const querySortAsc = { attribute: 'name', direction: 'asc' };
const querySortDesc = { attribute: 'name', direction: 'desc' };
const querySortAsc = { attributes: ['name'], direction: 'asc' };
const querySortDesc = { attributes: ['name'], direction: 'desc' };
const schema = { attributes: new Map() };
schema.attributes.set('name', { type: 'string' });
@@ -272,7 +272,7 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySort = { attribute: 'date' };
const querySort = { attributes: ['date'] };
const schema = { attributes: new Map() };
schema.attributes.set('date', { type: 'date' });
@@ -304,8 +304,8 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySortAsc = { attribute: 'date', direction: 'asc' };
const querySortDesc = { attribute: 'date', direction: 'desc' };
const querySortAsc = { attributes: ['date'], direction: 'asc' };
const querySortDesc = { attributes: ['date'], direction: 'desc' };
const schema = { attributes: new Map() };
schema.attributes.set('date', { type: 'date' });
@@ -350,7 +350,7 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySort = { attribute: 'sessions' };
const querySort = { attributes: ['sessions'] };
const schema = { attributes: new Map() };
schema.attributes.set('sessions', { type: 'number' });
@@ -382,8 +382,8 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySortAsc = { attribute: 'sessions', direction: 'asc' };
const querySortDesc = { attribute: 'sessions', direction: 'desc' };
const querySortAsc = { attributes: ['sessions'], direction: 'asc' };
const querySortDesc = { attributes: ['sessions'], direction: 'desc' };
const schema = { attributes: new Map() };
schema.attributes.set('sessions', { type: 'number' });
@@ -419,7 +419,7 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySort = { attribute: 'active' };
const querySort = { attributes: ['active'] };
const schema = { attributes: new Map() };
schema.attributes.set('active', { type: 'boolean' });
@@ -442,8 +442,8 @@ module('Unit | Utility | sortResults', function (hooks) {
result03,
result04,
]);
const querySortAsc = { attribute: 'active', direction: 'asc' };
const querySortDesc = { attribute: 'active', direction: 'desc' };
const querySortAsc = { attributes: ['active'], direction: 'asc' };
const querySortDesc = { attributes: ['active'], direction: 'desc' };
const schema = { attributes: new Map() };
schema.attributes.set('active', { type: 'boolean' });
@@ -482,32 +482,39 @@ module('Unit | Utility | sortResults', function (hooks) {
name: 'target4',
});
const sortFunction = (recordA, recordB) => {
const nameMap = {
target3: 'Alpha',
target4: 'Beta',
target1: 'Delta',
target2: 'Epsilon',
};
return String(nameMap[recordA.attributes.name]).localeCompare(
String(nameMap[recordB.attributes.name]),
);
const nameMap = {
target3: 'Alpha',
target4: 'Beta',
target1: 'Delta',
target2: 'Epsilon',
};
const schema = { attributes: new Map() };
test.each(
'it sorts by `name` using custom sorting function',
{
'ascending (default)': {
querySort: { sortFunction },
querySort: {
attributes: ['name'],
customSort: { attributeMap: nameMap },
},
expectedResults: ['target3', 'target4', 'target1', 'target2'],
},
descending: {
querySort: { sortFunction, direction: 'desc' },
querySort: {
attributes: ['name'],
customSort: { attributeMap: nameMap },
direction: 'desc',
},
expectedResults: ['target2', 'target1', 'target4', 'target3'],
},
ascending: {
querySort: { sortFunction, direction: 'asc' },
querySort: {
attributes: ['name'],
customSort: { attributeMap: nameMap },
direction: 'asc',
},
expectedResults: ['target3', 'target4', 'target1', 'target2'],
},
},
@@ -28,7 +28,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
assert.strictEqual(
sql,
`
SELECT * FROM target
SELECT * FROM "target"
WHERE rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
);
@@ -44,7 +44,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
assert.strictEqual(
sql,
`
SELECT * FROM token
SELECT * FROM "token"
WHERE (id = ?)`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, ['tokenKey']);
@@ -59,8 +59,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
assert.strictEqual(
sql,
`
SELECT count(*) as total FROM target
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
SELECT count(*) as total FROM "target"
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, []);
});
@@ -160,7 +160,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
assert.strictEqual(
sql,
`
SELECT * FROM target
SELECT * FROM "target"
${expectedWhereClause}
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
);
@@ -176,15 +176,55 @@ module('Unit | Utility | sqlite-query', function (hooks) {
],
function (assert, [direction, expectedDirection]) {
const query = {
sort: { attribute: 'name', direction },
sort: { attributes: ['name'], direction },
};
const { sql, parameters } = generateSQLExpressions('target', query);
assert.strictEqual(
sql,
`
SELECT * FROM target
ORDER BY name ${expectedDirection}`.removeExtraWhiteSpace(),
SELECT * FROM "target"
ORDER BY name COLLATE NOCASE ${expectedDirection}, name ${expectedDirection}`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, []);
},
);
test.each(
'it generates sort order clause correctly',
{
'sort on multiple attributes and coalesced': {
sort: {
attributes: ['name', 'id'],
isCoalesced: true,
},
expectedOrderByClause: `ORDER BY COALESCE(name, id) COLLATE NOCASE DESC, COALESCE(name, id) DESC`,
},
'sort on multiple attributes': {
sort: {
attributes: ['name', 'id'],
},
expectedOrderByClause: `ORDER BY name COLLATE NOCASE DESC, id COLLATE NOCASE DESC, name DESC, id DESC`,
},
'sort on mapped attributes': {
sort: {
attributes: ['type'],
customSort: { attributeMap: { ssh: 'SSH', tcp: 'Generic TCP' } },
},
expectedOrderByClause: `ORDER BY CASE type WHEN 'ssh' THEN 'SSH' WHEN 'tcp' THEN 'Generic TCP' END DESC`,
},
},
function (assert, { sort, expectedOrderByClause }) {
const query = {
sort: { ...sort, direction: 'desc' },
};
const { sql, parameters } = generateSQLExpressions('target', query);
assert.strictEqual(
sql,
`
SELECT * FROM "target"
${expectedOrderByClause}`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, []);
},
@@ -206,7 +246,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
assert.strictEqual(
sql,
`
SELECT * FROM target
SELECT * FROM "target"
ORDER BY created_time DESC
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
);
@@ -230,7 +270,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
assert.strictEqual(
sql,
`
SELECT * FROM target
SELECT * FROM "target"
WHERE (id != ? AND id != ?) AND (status = ? OR status = ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
);
@@ -254,7 +294,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
},
created_time: [{ gte: date }],
},
sort: { attribute: 'name', direction: 'desc' },
sort: { attributes: ['name'], direction: 'desc' },
};
const { sql, parameters } = generateSQLExpressions('target', query, {
@@ -266,9 +306,9 @@ module('Unit | Utility | sqlite-query', function (hooks) {
assert.strictEqual(
sql,
`
SELECT data FROM target
SELECT data FROM "target"
WHERE (type = ?) AND (status = ? OR status = ?) AND (created_time >= ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY name DESC
ORDER BY name COLLATE NOCASE DESC, name DESC
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
);
@@ -290,19 +330,38 @@ module('Unit | Utility | sqlite-query', function (hooks) {
emptySearch: { search: '' },
emptyFilter: { filters: {} },
emptyFilterAttributes: { filters: { type: [] } },
invalidSortAttribute: { attribute: 'test', direction: 'asc' },
invalidSortAttribute: {
sort: { attributes: ['test'], direction: 'asc' },
},
invalidCoalescedSortAttribute: {
sort: {
attributes: ['id', 'test'],
direction: 'asc',
isCoalesced: true,
},
},
onlyPage: { page: 10 },
onlyPageSize: { pageSize: 5 },
},
function (assert, query) {
const { sql, parameters } = generateSQLExpressions('target', query);
const { sql: targetSql, parameters: targetParameters } =
generateSQLExpressions('target', query);
assert.strictEqual(
sql,
targetSql,
`
SELECT * FROM target
SELECT * FROM "target"
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, []);
assert.deepEqual(targetParameters, []);
const { sql: resourceSql, parameters: resourceParameters } =
generateSQLExpressions('resource', query);
assert.strictEqual(
resourceSql,
`
SELECT * FROM "resource"`.removeExtraWhiteSpace(),
);
assert.deepEqual(resourceParameters, []);
},
);
});
@@ -75,7 +75,7 @@ export default class ScopesScopeAliasesIndexRoute extends Route {
})
) {
const sort = {
attribute: sortAttribute,
attributes: [sortAttribute],
direction: sortDirection,
};
@@ -6,7 +6,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeAuthMethodsIndexRoute extends Route {
// =services
@@ -83,8 +82,12 @@ export default class ScopesScopeAuthMethodsIndexRoute extends Route {
const sort =
sortAttribute === 'name'
? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute, 'id'],
direction: sortDirection,
isCoalesced: true,
}
: { attributes: [sortAttribute], direction: sortDirection };
types.forEach((type) => {
filters.type.push({ equals: type });
@@ -6,7 +6,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeCredentialStoresIndexRoute extends Route {
// =services
@@ -80,8 +79,12 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
const sort =
sortAttribute === 'name'
? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute, 'id'],
direction: sortDirection,
isCoalesced: true,
}
: { attributes: [sortAttribute], direction: sortDirection };
let credentialStores;
let totalItems = 0;
@@ -6,7 +6,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeGroupsIndexRoute extends Route {
// =services
@@ -68,8 +67,12 @@ export default class ScopesScopeGroupsIndexRoute extends Route {
const sort =
sortAttribute === 'name'
? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute, 'id'],
direction: sortDirection,
isCoalesced: true,
}
: { attributes: [sortAttribute], direction: sortDirection };
let groups = [];
let totalItems = 0;
@@ -6,7 +6,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeHostCatalogsIndexRoute extends Route {
// =services
@@ -73,8 +72,12 @@ export default class ScopesScopeHostCatalogsIndexRoute extends Route {
const sort =
sortAttribute === 'name'
? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute, 'id'],
direction: sortDirection,
isCoalesced: true,
}
: { attributes: [sortAttribute], direction: sortDirection };
let hostCatalogs;
let totalItems = 0;
@@ -6,8 +6,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
import { GRANT_SCOPE_THIS } from 'api/models/role';
export default class ScopesScopeRolesIndexRoute extends Route {
// =attributes
@@ -33,21 +31,6 @@ export default class ScopesScopeRolesIndexRoute extends Route {
},
};
customSortFunction = {
name: sortNameWithIdFallback,
grant_scope_ids: (recordA, recordB) => {
const a = recordA.attributes?.grant_scope_ids.includes(GRANT_SCOPE_THIS);
const b = recordB.attributes?.grant_scope_ids.includes(GRANT_SCOPE_THIS);
if (a === b) {
return 0;
} else if (a) {
return 1;
} else {
return -1;
}
},
};
// =services
@service can;
@@ -84,10 +67,14 @@ export default class ScopesScopeRolesIndexRoute extends Route {
scope_id: [{ equals: scope_id }],
};
const sortFunction = this.customSortFunction[sortAttribute];
const sort = sortFunction
? { sortFunction, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
const sort =
sortAttribute === 'name'
? {
attributes: [sortAttribute, 'id'],
direction: sortDirection,
isCoalesced: true,
}
: { attributes: [sortAttribute], direction: sortDirection };
let roles;
let totalItems = 0;
@@ -6,7 +6,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeScopesIndexRoute extends Route {
// =attributes
@@ -70,8 +69,12 @@ export default class ScopesScopeScopesIndexRoute extends Route {
const sort =
sortAttribute === 'name'
? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute, 'id'],
direction: sortDirection,
isCoalesced: true,
}
: { attributes: [sortAttribute], direction: sortDirection };
const subScopes = await this.store.query('scope', {
scope_id,
@@ -71,17 +71,6 @@ export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
return this.retrieveData.perform({ ...params, useDebounce });
}
sortState = (recordA, recordB) => {
const stateMap = {
[STATE_SESSION_RECORDING_AVAILABLE]: this.intl.t('states.completed'),
[STATE_SESSION_RECORDING_STARTED]: this.intl.t('states.recording'),
[STATE_SESSION_RECORDING_UNKNOWN]: this.intl.t('states.failed'),
};
return String(stateMap[recordA.attributes.state]).localeCompare(
String(stateMap[recordB.attributes.state]),
);
};
retrieveData = restartableTask(
async ({
search,
@@ -122,10 +111,20 @@ export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
filters['create_time_values.target.id'].push({ equals: target });
});
const stateMap = {
[STATE_SESSION_RECORDING_AVAILABLE]: this.intl.t('states.completed'),
[STATE_SESSION_RECORDING_STARTED]: this.intl.t('states.recording'),
[STATE_SESSION_RECORDING_UNKNOWN]: this.intl.t('states.failed'),
};
const sort =
sortAttribute === 'state'
? { sortFunction: this.sortState, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute],
customSort: { attributeMap: stateMap },
direction: sortDirection,
}
: { attributes: [sortAttribute], direction: sortDirection };
if (
this.can.can('list scope', scope, {
@@ -98,7 +98,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
});
const sort = {
attribute: sortAttribute,
attributes: [sortAttribute],
direction: sortDirection,
};
@@ -64,16 +64,6 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
return this.retrieveData.perform({ ...params, useDebounce });
}
sortType = (recordA, recordB) => {
const typeMap = {
[TYPE_TARGET_SSH]: this.intl.t('resources.target.types.ssh'),
[TYPE_TARGET_TCP]: this.intl.t('resources.target.types.tcp'),
};
return String(typeMap[recordA.attributes.type]).localeCompare(
String(typeMap[recordB.attributes.type]),
);
};
retrieveData = restartableTask(
async ({
search,
@@ -117,10 +107,19 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
this.addActiveSessionFilters(filters, availableSessions, sessions);
}
const typeMap = {
[TYPE_TARGET_SSH]: this.intl.t('resources.target.types.ssh'),
[TYPE_TARGET_TCP]: this.intl.t('resources.target.types.tcp'),
};
const sort =
sortAttribute === 'type'
? { sortFunction: this.sortType, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute],
customSort: { attributeMap: typeMap },
direction: sortDirection,
}
: { attributes: [sortAttribute], direction: sortDirection };
let targets;
let totalItems = 0;
@@ -6,7 +6,6 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { restartableTask, timeout } from 'ember-concurrency';
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
export default class ScopesScopeUsersIndexRoute extends Route {
// =attributes
@@ -71,8 +70,12 @@ export default class ScopesScopeUsersIndexRoute extends Route {
const sort =
sortAttribute === 'name'
? { sortFunction: sortNameWithIdFallback, direction: sortDirection }
: { attribute: sortAttribute, direction: sortDirection };
? {
attributes: [sortAttribute, 'id'],
direction: sortDirection,
isCoalesced: true,
}
: { attributes: [sortAttribute], direction: sortDirection };
let users;
let totalItems = 0;
@@ -59,10 +59,7 @@
>
{{t 'form.name.label'}}
</H.ThSort>
<H.ThSort
@onClickSort={{fn H.setSortBy 'grant_scope_ids'}}
@sortOrder={{if (eq 'grant_scope_ids' H.sortBy) H.sortOrder}}
>{{t 'resources.role.titles.grants-applied'}}</H.ThSort>
<H.Th>{{t 'resources.role.titles.grants-applied'}}</H.Th>
<H.ThSort
@onClickSort={{fn H.setSortBy 'id'}}
@sortOrder={{if (eq 'id' H.sortBy) H.sortOrder}}
@@ -1,12 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export function sortNameWithIdFallback(recordA, recordB) {
let a = recordA.attributes.name;
let b = recordB.attributes.name;
a = a ? a : recordA.id;
b = b ? b : recordB.id;
return String(a).localeCompare(String(b));
}
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { Response } from 'miragejs';
import { authenticateSession } from 'ember-simple-auth/test-support';
@@ -16,7 +17,9 @@ import * as commonSelectors from 'admin/tests/helpers/selectors';
module('Acceptance | groups | create', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupSqlite(hooks);
setupIndexedDb(hooks);
let groupsCount;
const instances = {
@@ -14,6 +14,7 @@ import {
} from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { authenticateSession } from 'ember-simple-auth/test-support';
import * as commonSelectors from 'admin/tests/helpers/selectors';
@@ -22,6 +23,7 @@ import { faker } from '@faker-js/faker';
module('Acceptance | groups | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupSqlite(hooks);
setupIndexedDb(hooks);
const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
@@ -242,14 +242,6 @@ module('Acceptance | roles | list', function (hooks) {
expectedAscendingSort: ['Alpha', 'Beta', 'Delta', 'Epsilon', 'Gamma'],
column: 1,
},
'on grants applied': {
attribute: {
key: 'grant_scope_ids',
values: [[GRANT_SCOPE_THIS], [GRANT_SCOPE_THIS], [], [], []],
},
expectedAscendingSort: ['No', 'No', 'No', 'Yes', 'Yes'],
column: 2,
},
'on id': {
attribute: {
key: 'id',
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, findAll } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { Response } from 'miragejs';
import { authenticateSession } from 'ember-simple-auth/test-support';
@@ -17,6 +18,7 @@ import { setRunOptions } from 'ember-a11y-testing/test-support';
module('Acceptance | roles | principals', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupSqlite(hooks);
setupIndexedDb(hooks);
const instances = {
+8 -11
View File
@@ -16,7 +16,6 @@ import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { setupIntl } from 'ember-intl/test-support';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
import { STATUS_SESSION_ACTIVE } from 'api/models/session';
@@ -30,7 +29,6 @@ module('Acceptance | targets | list', function (hooks) {
setupMirage(hooks);
setupSqlite(hooks);
setupIndexedDb(hooks);
setupIntl(hooks, 'en-us');
const NAME_VALUES_ARRAY = ['Alpha', 'Beta', 'Delta', 'Epsilon', 'Gamma'];
const ID_VALUES_ARRAY = ['i_0001', 'i_0010', 'i_0100', 'i_1000', 'i_10000'];
@@ -397,15 +395,14 @@ module('Acceptance | targets | list', function (hooks) {
expectedAscendingSort: NAME_VALUES_ARRAY,
column: 1,
},
// TODO: Fix this custom sort
// 'on type': {
// attribute: {
// key: 'type',
// values: [TYPE_TARGET_SSH, TYPE_TARGET_TCP, TYPE_TARGET_SSH],
// },
// expectedAscendingSort: ['Generic TCP', 'SSH', 'SSH'],
// column: 2,
// },
'on type': {
attribute: {
key: 'type',
values: [TYPE_TARGET_SSH, TYPE_TARGET_TCP, TYPE_TARGET_SSH],
},
expectedAscendingSort: ['Generic TCP', 'SSH', 'SSH'],
column: 2,
},
'on id': {
attribute: {
key: 'id',
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { waitUntil, visit } from '@ember/test-helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { setupIntl } from 'ember-intl/test-support';
import { authenticateSession } from 'ember-simple-auth/test-support';
@@ -14,6 +15,7 @@ import { authenticateSession } from 'ember-simple-auth/test-support';
module('Unit | Controller | scopes/scope/groups/index', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
setupSqlite(hooks);
setupIndexedDb(hooks);
setupIntl(hooks, 'en-us');
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { visit } from '@ember/test-helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { authenticateSession } from 'ember-simple-auth/test-support';
module(
@@ -14,6 +15,7 @@ module(
function (hooks) {
setupTest(hooks);
setupMirage(hooks);
setupSqlite(hooks);
let store;
let controller;
@@ -1,67 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { sortNameWithIdFallback } from 'admin/utils/sort-name-with-id-fallback';
import { module, test } from 'qunit';
function createMockRecord(id, attributes = {}) {
return {
id,
attributes,
};
}
module('Unit | Utility | sort-name-with-id-fallback', function () {
test.each(
'sort on name with id fallback',
[
{
recordA: createMockRecord('id_01'),
recordB: createMockRecord('id_01'),
expectedResult: 0,
},
{
recordA: createMockRecord('id_02'),
recordB: createMockRecord('id_01'),
expectedResult: 1,
},
{
recordA: createMockRecord('id_01'),
recordB: createMockRecord('id_02'),
expectedResult: -1,
},
{
recordA: createMockRecord('id_01', { name: 'Alpha' }),
recordB: createMockRecord('id_02'),
expectedResult: -1,
},
{
recordA: createMockRecord('id_01'),
recordB: createMockRecord('id_02', { name: 'Alpha' }),
expectedResult: 1,
},
{
recordA: createMockRecord('id_01', { name: 'Alpha' }),
recordB: createMockRecord('id_02', { name: 'Beta' }),
expectedResult: -1,
},
{
recordA: createMockRecord('id_01', { name: 'Beta' }),
recordB: createMockRecord('id_02', { name: 'Alpha' }),
expectedResult: 1,
},
{
recordA: createMockRecord('id_01', { name: 'Alpha' }),
recordB: createMockRecord('id_02', { name: 'Alpha' }),
expectedResult: 0,
},
],
function (assert, input) {
const result = sortNameWithIdFallback(input.recordA, input.recordB);
assert.strictEqual(result, input.expectedResult);
},
);
});