/** * Copyright (c) 2020 Gitpod GmbH. All rights reserved. * Licensed under the GNU Affero General Public License (AGPL). * See License-AGPL.txt in the project root for license information. */ import { injectable } from "inversify"; export interface TableDependency { table: string; fields: string[]; } export interface TableDescription { name: string; primaryKeys: string[]; timeColumn: string; dependencies?: string[]; expiryColumn?: string; deletionColumn?: string; ignoreColumns?: string[]; } export interface TableDescriptionProvider { readonly name: string; getSortedTables(): TableDescription[]; } export const TableDescriptionProvider = Symbol('TableDescriptionProvider'); @injectable() export class GitpodSessionTableDescriptionProvider implements TableDescriptionProvider { readonly name = "gitpod-sessions"; getSortedTables(): TableDescription[] { return [ { name: "sessions", primaryKeys: ['session_id'], timeColumn: '_lastModified', expiryColumn: 'expires' } ] } } /** * BEWARE * * When updating this list, make sure you update the deleted-entry-gc in gitpod-db * as well, if you're adding a table that needs some of its entries deleted. */ @injectable() export class GitpodTableDescriptionProvider implements TableDescriptionProvider { readonly name = "gitpod"; protected readonly tables: TableDescription[] = [ { name: 'd_b_account_entry', primaryKeys: ['uid'], timeColumn: '_lastModified' }, { name: 'd_b_subscription', primaryKeys: ['uid'], timeColumn: '_lastModified', dependencies: ['d_b_user'] }, { name: 'd_b_subscription_additional_data', primaryKeys: ['paymentReference'], timeColumn: 'lastModified' }, { name: 'd_b_identity', primaryKeys: ['authProviderId', 'authId'], timeColumn: '_lastModified', deletionColumn: 'deleted', dependencies: ['d_b_user'] }, { name: 'd_b_user', primaryKeys: ['id'], timeColumn: '_lastModified' }, { name: 'd_b_user_message_view_entry', primaryKeys: ['id'], timeColumn: 'viewedAt', dependencies: ['d_b_user'] }, { name: 'd_b_user_storage_resource', primaryKeys: ['id'], timeColumn: '_lastModified', deletionColumn: 'deleted', dependencies: ['d_b_user'] }, { name: 'd_b_workspace', primaryKeys: ['id'], timeColumn: '_lastModified', deletionColumn: 'deleted', dependencies: ['d_b_user'] }, { name: 'd_b_workspace_instance', primaryKeys: ['id'], timeColumn: '_lastModified', dependencies: ['d_b_workspace'], deletionColumn: 'deleted', ignoreColumns: ['phase'] }, { name: 'd_b_workspace_instance_user', primaryKeys: ['instanceId', 'userId'], timeColumn: '_lastModified', dependencies: ['d_b_workspace_instance', 'd_b_user'] }, { name: 'd_b_workspace_report_entry', primaryKeys: ['uid'], timeColumn: 'time', dependencies: ['d_b_workspace'] }, { name: 'd_b_snapshot', primaryKeys: ['id'], timeColumn: 'creationTime', dependencies: ['d_b_workspace'] }, { name: 'd_b_email_domain_filter', primaryKeys: ['domain'], timeColumn: '_lastModified' }, { name: 'd_b_prebuilt_workspace', primaryKeys: ['id'], timeColumn: '_lastModified' }, { name: 'd_b_app_installation', primaryKeys: ['platform', 'installationID', 'state'], timeColumn: 'creationTime' }, { name: 'd_b_token_entry', primaryKeys: ['uid'], deletionColumn: 'deleted', timeColumn: '_lastModified' }, { name: 'd_b_team_subscription', primaryKeys: ['id'], timeColumn: '_lastModified' }, { name: 'd_b_team_subscription_slot', primaryKeys: ['id'], timeColumn: '_lastModified' }, { name: 'd_b_edu_email_domain', primaryKeys: ['domain'], timeColumn: '_lastModified' }, { name: 'd_b_theia_plugin', primaryKeys: ['id'], timeColumn: '_lastModified' }, { name: 'd_b_user_env_var', primaryKeys: ['id', 'userId'], timeColumn: '_lastModified' }, { name: 'd_b_email', primaryKeys: ['uid'], timeColumn: '_lastModified' }, { name: 'd_b_payment_source_info', primaryKeys: ['id', 'resourceVersion'], timeColumn: '_lastModified' }, { name: 'd_b_gitpod_token', primaryKeys: ['tokenHash'], deletionColumn: 'deleted', timeColumn: '_lastModified', dependencies: ['d_b_user'], }, { name: 'd_b_one_time_secret', primaryKeys: ['id'], deletionColumn: 'deleted', timeColumn: '_lastModified', }, { name: 'd_b_auth_provider_entry', primaryKeys: ['id'], deletionColumn: 'deleted', timeColumn: '_lastModified', }, { name: 'd_b_code_sync_resource', primaryKeys: ['userId','kind','rev'], deletionColumn: 'deleted', timeColumn: 'created', }, { name: 'd_b_terms_acceptance_entry', primaryKeys: ['userId'], timeColumn: '_lastModified', }, ] public getSortedTables(): TableDescription[] { return new TopologicalSort().sort(this.tables, t => t.name, t => (t.dependencies || []).map(e => this.tables.find(te => te.name == e)!)); } } class TopologicalSort { protected result: E[] = []; protected visitedNodes: Map; public sort(values: E[], key: (e: E)=>K, edge: (e: E)=>E[]): E[] { this.visitedNodes = new Map(); this.result = []; for(const e of values) { const k = key(e); const priorVisit = this.visitedNodes.get(k); if(priorVisit === undefined) { this.visit(e, key, edge); } } return this.result; } protected visit(e: E, key: (e: E)=>K, edges: (e: E)=>E[]) { if(this.isMarkedPermanently(key(e))) return; if(this.isMarkedTemporarily(key(e))) { throw new Error("Circle detected in " + key); } this.visitedNodes.set(key(e), false); edges(e).forEach(edge => this.visit(edge, key, edges)); this.visitedNodes.set(key(e), true); this.result.push(e); } protected isMarkedPermanently(k: K) { return this.visitedNodes.get(k) === true; } protected isMarkedTemporarily(k: K) { return this.visitedNodes.get(k) === false; } protected isUnmarked(k: K) { return this.visitedNodes.get(k) === undefined; } }