-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Conversation
b1e3b3e
to
7b8e966
Compare
Pinging @elastic/kibana-core (Team:Core) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-review
export const performFind = async <T = unknown, A = unknown>( | ||
{ options, internalOptions }: PerformFindParams, | ||
{ | ||
registry, | ||
helpers, | ||
allowedTypes: rawAllowedTypes, | ||
mappings, | ||
client, |
There was a problem hiding this comment.
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)
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); |
There was a problem hiding this comment.
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
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, |
There was a problem hiding this comment.
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.
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'; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
client: RepositoryEsClient; | ||
serializer: SavedObjectsSerializer; | ||
serializer: ISavedObjectsSerializer; |
There was a problem hiding this comment.
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
export const DEFAULT_REFRESH_SETTING = 'wait_for'; | ||
export const DEFAULT_RETRY_COUNT = 3; | ||
export const MAX_CONCURRENT_ALIAS_DELETIONS = 10; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔥
There was a problem hiding this comment.
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.
return await performCheckConflicts( | ||
{ | ||
objects, | ||
options, | ||
}, | ||
this.apiExecutionContext | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look at this beauty
const createApiExecutionContextMock = (): ApiExecutionContextMock => { | ||
return { | ||
registry: new SavedObjectTypeRegistry(), | ||
helpers: apiHelperMocks.create(), | ||
extensions: savedObjectsExtensionsMock.create(), | ||
client: elasticsearchClientMock.createElasticsearchClient(), |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙈
There was a problem hiding this 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 🚢
return { | ||
tag: 'Left', | ||
value: { id: requestId, type, error: errorContent(error) }, | ||
}; |
There was a problem hiding this comment.
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... })
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 32f9866
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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
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'; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥳
export interface ApiExecutionContext { | ||
registry: ISavedObjectTypeRegistry; | ||
helpers: RepositoryHelpers; | ||
extensions: SavedObjectsExtensions; | ||
client: RepositoryEsClient; | ||
allowedTypes: string[]; | ||
serializer: ISavedObjectsSerializer; | ||
migrator: IKibanaMigrator; | ||
logger: Logger; | ||
mappings: IndexMapping; | ||
} |
There was a problem hiding this comment.
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 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔥
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😍
There was a problem hiding this 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, |
There was a problem hiding this comment.
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. | ||
*/ | ||
|
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'; | |||
|
|||
/** |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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....
💚 Build Succeeded
Metrics [docs]Public APIs missing comments
Unknown metric groupsAPI count
ESLint disabled line counts
Total ESLint disabled count
History
To update your PR or re-run it, just comment with: |
## 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]>
## 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]>
## 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]>
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.gupdateObjectsSpaces
)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:
This separation allows a better isolation, testability, readability and therefore maintainability overall.
Structure
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.