Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC - Sharing index patterns #95958

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export type {
NavigateToAppOptions,
} from './application';

export { SimpleSavedObject } from './saved_objects';
export { ResolvedSimpleSavedObject, SimpleSavedObject } from './saved_objects';
export type {
SavedObjectsBatchResponse,
SavedObjectsBulkCreateObject,
Expand Down
1 change: 1 addition & 0 deletions src/core/public/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type {
SavedObjectsUpdateOptions,
SavedObjectsBulkUpdateOptions,
} from './saved_objects_client';
export { ResolvedSimpleSavedObject } from './resolved_simple_saved_object';
export { SimpleSavedObject } from './simple_saved_object';
export type { SavedObjectsStart } from './saved_objects_service';
export type {
Expand Down
27 changes: 27 additions & 0 deletions src/core/public/saved_objects/resolved_simple_saved_object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { SavedObjectsResolveResponse } from '../../server';
import type { SimpleSavedObject } from './simple_saved_object';

/**
* This class is a very simple wrapper for SavedObjects loaded from the server
* with the {@link SavedObjectsClient}.
*
* It provides basic functionality for creating/saving/deleting saved objects,
* but doesn't include any type-specific implementations.
*
* @public
*/
export class ResolvedSimpleSavedObject<T = unknown> {
constructor(
public savedObject: SimpleSavedObject<T>,
public outcome: SavedObjectsResolveResponse['outcome'],
public aliasTargetId: SavedObjectsResolveResponse['aliasTargetId']
) {}
}
61 changes: 61 additions & 0 deletions src/core/public/saved_objects/saved_objects_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
* Side Public License, v 1.
*/

import type { SavedObjectsResolveResponse } from 'src/core/server';

import { SavedObjectsClient } from './saved_objects_client';
import { SimpleSavedObject } from './simple_saved_object';
import { httpServiceMock } from '../http/http_service.mock';
import { ResolvedSimpleSavedObject } from './resolved_simple_saved_object';

describe('SavedObjectsClient', () => {
const doc = {
Expand Down Expand Up @@ -147,6 +150,64 @@ describe('SavedObjectsClient', () => {
});
});

describe('#resolve', () => {
beforeEach(() => {
beforeEach(() => {
http.fetch.mockResolvedValue({
saved_object: doc,
outcome: 'conflict',
aliasTargetId: 'another-id',
} as SavedObjectsResolveResponse);
});
});

test('rejects if `type` is undefined', async () => {
expect(savedObjectsClient.resolve(undefined as any, doc.id)).rejects.toMatchInlineSnapshot(
`[Error: requires type and id]`
);
});

test('rejects if `id` is undefined', async () => {
expect(savedObjectsClient.resolve(doc.type, undefined as any)).rejects.toMatchInlineSnapshot(
`[Error: requires type and id]`
);
});

test('makes HTTP call', () => {
savedObjectsClient.resolve(doc.type, doc.id);
expect(http.fetch.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"/api/saved_objects/resolve/config/AVwSwFxtcMV38qjDZoQg",
Object {
"body": undefined,
"method": undefined,
"query": undefined,
},
],
]
`);
});

test('rejects when HTTP call fails', async () => {
http.fetch.mockRejectedValueOnce(new Error('Request failed'));
await expect(savedObjectsClient.resolve(doc.type, doc.id)).rejects.toMatchInlineSnapshot(
`[Error: Request failed]`
);
});

test('resolves with ResolvedSimpleSavedObject instance', async () => {
const response = savedObjectsClient.resolve(doc.type, doc.id);
await expect(response).resolves.toBeInstanceOf(ResolvedSimpleSavedObject);

const result = await response;
expect(result.savedObject.type).toBe(doc.type);
expect(result.savedObject.get('title')).toBe('Example title');
expect(result.outcome).toBe('conflict');
expect(result.aliasTargetId).toBe('another-id');
});
});

describe('#delete', () => {
beforeEach(() => {
http.fetch.mockResolvedValue({});
Expand Down
22 changes: 22 additions & 0 deletions src/core/public/saved_objects/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
SavedObjectsClientContract as SavedObjectsApi,
SavedObjectsFindOptions as SavedObjectFindOptionsServer,
SavedObjectsMigrationVersion,
SavedObjectsResolveResponse,
} from '../../server';

import { ResolvedSimpleSavedObject } from './resolved_simple_saved_object';
import { SimpleSavedObject } from './simple_saved_object';
import { HttpFetchOptions, HttpSetup } from '../http';

Expand Down Expand Up @@ -422,6 +424,26 @@ export class SavedObjectsClient {
return request;
}

/**
* Resolves a single object
*
* @param {string} type
* @param {string} id
* @returns The resolve result for the saved object for the given type and id.
*/
public resolve = <T = unknown>(type: string, id: string) => {
if (!type || !id) {
return Promise.reject(new Error('requires type and id'));
}

const path = `${this.getPath(['resolve'])}/${type}/${id}`;
const request: Promise<SavedObjectsResolveResponse<T>> = this.savedObjectsFetch(path, {});
return request.then(({ saved_object: object, outcome, aliasTargetId }) => {
const savedObject = new SimpleSavedObject<T>(this, cloneDeep(object));
return new ResolvedSimpleSavedObject(savedObject, outcome, aliasTargetId);
});
};

/**
* Updates an object
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const createStartContractMock = () => {
bulkGet: jest.fn(),
find: jest.fn(),
get: jest.fn(),
resolve: jest.fn(),
update: jest.fn(),
},
};
Expand Down
3 changes: 3 additions & 0 deletions src/core/public/saved_objects/simple_saved_object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class SimpleSavedObject<T = unknown> {
public coreMigrationVersion: SavedObjectType<T>['coreMigrationVersion'];
public error: SavedObjectType<T>['error'];
public references: SavedObjectType<T>['references'];
public namespaces: SavedObjectType<T>['namespaces'];

constructor(
private client: SavedObjectsClientContract,
Expand All @@ -42,6 +43,7 @@ export class SimpleSavedObject<T = unknown> {
references,
migrationVersion,
coreMigrationVersion,
namespaces,
}: SavedObjectType<T>
) {
this.id = id;
Expand All @@ -51,6 +53,7 @@ export class SimpleSavedObject<T = unknown> {
this._version = version;
this.migrationVersion = migrationVersion;
this.coreMigrationVersion = coreMigrationVersion;
this.namespaces = namespaces;
if (error) {
this.error = error;
}
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/data/common/index_patterns/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
export * from './fields';
export * from './types';
export { IndexPatternsService, IndexPatternsContract } from './index_patterns';
export type { IndexPattern } from './index_patterns';
export type { IndexPattern, ResolvedIndexPattern } from './index_patterns';
export * from './errors';
export * from './expressions';
export * from './constants';
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@
* Side Public License, v 1.
*/

import { IndexPattern } from './index_pattern';

export interface PatternCache {
get: (id: string) => Promise<IndexPattern> | undefined;
set: (id: string, value: Promise<IndexPattern>) => Promise<IndexPattern>;
export interface PatternCache<T> {
get: (id: string) => Promise<T> | undefined;
set: (id: string, value: Promise<T>) => Promise<T>;
clear: (id: string) => void;
clearAll: () => void;
}

export function createIndexPatternCache(): PatternCache {
export function createIndexPatternCache<T>(): PatternCache<T> {
const vals: Record<string, any> = {};
const cache: PatternCache = {
const cache: PatternCache<T> = {
get: (id: string) => {
return vals[id];
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export class IndexPattern implements IIndexPattern {
* SavedObject version
*/
public version: string | undefined;
/** SavedObject namespaces */
public namespaces: string[];
public sourceFilters?: SourceFilter[];
private originalSavedObjectBody: SavedObjectBody = {};
private shortDotsEnable: boolean = false;
Expand Down Expand Up @@ -109,6 +111,7 @@ export class IndexPattern implements IIndexPattern {
this.fieldFormatMap = spec.fieldFormats || {};

this.version = spec.version;
this.namespaces = spec.namespaces || [];

this.title = spec.title || '';
this.timeFieldName = spec.timeFieldName;
Expand Down Expand Up @@ -209,6 +212,7 @@ export class IndexPattern implements IIndexPattern {
return {
id: this.id,
version: this.version,
namespaces: this.namespaces,

title: this.title,
timeFieldName: this.timeFieldName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n';
import { PublicMethodsOf } from '@kbn/utility-types';
import { SavedObjectsClientCommon } from '../..';

import { createIndexPatternCache } from '.';
import type { RuntimeField } from '../types';
import { PatternCache, createIndexPatternCache } from './_pattern_cache';
import type { ResolvedSavedObjectOutcome, RuntimeField } from '../types';
import { IndexPattern } from './index_pattern';
import {
createEnsureDefaultIndexPattern,
Expand Down Expand Up @@ -54,6 +54,12 @@ interface IndexPatternsServiceDeps {
onRedirectNoIndexPattern?: () => void;
}

export interface ResolvedIndexPattern {
indexPattern: IndexPattern;
outcome: ResolvedSavedObjectOutcome;
aliasTargetId?: string;
}

export class IndexPatternsService {
private config: UiSettingsCommon;
private savedObjectsClient: SavedObjectsClientCommon;
Expand All @@ -62,7 +68,8 @@ export class IndexPatternsService {
private fieldFormats: FieldFormatsStartCommon;
private onNotification: OnNotification;
private onError: OnError;
private indexPatternCache: ReturnType<typeof createIndexPatternCache>;
private indexPatternCache: PatternCache<IndexPattern>;
private resolvedIndexPatternCache: PatternCache<ResolvedIndexPattern>;

ensureDefaultIndexPattern: EnsureDefaultIndexPattern;

Expand All @@ -87,6 +94,7 @@ export class IndexPatternsService {
);

this.indexPatternCache = createIndexPatternCache();
this.resolvedIndexPatternCache = createIndexPatternCache();
}

/**
Expand Down Expand Up @@ -357,6 +365,7 @@ export class IndexPatternsService {
fieldAttrs,
allowNoIndex,
},
namespaces,
} = savedObject;

const parsedSourceFilters = sourceFilters ? JSON.parse(sourceFilters) : undefined;
Expand All @@ -382,6 +391,7 @@ export class IndexPatternsService {
fieldAttrs: parsedFieldAttrs,
allowNoIndex,
runtimeFieldMap: parsedRuntimeFieldMap,
namespaces,
};
};

Expand All @@ -390,7 +400,23 @@ export class IndexPatternsService {
savedObjectType,
id
);
return this.savedObjectToIndexPattern(savedObject);
};

private resolveSavedObjectAndInit = async (id: string): Promise<ResolvedIndexPattern> => {
const resolveResult = await this.savedObjectsClient.resolve<IndexPatternAttributes>(
savedObjectType,
id
);
return {
indexPattern: await this.savedObjectToIndexPattern(resolveResult.saved_object),
outcome: resolveResult.outcome,
aliasTargetId: resolveResult.aliasTargetId,
};
};

private savedObjectToIndexPattern = async (savedObject: SavedObject<IndexPatternAttributes>) => {
const { id } = savedObject;
if (!savedObject.version) {
throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns');
}
Expand Down Expand Up @@ -462,9 +488,10 @@ export class IndexPatternsService {
* @param id
*/

get = async (id: string): Promise<IndexPattern> => {
get = async (id: string, options: { forceRefresh?: boolean } = {}): Promise<IndexPattern> => {
const { forceRefresh } = options;
const indexPatternPromise =
this.indexPatternCache.get(id) ||
(!forceRefresh && this.indexPatternCache.get(id)) ||
this.indexPatternCache.set(id, this.getSavedObjectAndInit(id));

// don't cache failed requests
Expand All @@ -475,6 +502,24 @@ export class IndexPatternsService {
return indexPatternPromise;
};

/**
* Resolve an index pattern by id. Cache optimized
* @param id
*/

resolve = async (id: string): Promise<ResolvedIndexPattern> => {
const indexPatternPromise =
this.resolvedIndexPatternCache.get(id) ||
this.resolvedIndexPatternCache.set(id, this.resolveSavedObjectAndInit(id));

// don't cache failed requests
indexPatternPromise.catch(() => {
this.indexPatternCache.clear(id);
});

return indexPatternPromise;
};

/**
* Create a new index pattern instance
* @param spec
Expand Down
10 changes: 10 additions & 0 deletions src/plugins/data/common/index_patterns/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface SavedObjectsClientCommonFindArgs {
export interface SavedObjectsClientCommon {
find: <T = unknown>(options: SavedObjectsClientCommonFindArgs) => Promise<Array<SavedObject<T>>>;
get: <T = unknown>(type: string, id: string) => Promise<SavedObject<T>>;
resolve: <T = unknown>(type: string, id: string) => Promise<ResolvedSavedObject<T>>;
update: (
type: string,
id: string,
Expand Down Expand Up @@ -245,8 +246,17 @@ export interface IndexPatternSpec {
runtimeFieldMap?: Record<string, RuntimeField>;
fieldAttrs?: FieldAttrs;
allowNoIndex?: boolean;
namespaces?: string[];
}

export interface SourceFilter {
value: string;
}

export type ResolvedSavedObjectOutcome = 'exactMatch' | 'aliasMatch' | 'conflict';
export interface ResolvedSavedObject<T = unknown> {
// TODO: refactor types and use SavedObjectsResolveResponse instead
saved_object: SavedObject<T>;
outcome: ResolvedSavedObjectOutcome;
aliasTargetId?: string;
}
Loading