/*--------------------------------------------------------------------------------------------- * 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; constructor( private _memento: { [key: string]: any }, @ILogService readonly logService: ILogService ) { } // PUBLIC METHODS ////////////////////////////////////////////////////// public addOrUpdate(newAccount: azdata.Account): Promise { 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 { 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 { const accounts = await this.doOperation(async () => { await this.cleanupDeprecatedAccounts(); return this.readFromMemento(); }); return accounts ?? []; } public cleanupDeprecatedAccounts(): Promise { 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 { 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 { 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(op: () => Promise): Promise { // 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 >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 { // 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 { // 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; }