feat: 🎸 Update how we store tokens in indexed-db (#2223)
Added logic to make the list tokens stored in indexed DB more granular. I get a hash of the query parameters that will get sent to the controller and use that as the id for the token. This also means extra logic for when a token is invalid as we will need to delete all tokens of the same resource type.
This commit is contained in:
1 parent
6c59baa413
commit
5d32d987bc
13 files changed
+121
-33
No files matched your search
@@ -283,7 +283,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
const baseError = {
|
||||
isInvalid: true,
|
||||
status,
|
||||
code: payload.code,
|
||||
code: payload.kind,
|
||||
detail: payload.message,
|
||||
source: { pointer: '/data' },
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { queryIndexedDb } from '../utils/indexed-db-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { hashCode } from '../utils/hash-code';
|
||||
|
||||
/**
|
||||
* Handler to sit in front of the API request layer
|
||||
@@ -34,9 +35,10 @@ export default class IndexedDbHandler {
|
||||
}
|
||||
|
||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
||||
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
||||
|
||||
if (!peekIndexedDB) {
|
||||
const listToken = await indexedDb.token.get(type);
|
||||
const listToken = await indexedDb.token.get(tokenKey);
|
||||
const adapter = store.adapterFor(type);
|
||||
const schema = store.modelFor(type);
|
||||
|
||||
@@ -44,35 +46,41 @@ export default class IndexedDbHandler {
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
scope_id: 'global',
|
||||
recursive: true,
|
||||
list_token: listToken?.token,
|
||||
});
|
||||
} catch (e) {
|
||||
// If we get an invalid list token, we'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
if (
|
||||
e?.errors[0].status === 400 &&
|
||||
e?.errors[0].code === 'invalid list token'
|
||||
) {
|
||||
await indexedDb[type].clear();
|
||||
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
||||
// Even if other tokens may still be valid, we might have cleared data that they
|
||||
// depended on so we should clear all tokens of the same type when clearing the DB.
|
||||
const tokenKeys = await indexedDb.token
|
||||
.where(':id')
|
||||
.startsWith(type)
|
||||
.primaryKeys();
|
||||
|
||||
await indexedDb.token.bulkDelete(tokenKeys);
|
||||
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
});
|
||||
} catch (e) {
|
||||
// If we get any error that's not a rate limiting error,
|
||||
// assume the token is not good anymore. We'll delete the token and
|
||||
// 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[type].clear();
|
||||
await indexedDb.token.delete(type);
|
||||
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
scope_id: 'global',
|
||||
recursive: true,
|
||||
list_token: null,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the token we just got back from the payload
|
||||
// Store the token we just got back from the payload if it exists
|
||||
if (payload.list_token) {
|
||||
await indexedDb.token.put({
|
||||
id: type,
|
||||
id: tokenKey,
|
||||
token: payload.list_token,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove any records from the DB if the API indicates they've been deleted
|
||||
if (payload.removed_ids?.length > 0) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Takes an object and gets the JSON stringified version of it to be converted
|
||||
* to a hash code. Taken from https://stackoverflow.com/a/8076436
|
||||
* @param obj
|
||||
*/
|
||||
export const hashCode = (obj) => {
|
||||
if (!obj) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Use a replacer function to sort the keys of the object so we get the same hashcode
|
||||
// regardless of the order of the keys.
|
||||
const jsonString = JSON.stringify(obj, Object.keys(obj).sort());
|
||||
|
||||
let hash = 0;
|
||||
for (let i = 0; i < jsonString.length; i++) {
|
||||
let code = jsonString.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + code;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
|
||||
// Make sure the hash is positive since we're using it as an ID
|
||||
return Math.abs(hash);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { hashCode } from 'api/utils/hash-code';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | hash-code', function () {
|
||||
test('it hashes', function (assert) {
|
||||
const result = hashCode({ a: 1, b: 2 });
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('it hashes to same result regardless of object order', function (assert) {
|
||||
const first = hashCode({ a: 1, b: 2, c: 3 });
|
||||
const second = hashCode({ b: 2, c: 3, a: 1 });
|
||||
assert.strictEqual(first, second);
|
||||
});
|
||||
|
||||
test('it hashes to different result if objects are different', function (assert) {
|
||||
const first = hashCode({ a: 1, b: 2, c: 3 });
|
||||
const second = hashCode({ a: '1', b: '2', c: '3' });
|
||||
assert.notStrictEqual(first, second);
|
||||
});
|
||||
|
||||
test('it hashes an empty object', function (assert) {
|
||||
const result = hashCode({});
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test('it hashes null to 0', function (assert) {
|
||||
const result = hashCode(null);
|
||||
assert.strictEqual(result, 0);
|
||||
});
|
||||
|
||||
test('it hashes undefined to 0', function (assert) {
|
||||
const result = hashCode(undefined);
|
||||
assert.strictEqual(result, 0);
|
||||
});
|
||||
});
|
||||
@@ -58,6 +58,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
|
||||
})
|
||||
) {
|
||||
credentialStores = await this.store.query('credential-store', {
|
||||
scope_id,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
@@ -86,6 +87,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
|
||||
const credentialStore = await this.store.query(
|
||||
'credential-store',
|
||||
{
|
||||
scope_id,
|
||||
query: { filters: { scope_id: [{ equals: scope_id }] } },
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
|
||||
@@ -43,6 +43,7 @@ export default class ScopesScopeGroupsIndexRoute extends Route {
|
||||
|
||||
if (this.can.can('list model', scope, { collection: 'groups' })) {
|
||||
groups = await this.store.query('group', {
|
||||
scope_id,
|
||||
query: { filters, search },
|
||||
page,
|
||||
pageSize,
|
||||
@@ -67,6 +68,7 @@ export default class ScopesScopeGroupsIndexRoute extends Route {
|
||||
const group = await this.store.query(
|
||||
'group',
|
||||
{
|
||||
scope_id: scope_id,
|
||||
query: {
|
||||
filters: {
|
||||
scope_id: [{ equals: scope_id }],
|
||||
|
||||
@@ -48,6 +48,7 @@ export default class ScopesScopeHostCatalogsIndexRoute extends Route {
|
||||
let hostCatalogsExist = false;
|
||||
if (this.can.can('list model', scope, { collection: 'host-catalogs' })) {
|
||||
hostCatalogs = await this.store.query('host-catalog', {
|
||||
scope_id,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
@@ -74,6 +75,7 @@ export default class ScopesScopeHostCatalogsIndexRoute extends Route {
|
||||
const hostCatalogs = await this.store.query(
|
||||
'host-catalog',
|
||||
{
|
||||
scope_id,
|
||||
query: { filters: { scope_id: [{ equals: scope_id }] } },
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
|
||||
@@ -45,6 +45,7 @@ export default class ScopesScopeRolesIndexRoute extends Route {
|
||||
let rolesExist = false;
|
||||
if (this.can.can('list model', scope, { collection: 'roles' })) {
|
||||
roles = await this.store.query('role', {
|
||||
scope_id,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
@@ -70,6 +71,7 @@ export default class ScopesScopeRolesIndexRoute extends Route {
|
||||
const role = await this.store.query(
|
||||
'role',
|
||||
{
|
||||
scope_id,
|
||||
query: {
|
||||
filters: {
|
||||
scope_id: [{ equals: scope_id }],
|
||||
|
||||
@@ -67,6 +67,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
});
|
||||
|
||||
const queryOptions = {
|
||||
scope_id,
|
||||
include_terminated: true,
|
||||
query: { search, filters },
|
||||
page,
|
||||
@@ -84,7 +85,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
await this.getAssociatedUsers();
|
||||
}
|
||||
if (!this.associatedTargets) {
|
||||
await this.getAssociatedTargets();
|
||||
await this.getAssociatedTargets(scope_id);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -103,6 +104,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
*/
|
||||
async getAllSessions(scope_id) {
|
||||
const allSessionsQuery = {
|
||||
scope_id,
|
||||
include_terminated: true,
|
||||
query: { filters: { scope_id: [{ equals: scope_id }] } },
|
||||
};
|
||||
@@ -128,6 +130,8 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
filters.id.values.push({ equals: userId });
|
||||
});
|
||||
const associatedUsersQuery = {
|
||||
scope_id: 'global',
|
||||
recursive: true,
|
||||
query: { filters },
|
||||
};
|
||||
this.associatedUsers = await this.store.query('user', associatedUsersQuery);
|
||||
@@ -135,9 +139,10 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
|
||||
/**
|
||||
* Get all the targets but only load them once when entering the route.
|
||||
* @param scope_id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async getAssociatedTargets() {
|
||||
async getAssociatedTargets(scope_id) {
|
||||
const uniqueSessionTargetIds = new Set(
|
||||
this.allSessions
|
||||
.filter((session) => session.target_id)
|
||||
@@ -148,6 +153,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
filters.id.values.push({ equals: targetId });
|
||||
});
|
||||
const associatedTargetsQuery = {
|
||||
scope_id,
|
||||
query: { filters },
|
||||
};
|
||||
this.associatedTargets = await this.store.query(
|
||||
|
||||
@@ -61,6 +61,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
|
||||
});
|
||||
|
||||
const sessions = await this.store.query('session', {
|
||||
scope_id,
|
||||
query: {
|
||||
filters: {
|
||||
scope_id: [{ equals: scope_id }],
|
||||
@@ -78,6 +79,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
|
||||
let targetsExist = false;
|
||||
if (this.can.can('list model', scope, { collection: 'targets' })) {
|
||||
targets = await this.store.query('target', {
|
||||
scope_id,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
@@ -46,6 +46,7 @@ export default class ScopesScopeUsersIndexRoute extends Route {
|
||||
let usersExist = false;
|
||||
if (this.can.can('list model', scope, { collection: 'users' })) {
|
||||
users = await this.store.query('user', {
|
||||
scope_id,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
@@ -59,11 +60,11 @@ export default class ScopesScopeUsersIndexRoute extends Route {
|
||||
|
||||
/**
|
||||
* Sets usersExist to true if there exists any users.
|
||||
* @param {string} scopeId
|
||||
* @param {string} scope_id
|
||||
* @param {number} totalItems
|
||||
* @returns
|
||||
*/
|
||||
async getUsersExist(scopeId, totalItems) {
|
||||
async getUsersExist(scope_id, totalItems) {
|
||||
if (totalItems > 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -71,9 +72,10 @@ export default class ScopesScopeUsersIndexRoute extends Route {
|
||||
const user = await this.store.query(
|
||||
'user',
|
||||
{
|
||||
scope_id,
|
||||
query: {
|
||||
filters: {
|
||||
scope_id: [{ equals: scopeId }],
|
||||
scope_id: [{ equals: scope_id }],
|
||||
},
|
||||
},
|
||||
page: 1,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
}}
|
||||
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<Breadcrumbs::Container />
|
||||
</page.breadcrumbs>
|
||||
@@ -22,9 +21,10 @@
|
||||
<page.actions>
|
||||
{{#if (can 'create model' this.scope collection='targets')}}
|
||||
{{#if @model.targetsExist}}
|
||||
<Rose::LinkButton @route='scopes.scope.targets.new' @style='primary'>{{t
|
||||
'resources.target.titles.new'
|
||||
}}</Rose::LinkButton>
|
||||
<Hds::Button
|
||||
@route='scopes.scope.targets.new'
|
||||
@text={{t 'resources.target.titles.new'}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</page.actions>
|
||||
|
||||
@@ -22,10 +22,12 @@ import {
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
|
||||
module('Acceptance | targets | create', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
let getTargetCount;
|
||||
let getTCPTargetCount;
|
||||
|
||||
Reference in new issue
Block a user