feat: 🎸 Add sqlite support to sessions (#2978)

* feat: 🎸 Add sqlite support to sessions

* refactor: 💡 only peek sqlite db when batching queries

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17411
This commit is contained in:
Lisbet Alvarez authored and Zhihe Li committed 2025-09-03 11:41:42 -04:00
1 parent dc026c44a1
commit 4c7ea161cd
3 files changed
+144 -43

No files matched your search

+11
View File
@@ -99,6 +99,16 @@ export const modelMapping = {
target_scope_name: 'create_time_values.target.scope.name',
created_time: 'created_time',
},
session: {
id: 'id',
type: 'type',
status: 'status',
endpoint: 'endpoint',
target_id: 'target_id',
user_id: 'user_id',
scope_id: 'scope.scope_id',
created_time: 'created_time',
},
};
// A list of tables that we support searching using FTS5 in SQLite.
@@ -113,6 +123,7 @@ export const searchTables = new Set([
'auth-method',
'host-catalog',
'session-recording',
'session',
]);
export default class SqliteDbService extends Service {
+40 -10
View File
@@ -388,6 +388,45 @@ CREATE TRIGGER IF NOT EXISTS session_recording_ad AFTER DELETE ON session_record
VALUES('delete', old.rowid, old.id, old.type, old.state, old.start_time, old.end_time, old.duration, old.scope_id, old.user_id, old.user_name, old.target_id, old.target_name, old.target_scope_id, old.target_scope_name, old.created_time);
END;`;
const createSessionTables = `
CREATE TABLE IF NOT EXISTS session (
id TEXT NOT NULL PRIMARY KEY,
type TEXT,
status TEXT,
endpoint TEXT,
target_id TEXT,
user_id TEXT,
scope_id TEXT NOT NULL,
created_time TEXT NOT NULL,
data TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_session_scope_id_created_time ON session(scope_id, created_time DESC);
CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
id,
type,
status,
endpoint,
target_id,
user_id,
scope_id,
created_time,
content='',
);
CREATE TRIGGER IF NOT EXISTS session_ai AFTER INSERT ON session BEGIN
INSERT INTO session_fts(
id, type, status, endpoint, target_id, user_id, scope_id, created_time
) VALUES (
new.id, new.type, new.status, new.endpoint, new.target_id, new.user_id, new.scope_id, new.created_time
);
END;
CREATE TRIGGER IF NOT EXISTS session_ad AFTER DELETE ON session BEGIN
INSERT INTO session_fts(session_fts, rowid, id, type, status, endpoint, target_id, user_id, scope_id, created_time)
VALUES('delete', old.rowid, old.id, old.type, old.status, old.endpoint, old.target_id, old.user_id, old.scope_id, old.created_time);
END;`;
export const CREATE_TABLES = (version) => `
BEGIN;
@@ -408,16 +447,7 @@ ${createScopeTables}
${createAuthMethodTables}
${createHostCatalogTables}
${createSessionRecordingTables}
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);
${createSessionTables}
COMMIT;`;
@@ -7,6 +7,13 @@ import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { restartableTask, timeout } from 'ember-concurrency';
import chunk from 'lodash/chunk';
// Maximum expression tree depth is 1000 so
// we limit the number of expressions in a query.
// See "Maximum Depth Of An Expression Tree" in
// https://www.sqlite.org/limits.html
const MAX_EXPR_NUM = 999;
export default class ScopesScopeSessionsIndexRoute extends Route {
// =services
@@ -114,10 +121,11 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
const totalItems = sessions.meta?.totalItems;
const allSessions = await this.getAllSessions(scope_id);
const [associatedUsers, associatedTargets] = await Promise.all([
this.getAssociatedUsers(scope, allSessions),
this.getAssociatedTargets(scope, allSessions),
]);
const associatedUsers = await this.getAssociatedUsers(scope, allSessions);
const associatedTargets = await this.getAssociatedTargets(
scope,
allSessions,
);
return {
sessions,
@@ -159,24 +167,45 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
this.can.can('list model', globalScope, { collection: 'users' }) &&
this.can.can('list model', orgScope, { collection: 'users' })
) {
const uniqueSessionUserIds = new Set(
allSessions
.filter((session) => session.user_id)
.map((session) => session.user_id),
const users = await this.store.query(
'user',
{
scope_id: 'global',
recursive: true,
page: 1,
pageSize: 1,
},
{ pushToStore: false },
);
const filters = {
id: { values: [] },
};
uniqueSessionUserIds.forEach((userId) => {
filters.id.values.push({ equals: userId });
});
const associatedUsersQuery = {
scope_id: 'global',
recursive: true,
query: { filters },
};
return this.store.query('user', associatedUsersQuery);
if (!users.length) {
return [];
}
const uniqueSessionUserIds = [
...new Set(
allSessions
.filter((session) => session.user_id)
.map((session) => session.user_id),
),
];
const chunkedUserIds = chunk(uniqueSessionUserIds, MAX_EXPR_NUM);
const associatedUsersPromises = chunkedUserIds.map((userIds) =>
this.store.query(
'user',
{
scope_id: 'global',
query: {
filters: {
id: { values: userIds.map((userId) => ({ equals: userId })) },
},
},
},
{ peekDb: true },
),
);
const usersArray = await Promise.all(associatedUsersPromises);
return usersArray.flat();
}
return [];
@@ -189,21 +218,52 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
*/
async getAssociatedTargets(scope, allSessions) {
if (this.can.can('list model', scope, { collection: 'targets' })) {
const uniqueSessionTargetIds = new Set(
allSessions
.filter((session) => session.target_id)
.map((session) => session.target_id),
const targets = await this.store.query(
'target',
{
scope_id: scope.id,
query: {
filters: {
scope_id: [{ equals: scope.id }],
},
},
page: 1,
pageSize: 1,
},
{ pushToStore: false },
);
const filters = { id: { values: [] } };
uniqueSessionTargetIds.forEach((targetId) => {
filters.id.values.push({ equals: targetId });
});
const associatedTargetsQuery = {
scope_id: scope.id,
query: { filters },
};
return this.store.query('target', associatedTargetsQuery);
if (!targets.length) {
return [];
}
const uniqueSessionTargetIds = [
...new Set(
allSessions
.filter((session) => session.target_id)
.map((session) => session.target_id),
),
];
const chunkedTargetIds = chunk(uniqueSessionTargetIds, MAX_EXPR_NUM);
const associatedTargetsPromises = chunkedTargetIds.map((targetIds) =>
this.store.query(
'target',
{
scope_id: scope.id,
query: {
filters: {
id: {
values: targetIds.map((targetId) => ({ equals: targetId })),
},
},
},
},
{ peekDb: true },
),
);
const targetsArray = await Promise.all(associatedTargetsPromises);
return targetsArray.flat();
}
return [];