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

SavedObjectsRepository code cleanup #157154

Merged
merged 37 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
66c2db2
start extracting all the things
pgayvallet May 9, 2023
1baed7a
extract 'create'
pgayvallet May 9, 2023
3104023
extract bulk_create
pgayvallet May 9, 2023
e9c8d57
extract delete
pgayvallet May 9, 2023
2e1add9
extract checkConflicts
pgayvallet May 9, 2023
bd51cd3
extracting bulkDelete
pgayvallet May 9, 2023
e268959
extract delete by namespace
pgayvallet May 9, 2023
7b5a8b8
extract find
pgayvallet May 9, 2023
2dc4634
extract bulk_get
pgayvallet May 9, 2023
a2f90e8
extract get
pgayvallet May 9, 2023
1ed1f1c
extract update
pgayvallet May 9, 2023
8b550de
extract bulk_update
pgayvallet May 9, 2023
cb47abb
extract remove_references_to
pgayvallet May 9, 2023
d3b338f
extract open point in time
pgayvallet May 9, 2023
aeed912
extract increment_counter
pgayvallet May 9, 2023
109017d
implement resolve and bulk resolve
pgayvallet May 9, 2023
e7f1e34
move internals
pgayvallet May 9, 2023
aef0cfa
delete dead code
pgayvallet May 9, 2023
91cfdc0
remove obsolete snapshot
pgayvallet May 9, 2023
81ea647
some more cleanup
pgayvallet May 9, 2023
ccfe1c9
fix import path
pgayvallet May 9, 2023
65b4bfa
fix import path
pgayvallet May 9, 2023
b6007d8
fix binding
pgayvallet May 9, 2023
ba8563a
this was a tricky one
pgayvallet May 9, 2023
f640226
implement remaining missing methods
pgayvallet May 10, 2023
b63a1b9
Merge remote-tracking branch 'upstream/main' into kbn-xxx-SOR-cleanup
pgayvallet May 10, 2023
85a9c00
small repository cleanup
pgayvallet May 10, 2023
c83830d
move ALL the things again
pgayvallet May 10, 2023
eec0fc1
fix constructor error
pgayvallet May 10, 2023
2464efd
add mocks and test example
pgayvallet May 10, 2023
7b8e966
lint
pgayvallet May 10, 2023
48a1be0
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine May 10, 2023
5eac343
Merge remote-tracking branch 'upstream/main' into kbn-xxx-SOR-cleanup
pgayvallet May 10, 2023
37ccab3
merge util files
pgayvallet May 10, 2023
32f9866
add left and right helpers
pgayvallet May 11, 2023
04e5ac4
Merge remote-tracking branch 'upstream/main' into kbn-xxx-SOR-cleanup
pgayvallet May 11, 2023
69bab51
add description to the package's readme
pgayvallet May 11, 2023
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# @kbn/core-saved-objects-api-server-internal

This package contains the internal implementation of core's server-side savedObjects client and repository.

## Structure of the package

```
@kbn/core-saved-objects-api-server-internal
- /src/lib
- repository.ts
- /apis
- create.ts
- delete.ts
- ....
- /helpers
- /utils
- /internals
```

### lib/apis/utils
Base utility functions, receiving (mostly) parameters from a given API call's option
(e.g the type or id of a document, but not the type registry).

### lib/apis/helpers
'Stateful' helpers. These helpers were mostly here to receive the utility functions that were extracted from the SOR.
They are instantiated with the SOR's context (e.g type registry, mappings and so on), to avoid the caller to such
helpers to have to pass all the parameters again.

### lib/apis/internals
I would call them 'utilities with business logic'. These are the 'big' chunks of logic called by the APIs.
E.g preflightCheckForCreate, internalBulkResolve and so on.

This file was deleted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function still seems like such a beast 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I was planning on using the remaining time this week trying to do some quick wins simplifying some APIs

Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
/*
* 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 { Payload } from '@hapi/boom';
import {
SavedObjectsErrorHelpers,
type SavedObject,
type SavedObjectSanitizedDoc,
DecoratedError,
AuthorizeCreateObject,
SavedObjectsRawDoc,
} from '@kbn/core-saved-objects-server';
import { SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server';
import {
SavedObjectsCreateOptions,
SavedObjectsBulkCreateObject,
SavedObjectsBulkResponse,
} from '@kbn/core-saved-objects-api-server';
import { DEFAULT_REFRESH_SETTING } from '../constants';
import {
Either,
getBulkOperationError,
getCurrentTime,
getExpectedVersionProperties,
left,
right,
isLeft,
isRight,
normalizeNamespace,
setManaged,
errorContent,
} from './utils';
import { getSavedObjectNamespaces } from './utils';
import { PreflightCheckForCreateObject } from './internals/preflight_check_for_create';
import { ApiExecutionContext } from './types';

export interface PerformBulkCreateParams<T = unknown> {
objects: Array<SavedObjectsBulkCreateObject<T>>;
options: SavedObjectsCreateOptions;
}

type ExpectedResult = Either<
{ type: string; id?: string; error: Payload },
{
method: 'index' | 'create';
object: SavedObjectsBulkCreateObject & { id: string };
preflightCheckIndex?: number;
}
>;

export const performBulkCreate = async <T>(
{ objects, options }: PerformBulkCreateParams<T>,
{
registry,
helpers,
allowedTypes,
client,
serializer,
migrator,
extensions = {},
}: ApiExecutionContext
): Promise<SavedObjectsBulkResponse<T>> => {
const {
common: commonHelper,
validation: validationHelper,
encryption: encryptionHelper,
preflight: preflightHelper,
serializer: serializerHelper,
} = helpers;
const { securityExtension } = extensions;
const namespace = commonHelper.getCurrentNamespace(options.namespace);

const {
migrationVersionCompatibility,
overwrite = false,
refresh = DEFAULT_REFRESH_SETTING,
managed: optionsManaged,
} = options;
const time = getCurrentTime();

let preflightCheckIndexCounter = 0;
const expectedResults = objects.map<ExpectedResult>((object) => {
const { type, id: requestId, initialNamespaces, version, managed } = object;
let error: DecoratedError | undefined;
let id: string = ''; // Assign to make TS happy, the ID will be validated (or randomly generated if needed) during getValidId below
const objectManaged = managed;
if (!allowedTypes.includes(type)) {
error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
} else {
try {
id = commonHelper.getValidId(type, requestId, version, overwrite);
validationHelper.validateInitialNamespaces(type, initialNamespaces);
validationHelper.validateOriginId(type, object);
} catch (e) {
error = e;
}
}

if (error) {
return left({ id: requestId, type, error: errorContent(error) });
}

const method = requestId && overwrite ? 'index' : 'create';
const requiresNamespacesCheck = requestId && registry.isMultiNamespace(type);

return right({
method,
object: {
...object,
id,
managed: setManaged({ optionsManaged, objectManaged }),
},
...(requiresNamespacesCheck && { preflightCheckIndex: preflightCheckIndexCounter++ }),
}) as ExpectedResult;
});

const validObjects = expectedResults.filter(isRight);
if (validObjects.length === 0) {
// We only have error results; return early to avoid potentially trying authZ checks for 0 types which would result in an exception.
return {
// Technically the returned array should only contain SavedObject results, but for errors this is not true (we cast to 'unknown' below)
saved_objects: expectedResults.map<SavedObject<T>>(
({ value }) => value as unknown as SavedObject<T>
),
};
}

const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
const preflightCheckObjects = validObjects
.filter(({ value }) => value.preflightCheckIndex !== undefined)
.map<PreflightCheckForCreateObject>(({ value }) => {
const { type, id, initialNamespaces } = value.object;
const namespaces = initialNamespaces ?? [namespaceString];
return { type, id, overwrite, namespaces };
});
const preflightCheckResponse = await preflightHelper.preflightCheckForCreate(
preflightCheckObjects
);

const authObjects: AuthorizeCreateObject[] = validObjects.map((element) => {
const { object, preflightCheckIndex: index } = element.value;
const preflightResult = index !== undefined ? preflightCheckResponse[index] : undefined;
return {
type: object.type,
id: object.id,
initialNamespaces: object.initialNamespaces,
existingNamespaces: preflightResult?.existingDocument?._source.namespaces ?? [],
};
});

const authorizationResult = await securityExtension?.authorizeBulkCreate({
namespace,
objects: authObjects,
});

let bulkRequestIndexCounter = 0;
const bulkCreateParams: object[] = [];
type ExpectedBulkResult = Either<
{ type: string; id?: string; error: Payload },
{ esRequestIndex: number; requestedId: string; rawMigratedDoc: SavedObjectsRawDoc }
>;
const expectedBulkResults = await Promise.all(
expectedResults.map<Promise<ExpectedBulkResult>>(async (expectedBulkGetResult) => {
if (isLeft(expectedBulkGetResult)) {
return expectedBulkGetResult;
}

let savedObjectNamespace: string | undefined;
let savedObjectNamespaces: string[] | undefined;
let existingOriginId: string | undefined;
let versionProperties;
const {
preflightCheckIndex,
object: { initialNamespaces, version, ...object },
method,
} = expectedBulkGetResult.value;
if (preflightCheckIndex !== undefined) {
const preflightResult = preflightCheckResponse[preflightCheckIndex];
const { type, id, existingDocument, error } = preflightResult;
if (error) {
const { metadata } = error;
return left({
id,
type,
error: {
...errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)),
...(metadata && { metadata }),
},
});
}
savedObjectNamespaces =
initialNamespaces || getSavedObjectNamespaces(namespace, existingDocument);
versionProperties = getExpectedVersionProperties(version);
existingOriginId = existingDocument?._source?.originId;
} else {
if (registry.isSingleNamespace(object.type)) {
savedObjectNamespace = initialNamespaces
? normalizeNamespace(initialNamespaces[0])
: namespace;
} else if (registry.isMultiNamespace(object.type)) {
savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace);
}
versionProperties = getExpectedVersionProperties(version);
}

// 1. If the originId has been *explicitly set* in the options (defined or undefined), respect that.
// 2. Otherwise, preserve the originId of the existing object that is being overwritten, if any.
const originId = Object.keys(object).includes('originId')
? object.originId
: existingOriginId;
const migrated = migrator.migrateDocument({
id: object.id,
type: object.type,
attributes: await encryptionHelper.optionallyEncryptAttributes(
object.type,
object.id,
savedObjectNamespace, // only used for multi-namespace object types
object.attributes
),
migrationVersion: object.migrationVersion,
coreMigrationVersion: object.coreMigrationVersion,
typeMigrationVersion: object.typeMigrationVersion,
...(savedObjectNamespace && { namespace: savedObjectNamespace }),
...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
managed: setManaged({ optionsManaged, objectManaged: object.managed }),
updated_at: time,
created_at: time,
references: object.references || [],
originId,
}) as SavedObjectSanitizedDoc<T>;

/**
* If a validation has been registered for this type, we run it against the migrated attributes.
* This is an imperfect solution because malformed attributes could have already caused the
* migration to fail, but it's the best we can do without devising a way to run validations
* inside the migration algorithm itself.
*/
try {
validationHelper.validateObjectForCreate(object.type, migrated);
} catch (error) {
return left({
id: object.id,
type: object.type,
error,
});
}

const expectedResult = {
esRequestIndex: bulkRequestIndexCounter++,
requestedId: object.id,
rawMigratedDoc: serializer.savedObjectToRaw(migrated),
};

bulkCreateParams.push(
{
[method]: {
_id: expectedResult.rawMigratedDoc._id,
_index: commonHelper.getIndexForType(object.type),
...(overwrite && versionProperties),
},
},
expectedResult.rawMigratedDoc._source
);

return right(expectedResult);
})
);

const bulkResponse = bulkCreateParams.length
? await client.bulk({
refresh,
require_alias: true,
body: bulkCreateParams,
})
: undefined;

const result = {
saved_objects: expectedBulkResults.map((expectedResult) => {
if (isLeft(expectedResult)) {
return expectedResult.value as any;
}

const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value;
const rawResponse = Object.values(bulkResponse?.items[esRequestIndex] ?? {})[0] as any;

const error = getBulkOperationError(rawMigratedDoc._source.type, requestedId, rawResponse);
if (error) {
return { type: rawMigratedDoc._source.type, id: requestedId, error };
}

// When method == 'index' the bulkResponse doesn't include the indexed
// _source so we return rawMigratedDoc but have to spread the latest
// _seq_no and _primary_term values from the rawResponse.
return serializerHelper.rawToSavedObject(
{
...rawMigratedDoc,
...{ _seq_no: rawResponse._seq_no, _primary_term: rawResponse._primary_term },
},
{ migrationVersionCompatibility }
);
}),
};
return encryptionHelper.optionallyDecryptAndRedactBulkResult(
result,
authorizationResult?.typeMap,
objects
);
};
Loading