mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-15 02:48:30 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
89
src/vs/platform/telemetry/common/experiments.ts
Normal file
89
src/vs/platform/telemetry/common/experiments.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export interface IExperiments {
|
||||
deployToAzureQuickLink: boolean;
|
||||
}
|
||||
|
||||
export const IExperimentService = createDecorator<IExperimentService>('experimentService');
|
||||
|
||||
export interface IExperimentService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
getExperiments(): IExperiments;
|
||||
}
|
||||
|
||||
export class ExperimentService implements IExperimentService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private experiments: IExperiments;
|
||||
|
||||
constructor(
|
||||
@IStorageService private storageService: IStorageService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
) { }
|
||||
|
||||
getExperiments() {
|
||||
if (!this.experiments) {
|
||||
this.experiments = loadExperiments(this.storageService, this.configurationService);
|
||||
}
|
||||
return this.experiments;
|
||||
}
|
||||
}
|
||||
|
||||
function loadExperiments(storageService: IStorageService, configurationService: IConfigurationService): IExperiments {
|
||||
const experiments = splitExperimentsRandomness(storageService);
|
||||
return applyOverrides(experiments, configurationService);
|
||||
}
|
||||
|
||||
function applyOverrides(experiments: IExperiments, configurationService: IConfigurationService): IExperiments {
|
||||
const experimentsConfig = getExperimentsOverrides(configurationService);
|
||||
Object.keys(experiments).forEach(key => {
|
||||
if (key in experimentsConfig) {
|
||||
experiments[key] = experimentsConfig[key];
|
||||
}
|
||||
});
|
||||
return experiments;
|
||||
}
|
||||
|
||||
function splitExperimentsRandomness(storageService: IStorageService): IExperiments {
|
||||
const random1 = getExperimentsRandomness(storageService);
|
||||
const [random2, /* showTaskDocumentation */] = splitRandom(random1);
|
||||
const [/* random3 */, deployToAzureQuickLink] = splitRandom(random2);
|
||||
// const [random4, /* mergeQuickLinks */] = splitRandom(random3);
|
||||
// const [random5, /* enableWelcomePage */] = splitRandom(random4);
|
||||
return {
|
||||
deployToAzureQuickLink
|
||||
};
|
||||
}
|
||||
|
||||
function getExperimentsRandomness(storageService: IStorageService) {
|
||||
const key = 'experiments.randomness';
|
||||
let valueString = storageService.get(key);
|
||||
if (!valueString) {
|
||||
valueString = Math.random().toString();
|
||||
storageService.store(key, valueString);
|
||||
}
|
||||
|
||||
return parseFloat(valueString);
|
||||
}
|
||||
|
||||
function splitRandom(random: number): [number, boolean] {
|
||||
const scaled = random * 2;
|
||||
const i = Math.floor(scaled);
|
||||
return [scaled - i, i === 1];
|
||||
}
|
||||
|
||||
function getExperimentsOverrides(configurationService: IConfigurationService): IExperiments {
|
||||
const config: any = configurationService.getConfiguration('telemetry');
|
||||
return config && config.experiments || {};
|
||||
}
|
||||
37
src/vs/platform/telemetry/common/telemetry.ts
Normal file
37
src/vs/platform/telemetry/common/telemetry.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const ITelemetryService = createDecorator<ITelemetryService>('telemetryService');
|
||||
|
||||
export interface ITelemetryInfo {
|
||||
sessionId: string;
|
||||
machineId: string;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
export interface ITelemetryData {
|
||||
from?: string;
|
||||
target?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ITelemetryService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
* Sends a telemetry event that has been privacy approved.
|
||||
* Do not call this unless you have been given approval.
|
||||
*/
|
||||
publicLog(eventName: string, data?: ITelemetryData): TPromise<void>;
|
||||
|
||||
getTelemetryInfo(): TPromise<ITelemetryInfo>;
|
||||
|
||||
isOptedIn: boolean;
|
||||
}
|
||||
43
src/vs/platform/telemetry/common/telemetryIpc.ts
Normal file
43
src/vs/platform/telemetry/common/telemetryIpc.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
|
||||
export interface ITelemetryLog {
|
||||
eventName: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface ITelemetryAppenderChannel extends IChannel {
|
||||
call(command: 'log', data: ITelemetryLog): TPromise<void>;
|
||||
call(command: string, arg: any): TPromise<any>;
|
||||
}
|
||||
|
||||
export class TelemetryAppenderChannel implements ITelemetryAppenderChannel {
|
||||
|
||||
constructor(private appender: ITelemetryAppender) { }
|
||||
|
||||
call(command: string, { eventName, data }: ITelemetryLog): TPromise<any> {
|
||||
this.appender.log(eventName, data);
|
||||
return TPromise.as(null);
|
||||
}
|
||||
}
|
||||
|
||||
export class TelemetryAppenderClient implements ITelemetryAppender {
|
||||
|
||||
constructor(private channel: ITelemetryAppenderChannel) { }
|
||||
|
||||
log(eventName: string, data?: any): any {
|
||||
return this.channel.call('log', { eventName, data });
|
||||
}
|
||||
|
||||
dispose(): any {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
150
src/vs/platform/telemetry/common/telemetryService.ts
Normal file
150
src/vs/platform/telemetry/common/telemetryService.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { optional } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { cloneAndChange, mixin } from 'vs/base/common/objects';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
export interface ITelemetryServiceConfig {
|
||||
appender: ITelemetryAppender;
|
||||
commonProperties?: TPromise<{ [name: string]: any }>;
|
||||
piiPaths?: string[];
|
||||
userOptIn?: boolean;
|
||||
}
|
||||
|
||||
export class TelemetryService implements ITelemetryService {
|
||||
|
||||
static IDLE_START_EVENT_NAME = 'UserIdleStart';
|
||||
static IDLE_STOP_EVENT_NAME = 'UserIdleStop';
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _appender: ITelemetryAppender;
|
||||
private _commonProperties: TPromise<{ [name: string]: any; }>;
|
||||
private _piiPaths: string[];
|
||||
private _userOptIn: boolean;
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _cleanupPatterns: [RegExp, string][] = [];
|
||||
|
||||
constructor(
|
||||
config: ITelemetryServiceConfig,
|
||||
@optional(IConfigurationService) private _configurationService: IConfigurationService
|
||||
) {
|
||||
this._appender = config.appender;
|
||||
this._commonProperties = config.commonProperties || TPromise.as({});
|
||||
this._piiPaths = config.piiPaths || [];
|
||||
this._userOptIn = typeof config.userOptIn === 'undefined' ? true : config.userOptIn;
|
||||
|
||||
// static cleanup patterns for:
|
||||
// #1 `file:///DANGEROUS/PATH/resources/app/Useful/Information`
|
||||
// #2 // Any other file path that doesn't match the approved form above should be cleaned.
|
||||
// #3 "Error: ENOENT; no such file or directory" is often followed with PII, clean it
|
||||
this._cleanupPatterns.push(
|
||||
[/file:\/\/\/.*?\/resources\/app\//gi, ''],
|
||||
[/file:\/\/\/.*/gi, ''],
|
||||
[/ENOENT: no such file or directory.*?\'([^\']+)\'/gi, 'ENOENT: no such file or directory']
|
||||
);
|
||||
|
||||
for (let piiPath of this._piiPaths) {
|
||||
this._cleanupPatterns.push([new RegExp(escapeRegExpCharacters(piiPath), 'gi'), '']);
|
||||
}
|
||||
|
||||
if (this._configurationService) {
|
||||
this._updateUserOptIn();
|
||||
this._configurationService.onDidUpdateConfiguration(this._updateUserOptIn, this, this._disposables);
|
||||
this.publicLog('optInStatus', { optIn: this._userOptIn });
|
||||
}
|
||||
}
|
||||
|
||||
private _updateUserOptIn(): void {
|
||||
const config = this._configurationService.getConfiguration<any>(TELEMETRY_SECTION_ID);
|
||||
this._userOptIn = config ? config.enableTelemetry : this._userOptIn;
|
||||
}
|
||||
|
||||
get isOptedIn(): boolean {
|
||||
return this._userOptIn;
|
||||
}
|
||||
|
||||
getTelemetryInfo(): TPromise<ITelemetryInfo> {
|
||||
return this._commonProperties.then(values => {
|
||||
// well known properties
|
||||
let sessionId = values['sessionID'];
|
||||
let instanceId = values['common.instanceId'];
|
||||
let machineId = values['common.machineId'];
|
||||
|
||||
return { sessionId, instanceId, machineId };
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
|
||||
publicLog(eventName: string, data?: ITelemetryData): TPromise<any> {
|
||||
// don't send events when the user is optout
|
||||
if (!this._userOptIn) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
return this._commonProperties.then(values => {
|
||||
|
||||
// (first) add common properties
|
||||
data = mixin(data, values);
|
||||
|
||||
// (last) remove all PII from data
|
||||
data = cloneAndChange(data, value => {
|
||||
if (typeof value === 'string') {
|
||||
return this._cleanupInfo(value);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
this._appender.log(eventName, data);
|
||||
|
||||
}, err => {
|
||||
// unsure what to do now...
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
private _cleanupInfo(stack: string): string {
|
||||
|
||||
// sanitize with configured cleanup patterns
|
||||
for (let tuple of this._cleanupPatterns) {
|
||||
let [regexp, replaceValue] = tuple;
|
||||
stack = stack.replace(regexp, replaceValue);
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const TELEMETRY_SECTION_ID = 'telemetry';
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
'id': TELEMETRY_SECTION_ID,
|
||||
'order': 110,
|
||||
'type': 'object',
|
||||
'title': localize('telemetryConfigurationTitle', "Telemetry"),
|
||||
'properties': {
|
||||
'telemetry.enableTelemetry': {
|
||||
'type': 'boolean',
|
||||
'description': localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to Microsoft."),
|
||||
'default': true
|
||||
}
|
||||
}
|
||||
});
|
||||
247
src/vs/platform/telemetry/common/telemetryUtils.ts
Normal file
247
src/vs/platform/telemetry/common/telemetryUtils.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { guessMimeTypes } from 'vs/base/common/mime';
|
||||
import paths = require('vs/base/common/paths');
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { ConfigurationSource, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IKeybindingService, KeybindingSource } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export const NullTelemetryService = {
|
||||
_serviceBrand: undefined,
|
||||
publicLog(eventName: string, data?: ITelemetryData) {
|
||||
return TPromise.as<void>(null);
|
||||
},
|
||||
isOptedIn: true,
|
||||
getTelemetryInfo(): TPromise<ITelemetryInfo> {
|
||||
return TPromise.as({
|
||||
instanceId: 'someValue.instanceId',
|
||||
sessionId: 'someValue.sessionId',
|
||||
machineId: 'someValue.machineId'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export interface ITelemetryAppender {
|
||||
log(eventName: string, data: any): void;
|
||||
}
|
||||
|
||||
export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetryAppender {
|
||||
return { log: (e, d) => appenders.forEach(a => a.log(e, d)) };
|
||||
}
|
||||
|
||||
export const NullAppender: ITelemetryAppender = { log: () => null };
|
||||
|
||||
// --- util
|
||||
|
||||
export function anonymize(input: string): string {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
let r = '';
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
let ch = input[i];
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
r += '0';
|
||||
continue;
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'z') {
|
||||
r += 'a';
|
||||
continue;
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'Z') {
|
||||
r += 'A';
|
||||
continue;
|
||||
}
|
||||
r += ch;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
export interface URIDescriptor {
|
||||
mimeType?: string;
|
||||
ext?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export function telemetryURIDescriptor(uri: URI): URIDescriptor {
|
||||
const fsPath = uri && uri.fsPath;
|
||||
return fsPath ? { mimeType: guessMimeTypes(fsPath).join(', '), ext: paths.extname(fsPath), path: anonymize(fsPath) } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Only add settings that cannot contain any personal/private information of users (PII).
|
||||
*/
|
||||
const configurationValueWhitelist = [
|
||||
'editor.tabCompletion',
|
||||
'editor.fontFamily',
|
||||
'editor.fontWeight',
|
||||
'editor.fontSize',
|
||||
'editor.lineHeight',
|
||||
'editor.letterSpacing',
|
||||
'editor.lineNumbers',
|
||||
'editor.rulers',
|
||||
'editor.wordSeparators',
|
||||
'editor.tabSize',
|
||||
'editor.insertSpaces',
|
||||
'editor.detectIndentation',
|
||||
'editor.roundedSelection',
|
||||
'editor.scrollBeyondLastLine',
|
||||
'editor.minimap.enabled',
|
||||
'editor.minimap.renderCharacters',
|
||||
'editor.minimap.maxColumn',
|
||||
'editor.find.seedSearchStringFromSelection',
|
||||
'editor.find.autoFindInSelection',
|
||||
'editor.wordWrap',
|
||||
'editor.wordWrapColumn',
|
||||
'editor.wrappingIndent',
|
||||
'editor.mouseWheelScrollSensitivity',
|
||||
'editor.multiCursorModifier',
|
||||
'editor.quickSuggestions',
|
||||
'editor.quickSuggestionsDelay',
|
||||
'editor.parameterHints',
|
||||
'editor.autoClosingBrackets',
|
||||
'editor.autoIndent',
|
||||
'editor.formatOnType',
|
||||
'editor.formatOnPaste',
|
||||
'editor.suggestOnTriggerCharacters',
|
||||
'editor.acceptSuggestionOnEnter',
|
||||
'editor.acceptSuggestionOnCommitCharacter',
|
||||
'editor.snippetSuggestions',
|
||||
'editor.emptySelectionClipboard',
|
||||
'editor.wordBasedSuggestions',
|
||||
'editor.suggestFontSize',
|
||||
'editor.suggestLineHeight',
|
||||
'editor.selectionHighlight',
|
||||
'editor.occurrencesHighlight',
|
||||
'editor.overviewRulerLanes',
|
||||
'editor.overviewRulerBorder',
|
||||
'editor.cursorBlinking',
|
||||
'editor.cursorStyle',
|
||||
'editor.mouseWheelZoom',
|
||||
'editor.fontLigatures',
|
||||
'editor.hideCursorInOverviewRuler',
|
||||
'editor.renderWhitespace',
|
||||
'editor.renderControlCharacters',
|
||||
'editor.renderIndentGuides',
|
||||
'editor.renderLineHighlight',
|
||||
'editor.codeLens',
|
||||
'editor.folding',
|
||||
'editor.showFoldingControls',
|
||||
'editor.matchBrackets',
|
||||
'editor.glyphMargin',
|
||||
'editor.useTabStops',
|
||||
'editor.trimAutoWhitespace',
|
||||
'editor.stablePeek',
|
||||
'editor.dragAndDrop',
|
||||
'editor.formatOnSave',
|
||||
'editor.colorDecorators',
|
||||
|
||||
'window.zoomLevel',
|
||||
'files.autoSave',
|
||||
'files.hotExit',
|
||||
'files.associations',
|
||||
'workbench.statusBar.visible',
|
||||
'files.trimTrailingWhitespace',
|
||||
'git.confirmSync',
|
||||
'workbench.sideBar.location',
|
||||
'window.openFilesInNewWindow',
|
||||
'javascript.validate.enable',
|
||||
'window.reopenFolders',
|
||||
'window.restoreWindows',
|
||||
'extensions.autoUpdate',
|
||||
'files.eol',
|
||||
'explorer.openEditors.visible',
|
||||
'workbench.editor.enablePreview',
|
||||
'files.autoSaveDelay',
|
||||
'workbench.editor.showTabs',
|
||||
'files.encoding',
|
||||
'files.autoGuessEncoding',
|
||||
'git.enabled',
|
||||
'http.proxyStrictSSL',
|
||||
'terminal.integrated.fontFamily',
|
||||
'workbench.editor.enablePreviewFromQuickOpen',
|
||||
'workbench.editor.swipeToNavigate',
|
||||
'php.builtInCompletions.enable',
|
||||
'php.validate.enable',
|
||||
'php.validate.run',
|
||||
'workbench.welcome.enabled',
|
||||
'workbench.startupEditor',
|
||||
];
|
||||
|
||||
export function configurationTelemetry(telemetryService: ITelemetryService, configurationService: IConfigurationService): IDisposable {
|
||||
return configurationService.onDidUpdateConfiguration(event => {
|
||||
if (event.source !== ConfigurationSource.Default) {
|
||||
telemetryService.publicLog('updateConfiguration', {
|
||||
configurationSource: ConfigurationSource[event.source],
|
||||
configurationKeys: flattenKeys(event.sourceConfig)
|
||||
});
|
||||
telemetryService.publicLog('updateConfigurationValues', {
|
||||
configurationSource: ConfigurationSource[event.source],
|
||||
configurationValues: flattenValues(event.sourceConfig, configurationValueWhitelist)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function lifecycleTelemetry(telemetryService: ITelemetryService, lifecycleService: ILifecycleService): IDisposable {
|
||||
return lifecycleService.onShutdown(event => {
|
||||
telemetryService.publicLog('shutdown', { reason: ShutdownReason[event] });
|
||||
});
|
||||
}
|
||||
|
||||
export function keybindingsTelemetry(telemetryService: ITelemetryService, keybindingService: IKeybindingService): IDisposable {
|
||||
return keybindingService.onDidUpdateKeybindings(event => {
|
||||
if (event.source === KeybindingSource.User && event.keybindings) {
|
||||
telemetryService.publicLog('updateKeybindings', {
|
||||
bindings: event.keybindings.map(binding => ({
|
||||
key: binding.key,
|
||||
command: binding.command,
|
||||
when: binding.when,
|
||||
args: binding.args ? true : undefined
|
||||
}))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function flattenKeys(value: Object): string[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
const result: string[] = [];
|
||||
flatKeys(result, '', value);
|
||||
return result;
|
||||
}
|
||||
|
||||
function flatKeys(result: string[], prefix: string, value: Object): void {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
Object.keys(value)
|
||||
.forEach(key => flatKeys(result, prefix ? `${prefix}.${key}` : key, value[key]));
|
||||
} else {
|
||||
result.push(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenValues(value: Object, keys: string[]): { [key: string]: any }[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return keys.reduce((array, key) => {
|
||||
const v = key.split('.')
|
||||
.reduce((tmp, k) => tmp && typeof tmp === 'object' ? tmp[k] : undefined, value);
|
||||
if (typeof v !== 'undefined') {
|
||||
array.push({ [key]: v });
|
||||
}
|
||||
return array;
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user