[Core] Rewrite saved objects in typescript (#36829)

* Convert simple files to TS

* Fix jest tests

* Rename saved_objects_client{.js => .ts}

* WIP saved_objects_client

* saved_objects repository{.js => .ts}

* includedFields support string[] for type paramater

* Repository/saved_objects_client -> TS

* Fix tests and dependencies

* Fix saved objects type errors and simplify

* saved_objects/index saved_objects/service/index -> ts

* Fix saved objects export test after switching to typed mock

* Workaround type error

* Revert "Workaround type error"

This reverts commit de3252267eb2e6bf56a5584d271b55a7afdc1c53.

* Correctly type Server.savedObjects.SaveObjectsClient constructor

* saved_objects/service/lib/index.{js -> ts}

* saved_objects/service/lib/scoped_client_provider{js -> ts}

* Typescriptify scoped_client_provider

* Fix x-pack jest imports

* Add lodash/internal/toPath typings to xpath

* Introduce SavedObjectsClientContract

We need a way to specify that injected dependencies should adhere to the
SavedObjectsClient "contract". We can't use the SavedObjectsClient class
itself since it contains the private _repository property which in TS is
included in the type signature of a class.

* Cleanup and simplify types

* Fix repository#delete should return {}

* Add SavedObjects repository test for uncovered bug

Test for a bug in our previous js implementation that can lead to data
corruption and data loss.

If a bulkGet request is made where one of the objects to fetch is of a type
that isn't allowed, the returned result will include documents which have the
incorrect id and type assigned. E.g. the data of an object with id '1' is
returned with id '2'. Saving '2' will incorrectly override it's data with that
of the data of object '1'.

* SavedObject.updated_at: string and unify saved_object / serializer types

* Cleanup

* Address code review feedback

* Don't mock errors helpers in SavedObjectsClient Mock

* Address CR feedback

* CR Feedback #2

* Add kibana-platform as code owners of Saved Objects

* Better typings for SavedObjectsClient.errors

* Use unknown as default for generic type request paramater

* Bump @types/elasticsearch

* Fix types for isForbiddenError

* Bump x-pack @types/elasticsearch
This commit is contained in:
Rudolf Meijering 2019-06-06 10:49:13 +02:00 committed by GitHub
parent d43ee7fe3d
commit ea9721ad13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 933 additions and 976 deletions

2
.github/CODEOWNERS vendored
View file

@ -29,6 +29,8 @@
# Platform
/src/core/ @elastic/kibana-platform
/src/legacy/server/saved_objects/ @elastic/kibana-platform
/src/legacy/ui/public/saved_objects @elastic/kibana-platform
# Security
/x-pack/plugins/security/ @elastic/kibana-security

View file

@ -285,7 +285,7 @@
"@types/d3": "^3.5.41",
"@types/dedent": "^0.7.0",
"@types/delete-empty": "^2.0.0",
"@types/elasticsearch": "^5.0.30",
"@types/elasticsearch": "^5.0.33",
"@types/enzyme": "^3.1.12",
"@types/eslint": "^4.16.6",
"@types/execa": "^0.9.0",

View file

@ -36,7 +36,7 @@ import configCompleteMixin from './config/complete';
import optimizeMixin from '../../optimize';
import * as Plugins from './plugins';
import { indexPatternsMixin } from './index_patterns';
import { savedObjectsMixin } from './saved_objects';
import { savedObjectsMixin } from './saved_objects/saved_objects_mixin';
import { sampleDataMixin } from './sample_data';
import { capabilitiesMixin } from './capabilities';
import { urlShorteningMixin } from './url_shortening';

View file

@ -18,18 +18,10 @@
*/
import { getSortedObjectsForExport } from './get_sorted_objects_for_export';
import { SavedObjectsClientMock } from '../service/saved_objects_client.mock';
describe('getSortedObjectsForExport()', () => {
const savedObjectsClient = {
errors: {} as any,
find: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
bulkCreate: jest.fn(),
delete: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
const savedObjectsClient = SavedObjectsClientMock.create();
afterEach(() => {
savedObjectsClient.find.mockReset();
@ -48,8 +40,10 @@ describe('getSortedObjectsForExport()', () => {
{
id: '2',
type: 'search',
attributes: {},
references: [
{
name: 'name',
type: 'index-pattern',
id: '1',
},
@ -58,9 +52,12 @@ describe('getSortedObjectsForExport()', () => {
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
],
per_page: 1,
page: 0,
});
const response = await getSortedObjectsForExport({
savedObjectsClient,
@ -70,15 +67,18 @@ describe('getSortedObjectsForExport()', () => {
expect(response).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "index-pattern",
},
Object {
"attributes": Object {},
"id": "2",
"references": Array [
Object {
"id": "1",
"name": "name",
"type": "index-pattern",
},
],
@ -118,9 +118,11 @@ Array [
{
id: '2',
type: 'search',
attributes: {},
references: [
{
type: 'index-pattern',
name: 'name',
id: '1',
},
],
@ -128,9 +130,12 @@ Array [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
],
per_page: 1,
page: 0,
});
await expect(
getSortedObjectsForExport({
@ -147,16 +152,19 @@ Array [
{
id: '2',
type: 'search',
attributes: {},
references: [
{
type: 'index-pattern',
id: '1',
name: 'name',
type: 'index-pattern',
},
],
},
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
],
@ -179,15 +187,18 @@ Array [
expect(response).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "index-pattern",
},
Object {
"attributes": Object {},
"id": "2",
"references": Array [
Object {
"id": "1",
"name": "name",
"type": "index-pattern",
},
],
@ -227,9 +238,11 @@ Array [
{
id: '2',
type: 'search',
attributes: {},
references: [
{
type: 'index-pattern',
name: 'name',
id: '1',
},
],
@ -241,6 +254,7 @@ Array [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
],
@ -260,15 +274,18 @@ Array [
expect(response).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "index-pattern",
},
Object {
"attributes": Object {},
"id": "2",
"references": Array [
Object {
"id": "1",
"name": "name",
"type": "index-pattern",
},
],

View file

@ -18,7 +18,7 @@
*/
import Boom from 'boom';
import { SavedObjectsClient } from '../service/saved_objects_client';
import { SavedObjectsClientContract } from '../';
import { injectNestedDependencies } from './inject_nested_depdendencies';
import { sortObjects } from './sort_objects';
@ -30,7 +30,7 @@ interface ObjectToExport {
interface ExportObjectsOptions {
types?: string[];
objects?: ObjectToExport[];
savedObjectsClient: SavedObjectsClient;
savedObjectsClient: SavedObjectsClientContract;
exportSizeLimit: number;
includeReferencesDeep?: boolean;
}
@ -44,7 +44,7 @@ async function fetchObjectsToExport({
objects?: ObjectToExport[];
types?: string[];
exportSizeLimit: number;
savedObjectsClient: SavedObjectsClient;
savedObjectsClient: SavedObjectsClientContract;
}) {
if (objects) {
if (objects.length > exportSizeLimit) {

View file

@ -18,7 +18,7 @@
*/
import Boom from 'boom';
import { SavedObject, SavedObjectsClient } from '../service/saved_objects_client';
import { SavedObject, SavedObjectsClientContract } from '../service/saved_objects_client';
export function getObjectReferencesToFetch(savedObjectsMap: Map<string, SavedObject>) {
const objectsToFetch = new Map<string, { type: string; id: string }>();
@ -34,7 +34,7 @@ export function getObjectReferencesToFetch(savedObjectsMap: Map<string, SavedObj
export async function injectNestedDependencies(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient
savedObjectsClient: SavedObjectsClientContract
) {
const savedObjectsMap = new Map<string, SavedObject>();
for (const savedObject of savedObjects) {

View file

@ -18,17 +18,17 @@
*/
import { Readable } from 'stream';
import { SavedObjectsClient } from '../service';
import { collectSavedObjects } from './collect_saved_objects';
import { extractErrors } from './extract_errors';
import { ImportError } from './types';
import { validateReferences } from './validate_references';
import { SavedObjectsClientContract } from '../';
interface ImportSavedObjectsOptions {
readStream: Readable;
objectLimit: number;
overwrite: boolean;
savedObjectsClient: SavedObjectsClient;
savedObjectsClient: SavedObjectsClientContract;
supportedTypes: string[];
}

View file

@ -18,7 +18,7 @@
*/
import { Readable } from 'stream';
import { SavedObjectsClient } from '../service';
import { SavedObjectsClientContract } from '../';
import { collectSavedObjects } from './collect_saved_objects';
import { createObjectsFilter } from './create_objects_filter';
import { extractErrors } from './extract_errors';
@ -29,7 +29,7 @@ import { validateReferences } from './validate_references';
interface ResolveImportErrorsOptions {
readStream: Readable;
objectLimit: number;
savedObjectsClient: SavedObjectsClient;
savedObjectsClient: SavedObjectsClientContract;
retries: Retry[];
supportedTypes: string[];
}

View file

@ -18,7 +18,7 @@
*/
import Boom from 'boom';
import { SavedObject, SavedObjectsClient } from '../service';
import { SavedObject, SavedObjectsClientContract } from '../';
import { ImportError } from './types';
const REF_TYPES_TO_VLIDATE = ['index-pattern', 'search'];
@ -29,7 +29,7 @@ function filterReferencesToValidate({ type }: { type: string }) {
export async function getNonExistingReferenceAsKeys(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient
savedObjectsClient: SavedObjectsClientContract
) {
const collector = new Map();
// Collect all references within objects
@ -77,7 +77,7 @@ export async function getNonExistingReferenceAsKeys(
export async function validateReferences(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient
savedObjectsClient: SavedObjectsClientContract
) {
const errorMap: { [key: string]: ImportError } = {};
const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys(

View file

@ -17,5 +17,8 @@
* under the License.
*/
export { savedObjectsMixin } from './saved_objects_mixin';
export { SavedObjectsClient } from './service';
export * from './service';
export { SavedObjectsSchema } from './schema';
export { SavedObjectsManagement } from './management';

View file

@ -64,7 +64,8 @@ import Boom from 'boom';
import _ from 'lodash';
import cloneDeep from 'lodash.clonedeep';
import Semver from 'semver';
import { MigrationVersion, RawSavedObjectDoc } from '../../serialization';
import { RawSavedObjectDoc } from '../../serialization';
import { MigrationVersion } from '../../';
import { LogFn, Logger, MigrationLogger } from './migration_logger';
export type TransformFn = (doc: RawSavedObjectDoc, log?: Logger) => RawSavedObjectDoc;

View file

@ -24,12 +24,9 @@
import _ from 'lodash';
import { IndexMapping } from '../../../mappings';
import { MigrationVersion } from '../../serialization';
import { MigrationVersion } from '../../';
import { AliasAction, CallCluster, NotFound, RawDoc, ShardsInfo } from './call_cluster';
// @ts-ignore untyped dependency
import { getTypes } from '../../../mappings';
const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' };
export interface FullIndexInfo {

View file

@ -20,22 +20,14 @@
import Hapi from 'hapi';
import { createMockServer } from './_mock_server';
import { createBulkCreateRoute } from './bulk_create';
import { SavedObjectsClientMock } from '../service/saved_objects_client.mock';
describe('POST /api/saved_objects/_bulk_create', () => {
let server: Hapi.Server;
const savedObjectsClient = {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
const savedObjectsClient = SavedObjectsClientMock.create();
beforeEach(() => {
savedObjectsClient.bulkCreate.mockImplementation(() => Promise.resolve(''));
savedObjectsClient.bulkCreate.mockImplementation(() => Promise.resolve('' as any));
server = createMockServer();
const prereqs = {
@ -75,7 +67,8 @@ describe('POST /api/saved_objects/_bulk_create', () => {
id: 'abc123',
type: 'index-pattern',
title: 'logstash-*',
version: 2,
attributes: {},
version: '2',
references: [],
},
],

View file

@ -19,7 +19,7 @@
import Hapi from 'hapi';
import Joi from 'joi';
import { SavedObjectAttributes, SavedObjectsClient } from '../';
import { SavedObjectAttributes, SavedObjectsClientContract } from '../';
import { Prerequisites, SavedObjectReference, WithoutQueryAndParams } from './types';
interface SavedObject {
@ -33,7 +33,7 @@ interface SavedObject {
interface BulkCreateRequest extends WithoutQueryAndParams<Hapi.Request> {
pre: {
savedObjectsClient: SavedObjectsClient;
savedObjectsClient: SavedObjectsClientContract;
};
query: {
overwrite: boolean;

View file

@ -18,7 +18,7 @@
*/
import Hapi from 'hapi';
import { SavedObjectsClient } from '../';
import { SavedObjectsClientContract } from '../';
export interface SavedObjectReference {
name: string;
@ -29,7 +29,7 @@ export interface SavedObjectReference {
export interface Prerequisites {
getSavedObjectsClient: {
assign: string;
method: (req: Hapi.Request) => SavedObjectsClient;
method: (req: Hapi.Request) => SavedObjectsClientContract;
};
}

View file

@ -22,6 +22,7 @@ import { SavedObjectsSchema } from './schema';
type Schema = PublicMethodsOf<SavedObjectsSchema>;
const createSchemaMock = () => {
const mocked: jest.Mocked<Schema> = {
getIndexForType: jest.fn().mockReturnValue('.kibana-test'),
isHiddenType: jest.fn().mockReturnValue(false),
isNamespaceAgnostic: jest.fn((type: string) => type === 'global'),
};

View file

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
interface SavedObjectsSchemaTypeDefinition {
isNamespaceAgnostic: boolean;
hidden?: boolean;
@ -41,6 +40,14 @@ export class SavedObjectsSchema {
return false;
}
public getIndexForType(type: string): string | undefined {
if (this.definition != null && this.definition.hasOwnProperty(type)) {
return this.definition[type].indexPattern;
} else {
return undefined;
}
}
public isNamespaceAgnostic(type: string) {
// if no plugins have registered a uiExports.savedObjectSchemas,
// this.schema will be undefined, and no types are namespace agnostic

View file

@ -27,6 +27,7 @@
import uuid from 'uuid';
import { SavedObjectsSchema } from '../schema';
import { decodeVersion, encodeVersion } from '../version';
import { MigrationVersion, SavedObjectReference } from '../service/saved_objects_client';
/**
* A raw document as represented directly in the saved object index.
@ -39,23 +40,6 @@ export interface RawDoc {
_primary_term?: number;
}
/**
* A dictionary of saved object type -> version used to determine
* what migrations need to be applied to a saved object.
*/
export interface MigrationVersion {
[type: string]: string;
}
/**
* A reference object to anohter saved object.
*/
export interface SavedObjectReference {
name: string;
type: string;
id: string;
}
/**
* A saved object type definition that allows for miscellaneous, unknown
* properties, as current discussions around security, ACLs, etc indicate
@ -64,12 +48,12 @@ export interface SavedObjectReference {
*/
interface SavedObjectDoc {
attributes: object;
id: string;
id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional
type: string;
namespace?: string;
migrationVersion?: MigrationVersion;
version?: string;
updated_at?: Date;
updated_at?: string;
[rootProp: string]: any;
}

View file

@ -1,21 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { SavedObjectsClient } from './saved_objects_client';
export { SavedObjectsRepository, ScopedSavedObjectsClientProvider } from './lib';

View file

@ -31,15 +31,10 @@ export interface SavedObjectsService<Request = any> {
getSavedObjectsRepository(...rest: any[]): any;
}
export { SavedObjectsClientWrapperFactory } from './lib';
export {
FindOptions,
GetResponse,
UpdateResponse,
CreateResponse,
MigrationVersion,
SavedObject,
SavedObjectAttributes,
SavedObjectsClient,
SavedObjectReference,
} from './saved_objects_client';
SavedObjectsRepository,
ScopedSavedObjectsClientProvider,
SavedObjectsClientWrapperFactory,
} from './lib';
export * from './saved_objects_client';

View file

@ -21,13 +21,13 @@ import { errors as esErrors } from 'elasticsearch';
import { decorateEsError } from './decorate_es_error';
import {
isEsUnavailableError,
isConflictError,
isNotAuthorizedError,
isForbiddenError,
isRequestEntityTooLargeError,
isNotFoundError,
isBadRequestError,
isConflictError,
isEsUnavailableError,
isForbiddenError,
isNotAuthorizedError,
isNotFoundError,
isRequestEntityTooLargeError,
} from './errors';
describe('savedObjectsClient/decorateEsError', () => {

View file

@ -26,30 +26,33 @@ const {
NoConnections,
RequestTimeout,
Conflict,
// @ts-ignore
401: NotAuthorized,
// @ts-ignore
403: Forbidden,
// @ts-ignore
413: RequestEntityTooLarge,
NotFound,
BadRequest,
} = elasticsearch.errors;
import {
decorateBadRequestError,
decorateNotAuthorizedError,
decorateForbiddenError,
decorateRequestEntityTooLargeError,
createGenericNotFoundError,
decorateBadRequestError,
decorateConflictError,
decorateEsUnavailableError,
decorateForbiddenError,
decorateGeneralError,
decorateNotAuthorizedError,
decorateRequestEntityTooLargeError,
} from './errors';
export function decorateEsError(error) {
export function decorateEsError(error: Error) {
if (!(error instanceof Error)) {
throw new Error('Expected an instance of Error');
}
const { reason } = get(error, 'body.error', {});
const { reason } = get(error, 'body.error', { reason: undefined });
if (
error instanceof ConnectionFault ||
error instanceof ServiceUnavailable ||

View file

@ -1,30 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function isBadRequestError(maybeError: any): boolean;
export function isNotAuthorizedError(maybeError: any): boolean;
export function isForbiddenError(maybeError: any): boolean;
export function isRequestEntityTooLargeError(maybeError: any): boolean;
export function isNotFoundError(maybeError: any): boolean;
export function isConflictError(maybeError: any): boolean;
export function isEsUnavailableError(maybeError: any): boolean;
export function isEsAutoCreateIndexError(maybeError: any): boolean;
export function createInvalidVersionError(version: any): Error;
export function isInvalidVersionError(maybeError: Error): boolean;

View file

@ -21,22 +21,22 @@ import Boom from 'boom';
import {
createBadRequestError,
createEsAutoCreateIndexError,
createGenericNotFoundError,
createUnsupportedTypeError,
decorateBadRequestError,
isBadRequestError,
decorateNotAuthorizedError,
isNotAuthorizedError,
decorateForbiddenError,
isForbiddenError,
createGenericNotFoundError,
isNotFoundError,
decorateConflictError,
isConflictError,
decorateEsUnavailableError,
isEsUnavailableError,
decorateForbiddenError,
decorateGeneralError,
decorateNotAuthorizedError,
isBadRequestError,
isConflictError,
isEsAutoCreateIndexError,
createEsAutoCreateIndexError,
isEsUnavailableError,
isForbiddenError,
isNotAuthorizedError,
isNotFoundError,
} from './errors';
describe('savedObjectsClient/errorTypes', () => {
@ -354,6 +354,7 @@ describe('savedObjectsClient/errorTypes', () => {
describe('createEsAutoCreateIndexError', () => {
it('does not take an error argument', () => {
const error = new Error();
// @ts-ignore
expect(createEsAutoCreateIndexError(error)).not.toBe(error);
});

View file

@ -21,7 +21,16 @@ import Boom from 'boom';
const code = Symbol('SavedObjectsClientErrorCode');
function decorate(error, errorCode, statusCode, message) {
interface DecoratedError extends Boom {
[code]?: string;
}
function decorate(
error: Error | DecoratedError,
errorCode: string,
statusCode: number,
message?: string
): DecoratedError {
if (isSavedObjectsClientError(error)) {
return error;
}
@ -30,112 +39,113 @@ function decorate(error, errorCode, statusCode, message) {
statusCode,
message,
override: false,
});
}) as DecoratedError;
boom[code] = errorCode;
return boom;
}
export function isSavedObjectsClientError(error) {
return error && !!error[code];
export function isSavedObjectsClientError(error: any): error is DecoratedError {
return Boolean(error && error[code]);
}
// 400 - badRequest
const CODE_BAD_REQUEST = 'SavedObjectsClient/badRequest';
export function decorateBadRequestError(error, reason) {
export function decorateBadRequestError(error: Error, reason?: string) {
return decorate(error, CODE_BAD_REQUEST, 400, reason);
}
export function createBadRequestError(reason) {
export function createBadRequestError(reason?: string) {
return decorateBadRequestError(new Error('Bad Request'), reason);
}
export function createUnsupportedTypeError(type) {
export function createUnsupportedTypeError(type: string) {
return createBadRequestError(`Unsupported saved object type: '${type}'`);
}
export function isBadRequestError(error) {
return error && error[code] === CODE_BAD_REQUEST;
export function isBadRequestError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_BAD_REQUEST;
}
// 400 - invalid version
const CODE_INVALID_VERSION = 'SavedObjectsClient/invalidVersion';
export function createInvalidVersionError(versionInput) {
export function createInvalidVersionError(versionInput?: string) {
return decorate(Boom.badRequest(`Invalid version [${versionInput}]`), CODE_INVALID_VERSION, 400);
}
export function isInvalidVersionError(error) {
return error && error[code] === CODE_INVALID_VERSION;
export function isInvalidVersionError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_INVALID_VERSION;
}
// 401 - Not Authorized
const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized';
export function decorateNotAuthorizedError(error, reason) {
export function decorateNotAuthorizedError(error: Error, reason?: string) {
return decorate(error, CODE_NOT_AUTHORIZED, 401, reason);
}
export function isNotAuthorizedError(error) {
return error && error[code] === CODE_NOT_AUTHORIZED;
export function isNotAuthorizedError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_NOT_AUTHORIZED;
}
// 403 - Forbidden
const CODE_FORBIDDEN = 'SavedObjectsClient/forbidden';
export function decorateForbiddenError(error, reason) {
export function decorateForbiddenError(error: Error, reason?: string) {
return decorate(error, CODE_FORBIDDEN, 403, reason);
}
export function isForbiddenError(error) {
return error && error[code] === CODE_FORBIDDEN;
export function isForbiddenError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_FORBIDDEN;
}
// 413 - Request Entity Too Large
const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge';
export function decorateRequestEntityTooLargeError(error, reason) {
export function decorateRequestEntityTooLargeError(error: Error, reason?: string) {
return decorate(error, CODE_REQUEST_ENTITY_TOO_LARGE, 413, reason);
}
export function isRequestEntityTooLargeError(error) {
return error && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE;
export function isRequestEntityTooLargeError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE;
}
// 404 - Not Found
const CODE_NOT_FOUND = 'SavedObjectsClient/notFound';
export function createGenericNotFoundError(type = null, id = null) {
export function createGenericNotFoundError(type: string | null = null, id: string | null = null) {
if (type && id) {
return decorate(Boom.notFound(`Saved object [${type}/${id}] not found`), CODE_NOT_FOUND, 404);
}
return decorate(Boom.notFound(), CODE_NOT_FOUND, 404);
}
export function isNotFoundError(error) {
return error && error[code] === CODE_NOT_FOUND;
export function isNotFoundError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND;
}
// 409 - Conflict
const CODE_CONFLICT = 'SavedObjectsClient/conflict';
export function decorateConflictError(error, reason) {
export function decorateConflictError(error: Error, reason?: string) {
return decorate(error, CODE_CONFLICT, 409, reason);
}
export function isConflictError(error) {
return error && error[code] === CODE_CONFLICT;
export function isConflictError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT;
}
// 503 - Es Unavailable
const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable';
export function decorateEsUnavailableError(error, reason) {
export function decorateEsUnavailableError(error: Error, reason?: string) {
return decorate(error, CODE_ES_UNAVAILABLE, 503, reason);
}
export function isEsUnavailableError(error) {
return error && error[code] === CODE_ES_UNAVAILABLE;
export function isEsUnavailableError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_ES_UNAVAILABLE;
}
// 503 - Unable to automatically create index because of action.auto_create_index setting
const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex';
export function createEsAutoCreateIndexError() {
const error = Boom.serverUnavailable('Automatic index creation failed');
error.output.payload.code = 'ES_AUTO_CREATE_INDEX_ERROR';
error.output.payload.attributes = error.output.payload.attributes || {};
error.output.payload.attributes.code = 'ES_AUTO_CREATE_INDEX_ERROR';
return decorate(error, CODE_ES_AUTO_CREATE_INDEX_ERROR, 503);
}
export function isEsAutoCreateIndexError(error) {
return error && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR;
export function isEsAutoCreateIndexError(error: Error | DecoratedError) {
return isSavedObjectsClientError(error) && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR;
}
// 500 - General Error
const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError';
export function decorateGeneralError(error, reason) {
export function decorateGeneralError(error: Error, reason?: string) {
return decorate(error, CODE_GENERAL_ERROR, 500, reason);
}

View file

@ -24,12 +24,61 @@ describe('includedFields', () => {
expect(includedFields()).toBe(undefined);
});
it('includes type', () => {
it('accepts type string', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toContain('type');
});
it('accepts type as string array', () => {
const fields = includedFields(['config', 'secret'], 'foo');
expect(fields).toMatchInlineSnapshot(`
Array [
"config.foo",
"secret.foo",
"namespace",
"type",
"references",
"migrationVersion",
"updated_at",
"foo",
]
`);
});
it('accepts field as string', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toContain('config.foo');
});
it('accepts fields as an array', () => {
const fields = includedFields('config', ['foo', 'bar']);
expect(fields).toHaveLength(9);
expect(fields).toContain('config.foo');
expect(fields).toContain('config.bar');
});
it('accepts type as string array and fields as string array', () => {
const fields = includedFields(['config', 'secret'], ['foo', 'bar']);
expect(fields).toMatchInlineSnapshot(`
Array [
"config.foo",
"config.bar",
"secret.foo",
"secret.bar",
"namespace",
"type",
"references",
"migrationVersion",
"updated_at",
"foo",
"bar",
]
`);
});
it('includes namespace', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
@ -54,20 +103,6 @@ describe('includedFields', () => {
expect(fields).toContain('updated_at');
});
it('accepts field as string', () => {
const fields = includedFields('config', 'foo');
expect(fields).toHaveLength(7);
expect(fields).toContain('config.foo');
});
it('accepts fields as an array', () => {
const fields = includedFields('config', ['foo', 'bar']);
expect(fields).toHaveLength(9);
expect(fields).toContain('config.foo');
expect(fields).toContain('config.bar');
});
it('uses wildcard when type is not provided', () => {
const fields = includedFields(undefined, 'foo');
expect(fields).toHaveLength(7);

View file

@ -17,22 +17,25 @@
* under the License.
*/
function toArray(value: string | string[]): string[] {
return typeof value === 'string' ? [value] : value;
}
/**
* Provides an array of paths for ES source filtering
*
* @param {string} type
* @param {string|array} fields
* @returns {array}
*/
export function includedFields(type, fields) {
if (!fields || fields.length === 0) return;
export function includedFields(type: string | string[] = '*', fields?: string[] | string) {
if (!fields || fields.length === 0) {
return;
}
// convert to an array
const sourceFields = typeof fields === 'string' ? [fields] : fields;
const sourceType = type || '*';
const sourceFields = toArray(fields);
const sourceType = toArray(type);
return sourceFields
.map(f => `${sourceType}.${f}`)
return sourceType
.reduce((acc: string[], t) => {
return [...acc, ...sourceFields.map(f => `${t}.${f}`)];
}, [])
.concat('namespace')
.concat('type')
.concat('references')

View file

@ -1,24 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { SavedObjectsRepository } from './repository';
export { ScopedSavedObjectsClientProvider } from './scoped_client_provider';
import * as errors from './errors';
export { errors };

View file

@ -17,14 +17,12 @@
* under the License.
*/
import errors from './errors';
export { errors };
export { SavedObjectsRepository, SavedObjectsRepositoryOptions } from './repository';
export {
SavedObjectsClientWrapperFactory,
SavedObjectsClientWrapperOptions,
ScopedSavedObjectsClientProvider,
} from './scoped_client_provider';
import * as errors from './errors';
export { errors };

View file

@ -1,59 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { BaseOptions, SavedObject } from '../saved_objects_client';
export interface SavedObjectsRepositoryOptions {
index: string | string[];
mappings: unknown;
callCluster: unknown;
schema: unknown;
serializer: unknown;
migrator: unknown;
onBeforeWrite?: (
action: 'create' | 'index' | 'update' | 'bulk' | 'delete' | 'deleteByQuery',
params: {
index: string;
id?: string;
body: any;
}
) => void;
onBeforeRead?: (
action: 'get' | 'bulk',
params: {
index: string;
id?: string;
body: any;
}
) => void;
}
export declare class SavedObjectsRepository {
// ATTENTION: this interface is incomplete
public get: (type: string, id: string, options?: BaseOptions) => Promise<SavedObject>;
public incrementCounter: (
type: string,
id: string,
counterFieldName: string,
options?: BaseOptions
) => Promise<SavedObject>;
constructor(options: SavedObjectsRepositoryOptions);
}

View file

@ -1106,7 +1106,7 @@ describe('SavedObjectsRepository', () => {
expect(callAdminCluster).toHaveBeenCalledWith('deleteByQuery', {
body: { conflicts: 'proceed' },
ignore: [404],
index: ['beats', '.kibana-test'],
index: ['.kibana-test', 'beats'],
refresh: 'wait_for',
});
});
@ -1160,7 +1160,7 @@ describe('SavedObjectsRepository', () => {
namespace: 'foo-namespace',
search: 'foo*',
searchFields: ['foo'],
type: 'bar',
type: ['bar'],
sortField: 'name',
sortOrder: 'desc',
defaultSearchOperator: 'AND',
@ -1560,6 +1560,90 @@ describe('SavedObjectsRepository', () => {
error: { statusCode: 404, message: 'Not found' },
});
});
it('returns errors when requesting unsupported types', async () => {
callAdminCluster.mockResolvedValue({
docs: [
{
_type: '_doc',
_id: 'one',
found: true,
...mockVersionProps,
_source: { ...mockTimestampFields, config: { title: 'Test1' } },
},
{
_type: '_doc',
_id: 'three',
found: true,
...mockVersionProps,
_source: { ...mockTimestampFields, config: { title: 'Test3' } },
},
{
_type: '_doc',
_id: 'five',
found: true,
...mockVersionProps,
_source: { ...mockTimestampFields, config: { title: 'Test5' } },
},
],
});
const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([
{ id: 'one', type: 'config' },
{ id: 'two', type: 'invalidtype' },
{ id: 'three', type: 'config' },
{ id: 'four', type: 'invalidtype' },
{ id: 'five', type: 'config' },
]);
expect(savedObjects).toEqual([
{
attributes: { title: 'Test1' },
id: 'one',
...mockTimestampFields,
references: [],
type: 'config',
version: mockVersion,
migrationVersion: undefined,
},
{
attributes: { title: 'Test3' },
id: 'three',
...mockTimestampFields,
references: [],
type: 'config',
version: mockVersion,
migrationVersion: undefined,
},
{
attributes: { title: 'Test5' },
id: 'five',
...mockTimestampFields,
references: [],
type: 'config',
version: mockVersion,
migrationVersion: undefined,
},
{
error: {
error: 'Bad Request',
message: "Unsupported saved object type: 'invalidtype': Bad Request",
statusCode: 400,
},
id: 'two',
type: 'invalidtype',
},
{
error: {
error: 'Bad Request',
message: "Unsupported saved object type: 'invalidtype': Bad Request",
statusCode: 400,
},
id: 'four',
type: 'invalidtype',
},
]);
});
});
describe('#update', () => {
@ -2030,59 +2114,6 @@ describe('SavedObjectsRepository', () => {
).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request"));
});
it("should return an error object when attempting to 'bulkGet' an unsupported type", async () => {
callAdminCluster.mockReturnValue({
docs: [
{
id: 'one',
type: 'config',
_primary_term: 1,
_seq_no: 1,
found: true,
_source: {
updated_at: mockTimestamp,
},
},
{
id: 'bad',
type: 'config',
found: false,
},
],
});
const { saved_objects: savedObjects } = await savedObjectsRepository.bulkGet([
{ id: 'one', type: 'config' },
{ id: 'bad', type: 'config' },
{ id: 'four', type: 'hiddenType' },
]);
expect(savedObjects).toEqual([
{
id: 'one',
type: 'config',
updated_at: mockTimestamp,
references: [],
version: 'WzEsMV0=',
},
{
error: {
message: 'Not found',
statusCode: 404,
},
id: 'bad',
type: 'config',
},
{
id: 'four',
error: {
error: 'Bad Request',
message: "Unsupported saved object type: 'hiddenType': Bad Request",
statusCode: 400,
},
type: 'hiddenType',
},
]);
});
it("should not return hidden saved ojects when attempting to 'find' support and unsupported types", async () => {
callAdminCluster.mockReturnValue({
hits: {

View file

@ -17,19 +17,77 @@
* under the License.
*/
import { omit, flatten } from 'lodash';
import { getRootPropertiesObjects } from '../../../mappings';
import { omit } from 'lodash';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { getRootPropertiesObjects, IndexMapping } from '../../../mappings';
import { getSearchDsl } from './search_dsl';
import { includedFields } from './included_fields';
import { decorateEsError } from './decorate_es_error';
import * as errors from './errors';
import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version';
import { SavedObjectsSchema } from '../../schema';
import { KibanaMigrator } from '../../migrations';
import { SavedObjectsSerializer, SanitizedSavedObjectDoc, RawDoc } from '../../serialization';
import {
BulkCreateObject,
CreateOptions,
SavedObject,
FindOptions,
SavedObjectAttributes,
FindResponse,
BulkGetObject,
BulkResponse,
UpdateOptions,
BaseOptions,
MigrationVersion,
UpdateResponse,
} from '../saved_objects_client';
// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
// eslint-disable-next-line @typescript-eslint/prefer-interface
type Left<T> = {
tag: 'Left';
error: T;
};
// eslint-disable-next-line @typescript-eslint/prefer-interface
type Right<T> = {
tag: 'Right';
value: T;
};
type Either<L, R> = Left<L> | Right<R>;
const isLeft = <L, R>(either: Either<L, R>): either is Left<L> => {
return either.tag === 'Left';
};
export interface SavedObjectsRepositoryOptions {
index: string;
mappings: IndexMapping;
callCluster: CallCluster;
schema: SavedObjectsSchema;
serializer: SavedObjectsSerializer;
migrator: KibanaMigrator;
allowedTypes: string[];
onBeforeWrite?: (...args: Parameters<CallCluster>) => Promise<void>;
}
export interface IncrementCounterOptions extends BaseOptions {
migrationVersion?: MigrationVersion;
}
export class SavedObjectsRepository {
constructor(options) {
private _migrator: KibanaMigrator;
private _index: string;
private _mappings: IndexMapping;
private _schema: SavedObjectsSchema;
private _allowedTypes: string[];
private _onBeforeWrite: (...args: Parameters<CallCluster>) => Promise<void>;
private _unwrappedCallCluster: CallCluster;
private _serializer: SavedObjectsSerializer;
constructor(options: SavedObjectsRepositoryOptions) {
const {
index,
mappings,
@ -38,8 +96,7 @@ export class SavedObjectsRepository {
serializer,
migrator,
allowedTypes = [],
onBeforeWrite = () => {},
onBeforeRead = () => {},
onBeforeWrite = () => Promise.resolve(),
} = options;
// It's important that we migrate documents / mark them as up-to-date
@ -59,9 +116,8 @@ export class SavedObjectsRepository {
this._allowedTypes = allowedTypes;
this._onBeforeWrite = onBeforeWrite;
this._onBeforeRead = onBeforeRead;
this._unwrappedCallCluster = async (...args) => {
this._unwrappedCallCluster = async (...args: Parameters<CallCluster>) => {
await migrator.awaitMigration();
return callCluster(...args);
};
@ -82,10 +138,14 @@ export class SavedObjectsRepository {
* @property {array} [options.references] - [{ name, type, id }]
* @returns {promise} - { id, type, version, attributes }
*/
async create(type, attributes = {}, options = {}) {
const { id, migrationVersion, overwrite = false, namespace, references = [] } = options;
public async create<T extends SavedObjectAttributes>(
type: string,
attributes: T,
options: CreateOptions = { overwrite: false, references: [] }
): Promise<SavedObject<T>> {
const { id, migrationVersion, overwrite, namespace, references } = options;
if (!this._isTypeAllowed(type)) {
if (!this._allowedTypes.includes(type)) {
throw errors.createUnsupportedTypeError(type);
}
@ -103,11 +163,11 @@ export class SavedObjectsRepository {
references,
});
const raw = this._serializer.savedObjectToRaw(migrated);
const raw = this._serializer.savedObjectToRaw(migrated as SanitizedSavedObjectDoc);
const response = await this._writeToCluster(method, {
id: raw._id,
index: this._getIndexForType(type),
index: this.getIndexForType(type),
refresh: 'wait_for',
body: raw._source,
});
@ -135,16 +195,20 @@ export class SavedObjectsRepository {
* @property {string} [options.namespace]
* @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]}
*/
async bulkCreate(objects, options = {}) {
async bulkCreate<T extends SavedObjectAttributes = any>(
objects: Array<BulkCreateObject<T>>,
options: CreateOptions = {}
): Promise<BulkResponse<T>> {
const { namespace, overwrite = false } = options;
const time = this._getCurrentTime();
const bulkCreateParams = [];
const bulkCreateParams: object[] = [];
let requestIndexCounter = 0;
const expectedResults = objects.map(object => {
if (!this._isTypeAllowed(object.type)) {
const expectedResults: Array<Either<any, any>> = objects.map(object => {
if (!this._allowedTypes.includes(object.type)) {
return {
response: {
tag: 'Left' as 'Left',
error: {
id: object.id,
type: object.type,
error: errors.createUnsupportedTypeError(object.type).output.payload,
@ -156,30 +220,28 @@ export class SavedObjectsRepository {
const expectedResult = {
esRequestIndex: requestIndexCounter++,
requestedId: object.id,
rawMigratedDoc: this._serializer.savedObjectToRaw(
this._migrator.migrateDocument({
id: object.id,
type: object.type,
attributes: object.attributes,
migrationVersion: object.migrationVersion,
namespace,
updated_at: time,
references: object.references || [],
})
),
rawMigratedDoc: this._serializer.savedObjectToRaw(this._migrator.migrateDocument({
id: object.id,
type: object.type,
attributes: object.attributes,
migrationVersion: object.migrationVersion,
namespace,
updated_at: time,
references: object.references || [],
}) as SanitizedSavedObjectDoc),
};
bulkCreateParams.push(
{
[method]: {
_id: expectedResult.rawMigratedDoc._id,
_index: this._getIndexForType(object.type),
_index: this.getIndexForType(object.type),
},
},
expectedResult.rawMigratedDoc._source
);
return expectedResult;
return { tag: 'Right' as 'Right', value: expectedResult };
});
const esResponse = await this._writeToCluster('bulk', {
@ -189,18 +251,18 @@ export class SavedObjectsRepository {
return {
saved_objects: expectedResults.map(expectedResult => {
if (expectedResult.response) {
return expectedResult.response;
if (isLeft(expectedResult)) {
return expectedResult.error;
}
const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult;
const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value;
const response = esResponse.items[esRequestIndex];
const {
error,
_id: responseId,
_seq_no: seqNo,
_primary_term: primaryTerm,
} = Object.values(response)[0];
} = Object.values(response)[0] as any;
const {
_source: { type, [type]: attributes, references = [] },
@ -245,8 +307,8 @@ export class SavedObjectsRepository {
* @property {string} [options.namespace]
* @returns {promise}
*/
async delete(type, id, options = {}) {
if (!this._isTypeAllowed(type)) {
async delete(type: string, id: string, options: BaseOptions = {}): Promise<{}> {
if (!this._allowedTypes.includes(type)) {
throw errors.createGenericNotFoundError();
}
@ -254,7 +316,7 @@ export class SavedObjectsRepository {
const response = await this._writeToCluster('delete', {
id: this._serializer.generateRawId(namespace, type, id),
index: this._getIndexForType(type),
index: this.getIndexForType(type),
refresh: 'wait_for',
ignore: [404],
});
@ -282,7 +344,7 @@ export class SavedObjectsRepository {
* @param {string} namespace
* @returns {promise} - { took, timed_out, total, deleted, batches, version_conflicts, noops, retries, failures }
*/
async deleteByNamespace(namespace) {
async deleteByNamespace(namespace: string): Promise<any> {
if (!namespace || typeof namespace !== 'string') {
throw new TypeError(`namespace is required, and must be a string`);
}
@ -291,16 +353,8 @@ export class SavedObjectsRepository {
const typesToDelete = allTypes.filter(type => !this._schema.isNamespaceAgnostic(type));
const indexes = flatten(
Object.values(this._schema).map(schema =>
Object.values(schema).map(props => props.indexPattern)
)
)
.filter(pattern => pattern !== undefined)
.concat([this._index]);
const esOptions = {
index: indexes,
index: this.getIndicesForTypes(typesToDelete),
ignore: [404],
refresh: 'wait_for',
body: {
@ -331,44 +385,32 @@ export class SavedObjectsRepository {
* @property {object} [options.hasReference] - { type, id }
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
*/
async find(options = {}) {
const {
search,
defaultSearchOperator = 'OR',
searchFields,
hasReference,
page = 1,
perPage = 20,
sortField,
sortOrder,
fields,
namespace,
} = options;
let { type } = options;
async find<T extends SavedObjectAttributes = any>({
search,
defaultSearchOperator = 'OR',
searchFields,
hasReference,
page = 1,
perPage = 20,
sortField,
sortOrder,
fields,
namespace,
type,
}: FindOptions): Promise<FindResponse<T>> {
if (!type) {
throw new TypeError(`options.type must be a string or an array of strings`);
}
if (Array.isArray(type)) {
type = type.filter(type => this._isTypeAllowed(type));
if (type.length === 0) {
return {
page,
per_page: perPage,
total: 0,
saved_objects: [],
};
}
} else {
if (!this._isTypeAllowed(type)) {
return {
page,
per_page: perPage,
total: 0,
saved_objects: [],
};
}
const types = Array.isArray(type) ? type : [type];
const allowedTypes = types.filter(t => this._allowedTypes.includes(t));
if (allowedTypes.length === 0) {
return {
page,
per_page: perPage,
total: 0,
saved_objects: [],
};
}
if (searchFields && !Array.isArray(searchFields)) {
@ -380,7 +422,7 @@ export class SavedObjectsRepository {
}
const esOptions = {
index: this._getIndexForType(type),
index: this.getIndicesForTypes(allowedTypes),
size: perPage,
from: perPage * (page - 1),
_source: includedFields(type, fields),
@ -392,7 +434,7 @@ export class SavedObjectsRepository {
search,
defaultSearchOperator,
searchFields,
type,
type: allowedTypes,
sortField,
sortOrder,
namespace,
@ -418,7 +460,7 @@ export class SavedObjectsRepository {
page,
per_page: perPage,
total: response.hits.total,
saved_objects: response.hits.hits.map(hit => this._rawToSavedObject(hit)),
saved_objects: response.hits.hits.map((hit: RawDoc) => this._rawToSavedObject(hit)),
};
}
@ -436,46 +478,51 @@ export class SavedObjectsRepository {
* { id: 'foo', type: 'index-pattern' }
* ])
*/
async bulkGet(objects = [], options = {}) {
async bulkGet<T extends SavedObjectAttributes = any>(
objects: BulkGetObject[] = [],
options: BaseOptions = {}
): Promise<BulkResponse<T>> {
const { namespace } = options;
if (objects.length === 0) {
return { saved_objects: [] };
}
const unsupportedTypes = [];
const unsupportedTypeObjects = objects
.filter(o => !this._allowedTypes.includes(o.type))
.map(({ type, id }) => {
return ({
id,
type,
error: errors.createUnsupportedTypeError(type).output.payload,
} as any) as SavedObject<T>;
});
const supportedTypeObjects = objects.filter(o => this._allowedTypes.includes(o.type));
const response = await this._callCluster('mget', {
body: {
docs: objects.reduce((acc, { type, id, fields }) => {
if (this._isTypeAllowed(type)) {
acc.push({
_id: this._serializer.generateRawId(namespace, type, id),
_index: this._getIndexForType(type),
_source: includedFields(type, fields),
});
} else {
unsupportedTypes.push({
id,
type,
error: errors.createUnsupportedTypeError(type).output.payload,
});
}
return acc;
}, []),
docs: supportedTypeObjects.map(({ type, id, fields }) => {
return {
_id: this._serializer.generateRawId(namespace, type, id),
_index: this.getIndexForType(type),
_source: includedFields(type, fields),
};
}),
},
});
return {
saved_objects: response.docs
saved_objects: (response.docs as any[])
.map((doc, i) => {
const { id, type } = objects[i];
const { id, type } = supportedTypeObjects[i];
if (!doc.found) {
return {
return ({
id,
type,
error: { statusCode: 404, message: 'Not found' },
};
} as any) as SavedObject<T>;
}
const time = doc._source.updated_at;
@ -489,7 +536,7 @@ export class SavedObjectsRepository {
migrationVersion: doc._source.migrationVersion,
};
})
.concat(unsupportedTypes),
.concat(unsupportedTypeObjects),
};
}
@ -502,8 +549,12 @@ export class SavedObjectsRepository {
* @property {string} [options.namespace]
* @returns {promise} - { id, type, version, attributes }
*/
async get(type, id, options = {}) {
if (!this._isTypeAllowed(type)) {
async get<T extends SavedObjectAttributes = any>(
type: string,
id: string,
options: BaseOptions = {}
): Promise<SavedObject<T>> {
if (!this._allowedTypes.includes(type)) {
throw errors.createGenericNotFoundError(type, id);
}
@ -511,7 +562,7 @@ export class SavedObjectsRepository {
const response = await this._callCluster('get', {
id: this._serializer.generateRawId(namespace, type, id),
index: this._getIndexForType(type),
index: this.getIndexForType(type),
ignore: [404],
});
@ -546,8 +597,13 @@ export class SavedObjectsRepository {
* @property {array} [options.references] - [{ name, type, id }]
* @returns {promise}
*/
async update(type, id, attributes, options = {}) {
if (!this._isTypeAllowed(type)) {
async update<T extends SavedObjectAttributes = any>(
type: string,
id: string,
attributes: Partial<T>,
options: UpdateOptions = {}
): Promise<UpdateResponse<T>> {
if (!this._allowedTypes.includes(type)) {
throw errors.createGenericNotFoundError(type, id);
}
@ -556,7 +612,7 @@ export class SavedObjectsRepository {
const time = this._getCurrentTime();
const response = await this._writeToCluster('update', {
id: this._serializer.generateRawId(namespace, type, id),
index: this._getIndexForType(type),
index: this.getIndexForType(type),
...(version && decodeRequestVersion(version)),
refresh: 'wait_for',
ignore: [404],
@ -594,14 +650,19 @@ export class SavedObjectsRepository {
* @property {object} [options.migrationVersion=undefined]
* @returns {promise}
*/
async incrementCounter(type, id, counterFieldName, options = {}) {
async incrementCounter(
type: string,
id: string,
counterFieldName: string,
options: IncrementCounterOptions = {}
) {
if (typeof type !== 'string') {
throw new Error('"type" argument must be a string');
}
if (typeof counterFieldName !== 'string') {
throw new Error('"counterFieldName" argument must be a string');
}
if (!this._isTypeAllowed(type)) {
if (!this._allowedTypes.includes(type)) {
throw errors.createUnsupportedTypeError(type);
}
@ -617,11 +678,11 @@ export class SavedObjectsRepository {
updated_at: time,
});
const raw = this._serializer.savedObjectToRaw(migrated);
const raw = this._serializer.savedObjectToRaw(migrated as SanitizedSavedObjectDoc);
const response = await this._writeToCluster('update', {
id: this._serializer.generateRawId(namespace, type, id),
index: this._getIndexForType(type),
index: this.getIndexForType(type),
refresh: 'wait_for',
_source: true,
body: {
@ -657,42 +718,45 @@ export class SavedObjectsRepository {
};
}
async _writeToCluster(method, params) {
private async _writeToCluster(...args: Parameters<CallCluster>) {
try {
await this._onBeforeWrite(method, params);
return await this._callCluster(method, params);
await this._onBeforeWrite(...args);
return await this._callCluster(...args);
} catch (err) {
throw decorateEsError(err);
}
}
async _readFromCluster(method, params) {
private async _callCluster(...args: Parameters<CallCluster>) {
try {
await this._onBeforeRead(method, params);
return await this._callCluster(method, params);
return await this._unwrappedCallCluster(...args);
} catch (err) {
throw decorateEsError(err);
}
}
async _callCluster(method, params) {
try {
return await this._unwrappedCallCluster(method, params);
} catch (err) {
throw decorateEsError(err);
}
/**
* Returns index specified by the given type or the default index
*
* @param type - the type
*/
private getIndexForType(type: string) {
return this._schema.getIndexForType(type) || this._index;
}
_getIndexForType(type) {
return (
(this._schema.definition &&
this._schema.definition[type] &&
this._schema.definition[type].indexPattern) ||
this._index
);
/**
* Returns an array of indices as specified in `this._schema` for each of the
* given `types`. If any of the types don't have an associated index, the
* default index `this._index` will be included.
*
* @param types The types whose indices should be retrieved
*/
private getIndicesForTypes(types: string[]) {
const unique = (array: string[]) => [...new Set(array)];
return unique(types.map(t => this._schema.getIndexForType(t) || this._index));
}
_getCurrentTime() {
private _getCurrentTime() {
return new Date().toISOString();
}
@ -700,18 +764,8 @@ export class SavedObjectsRepository {
// includes the namespace, and we use this for migrating documents. However, we don't
// want the namespcae to be returned from the repository, as the repository scopes each
// method transparently to the specified namespace.
_rawToSavedObject(raw) {
private _rawToSavedObject(raw: RawDoc): SavedObject {
const savedObject = this._serializer.rawToSavedObject(raw);
return omit(savedObject, 'namespace');
}
_isTypeAllowed(types) {
const toCheck = [].concat(types);
for (const type of toCheck) {
if (!this._allowedTypes.includes(type)) {
return false;
}
}
return true;
}
}

View file

@ -1,39 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SavedObjectsClient } from '..';
export interface SavedObjectsClientWrapperOptions<Request = any> {
client: SavedObjectsClient;
request: Request;
}
export type SavedObjectsClientWrapperFactory<Request = any> = (
options: SavedObjectsClientWrapperOptions<Request>
) => SavedObjectsClient;
export interface ScopedSavedObjectsClientProvider<Request = any> {
// ATTENTION: these types are incomplete
addClientWrapperFactory(
priority: number,
wrapperFactory: SavedObjectsClientWrapperFactory<Request>
): void;
getClient(request: Request): SavedObjectsClient;
}

View file

@ -17,22 +17,47 @@
* under the License.
*/
import { PriorityCollection } from './priority_collection';
import { SavedObjectsClientContract } from '..';
export interface SavedObjectsClientWrapperOptions<Request = unknown> {
client: SavedObjectsClientContract;
request: Request;
}
export type SavedObjectsClientWrapperFactory<Request = unknown> = (
options: SavedObjectsClientWrapperOptions<Request>
) => SavedObjectsClientContract;
export type SavedObjectsClientFactory<Request = unknown> = (
{ request }: { request: Request }
) => SavedObjectsClientContract;
/**
* Provider for the Scoped Saved Object Client.
*/
export class ScopedSavedObjectsClientProvider {
_wrapperFactories = new PriorityCollection();
export class ScopedSavedObjectsClientProvider<Request = unknown> {
private readonly _wrapperFactories = new PriorityCollection<
SavedObjectsClientWrapperFactory<Request>
>();
private _clientFactory: SavedObjectsClientFactory<Request>;
private readonly _originalClientFactory: SavedObjectsClientFactory<Request>;
constructor({ defaultClientFactory }) {
constructor({
defaultClientFactory,
}: {
defaultClientFactory: SavedObjectsClientFactory<Request>;
}) {
this._originalClientFactory = this._clientFactory = defaultClientFactory;
}
addClientWrapperFactory(priority, wrapperFactory) {
addClientWrapperFactory(
priority: number,
wrapperFactory: SavedObjectsClientWrapperFactory<Request>
): void {
this._wrapperFactories.add(priority, wrapperFactory);
}
setClientFactory(customClientFactory) {
setClientFactory(customClientFactory: SavedObjectsClientFactory) {
if (this._clientFactory !== this._originalClientFactory) {
throw new Error(`custom client factory is already set, unable to replace the current one`);
}
@ -40,7 +65,7 @@ export class ScopedSavedObjectsClientProvider {
this._clientFactory = customClientFactory;
}
getClient(request) {
getClient(request: Request): SavedObjectsClientContract {
const client = this._clientFactory({
request,
});

View file

@ -25,7 +25,7 @@ import { getQueryParams } from './query_params';
import { getSortingParams } from './sorting_params';
interface GetSearchDslOptions {
type: string;
type: string | string[];
search?: string;
defaultSearchOperator?: string;
searchFields?: string[];

View file

@ -1,149 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { errors, SavedObjectsRepository } from './lib';
export interface BaseOptions {
namespace?: string;
}
export interface CreateOptions extends BaseOptions {
id?: string;
overwrite?: boolean;
migrationVersion?: MigrationVersion;
references?: SavedObjectReference[];
}
export interface BulkCreateObject<T extends SavedObjectAttributes = any> {
id?: string;
type: string;
attributes: T;
extraDocumentProperties?: string[];
}
export interface BulkCreateResponse<T extends SavedObjectAttributes = any> {
saved_objects: Array<SavedObject<T>>;
}
export interface FindOptions extends BaseOptions {
type?: string | string[];
page?: number;
perPage?: number;
sortField?: string;
sortOrder?: string;
fields?: string[];
search?: string;
searchFields?: string[];
hasReference?: { type: string; id: string };
defaultSearchOperator?: 'AND' | 'OR';
}
export interface FindResponse<T extends SavedObjectAttributes = any> {
saved_objects: Array<SavedObject<T>>;
total: number;
per_page: number;
page: number;
}
export interface UpdateOptions extends BaseOptions {
version?: string;
}
export interface BulkGetObject {
id: string;
type: string;
fields?: string[];
}
export type BulkGetObjects = BulkGetObject[];
export interface BulkGetResponse<T extends SavedObjectAttributes = any> {
saved_objects: Array<SavedObject<T>>;
}
export interface MigrationVersion {
[pluginName: string]: string;
}
export interface SavedObjectAttributes {
[key: string]: SavedObjectAttributes | string | number | boolean | null;
}
export interface VisualizationAttributes extends SavedObjectAttributes {
visState: string;
}
export interface SavedObject<T extends SavedObjectAttributes = any> {
id: string;
type: string;
version?: string;
updated_at?: string;
error?: {
message: string;
statusCode: number;
};
attributes: T;
references: SavedObjectReference[];
migrationVersion?: MigrationVersion;
}
export interface SavedObjectReference {
name: string;
type: string;
id: string;
}
export type GetResponse<T extends SavedObjectAttributes = any> = SavedObject<T>;
export type CreateResponse<T extends SavedObjectAttributes = any> = SavedObject<T>;
export type UpdateResponse<T extends SavedObjectAttributes = any> = SavedObject<T>;
export declare class SavedObjectsClient {
public static errors: typeof errors;
public errors: typeof errors;
constructor(repository: SavedObjectsRepository);
public create<T extends SavedObjectAttributes = any>(
type: string,
attributes: T,
options?: CreateOptions
): Promise<CreateResponse<T>>;
public bulkCreate<T extends SavedObjectAttributes = any>(
objects: Array<BulkCreateObject<T>>,
options?: CreateOptions
): Promise<BulkCreateResponse<T>>;
public delete(type: string, id: string, options?: BaseOptions): Promise<{}>;
public find<T extends SavedObjectAttributes = any>(
options: FindOptions
): Promise<FindResponse<T>>;
public bulkGet<T extends SavedObjectAttributes = any>(
objects: BulkGetObjects,
options?: BaseOptions
): Promise<BulkGetResponse<T>>;
public get<T extends SavedObjectAttributes = any>(
type: string,
id: string,
options?: BaseOptions
): Promise<GetResponse<T>>;
public update<T extends SavedObjectAttributes = any>(
type: string,
id: string,
attributes: Partial<T>,
options?: UpdateOptions
): Promise<UpdateResponse<T>>;
}

View file

@ -1,202 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { errors } from './lib';
export class SavedObjectsClient {
constructor(repository) {
this._repository = repository;
}
/**
* ## SavedObjectsClient errors
*
* Since the SavedObjectsClient has its hands in everything we
* are a little paranoid about the way we present errors back to
* to application code. Ideally, all errors will be either:
*
* 1. Caused by bad implementation (ie. undefined is not a function) and
* as such unpredictable
* 2. An error that has been classified and decorated appropriately
* by the decorators in `./lib/errors`
*
* Type 1 errors are inevitable, but since all expected/handle-able errors
* should be Type 2 the `isXYZError()` helpers exposed at
* `savedObjectsClient.errors` should be used to understand and manage error
* responses from the `SavedObjectsClient`.
*
* Type 2 errors are decorated versions of the source error, so if
* the elasticsearch client threw an error it will be decorated based
* on its type. That means that rather than looking for `error.body.error.type` or
* doing substring checks on `error.body.error.reason`, just use the helpers to
* understand the meaning of the error:
*
* ```js
* if (savedObjectsClient.errors.isNotFoundError(error)) {
* // handle 404
* }
*
* if (savedObjectsClient.errors.isNotAuthorizedError(error)) {
* // 401 handling should be automatic, but in case you wanted to know
* }
*
* // always rethrow the error unless you handle it
* throw error;
* ```
*
* ### 404s from missing index
*
* From the perspective of application code and APIs the SavedObjectsClient is
* a black box that persists objects. One of the internal details that users have
* no control over is that we use an elasticsearch index for persistance and that
* index might be missing.
*
* At the time of writing we are in the process of transitioning away from the
* operating assumption that the SavedObjects index is always available. Part of
* this transition is handling errors resulting from an index missing. These used
* to trigger a 500 error in most cases, and in others cause 404s with different
* error messages.
*
* From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The
* object the request/call was targeting could not be found. This is why #14141
* takes special care to ensure that 404 errors are generic and don't distinguish
* between index missing or document missing.
*
* ### 503s from missing index
*
* Unlike all other methods, create requests are supposed to succeed even when
* the Kibana index does not exist because it will be automatically created by
* elasticsearch. When that is not the case it is because Elasticsearch's
* `action.auto_create_index` setting prevents it from being created automatically
* so we throw a special 503 with the intention of informing the user that their
* Elasticsearch settings need to be updated.
*
* @type {ErrorHelpers} see ./lib/errors
*/
static errors = errors;
errors = errors;
/**
* Persists an object
*
* @param {string} type
* @param {object} attributes
* @param {object} [options={}]
* @property {string} [options.id] - force id on creation, not recommended
* @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion=undefined]
* @property {string} [options.namespace]
* @property {array} [options.references] - [{ name, type, id }]
* @returns {promise} - { id, type, version, attributes }
*/
async create(type, attributes = {}, options = {}) {
return this._repository.create(type, attributes, options);
}
/**
* Creates multiple documents at once
*
* @param {array} objects - [{ type, id, attributes }]
* @param {object} [options={}]
* @property {boolean} [options.overwrite=false] - overwrites existing documents
* @property {string} [options.namespace]
* @returns {promise} - { saved_objects: [{ id, type, version, attributes, error: { message } }]}
*/
async bulkCreate(objects, options = {}) {
return this._repository.bulkCreate(objects, options);
}
/**
* Deletes an object
*
* @param {string} type
* @param {string} id
* @param {object} [options={}]
* @property {string} [options.namespace]
* @returns {promise}
*/
async delete(type, id, options = {}) {
return this._repository.delete(type, id, options);
}
/**
* @param {object} [options={}]
* @property {(string|Array<string>)} [options.type]
* @property {string} [options.search]
* @property {string} [options.defaultSearchOperator]
* @property {Array<string>} [options.searchFields] - see Elasticsearch Simple Query String
* Query field argument for more information
* @property {integer} [options.page=1]
* @property {integer} [options.perPage=20]
* @property {string} [options.sortField]
* @property {string} [options.sortOrder]
* @property {Array<string>} [options.fields]
* @property {string} [options.namespace]
* @property {object} [options.hasReference] - { type, id }
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
*/
async find(options = {}) {
return this._repository.find(options);
}
/**
* Returns an array of objects by id
*
* @param {array} objects - an array ids, or an array of objects containing id and optionally type
* @param {object} [options={}]
* @property {string} [options.namespace]
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }] }
* @example
*
* bulkGet([
* { id: 'one', type: 'config' },
* { id: 'foo', type: 'index-pattern' }
* ])
*/
async bulkGet(objects = [], options = {}) {
return this._repository.bulkGet(objects, options);
}
/**
* Gets a single object
*
* @param {string} type
* @param {string} id
* @param {object} [options={}]
* @property {string} [options.namespace]
* @returns {promise} - { id, type, version, attributes }
*/
async get(type, id, options = {}) {
return this._repository.get(type, id, options);
}
/**
* Updates an object
*
* @param {string} type
* @param {string} id
* @param {object} [options={}]
* @property {integer} options.version - ensures version matches that of persisted object
* @property {string} [options.namespace]
* @returns {promise}
*/
async update(type, id, attributes, options = {}) {
return this._repository.update(type, id, attributes, options);
}
}

View file

@ -17,16 +17,18 @@
* under the License.
*/
export {
MigrationVersion,
SavedObject,
SavedObjectAttributes,
SavedObjectsClient,
SavedObjectsClientWrapperFactory,
SavedObjectReference,
SavedObjectsService,
} from './service';
import { SavedObjectsClientContract } from './saved_objects_client';
import * as errors from './lib/errors';
export { SavedObjectsSchema } from './schema';
const create = (): jest.Mocked<SavedObjectsClientContract> => ({
errors,
create: jest.fn(),
bulkCreate: jest.fn(),
delete: jest.fn(),
bulkGet: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
});
export { SavedObjectsManagement } from './management';
export const SavedObjectsClientMock = { create };

View file

@ -0,0 +1,307 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { errors, SavedObjectsRepository } from './lib';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export interface BaseOptions {
/** Specify the namespace for this operation */
namespace?: string;
}
export interface CreateOptions extends BaseOptions {
/** (not recommended) Specify an id for the document */
id?: string;
/** Overwrite existing documents (defaults to false) */
overwrite?: boolean;
migrationVersion?: MigrationVersion;
references?: SavedObjectReference[];
}
export interface BulkCreateObject<T extends SavedObjectAttributes = any> {
id?: string;
type: string;
attributes: T;
references?: SavedObjectReference[];
migrationVersion?: MigrationVersion;
}
export interface BulkResponse<T extends SavedObjectAttributes = any> {
saved_objects: Array<SavedObject<T>>;
}
export interface FindOptions extends BaseOptions {
type?: string | string[];
page?: number;
perPage?: number;
sortField?: string;
sortOrder?: string;
fields?: string[];
search?: string;
/** see Elasticsearch Simple Query String Query field argument for more information */
searchFields?: string[];
hasReference?: { type: string; id: string };
defaultSearchOperator?: 'AND' | 'OR';
}
export interface FindResponse<T extends SavedObjectAttributes = any> {
saved_objects: Array<SavedObject<T>>;
total: number;
per_page: number;
page: number;
}
export interface UpdateOptions extends BaseOptions {
/** Ensures version matches that of persisted object */
version?: string;
references?: SavedObjectReference[];
}
export interface BulkGetObject {
id: string;
type: string;
/** SavedObject fields to include in the response */
fields?: string[];
}
export interface BulkResponse<T extends SavedObjectAttributes = any> {
saved_objects: Array<SavedObject<T>>;
}
export interface UpdateResponse<T extends SavedObjectAttributes = any>
extends Omit<SavedObject<T>, 'attributes'> {
attributes: Partial<T>;
}
/**
* A dictionary of saved object type -> version used to determine
* what migrations need to be applied to a saved object.
*/
export interface MigrationVersion {
[pluginName: string]: string;
}
export interface SavedObjectAttributes {
[key: string]: SavedObjectAttributes | string | number | boolean | null;
}
export interface VisualizationAttributes extends SavedObjectAttributes {
visState: string;
}
export interface SavedObject<T extends SavedObjectAttributes = any> {
id: string;
type: string;
version?: string;
updated_at?: string;
error?: {
message: string;
statusCode: number;
};
attributes: T;
references: SavedObjectReference[];
migrationVersion?: MigrationVersion;
}
/**
* A reference to another saved object.
*/
export interface SavedObjectReference {
name: string;
type: string;
id: string;
}
export type SavedObjectsClientContract = Pick<SavedObjectsClient, keyof SavedObjectsClient>;
export class SavedObjectsClient {
/**
* ## SavedObjectsClient errors
*
* Since the SavedObjectsClient has its hands in everything we
* are a little paranoid about the way we present errors back to
* to application code. Ideally, all errors will be either:
*
* 1. Caused by bad implementation (ie. undefined is not a function) and
* as such unpredictable
* 2. An error that has been classified and decorated appropriately
* by the decorators in `./lib/errors`
*
* Type 1 errors are inevitable, but since all expected/handle-able errors
* should be Type 2 the `isXYZError()` helpers exposed at
* `savedObjectsClient.errors` should be used to understand and manage error
* responses from the `SavedObjectsClient`.
*
* Type 2 errors are decorated versions of the source error, so if
* the elasticsearch client threw an error it will be decorated based
* on its type. That means that rather than looking for `error.body.error.type` or
* doing substring checks on `error.body.error.reason`, just use the helpers to
* understand the meaning of the error:
*
* ```js
* if (savedObjectsClient.errors.isNotFoundError(error)) {
* // handle 404
* }
*
* if (savedObjectsClient.errors.isNotAuthorizedError(error)) {
* // 401 handling should be automatic, but in case you wanted to know
* }
*
* // always rethrow the error unless you handle it
* throw error;
* ```
*
* ### 404s from missing index
*
* From the perspective of application code and APIs the SavedObjectsClient is
* a black box that persists objects. One of the internal details that users have
* no control over is that we use an elasticsearch index for persistance and that
* index might be missing.
*
* At the time of writing we are in the process of transitioning away from the
* operating assumption that the SavedObjects index is always available. Part of
* this transition is handling errors resulting from an index missing. These used
* to trigger a 500 error in most cases, and in others cause 404s with different
* error messages.
*
* From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The
* object the request/call was targeting could not be found. This is why #14141
* takes special care to ensure that 404 errors are generic and don't distinguish
* between index missing or document missing.
*
* ### 503s from missing index
*
* Unlike all other methods, create requests are supposed to succeed even when
* the Kibana index does not exist because it will be automatically created by
* elasticsearch. When that is not the case it is because Elasticsearch's
* `action.auto_create_index` setting prevents it from being created automatically
* so we throw a special 503 with the intention of informing the user that their
* Elasticsearch settings need to be updated.
*
* @type {ErrorHelpers} see ./lib/errors
*/
public static errors = errors;
public errors = errors;
private _repository: SavedObjectsRepository;
constructor(repository: SavedObjectsRepository) {
this._repository = repository;
}
/**
* Persists a SavedObject
*
* @param type
* @param attributes
* @param options
*/
async create<T extends SavedObjectAttributes = any>(
type: string,
attributes: T,
options?: CreateOptions
) {
return await this._repository.create(type, attributes, options);
}
/**
* Persists multiple documents batched together as a single request
*
* @param objects
* @param options
*/
async bulkCreate<T extends SavedObjectAttributes = any>(
objects: Array<BulkCreateObject<T>>,
options?: CreateOptions
) {
return await this._repository.bulkCreate(objects, options);
}
/**
* Deletes a SavedObject
*
* @param type
* @param id
* @param options
*/
async delete(type: string, id: string, options: BaseOptions = {}) {
return await this._repository.delete(type, id, options);
}
/**
* Find all SavedObjects matching the search query
*
* @param options
*/
async find<T extends SavedObjectAttributes = any>(
options: FindOptions
): Promise<FindResponse<T>> {
return await this._repository.find(options);
}
/**
* Returns an array of objects by id
*
* @param objects - an array of ids, or an array of objects containing id, type and optionally fields
* @example
*
* bulkGet([
* { id: 'one', type: 'config' },
* { id: 'foo', type: 'index-pattern' }
* ])
*/
async bulkGet<T extends SavedObjectAttributes = any>(
objects: BulkGetObject[] = [],
options: BaseOptions = {}
): Promise<BulkResponse<T>> {
return await this._repository.bulkGet(objects, options);
}
/**
* Retrieves a single object
*
* @param type - The type of SavedObject to retrieve
* @param id - The ID of the SavedObject to retrieve
* @param options
*/
async get<T extends SavedObjectAttributes = any>(
type: string,
id: string,
options: BaseOptions = {}
): Promise<SavedObject<T>> {
return await this._repository.get(type, id, options);
}
/**
* Updates an SavedObject
*
* @param type
* @param id
* @param options
*/
async update<T extends SavedObjectAttributes = any>(
type: string,
id: string,
attributes: Partial<T>,
options: UpdateOptions = {}
): Promise<UpdateResponse<T>> {
return await this._repository.update(type, id, attributes, options);
}
}

View file

@ -73,7 +73,9 @@ describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch'
matcher: '*',
response: {
body: {
code: 'ES_AUTO_CREATE_INDEX_ERROR',
attributes: {
code: 'ES_AUTO_CREATE_INDEX_ERROR',
},
},
status: 503,
},

View file

@ -37,7 +37,8 @@ uiRoutes.when('/error/action.auto_create_index', {
export function isAutoCreateIndexError(error: object) {
return (
get(error, 'res.status') === 503 && get(error, 'body.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
get(error, 'res.status') === 503 &&
get(error, 'body.attributes.code') === 'ES_AUTO_CREATE_INDEX_ERROR'
);
}

View file

@ -22,12 +22,12 @@ import { resolve as resolveUrl } from 'url';
import {
MigrationVersion,
SavedObject as PlainSavedObject,
SavedObject,
SavedObjectAttributes,
SavedObjectReference,
SavedObjectsClient as SavedObjectsApi,
} from '../../../server/saved_objects';
import { CreateResponse, FindOptions, UpdateResponse } from '../../../server/saved_objects/service';
import { FindOptions } from '../../../server/saved_objects/service';
import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index';
import { kfetch, KFetchQuery } from '../kfetch';
import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion';
@ -73,9 +73,7 @@ interface FindResults<T extends SavedObjectAttributes = SavedObjectAttributes>
interface BatchQueueEntry {
type: string;
id: string;
resolve: <T extends SavedObjectAttributes>(
value: SimpleSavedObject<T> | PlainSavedObject<T>
) => void;
resolve: <T extends SavedObjectAttributes>(value: SimpleSavedObject<T> | SavedObject<T>) => void;
reject: (reason?: any) => void;
}
@ -165,7 +163,7 @@ export class SavedObjectsClient {
overwrite: options.overwrite,
};
const createRequest: Promise<CreateResponse<T>> = this.request({
const createRequest: Promise<SavedObject<T>> = this.request({
method: 'POST',
path,
query,
@ -334,18 +332,17 @@ export class SavedObjectsClient {
version,
};
const request: Promise<UpdateResponse<T>> = this.request({
return this.request({
method: 'PUT',
path,
body,
});
return request.then(resp => {
}).then((resp: SavedObject<T>) => {
return this.createSavedObject(resp);
});
}
private createSavedObject<T extends SavedObjectAttributes>(
options: PlainSavedObject<T>
options: SavedObject<T>
): SimpleSavedObject<T> {
return new SimpleSavedObject(this, options);
}

View file

@ -70,7 +70,7 @@ export class SimpleSavedObject<T extends SavedObjectAttributes> {
return has(this.attributes, key);
}
public save() {
public save(): Promise<SimpleSavedObject<T>> {
if (this.id) {
return this.client.update(this.type, this.id, this.attributes, {
migrationVersion: this.migrationVersion,

View file

@ -52,7 +52,7 @@
"@types/d3-shape": "^1.3.1",
"@types/d3-time": "^1.0.7",
"@types/d3-time-format": "^2.1.0",
"@types/elasticsearch": "^5.0.30",
"@types/elasticsearch": "^5.0.33",
"@types/file-saver": "^2.0.0",
"@types/git-url-parse": "^9.0.0",
"@types/glob": "^7.1.1",
@ -262,12 +262,12 @@
"mapbox-gl": "0.54.0",
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
"markdown-it": "^8.4.1",
"memoize-one": "^5.0.0",
"mime": "^2.2.2",
"mkdirp": "0.5.1",
"moment": "^2.20.1",
"moment-duration-format": "^1.3.0",
"moment-timezone": "^0.5.14",
"memoize-one": "^5.0.0",
"monaco-editor": "^0.17.0",
"ngreact": "^0.5.1",
"nock": "10.0.4",

View file

@ -9,26 +9,14 @@ jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid-v4-id') }));
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service';
import { createEncryptedSavedObjectsServiceMock } from './encrypted_saved_objects_service.mock';
import { SavedObjectsClient } from 'src/legacy/server/saved_objects/service/saved_objects_client';
function createSavedObjectsClientMock(): jest.Mocked<SavedObjectsClient> {
return {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
}
import { SavedObjectsClientMock } from '../../../../../src/legacy/server/saved_objects/service/saved_objects_client.mock';
import { SavedObjectsClientContract } from 'src/legacy/server/saved_objects';
let wrapper: EncryptedSavedObjectsClientWrapper;
let mockBaseClient: jest.Mocked<SavedObjectsClient>;
let mockBaseClient: jest.Mocked<SavedObjectsClientContract>;
let encryptedSavedObjectsServiceMock: jest.Mocked<EncryptedSavedObjectsService>;
beforeEach(() => {
mockBaseClient = createSavedObjectsClientMock();
mockBaseClient = SavedObjectsClientMock.create();
encryptedSavedObjectsServiceMock = createEncryptedSavedObjectsServiceMock([
{
type: 'known-type',

View file

@ -8,23 +8,21 @@ import uuid from 'uuid';
import {
BaseOptions,
BulkCreateObject,
BulkCreateResponse,
BulkGetObjects,
BulkGetResponse,
BulkGetObject,
BulkResponse,
CreateOptions,
CreateResponse,
FindOptions,
FindResponse,
GetResponse,
SavedObjectAttributes,
SavedObjectsClient,
SavedObjectsClientContract,
UpdateOptions,
UpdateResponse,
} from 'src/legacy/server/saved_objects/service/saved_objects_client';
SavedObject,
} from 'src/legacy/server/saved_objects';
import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service';
interface EncryptedSavedObjectsClientOptions {
baseClient: SavedObjectsClient;
baseClient: SavedObjectsClientContract;
service: Readonly<EncryptedSavedObjectsService>;
}
@ -36,10 +34,10 @@ function generateID() {
return uuid.v4();
}
export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClient {
export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientContract {
constructor(
private readonly options: EncryptedSavedObjectsClientOptions,
public readonly errors: SavedObjectsClient['errors'] = options.baseClient.errors
public readonly errors = options.baseClient.errors
) {}
public async create<T extends SavedObjectAttributes>(
@ -119,7 +117,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClient {
);
}
public async bulkGet(objects: BulkGetObjects = [], options?: BaseOptions) {
public async bulkGet(objects: BulkGetObject[] = [], options?: BaseOptions) {
return this.stripEncryptedAttributesFromBulkResponse(
await this.options.baseClient.bulkGet(objects, options)
);
@ -159,9 +157,9 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClient {
* registered, response is returned as is.
* @param response Raw response returned by the underlying base client.
*/
private stripEncryptedAttributesFromResponse<
T extends UpdateResponse | CreateResponse | GetResponse
>(response: T): T {
private stripEncryptedAttributesFromResponse<T extends UpdateResponse | SavedObject>(
response: T
): T {
if (this.options.service.isRegistered(response.type)) {
response.attributes = this.options.service.stripEncryptedAttributes(
response.type,
@ -177,9 +175,9 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClient {
* response portion isn't registered, it is returned as is.
* @param response Raw response returned by the underlying base client.
*/
private stripEncryptedAttributesFromBulkResponse<
T extends BulkCreateResponse | BulkGetResponse | FindResponse
>(response: T): T {
private stripEncryptedAttributesFromBulkResponse<T extends BulkResponse | FindResponse>(
response: T
): T {
for (const savedObject of response.saved_objects) {
if (this.options.service.isRegistered(savedObject.type)) {
savedObject.attributes = this.options.service.stripEncryptedAttributes(

View file

@ -7,18 +7,18 @@
import {
BaseOptions,
BulkCreateObject,
BulkGetObjects,
BulkGetObject,
CreateOptions,
FindOptions,
SavedObjectAttributes,
SavedObjectsClient,
SavedObjectsClientContract,
UpdateOptions,
} from 'src/legacy/server/saved_objects/service/saved_objects_client';
} from 'src/legacy/server/saved_objects';
import { DEFAULT_SPACE_ID } from '../../../common/constants';
import { SpacesService } from '../create_spaces_service';
interface SpacesSavedObjectsClientOptions {
baseClient: SavedObjectsClient;
baseClient: SavedObjectsClientContract;
request: any;
spacesService: SpacesService;
types: string[];
@ -58,19 +58,19 @@ const throwErrorIfTypesContainsSpace = (types: string[]) => {
}
};
export class SpacesSavedObjectsClient implements SavedObjectsClient {
public readonly errors: any;
private readonly client: SavedObjectsClient;
export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
private readonly client: SavedObjectsClientContract;
private readonly spaceId: string;
private readonly types: string[];
public readonly errors: SavedObjectsClientContract['errors'];
constructor(options: SpacesSavedObjectsClientOptions) {
const { baseClient, request, spacesService, types } = options;
this.errors = baseClient.errors;
this.client = baseClient;
this.spaceId = spacesService.getSpaceId(request);
this.types = types;
this.errors = baseClient.errors;
}
/**
@ -101,7 +101,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClient {
/**
* Creates multiple documents at once
*
* @param {array} objects - [{ type, id, attributes, extraDocumentProperties }]
* @param {array} objects - [{ type, id, attributes }]
* @param {object} [options={}]
* @property {boolean} [options.overwrite=false] - overwrites existing documents
* @property {string} [options.namespace]
@ -182,7 +182,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClient {
* { id: 'foo', type: 'index-pattern' }
* ])
*/
public async bulkGet(objects: BulkGetObjects = [], options: BaseOptions = {}) {
public async bulkGet(objects: BulkGetObject[] = [], options: BaseOptions = {}) {
throwErrorIfTypesContainsSpace(objects.map(object => object.type));
throwErrorIfNamespaceSpecified(options);

View file

@ -140,7 +140,7 @@ export const reindexActionsFactory = (
reindexOp.id,
{ ...reindexOp.attributes, locked: moment().format() },
{ version: reindexOp.version }
);
) as Promise<ReindexSavedObject>;
};
const releaseLock = (reindexOp: ReindexSavedObject) => {
@ -149,7 +149,7 @@ export const reindexActionsFactory = (
reindexOp.id,
{ ...reindexOp.attributes, locked: null },
{ version: reindexOp.version }
);
) as Promise<ReindexSavedObject>;
};
// ----- Public interface
@ -180,7 +180,7 @@ export const reindexActionsFactory = (
const newAttrs = { ...reindexOp.attributes, locked: moment().format(), ...attrs };
return client.update<ReindexOperation>(REINDEX_OP_TYPE, reindexOp.id, newAttrs, {
version: reindexOp.version,
});
}) as Promise<ReindexSavedObject>;
},
async runWhileLocked(reindexOp, func) {

View file

@ -9,3 +9,8 @@ declare module '*.html' {
// eslint-disable-next-line import/no-default-export
export default template;
}
declare module 'lodash/internal/toPath' {
function toPath(value: string | string[]): string[];
export = toPath;
}

View file

@ -9,3 +9,14 @@ declare module '*.html' {
// eslint-disable-next-line import/no-default-export
export default template;
}
declare module 'lodash/internal/toPath' {
function toPath(value: string | string[]): string[];
export = toPath;
}
type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];
type PublicMethodsOf<T> = Pick<T, MethodKeysOf<T>>;

View file

@ -3376,10 +3376,10 @@
resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-2.0.0.tgz#1647ae9e68f708a6ba778531af667ec55bc61964"
integrity sha512-sq+kwx8zA9BSugT9N+Jr8/uWjbHMZ+N/meJEzRyT3gmLq/WMtx/iSIpvdpmBUi/cvXl6Kzpvve8G2ESkabFwmg==
"@types/elasticsearch@^5.0.30":
version "5.0.30"
resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.30.tgz#3c52f7119e3a20a47e2feb8e2b4cc54030a54e23"
integrity sha512-swxiNcLOtnHhJhAE5HcUL3WsKLHr8rEQ+fwpaJ0x4dfEE3oK2kGUoyz4wCcQfvulcMm2lShyxZ+2E4BQJzsAlg==
"@types/elasticsearch@^5.0.33":
version "5.0.33"
resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.33.tgz#b0fd37dc674f498223b6d68c313bdfd71f4d812b"
integrity sha512-n/g9pqJEpE4fyUE8VvHNGtl7E2Wv8TCroNwfgAeJKRV4ghDENahtrAo1KMsFNIejBD2gDAlEUa4CM4oEEd8p9Q==
"@types/enzyme@^3.1.12":
version "3.1.18"
@ -26639,11 +26639,21 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0:
resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf"
integrity sha1-G67AG16PXzTDImedEycBbp4pT68=
typescript@^3.3.3333, typescript@~3.0.3, typescript@~3.3.3333, typescript@~3.4.3:
typescript@^3.3.3333, typescript@~3.3.3333:
version "3.3.3333"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6"
integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==
typescript@~3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8"
integrity sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==
typescript@~3.4.3:
version "3.4.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99"
integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==
typings-tester@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b"