feat: 🎸 Add indexedDb parser (#2068)
* feat: 🎸 Add indexedDb parser ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12164 --------- Co-authored-by: Lisbet Alvarez <lisbet.alvarez@hashicorp.com>
This commit is contained in:
1 parent
bd5e3da1f4
commit
d72fa0d6e3
24 files changed
+890
-178
No files matched your search
@@ -7,6 +7,7 @@ import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { generateMQLExpression } from '../utils/mql-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
|
||||
/**
|
||||
* Not all types are yet supported by the client daemon so we'll
|
||||
@@ -19,31 +20,6 @@ const supportedTypes = {
|
||||
session: ['id', 'type', 'status', 'endpoint', 'scope_id', 'target_id'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an array and the current page and pagesize to calculate the correct
|
||||
* number of results to return to the caller.
|
||||
*
|
||||
* @param array
|
||||
* @param page
|
||||
* @param pageSize
|
||||
* @returns {[*]}
|
||||
*/
|
||||
const paginateResults = (array, page, pageSize) => {
|
||||
const length = array?.length;
|
||||
if (!array || length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (!page || !pageSize) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const start = Math.min(length - 1, offset);
|
||||
const end = Math.min(length, offset + pageSize);
|
||||
|
||||
return array.slice(start, end);
|
||||
};
|
||||
|
||||
const fetchControllerData = async (context, next) => {
|
||||
const { data } = context.request;
|
||||
const { query: originalQuery } = data;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { modelIndexes } from 'api/services/indexed-db';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { queryIndexedDb } from '../utils/indexed-db-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
|
||||
/**
|
||||
* Handler to sit in front of the API request layer
|
||||
@@ -21,22 +22,23 @@ export default class IndexedDbHandler {
|
||||
const { type, query, options: { pushToStore = true } = {} } = data;
|
||||
const supportedModels = Object.keys(modelIndexes);
|
||||
const { db: indexedDb } = this.indexedDb ?? {};
|
||||
const pluralizedType = pluralize(type);
|
||||
|
||||
// Go through normal flow if we don't yet support the model
|
||||
// or if we don't have an indexedDb instance
|
||||
if (!supportedModels.includes(pluralizedType) || !indexedDb) {
|
||||
if (!supportedModels.includes(type) || !indexedDb) {
|
||||
return next(context.request);
|
||||
}
|
||||
|
||||
const listToken = await indexedDb.tokens.get(pluralizedType);
|
||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
||||
|
||||
const listToken = await indexedDb.token.get(type);
|
||||
const adapter = store.adapterFor(type);
|
||||
const schema = store.modelFor(type);
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...query,
|
||||
...remainingQuery,
|
||||
scope_id: 'global',
|
||||
recursive: true,
|
||||
list_token: listToken?.token,
|
||||
@@ -47,11 +49,11 @@ export default class IndexedDbHandler {
|
||||
// clear the DB and try again without a token.
|
||||
// TODO: Probably should check for 400 and bad token error specifically instead?
|
||||
if (e?.errors[0].status !== 429) {
|
||||
await indexedDb[pluralizedType].clear();
|
||||
await indexedDb.tokens.delete(pluralizedType);
|
||||
await indexedDb[type].clear();
|
||||
await indexedDb.token.delete(type);
|
||||
|
||||
payload = await adapter.query(store, schema, {
|
||||
...query,
|
||||
...remainingQuery,
|
||||
scope_id: 'global',
|
||||
recursive: true,
|
||||
list_token: null,
|
||||
@@ -62,14 +64,14 @@ export default class IndexedDbHandler {
|
||||
}
|
||||
|
||||
// Store the token we just got back from the payload
|
||||
await indexedDb.tokens.put({
|
||||
id: pluralizedType,
|
||||
await indexedDb.token.put({
|
||||
id: type,
|
||||
token: payload.list_token,
|
||||
});
|
||||
|
||||
// Remove any records from the DB if the API indicates they've been deleted
|
||||
if (payload.removed_ids?.length > 0) {
|
||||
await indexedDb[pluralizedType].bulkDelete(payload.removed_ids);
|
||||
await indexedDb[type].bulkDelete(payload.removed_ids);
|
||||
}
|
||||
|
||||
const serializer = store.serializerFor(type);
|
||||
@@ -84,18 +86,20 @@ export default class IndexedDbHandler {
|
||||
const { data: payloadData } = normalizedPayload;
|
||||
|
||||
// Store the new data we just got back from the API refresh
|
||||
await indexedDb[pluralizedType].bulkPut(
|
||||
payloadData.map((datum) => this.indexedDb.normalizeData(datum, true)),
|
||||
const items = payloadData.map((datum) =>
|
||||
this.indexedDb.normalizeData(datum, true),
|
||||
);
|
||||
await indexedDb[type].bulkPut(items);
|
||||
|
||||
const indexedDbResults = await queryIndexedDb(
|
||||
indexedDb,
|
||||
type,
|
||||
queryObj,
|
||||
);
|
||||
|
||||
// TODO: Filter here, this is just a placeholder for now
|
||||
const dbRecords = (
|
||||
await indexedDb[pluralizedType]
|
||||
.where('attributes.scope.scope_id')
|
||||
.equals(query.scope_id)
|
||||
.toArray()
|
||||
).map(this.indexedDb.normalizeData);
|
||||
const recordCount = await indexedDb[pluralizedType].count();
|
||||
const dbRecords = paginateResults(indexedDbResults, page, pageSize).map(
|
||||
this.indexedDb.normalizeData,
|
||||
);
|
||||
|
||||
// Return the raw data if we don't push to the store.
|
||||
let records = dbRecords.map((record) => ({
|
||||
@@ -111,7 +115,7 @@ export default class IndexedDbHandler {
|
||||
// This isn't conventional but is better than returning an ArrayProxy
|
||||
// or EmberArray since the ember store query method asserts it has to be an array
|
||||
// so we can't just return an object.
|
||||
records.meta = { totalItems: recordCount };
|
||||
records.meta = { totalItems: indexedDbResults.length };
|
||||
return records;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Service, { inject as service } from '@ember/service';
|
||||
import Service from '@ember/service';
|
||||
import Dexie from 'dexie';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
@@ -7,9 +7,11 @@ import { typeOf } from '@ember/utils';
|
||||
// this index. Increment the version number of the database whenever
|
||||
// indexes change.
|
||||
export const modelIndexes = {
|
||||
tokens: '&id, token',
|
||||
targets: '&id, attributes.type, attributes.name, attributes.scope.scope_id',
|
||||
sessions: '&id, attributes.type, attributes.scope.scope_id',
|
||||
token: '&id, token',
|
||||
target:
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.address, attributes.scope.scope_id',
|
||||
session:
|
||||
'&id, attributes.created_time, attributes.type, attributes.status, attributes.endpoint, attributes.target_id, attributes.user_id, attributes.scope.scope_id',
|
||||
};
|
||||
|
||||
export const formatDbName = (userId, clusterUrl) =>
|
||||
@@ -46,7 +48,7 @@ const unconvertBooleansInObject = (result, object) => {
|
||||
if (typeOf(value) === 'object') {
|
||||
result[key] = unconvertBooleansInObject({}, value);
|
||||
} else if (value === 'true' || value === 'false') {
|
||||
result[key] = value === 'true' ? true : false;
|
||||
result[key] = value === 'true';
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
@@ -60,9 +62,6 @@ const unconvertBooleansInObject = (result, object) => {
|
||||
* `setup` from the application root and
|
||||
*/
|
||||
export default class IndexedDbService extends Service {
|
||||
// =services
|
||||
@service session;
|
||||
|
||||
// =attributes
|
||||
#db;
|
||||
get db() {
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { get } from '@ember/object';
|
||||
import { modelIndexes } from '../services/indexed-db';
|
||||
|
||||
/**
|
||||
* Takes a POJO representing a filter query and builds a query to
|
||||
* query indexedDb with. Note that the first filter is important as that
|
||||
* will use the actual index in indexedDb while any subsequent ones will be less efficient.
|
||||
*
|
||||
* @example
|
||||
* const filter = queryIndexedDb(indexedDb, type, {
|
||||
* search: generated',
|
||||
* filters: {
|
||||
* scope_id: {
|
||||
* logicalOperator: 'and',
|
||||
* values: [{ notEquals: 'scope1' }, { notEquals: 'scope2' }],
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export async function queryIndexedDb(indexedDb, resource, query) {
|
||||
let { search, filters = {} } = query ?? {};
|
||||
let filterCollection = indexedDb[resource];
|
||||
|
||||
search = getSearchObject(search, resource);
|
||||
filters = cleanObject(filters);
|
||||
|
||||
if (Object.keys(filters).length) {
|
||||
// Remove the first item to use as the initial collection and then reduce the remaining ones
|
||||
const entries = Object.entries(filters);
|
||||
const [firstKey, firstFilterArrayOrObject] = entries.shift();
|
||||
|
||||
filterCollection = await entries.reduce(
|
||||
async (currCollection, [key, filterArrayOrObject]) => {
|
||||
// Grab the results from the set of one filters to be `AND` together with the rest of the filters
|
||||
const filterResults = await buildInitialWhereClause({
|
||||
table: indexedDb[resource],
|
||||
key,
|
||||
filterArrayOrObject,
|
||||
}).primaryKeys();
|
||||
const resultIdsSet = new Set(filterResults);
|
||||
|
||||
return (await currCollection).and((record) => {
|
||||
return resultIdsSet.has(record.id);
|
||||
});
|
||||
},
|
||||
buildInitialWhereClause({
|
||||
key: firstKey,
|
||||
table: indexedDb[resource],
|
||||
filterArrayOrObject: firstFilterArrayOrObject,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const { text, fields } = search;
|
||||
|
||||
filterCollection = filterCollection.filter((record) =>
|
||||
fields
|
||||
.map((key) => get(record, key))
|
||||
.some((element) => {
|
||||
if (typeof element === 'string') {
|
||||
return element?.toLowerCase().includes(text?.toLowerCase());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// OrderBy has to be used on where clauses while if it's a
|
||||
// collection already we have to use sortBy. We'll also sort by
|
||||
// descending order based on created time to match the API as any
|
||||
// "or" clause can change the order
|
||||
if (filterCollection.orderBy) {
|
||||
return filterCollection.orderBy(getKey('created_time')).reverse().toArray();
|
||||
}
|
||||
return filterCollection.reverse().sortBy(getKey('created_time'));
|
||||
}
|
||||
|
||||
const buildInitialWhereClause = ({ filterArrayOrObject, table, key }) => {
|
||||
const filterValueArray = Array.isArray(filterArrayOrObject)
|
||||
? filterArrayOrObject
|
||||
: filterArrayOrObject.values;
|
||||
|
||||
const { logicalOperator } = filterArrayOrObject;
|
||||
|
||||
// Short circuit if all the filter types were equals or notEquals since indexedDb
|
||||
// can more efficiently query if we pass all values at once
|
||||
const checkAllFilterType = (type) =>
|
||||
filterValueArray.every((obj) => Object.keys(obj)[0] === type);
|
||||
if (checkAllFilterType('equals')) {
|
||||
return table
|
||||
.where(getKey(key))
|
||||
.anyOf(filterValueArray.map((obj) => obj['equals']));
|
||||
}
|
||||
if (checkAllFilterType('notEquals')) {
|
||||
return table
|
||||
.where(getKey(key))
|
||||
.noneOf(filterValueArray.map((obj) => obj['notEquals']));
|
||||
}
|
||||
|
||||
// Remove the first item to use as the initial where clause and then reduce the remaining ones
|
||||
const [firstOperation, firstFilterValue] = Object.entries(
|
||||
filterValueArray[0],
|
||||
)[0];
|
||||
|
||||
// Not all are filters are equals or notEquals,
|
||||
// manually build the initial where clause and resulting collections
|
||||
return filterValueArray.slice(1).reduce(
|
||||
(result, currObj) => {
|
||||
const [operation, value] = Object.entries(currObj)[0];
|
||||
|
||||
return buildIndexedDbCollection({
|
||||
key: getKey(key),
|
||||
value,
|
||||
collection: result,
|
||||
operation,
|
||||
logicalOperator,
|
||||
});
|
||||
},
|
||||
buildIndexedDbWhere({
|
||||
table,
|
||||
key: getKey(key),
|
||||
operation: firstOperation,
|
||||
value: firstFilterValue,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the initial where clause for indexed DB to filter on. Note that this makes
|
||||
* the first filter important as that will do the real filtering from indexed DB while
|
||||
* any subsequent filter has to be done in memory.
|
||||
*/
|
||||
const buildIndexedDbWhere = ({ table, key, value, operation }) => {
|
||||
const baseWhereClause = table.where(key);
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
// We return an error due to not being able to support contains on first filter due to performance.
|
||||
throw new Error('Contains is not supported as the first filter.');
|
||||
case 'gt':
|
||||
return baseWhereClause.above(value);
|
||||
case 'gte':
|
||||
return baseWhereClause.aboveOrEqual(value);
|
||||
case 'lt':
|
||||
return baseWhereClause.below(value);
|
||||
case 'lte':
|
||||
return baseWhereClause.belowOrEqual(value);
|
||||
case 'notEquals':
|
||||
return baseWhereClause.notEqual(value);
|
||||
default:
|
||||
return baseWhereClause.equals(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Continues building the queries for indexed DB to filter on. Note that these are not true
|
||||
* indexed DB queries and in fact just standard JS filters for logical "and" queries and parallel queries
|
||||
* to indexed DB for logical "or" queries that get deduped.
|
||||
*/
|
||||
const buildIndexedDbCollection = ({
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
operation,
|
||||
logicalOperator,
|
||||
}) => {
|
||||
if (logicalOperator === 'and') {
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
return collection.and((record) => get(record, key)?.includes(value));
|
||||
case 'gt':
|
||||
return collection.and((record) => get(record, key) > value);
|
||||
case 'gte':
|
||||
return collection.and((record) => get(record, key) >= value);
|
||||
case 'lt':
|
||||
return collection.and((record) => get(record, key) < value);
|
||||
case 'lte':
|
||||
return collection.and((record) => get(record, key) <= value);
|
||||
case 'notEquals':
|
||||
return collection.and((record) => get(record, key) !== value);
|
||||
default:
|
||||
return collection.and((record) => get(record, key) === value);
|
||||
}
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
// We return an error due to not being able to support contains on this filter due to performance.
|
||||
throw new Error(
|
||||
'Contains is not supported as a filter option with "or" operator.',
|
||||
);
|
||||
case 'gt':
|
||||
return collection.or(key).above(value);
|
||||
case 'gte':
|
||||
return collection.or(key).aboveOrEqual(value);
|
||||
case 'lt':
|
||||
return collection.or(key).below(value);
|
||||
case 'lte':
|
||||
return collection.or(key).belowOrEqual(value);
|
||||
case 'notEquals':
|
||||
return collection.or(key).notEqual(value);
|
||||
default:
|
||||
return collection.or(key).equals(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates the fields passed in from the UI for resources to the keys indexed DB knows them as.
|
||||
* Usually it's the path of the actual normalized model.
|
||||
*/
|
||||
const getKey = (key) => {
|
||||
switch (key) {
|
||||
case 'scope_id':
|
||||
return `attributes.scope.${key}`;
|
||||
case 'id':
|
||||
return key;
|
||||
default:
|
||||
return `attributes.${key}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans the object to remove any keys that have a value of an empty array. This makes
|
||||
* it easier so we don't have to worry about empty arrays getting queried in indexed db.
|
||||
*/
|
||||
const cleanObject = (obj) => {
|
||||
if (!obj) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(obj).reduce((result, [key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
result[key] = value;
|
||||
}
|
||||
} else if (value.values.length > 0) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the search object that is expected when a user searches. Uses
|
||||
* the indexes we setup in indexed db.
|
||||
*/
|
||||
const getSearchObject = (text, resource) => {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
fields: modelIndexes[resource]
|
||||
.split(', ')
|
||||
.map((index) => index.replace('&', '')),
|
||||
};
|
||||
};
|
||||
@@ -9,8 +9,8 @@
|
||||
*
|
||||
* @example
|
||||
* const filter = generateMQLFilterExpression({
|
||||
* name: [{ contains: 'search text' }],
|
||||
* scope: [{ equals: 'scope1'}, { equals: 'scope2' }],
|
||||
* name: { logicalOperator: 'and', values: [{ notEquals: 'search text' }, { notEquals: 'test' }] },
|
||||
* scope_id: [{ equals: 'scope1'}, { equals: 'scope2' }],
|
||||
* });
|
||||
*
|
||||
* // (name % "search text") and (scope_id = "scope1" or scope_id = "scope2")
|
||||
@@ -21,10 +21,10 @@ export function generateMQLFilterExpression(filterObj) {
|
||||
}
|
||||
|
||||
const expressions = Object.entries(filterObj)
|
||||
.map(([key, filterObjValue]) => {
|
||||
const filterValueArray = Array.isArray(filterObjValue)
|
||||
? filterObjValue
|
||||
: filterObjValue.values;
|
||||
.map(([key, filterArrayOrObject]) => {
|
||||
const filterValueArray = Array.isArray(filterArrayOrObject)
|
||||
? filterArrayOrObject
|
||||
: filterArrayOrObject.values;
|
||||
|
||||
let clauses;
|
||||
const filterValues = filterValueArray.map((filterObjValue) => {
|
||||
@@ -53,7 +53,7 @@ export function generateMQLFilterExpression(filterObj) {
|
||||
});
|
||||
|
||||
// Default to `or` if we don't have a logical operator
|
||||
const { logicalOperator } = filterObjValue;
|
||||
const { logicalOperator } = filterArrayOrObject;
|
||||
if (logicalOperator === 'and') {
|
||||
clauses = and(filterValues);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Takes an array and the current page and pagesize to calculate the correct
|
||||
* number of results to return to the caller.
|
||||
*
|
||||
* @param array
|
||||
* @param page
|
||||
* @param pageSize
|
||||
* @returns {[*]}
|
||||
*/
|
||||
export const paginateResults = (array, page, pageSize) => {
|
||||
const length = array?.length;
|
||||
if (!array || length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (!page || !pageSize) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const start = Math.min(length - 1, offset);
|
||||
const end = Math.min(length, offset + pageSize);
|
||||
|
||||
return array.slice(start, end);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export { default } from 'api/utils/mql-query';
|
||||
@@ -31,7 +31,6 @@
|
||||
"ember-cli-mirage": "^2.4.0",
|
||||
"ember-data": "~4.12.0",
|
||||
"ember-fetch": "^8.1.1",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"tracked-built-ins": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -65,6 +64,7 @@
|
||||
"eslint-plugin-ember": "^11.11.1",
|
||||
"eslint-plugin-n": "^16.3.1",
|
||||
"eslint-plugin-qunit": "^8.0.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
|
||||
"loader.js": "^4.7.0",
|
||||
"prettier": "^3.0.0",
|
||||
|
||||
@@ -28,6 +28,10 @@ module.exports = function (environment) {
|
||||
host: '',
|
||||
namespace: 'v1',
|
||||
},
|
||||
|
||||
'ember-cli-mirage': {
|
||||
directory: './mirage',
|
||||
},
|
||||
};
|
||||
|
||||
if (environment === 'development') {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { createServer } from 'miragejs';
|
||||
|
||||
export default function (config) {
|
||||
return createServer({ ...config });
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
export default function (/* server */) {
|
||||
/*
|
||||
Seed your development database using your factories.
|
||||
This data will not be loaded in your tests.
|
||||
*/
|
||||
// server.createList('post', 10);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { JSONAPISerializer } from 'miragejs';
|
||||
|
||||
export default JSONAPISerializer.extend({});
|
||||
@@ -100,7 +100,7 @@ module('Unit | Adapter | application', function (hooks) {
|
||||
test('it can request records through the store from a specified scope', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/groups', (_, { queryParams: { scope_id } }) => {
|
||||
this.server.get('/groups', (_, { queryParams: { scope_id } }) => {
|
||||
assert.strictEqual(
|
||||
scope_id,
|
||||
'p_456',
|
||||
|
||||
@@ -29,19 +29,16 @@ module('Unit | Model | auth method', function (hooks) {
|
||||
|
||||
test('it has a `changeState` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/auth-methods/123abc:change-state',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
attributes: {
|
||||
state: 'foobar',
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/auth-methods/123abc:change-state', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
attributes: {
|
||||
state: 'foobar',
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
|
||||
@@ -80,7 +80,7 @@ module('Unit | Model | base', function (hooks) {
|
||||
attributes: {},
|
||||
},
|
||||
});
|
||||
this.server.patch('/v1/users/1', () => {
|
||||
this.server.patch('users/1', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.');
|
||||
return { id: 1 };
|
||||
});
|
||||
@@ -112,17 +112,17 @@ module('Unit | Model | base', function (hooks) {
|
||||
assert.expect(3);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const model = store.createRecord('user', { scopeID: 'o_123' });
|
||||
this.server.post('/v1/users', () => {
|
||||
this.server.post('users', () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save();
|
||||
this.server.patch('/v1/users/u_123', () => {
|
||||
this.server.patch('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save();
|
||||
this.server.delete('/v1/users/u_123', () => {
|
||||
this.server.delete('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped delete record URL was requested.');
|
||||
return {};
|
||||
});
|
||||
@@ -135,17 +135,17 @@ module('Unit | Model | base', function (hooks) {
|
||||
const customScopeID = 'global';
|
||||
const scope = { scope_id: 'o_123' };
|
||||
const model = store.createRecord('user', { scope });
|
||||
this.server.post('/v1/users', () => {
|
||||
this.server.post('users', () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save({ adapterOptions: { scopeID: customScopeID } });
|
||||
this.server.patch('/v1/users/u_123', () => {
|
||||
this.server.patch('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped update record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
await model.save({ adapterOptions: { scopeID: customScopeID } });
|
||||
this.server.delete('/v1/users/u_123', () => {
|
||||
this.server.delete('users/u_123', () => {
|
||||
assert.ok(true, 'Correctly scoped delete record URL was requested.');
|
||||
return {};
|
||||
});
|
||||
@@ -158,7 +158,7 @@ module('Unit | Model | base', function (hooks) {
|
||||
const method = 'my-custom-method';
|
||||
const scope = { scope_id: 'o_123' };
|
||||
const model = store.createRecord('user', { scope });
|
||||
this.server.post(`/v1/users:${method}`, () => {
|
||||
this.server.post(`users:${method}`, () => {
|
||||
assert.ok(true, 'Correctly scoped create record URL was requested.');
|
||||
return { id: 'u_123' };
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ module('Unit | Model | group', function (hooks) {
|
||||
|
||||
test('it has an `addMembers` method that groups a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/groups/123abc:add-members', (schema, request) => {
|
||||
this.server.post('/groups/123abc:add-members', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
member_ids: ['123_abc', 'foobar'],
|
||||
@@ -94,7 +94,7 @@ module('Unit | Model | group', function (hooks) {
|
||||
|
||||
test('it has a `removeMembers` method that groups a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/groups/123abc:remove-members', (schema, request) => {
|
||||
this.server.post('/groups/123abc:remove-members', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
member_ids: ['1', '3'],
|
||||
@@ -125,7 +125,7 @@ module('Unit | Model | group', function (hooks) {
|
||||
|
||||
test('it has a `removeMember` method that deletes a single member set using `removeMembers` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/groups/123abc:remove-members', (schema, request) => {
|
||||
this.server.post('/groups/123abc:remove-members', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
member_ids: ['3'],
|
||||
|
||||
@@ -13,7 +13,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has an `addHosts` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['123_abc', 'foobar'],
|
||||
@@ -44,7 +44,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has an `addHost` method that adds a single host using `addHosts` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:add-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['foobar'],
|
||||
@@ -75,7 +75,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has a `removeHosts` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['3'],
|
||||
@@ -106,7 +106,7 @@ module('Unit | Model | host set', function (hooks) {
|
||||
|
||||
test('it has a `removeHost` method that removes a single host using `removeHosts` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
this.server.post('/host-sets/123abc:remove-hosts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_ids: ['3'],
|
||||
|
||||
@@ -19,7 +19,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has a `saveGrantStrings` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/roles/123abc:set-grants', (schema, request) => {
|
||||
this.server.post('/roles/123abc:set-grants', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
grant_strings: ['foo', 'bar'],
|
||||
@@ -50,7 +50,7 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has an `addPrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/roles/123abc:add-principals', (schema, request) => {
|
||||
this.server.post('/roles/123abc:add-principals', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['123_abc', 'foobar'],
|
||||
@@ -85,17 +85,14 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has a `removePrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/roles/123abc:remove-principals',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['1', '3', '5'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/roles/123abc:remove-principals', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['1', '3', '5'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
@@ -123,17 +120,14 @@ module('Unit | Model | role', function (hooks) {
|
||||
|
||||
test('it has a `removePrincipal` method that deletes a single principal using `removePrincipals` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/roles/123abc:remove-principals',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['3'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/roles/123abc:remove-principals', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
principal_ids: ['3'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
|
||||
@@ -13,7 +13,7 @@ module('Unit | Model | scope', function (hooks) {
|
||||
|
||||
test('it may have a scope fragment', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/scopes', () => ({
|
||||
this.server.get('/scopes', () => ({
|
||||
items: [
|
||||
{ id: 'global', type: 'global' },
|
||||
{ id: 'o_1', type: 'org', scope: { scope_id: 'global' } },
|
||||
@@ -72,7 +72,7 @@ module('Unit | Model | scope', function (hooks) {
|
||||
test('it has an `attachStoragePolicy` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/scopes/123abc:attach-storage-policy',
|
||||
'/scopes/123abc:attach-storage-policy',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -106,7 +106,7 @@ module('Unit | Model | scope', function (hooks) {
|
||||
test('it has a `detachStoragePolicy` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/scopes/123abc:detach-storage-policy',
|
||||
'/scopes/123abc:detach-storage-policy',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
|
||||
@@ -79,17 +79,14 @@ module('Unit | Model | target', function (hooks) {
|
||||
|
||||
test('it has an `addHostSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:add-host-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_source_ids: ['123_abc', 'foobar'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
},
|
||||
);
|
||||
this.server.post('/targets/123abc:add-host-sources', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
host_source_ids: ['123_abc', 'foobar'],
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
@@ -117,7 +114,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeHostSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-host-sources',
|
||||
'/targets/123abc:remove-host-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -154,7 +151,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeHostSource` method that deletes a single host set using `removeHostSources` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-host-sources',
|
||||
'/targets/123abc:remove-host-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -283,7 +280,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has an `addBrokeredCredentialSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:add-credential-sources',
|
||||
'/targets/123abc:add-credential-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -317,7 +314,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeBrokeredCredentialSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-credential-sources',
|
||||
'/targets/123abc:remove-credential-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
@@ -351,7 +348,7 @@ module('Unit | Model | target', function (hooks) {
|
||||
test('it has a `removeBrokeredCredentialSource` method that deletes a single credential library using `removeBrokeredCredentialSources` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/targets/123abc:remove-credential-sources',
|
||||
'/targets/123abc:remove-credential-sources',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
|
||||
@@ -13,7 +13,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it has an `addAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:add-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:add-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['123_abc', 'foobar'],
|
||||
@@ -44,7 +44,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it has a `removeAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['1', '3'],
|
||||
@@ -75,7 +75,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it has a `removeAccount` method that deletes a single account set using `removeAccounts` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['3'],
|
||||
@@ -122,7 +122,7 @@ module('Unit | Model | user', function (hooks) {
|
||||
|
||||
test('it defaults to null for readonly attributes when response does not have fields', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['3'],
|
||||
|
||||
@@ -15,7 +15,7 @@ module('Unit | Model | worker', function (hooks) {
|
||||
assert.expect(2);
|
||||
const scopeId = 'global';
|
||||
const workerGeneratedAuthToken = 'token';
|
||||
this.server.post('/v1/workers:create:worker-led', (schema, request) => {
|
||||
this.server.post('/workers:create:worker-led', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
scope_id: scopeId,
|
||||
|
||||
@@ -379,7 +379,7 @@ module('Unit | Serializer | auth method', function (hooks) {
|
||||
const dryRun = true;
|
||||
const issuer = 'http://www.example.net';
|
||||
const maxAge = 500;
|
||||
this.server.get('/v1/auth-methods/oidc123', () => ({
|
||||
this.server.get('/auth-methods/oidc123', () => ({
|
||||
attributes: {
|
||||
account_claim_maps: ['from=to', 'foo=bar'],
|
||||
claims_scopes: ['profile', 'email'],
|
||||
@@ -447,7 +447,7 @@ module('Unit | Serializer | auth method', function (hooks) {
|
||||
const clientCerticate = 'certificate-1234';
|
||||
const startTls = true;
|
||||
const insecureTls = false;
|
||||
this.server.get('/v1/auth-methods/ldap123', () => ({
|
||||
this.server.get('/auth-methods/ldap123', () => ({
|
||||
attributes: {
|
||||
account_attribute_maps: ['from=to', 'foo=bar'],
|
||||
urls: [ldapUrl],
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
import { queryIndexedDb } from 'api/utils/indexed-db-query';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
|
||||
const seedIndexDb = async (resource, store, indexedDb, server) => {
|
||||
const resourceData = server.schema[pluralize(resource)].all().models;
|
||||
const serializedData = resourceData.map((resource) =>
|
||||
JSON.parse(JSON.stringify(server.serializerOrRegistry.serialize(resource))),
|
||||
);
|
||||
const serializer = store.serializerFor(resource);
|
||||
const schema = store.modelFor(resource);
|
||||
const normalizedPayload = serializer.normalizeResponse(
|
||||
store,
|
||||
schema,
|
||||
{ items: serializedData },
|
||||
null,
|
||||
'query',
|
||||
);
|
||||
|
||||
await indexedDb.db[resource].bulkPut(
|
||||
normalizedPayload.data.map((datum) => indexedDb.normalizeData(datum, true)),
|
||||
);
|
||||
};
|
||||
|
||||
module('Unit | Utility | indexed-db-query', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
let indexedDb;
|
||||
let store;
|
||||
|
||||
let scope1;
|
||||
let scope2;
|
||||
const resource = 'target';
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
indexedDb = this.owner.lookup('service:indexed-db');
|
||||
store = this.owner.lookup('service:store');
|
||||
scope1 = this.server.create('scope', {
|
||||
id: 'p_123',
|
||||
type: 'project',
|
||||
});
|
||||
scope2 = this.server.create('scope', {
|
||||
id: 'p_456',
|
||||
type: 'project',
|
||||
});
|
||||
this.server.createList(resource, 5, {
|
||||
type: TYPE_TARGET_SSH,
|
||||
scopeId: scope1.id,
|
||||
});
|
||||
this.server.createList(resource, 5, {
|
||||
type: TYPE_TARGET_TCP,
|
||||
scopeId: scope2.id,
|
||||
});
|
||||
});
|
||||
|
||||
test('it filters by single field', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "and" logicalOperator', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: TYPE_TARGET_SSH }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "or" logicalOperator', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields with "or" logicalOperator', async function (assert) {
|
||||
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 6);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields with "and" logicalOperator', async function (assert) {
|
||||
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { notEquals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields and uses two "or" logicalOperators', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: {
|
||||
logicalOperator: 'or',
|
||||
values: [{ equals: scope1.id }, { equals: scope2.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields and uses two "and" logicalOperators', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ notEquals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
|
||||
},
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it cannot filter with "contains" as initial operation', async function (assert) {
|
||||
assert.expect(1);
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ contains: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await queryIndexedDb(indexedDb.db, resource, query);
|
||||
} catch (e) {
|
||||
assert.strictEqual(
|
||||
e.message,
|
||||
'Contains is not supported as the first filter.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('it cannot filter with "contains" as subsequent operation', async function (assert) {
|
||||
assert.expect(2);
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope1.id }, { contains: scope2.id.slice(0, 2) }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
try {
|
||||
await queryIndexedDb(indexedDb.db, resource, query);
|
||||
} catch (e) {
|
||||
assert.strictEqual(
|
||||
e.message,
|
||||
'Contains is not supported as a filter option with "or" operator.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Modify test cases that use comparison opertators to compare fields that
|
||||
// are of type number. Additionally, add test case for searching on boolean fields.
|
||||
test('it filters by single field with "gt" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ gt: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "gt" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { gt: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "gte" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ gte: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "gte" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { gte: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lt" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ lt: scope2.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lt" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope1.id }, { lt: scope2.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lte" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ lte: scope2.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "lte" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope1.id }, { lte: scope2.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it filters by single field with "notEquals" as initial operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: [{ notEquals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by single field with "notEquals" as subsequent operation', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
scope_id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
|
||||
query.filters.scope_id.logicalOperator = 'or';
|
||||
|
||||
result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters and returns zero results', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
type: [{ equals: 'unknown' }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('it searches with a query that matches a field exactly', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = { search: scope1.id };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it searches with a query that matches a field partially', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = { search: scope2.id.slice(0, 2) };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 10);
|
||||
});
|
||||
|
||||
test('it searches and returns no results with a query that matches no fields', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = { search: `${scope1.id}456789` };
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('it filters by a single field and searches', async function (assert) {
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
search: scope1.id,
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 5);
|
||||
});
|
||||
|
||||
test('it filters by multiple fields and searches', async function (assert) {
|
||||
const target = this.server.create(resource, {
|
||||
name: 'Generated_ssh_target',
|
||||
type: TYPE_TARGET_SSH,
|
||||
scopeId: scope1.id,
|
||||
});
|
||||
await seedIndexDb(resource, store, indexedDb, this.server);
|
||||
const query = {
|
||||
search: target.name,
|
||||
filters: {
|
||||
type: [{ equals: TYPE_TARGET_SSH }],
|
||||
scope_id: [{ equals: scope1.id }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(indexedDb.db, resource, query);
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user