controller dropdown field to SQL MIAA and Postgres deployment. (#12217)

* saving first draft

* throw if no controllers

* cleanup

* bug fixes

* bug fixes and caching controller access

* pr comments and bug fixes.

* fixes

* fixes

* comment fix

* remove debug prints

* comment fixes

* remove debug logs

* inputValueTransformer returns string|Promise

* PR feedback

* pr fixes

* remove _ from protected fields

* anonymous to full methods

* small fixes
This commit is contained in:
Arvind Ranasaria
2020-09-15 14:47:49 -07:00
committed by GitHub
parent 92ed830564
commit 9cf80113fc
36 changed files with 754 additions and 190 deletions

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Deferred } from './promise';
const enum Status {
notStarted,
inProgress,
done
}
interface State<T> {
entry?: T,
error?: Error,
status: Status,
id: number,
pendingOperation: Deferred<void>
}
/**
* An implementation of Cache Manager which ensures that only one call to populate cache miss is pending at a given time.
* All remaining calls for retrieval are awaited until the one in progress finishes and then all awaited calls are resolved with the value
* from the cache.
*/
export class CacheManager<K, T> {
private _cache = new Map<K, State<T>>();
private _id = 0;
public async getCacheEntry(key: K, retrieveEntry: (key: K) => Promise<T>): Promise<T> {
const cacheHit: State<T> | undefined = this._cache.get(key);
// each branch either throws or returns the password.
if (cacheHit === undefined) {
// populate a new state entry and add it to the cache
const state: State<T> = {
status: Status.notStarted,
id: this._id++,
pendingOperation: new Deferred<void>()
};
this._cache.set(key, state);
// now that we have the state entry initialized, retry to fetch the cacheEntry
let returnValue: T = await this.getCacheEntry(key, retrieveEntry);
await state.pendingOperation;
return returnValue!;
} else {
switch (cacheHit.status) {
case Status.notStarted: {
cacheHit.status = Status.inProgress;
// retrieve and populate the missed cache hit.
try {
cacheHit.entry = await retrieveEntry(key);
} catch (error) {
cacheHit.error = error;
} finally {
cacheHit.status = Status.done;
// we do not reject here even in error case because we do not want our awaits on pendingOperation to throw
// We track our own error state and when all done we throw if an error had happened. This results
// in the rejection of the promised returned by this method.
cacheHit.pendingOperation.resolve();
}
return await this.getCacheEntry(key, retrieveEntry);
}
case Status.inProgress: {
await cacheHit.pendingOperation;
return await this.getCacheEntry(key, retrieveEntry);
}
case Status.done: {
if (cacheHit.error !== undefined) {
await cacheHit.pendingOperation;
throw cacheHit.error;
}
else {
await cacheHit.pendingOperation;
return cacheHit.entry!;
}
}
}
}
}
}

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arc from 'arc';
import { CategoryValue } from 'azdata';
import { IOptionsSource } from '../interfaces';
import * as loc from '../localizedConstants';
import { apiService } from '../services/apiService';
import { throwUnless } from '../utils';
import { CacheManager } from './cacheManager';
export type OptionsSourceType = 'ArcControllersOptionsSource';
const OptionsSources = new Map<OptionsSourceType, new () => OptionsSource>();
export abstract class OptionsSource implements IOptionsSource {
private _variableNames!: { [index: string]: string; };
private _type!: OptionsSourceType;
get type(): OptionsSourceType { return this._type; }
get variableNames(): { [index: string]: string; } { return this._variableNames; }
abstract async getOptions(): Promise<string[] | CategoryValue[]>;
abstract async getVariableValue(variableName: string, input: string): Promise<string>;
abstract getIsPassword(variableName: string): boolean;
protected constructor() {
}
static construct(optionsSourceType: OptionsSourceType, variableNames: { [index: string]: string }): OptionsSource {
const sourceConstructor = OptionsSources.get(optionsSourceType);
throwUnless(sourceConstructor !== undefined, loc.noOptionsSourceDefined(optionsSourceType));
const obj = new sourceConstructor();
obj._type = optionsSourceType;
obj._variableNames = variableNames;
return obj;
}
}
export class ArcControllersOptionsSource extends OptionsSource {
private _cacheManager = new CacheManager<string, string>();
constructor() {
super();
}
async getOptions(): Promise<string[] | CategoryValue[]> {
const controllers = await apiService.arcApi.getRegisteredDataControllers();
throwUnless(controllers !== undefined && controllers.length !== 0, loc.noControllersConnected);
return controllers.map(ci => {
return ci.label;
});
}
async getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
const retrieveVariable = async (key: string) => {
const [variableName, controllerLabel] = JSON.parse(key);
const controllers = await apiService.arcApi.getRegisteredDataControllers();
const controller = controllers!.find(ci => ci.label === controllerLabel);
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
switch (variableName) {
case 'endpoint':
return controller.info.url;
case 'username':
return controller.info.username;
case 'password':
const fetchedPassword = await this.getPassword(controller);
return fetchedPassword;
default:
throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName));
}
};
const variableValue = await this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), retrieveVariable);
return variableValue;
}
private async getPassword(controller: arc.DataController): Promise<string> {
let password = await apiService.arcApi.getControllerPassword(controller.info);
if (!password) {
password = await apiService.arcApi.reacquireControllerPassword(controller.info, password);
}
throwUnless(password !== undefined, loc.noPasswordFound(controller.label));
return password;
}
getIsPassword(variableName: string): boolean {
switch (variableName) {
case 'endpoint':
case 'username':
return false;
case 'password':
return true;
default:
throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName));
}
}
}
OptionsSources.set(<OptionsSourceType>ArcControllersOptionsSource.name, ArcControllersOptionsSource);

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Deferred promise
*/
export class Deferred<T> {
promise: Promise<T>;
resolve!: (value?: T | PromiseLike<T>) => void;
reject!: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
return this.promise.then(onfulfilled, onrejected);
}
}