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

Conversation

pgayvallet
Copy link
Contributor

@pgayvallet pgayvallet commented May 9, 2023

Summary

Structural cleanup of the SavedObjectsRepository code, by extracting the actual implementation of each API to their individual file (as it was initiated for some by Joe a while ago, e.g updateObjectsSpaces)

Why doing that, and why now?

I remember discussing about this extraction with Joe for the first time like, what, almost 3 years ago? The 2.5k line SOR is a beast, and the only reason we did not refactor that yet is because of (the lack of) priorization of tech debt (and lack of courage, probably).

So, why now? Well, with the changes we're planning to perform to the SOR code for serverless, I thought that doing a bit of cleanup beforehand was probably a wise thing. So I took this on-week time to tackle this (I know, so much for an on-week project, right?)

API extraction

All of these APIs in the SOR class now look like:

  /**
   * {@inheritDoc ISavedObjectsRepository.create}
   */
  public async create<T = unknown>(
    type: string,
    attributes: T,
    options: SavedObjectsCreateOptions = {}
  ): Promise<SavedObject<T>> {
    return await performCreate(
      {
        type,
        attributes,
        options,
      },
      this.apiExecutionContext
    );
  }

This separation allows a better isolation, testability, readability and therefore maintainability overall.

Structure

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

There was a massive amount of helpers, utilities and such, both as internal functions on the SOR, and as external utilities. Some being stateless, some requiring access to parts of the SOR to perform calls...

I introduced 3 concepts here, as you can see on the structure:

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).

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.

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.

Note that given the legacy of the code, the frontier between those concept is quite thin sometimes, but I wanted to regroups things a bit, and also I aimed at increasing the developer experience by avoiding to call methods with 99 parameters (which is why the helpers were created).

Tests

Test coverage was not altered by this PR. The base repository tests (packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts) and the extension tests (packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.{ext}_extension.test.ts) were remain untouched. These tests are performing 'almost unmocked' tests, somewhat close to integration tests, so it would probably be worth keeping them.

The new structure allow more low-level, unitary testing of the individual APIs. I did NOT add proper unit test coverage for the extracted APIs, as the amount of work it represent is way more significant than the refactor itself (and, once again, the existing coverage still applies / function here).

The testing utilities and mocks were added in the PR though, and an example of what the per-API unit test could look like was also added (packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/remove_references_to.test.ts).

Overall, I think it of course would be beneficial to add the missing unit test coverage, but given the amount of work it represent, and the fact that the code is already tested by the repository test and the (quite exhaustive) FTR test suites, I don't think it's worth the effort right now given our other priorities.

@pgayvallet pgayvallet added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc release_note:skip Skip the PR/issue when compiling release notes v8.9.0 labels May 9, 2023
@pgayvallet pgayvallet force-pushed the kbn-xxx-SOR-cleanup branch from b1e3b3e to 7b8e966 Compare May 10, 2023 08:33
@pgayvallet pgayvallet marked this pull request as ready for review May 10, 2023 09:59
@pgayvallet pgayvallet requested review from a team as code owners May 10, 2023 09:59
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

Copy link
Contributor Author

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

Self-review

Comment on lines +41 to +48
export const performFind = async <T = unknown, A = unknown>(
{ options, internalOptions }: PerformFindParams,
{
registry,
helpers,
allowedTypes: rawAllowedTypes,
mappings,
client,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All the APIs were extracted without any behavioural change. Only modifications were to fix the calls to the helper methods and/or access to the SOR properties (now passed via the context)

Comment on lines +16 to +24
describe('performRemoveReferencesTo', () => {
const namespace = 'some_ns';
const indices = ['.kib_1', '.kib_2'];
let apiExecutionContext: ApiExecutionContextMock;

beforeEach(() => {
apiExecutionContext = apiContextMock.create();
apiExecutionContext.registry.registerType(fooType);
apiExecutionContext.registry.registerType(barType);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As explained in the issue's description, I did not add full test coverage for all the newly created files, as the repository tests are still properly covering them (in addition to the FTR tests).

The mocks and tools are ready though, as shown by this test file

Comment on lines +23 to +34
export class CommonHelper {
private registry: ISavedObjectTypeRegistry;
private spaceExtension?: ISavedObjectsSpacesExtension;
private encryptionExtension?: ISavedObjectsEncryptionExtension;
private defaultIndex: string;
private kibanaVersion: string;

public readonly createPointInTimeFinder: CreatePointInTimeFinderFn;

constructor({
registry,
createPointInTimeFinder,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 'helpers' passed down to all API execution handlers. As explained in the issue's description, those are mostly used to replace calls to internal SOR helpers, and to avoid calls from the API handlers to pass the full list of parameters (e.g registry extensions and so on) by keeping reference of those dependencies in the helpers.

Comment on lines +9 to +13
import type { ICommonHelper } from './common';
import type { IEncryptionHelper } from './encryption';
import type { IValidationHelper } from './validation';
import type { IPreflightCheckHelper } from './preflight_check';
import type { ISerializerHelper } from './serializer';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NIT: I was forced to add these interfaces for the mocked versions (stupid TS still considering that private attributes are part of an instance's type)

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting, how did this manifest as an issue? Were you able to access private members in the repository functions?

Copy link
Contributor Author

@pgayvallet pgayvallet May 10, 2023

Choose a reason for hiding this comment

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

TS complained because the mocked version (that only implements the methods) could not be coerced as the concrete type because it was missing the private properties (when using the mocked api internal context in tests)

Comment on lines 61 to +62
client: RepositoryEsClient;
serializer: SavedObjectsSerializer;
serializer: ISavedObjectsSerializer;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NIT: lot of methods were using the concrete type instead of the interface. Fixed all those usages

Comment on lines +9 to +11
export const DEFAULT_REFRESH_SETTING = 'wait_for';
export const DEFAULT_RETRY_COUNT = 3;
export const MAX_CONCURRENT_ALIAS_DELETIONS = 10;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NIT: extracted the constants that were living in the repository.ts file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This file was plain unused. Not even sure for how long it has been a dead file.

Copy link
Contributor

Choose a reason for hiding this comment

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

🔥

Copy link
Contributor

Choose a reason for hiding this comment

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

The code was implemented during Spaces Phase 1 and the last implementation code change was ~ 3 years ago. I guess it became redundant since then.

Comment on lines +291 to +297
return await performCheckConflicts(
{
objects,
options,
},
this.apiExecutionContext
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Look at this beauty

Comment on lines +31 to +36
const createApiExecutionContextMock = (): ApiExecutionContextMock => {
return {
registry: new SavedObjectTypeRegistry(),
helpers: apiHelperMocks.create(),
extensions: savedObjectsExtensionsMock.create(),
client: elasticsearchClientMock.createElasticsearchClient(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ApiExecutionContext mock sure was something...

@@ -11,7 +11,7 @@ import type {
SavedObjectsResolveResponse,
} from '@kbn/core-saved-objects-api-server';
import type { SavedObjectsClient } from '@kbn/core-saved-objects-api-server-internal';
import { isBulkResolveError } from '@kbn/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve';
import { isBulkResolveError } from '@kbn/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@elastic/kibana-security 🙈 nested import for internal core package.

Copy link
Member

Choose a reason for hiding this comment

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

🙈

Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

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

Great work @pgayvallet !


I read this in my local editor and I really like how these functions are broken out. There is a lot of commonality in structure for this actions (namespaces, preflight, auth check, do the thing then encrypt/redact) but the way they are now still affords a lot of flexibility to tweak things over time.


The structure of utils and helpers makes sense off the bat, but I had to spend a bit of time thinking about how internals fits in -- like when would I choose to put something there? Then I saw the commonality of using the client and how these are shared extensions of the clients functionality + business logic. This is just my paraphrasing of what you said. Overall I like it!


Left a few non-blocker comments, approving to unblock progress 🚢

Comment on lines 102 to 105
return {
tag: 'Left',
value: { id: requestId, type, error: errorContent(error) },
};
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: WDYT about a util left(v: L): Left<L> and right(v: R): Right<R>? Could remove some boilerplate. This would become: return left({ id... })

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, we should add this to our own implementation of either. will take a shot in current PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 32f9866

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

Comment on lines +9 to +13
import type { ICommonHelper } from './common';
import type { IEncryptionHelper } from './encryption';
import type { IValidationHelper } from './validation';
import type { IPreflightCheckHelper } from './preflight_check';
import type { ISerializerHelper } from './serializer';
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting, how did this manifest as an issue? Were you able to access private members in the repository functions?

Copy link
Contributor

Choose a reason for hiding this comment

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

🥳

Comment on lines +19 to +29
export interface ApiExecutionContext {
registry: ISavedObjectTypeRegistry;
helpers: RepositoryHelpers;
extensions: SavedObjectsExtensions;
client: RepositoryEsClient;
allowedTypes: string[];
serializer: ISavedObjectsSerializer;
migrator: IKibanaMigrator;
logger: Logger;
mappings: IndexMapping;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

How about ApiInternalContext with a doc comment 😉

Copy link
Contributor

Choose a reason for hiding this comment

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

🔥

Copy link
Contributor

Choose a reason for hiding this comment

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

Could/should we move this file + test to ../internals/utils.ts + ../internals/utils.test.ts?

Copy link
Contributor

Choose a reason for hiding this comment

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

On second thought, that does not really fit the current pattern... Just thought seeing "internal" again was a bit confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just kept the old file name tbh, it's not only used by the /api/internals folder...

@@ -11,7 +11,7 @@ import type {
SavedObjectsResolveResponse,
} from '@kbn/core-saved-objects-api-server';
import type { SavedObjectsClient } from '@kbn/core-saved-objects-api-server-internal';
import { isBulkResolveError } from '@kbn/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve';
import { isBulkResolveError } from '@kbn/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve';
Copy link
Member

Choose a reason for hiding this comment

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

🙈

await pMap(objectsToDeleteAliasesFor, mapper, { concurrency: MAX_CONCURRENT_ALIAS_DELETIONS });

return { statuses: [...savedObjects] };
return await performBulkDelete(
Copy link
Contributor

@TinaHeiligers TinaHeiligers May 10, 2023

Choose a reason for hiding this comment

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

😍

Copy link
Contributor

@TinaHeiligers TinaHeiligers left a comment

Choose a reason for hiding this comment

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

Love it!

// 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.
type ApiExecutionContext,
performCreate,
Copy link
Contributor

Choose a reason for hiding this comment

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

I love the consistency!

* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

Copy link
Contributor

Choose a reason for hiding this comment

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

WDYT about adding a descriptor of what each folder contains as part of the whole repository code organization?
We could use the README as a start.

A copy-paste version of the PR description would work!


export type ICommonHelper = PublicMethodsOf<CommonHelper>;

export class CommonHelper {
Copy link
Contributor

Choose a reason for hiding this comment

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

WDYT about ApiExecutionContextGetters? Most of the helpers are getters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Naming is hard... Not sure these methods are strictly speaking 'getters' given they all hold some internal logic.

@@ -12,7 +12,7 @@ import type {
ErrorCause,
} from '@elastic/elasticsearch/lib/api/types';
import type { estypes, TransportResult } from '@elastic/elasticsearch';
import type { Either } from './internal_utils';
import type { Either } from './apis/utils';
import type { DeleteLegacyUrlAliasesParams } from './legacy_url_aliases';

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

With the new structure, these should probably move somewhere else. Now that the "rules" are less strict, we could move them to core-saved-objects-api-server or to base.
Alternatively, just add them to the file that needs them.

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 agree that our Either utils could be moved elsewhere. Although it's only used within the api-server-internal package (and will likely stay that way given we're using fp-ts in other parts of the code like the migration).

Ihmo the next step would probably be to delete those and using fp-ts too.

For the other utils thats harder. Utils are always a mess. Some are using only by one file, some are used by multiple APIs....

@pgayvallet pgayvallet added the backport:skip This commit does not require backporting label May 11, 2023
@pgayvallet pgayvallet enabled auto-merge (squash) May 11, 2023 06:24
@pgayvallet pgayvallet merged commit 3b6b7ad into elastic:main May 11, 2023
@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/core-saved-objects-api-server-internal 55 0 -55
Unknown metric groups

API count

id before after diff
@kbn/core-saved-objects-api-server-internal 77 0 -77

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
securitySolution 400 404 +4
total +6

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
securitySolution 480 484 +4
total +6

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

pgayvallet added a commit that referenced this pull request May 12, 2023
## Summary

Follow-up of #157154

Continue the cleanup started in the previous PR, by moving more things
around.

- Move everything search-related (dsl, aggregations, kql utils) to a
dedicated `search` folder
- Move a few more things to `/apis/internals`
- Remove the 'v5' field compatibility in the field list generation (see
comment)
- Cleanup some files a bit.

---------

Co-authored-by: kibanamachine <[email protected]>
jasonrhodes pushed a commit that referenced this pull request May 17, 2023
## Summary

Follow-up of #157154

Continue the cleanup started in the previous PR, by moving more things
around.

- Move everything search-related (dsl, aggregations, kql utils) to a
dedicated `search` folder
- Move a few more things to `/apis/internals`
- Remove the 'v5' field compatibility in the field list generation (see
comment)
- Cleanup some files a bit.

---------

Co-authored-by: kibanamachine <[email protected]>
TinaHeiligers added a commit that referenced this pull request Dec 13, 2023
## Summary

Structural cleanup of the `SavedObjectsRepository`, continued from where
#157154 left off.
This PR holds changes to `repository.test.ts`, and splits the api unit
tests into dedicated test files next to the api implementation.

### Tests
This PR does not alter test coverage, its primary aim is to improve
DevEx when it comes to SOR api unit testing. Hopefully, it'll make life
easier for everyone touching the code.

---------

Co-authored-by: Kibana Machine <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.9.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants