Files
azuredatastudio/src/sql/platform/accounts/common/accountStore.ts
2023-03-09 14:34:39 -08:00

234 lines
8.2 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { AccountAdditionResult } from 'sql/platform/accounts/common/eventTypes';
import { IAccountStore } from 'sql/platform/accounts/common/interfaces';
import { deepClone } from 'vs/base/common/objects';
import { ILogService } from 'vs/platform/log/common/log';
export default class AccountStore implements IAccountStore {
private readonly deprecatedProviders = ['azurePublicCloud'];
// CONSTANTS ///////////////////////////////////////////////////////////
public static MEMENTO_KEY: string = 'Microsoft.SqlTools.Accounts';
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _activeOperation?: Promise<any>;
constructor(
private _memento: { [key: string]: any },
@ILogService readonly logService: ILogService
) { }
// PUBLIC METHODS //////////////////////////////////////////////////////
public addOrUpdate(newAccount: azdata.Account): Promise<AccountAdditionResult> {
return this.doOperation(() => {
return this.readFromMemento()
.then(accounts => {
// Determine if account exists and proceed accordingly
const match = accounts.findIndex(account => AccountStore.isSameAccountKey(account.key, newAccount.key));
return match < 0
? this.addToAccountList(accounts, newAccount)
: this.updateAccountList(accounts, newAccount.key, matchAccount => AccountStore.mergeAccounts(newAccount, matchAccount));
})
.then(result => this.writeToMemento(result.updatedAccounts).then(() => result))
.then(result => ({ accountAdded: result.accountAdded, accountModified: result.accountModified, changedAccount: result.changedAccount! }));
});
}
public async getAccountsByProvider(providerId: string): Promise<azdata.Account[]> {
const accounts = await this.doOperation(async () => {
await this.cleanupDeprecatedAccounts();
const accounts = await this.readFromMemento();
return accounts.filter(account => account.key.providerId === providerId);
});
return accounts ?? [];
}
public async getAllAccounts(): Promise<azdata.Account[]> {
const accounts = await this.doOperation(async () => {
await this.cleanupDeprecatedAccounts();
return this.readFromMemento();
});
return accounts ?? [];
}
public cleanupDeprecatedAccounts(): Promise<void> {
return this.readFromMemento()
.then(accounts => {
// No need to waste cycles
if (!accounts || accounts.length === 0) {
return Promise.resolve();
}
// Remove old accounts that are now deprecated
try {
accounts = accounts.filter(account => {
const providerKey = account?.key?.providerId;
// Account has no provider, remove it.
if (providerKey === undefined) {
return false;
}
// Returns true if the account isn't from a deprecated provider
return !this.deprecatedProviders.includes(providerKey);
});
} catch (ex) {
this.logService.error(ex);
return Promise.resolve();
}
return this.writeToMemento(accounts);
});
}
public remove(key: azdata.AccountKey): Promise<boolean> {
return this.doOperation(() => {
return this.readFromMemento()
.then(accounts => this.removeFromAccountList(accounts, key))
.then(result => this.writeToMemento(result.updatedAccounts).then(() => result))
.then(result => result.accountRemoved);
});
}
public update(key: azdata.AccountKey, updateOperation: (account: azdata.Account) => void): Promise<boolean> {
return this.doOperation(() => {
return this.readFromMemento()
.then(accounts => this.updateAccountList(accounts, key, updateOperation))
.then(result => this.writeToMemento(result.updatedAccounts).then(() => result))
.then(result => result.accountModified);
});
}
// PRIVATE METHODS /////////////////////////////////////////////////////
private static isSameAccountKey(key1: azdata.AccountKey | undefined, key2: azdata.AccountKey | undefined): boolean {
// Provider ID and Account ID must match
return key1 && key2
? key1.providerId === key2.providerId && key1.accountId === key2.accountId
: false;
}
private static mergeAccounts(source: azdata.Account, target: azdata.Account): void {
// Take any display info changes
target.displayInfo = source.displayInfo;
// Take all property changes
target.properties = source.properties;
// Take any stale changes
target.isStale = source.isStale;
}
private doOperation<T>(op: () => Promise<T>): Promise<T | undefined> {
// Initialize the active operation to an empty promise if necessary
let activeOperation = this._activeOperation || Promise.resolve();
// Chain the operation to perform to the end of the existing promise
activeOperation = activeOperation.then(op);
// Add a catch at the end to make sure we can continue after any errors
activeOperation = activeOperation.then(undefined, err => {
this.logService.error(err);
});
// Point the current active operation to this one
this._activeOperation = activeOperation;
return <Promise<T>>this._activeOperation;
}
private addToAccountList(accounts: azdata.Account[], accountToAdd: azdata.Account): AccountListOperationResult {
// Check if the entry already exists
const match = accounts.findIndex(account => AccountStore.isSameAccountKey(account.key, accountToAdd.key));
if (match >= 0) {
// Account already exists, we won't do anything
return {
accountAdded: false,
accountModified: false,
accountRemoved: false,
changedAccount: undefined,
updatedAccounts: accounts
};
}
// Add the account to the store
accounts.push(accountToAdd);
return {
accountAdded: true,
accountModified: false,
accountRemoved: false,
changedAccount: accountToAdd,
updatedAccounts: accounts
};
}
private removeFromAccountList(accounts: azdata.Account[], accountToRemove: azdata.AccountKey): AccountListOperationResult {
// Check if the entry exists
const match = accounts.findIndex(account => AccountStore.isSameAccountKey(account.key, accountToRemove));
if (match >= 0) {
// Account exists, remove it from the account list
accounts.splice(match, 1);
}
return {
accountAdded: false,
accountModified: false,
accountRemoved: match >= 0,
changedAccount: undefined,
updatedAccounts: accounts
};
}
private updateAccountList(accounts: azdata.Account[], accountToUpdate: azdata.AccountKey, updateOperation: (account: azdata.Account) => void): AccountListOperationResult {
// Check if the entry exists
const match = accounts.findIndex(account => AccountStore.isSameAccountKey(account.key, accountToUpdate));
if (match < 0) {
// Account doesn't exist, we won't do anything
return {
accountAdded: false,
accountModified: false,
accountRemoved: false,
changedAccount: undefined,
updatedAccounts: accounts
};
}
// Account exists, apply the update operation to it
updateOperation(accounts[match]);
return {
accountAdded: false,
accountModified: true,
accountRemoved: false,
changedAccount: accounts[match],
updatedAccounts: accounts
};
}
// MEMENTO IO METHODS //////////////////////////////////////////////////
private readFromMemento(): Promise<azdata.Account[]> {
// Initialize the account list if it isn't already
let accounts = this._memento[AccountStore.MEMENTO_KEY];
if (!accounts) {
accounts = [];
}
this.logService.debug(`Read accounts from memento ${JSON.stringify(accounts)}`);
// Make a deep copy of the account list to ensure that the memento list isn't obliterated
accounts = deepClone(accounts);
return Promise.resolve(accounts);
}
private writeToMemento(accounts: azdata.Account[]): Promise<void> {
// Store a shallow copy of the account list to disconnect the memento list from the active list
this._memento[AccountStore.MEMENTO_KEY] = deepClone(accounts);
return Promise.resolve();
}
}
interface AccountListOperationResult {
accountRemoved: boolean;
updatedAccounts: azdata.Account[];
changedAccount: azdata.Account | undefined;
accountAdded: boolean;
accountModified: boolean;
}