-
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
Add SavedObjectsClient.bulkResolve #112025
Add SavedObjectsClient.bulkResolve #112025
Conversation
In abstracting this out to a separate module, I was able to reuse this instead of the existing resolve code, and I was able to simplify the unit tests. Note, there is currently an inconsistent API response between get and bulkGet (an unsupported type for the former returns 404, and the latter returns 400). I opted to return a 400 for bulkResolveInternal as that is the most consistent response, so that means I had to change the integration tests for resolve accordingly.
ba1b64b
to
cc26c0a
Compare
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.
Author's notes for reviewers
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.NOT_FOUND); | ||
return { | ||
type, | ||
id, | ||
error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, 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.
(Part 1 of 3)
I'm not thrilled with this, but I view it as an acceptable compromise.
Here is the current behavior of get
vs resolve
:
- get
- found: returns
{ type, id, attributes, references, ... }
- not found: throws error
- found: returns
- bulkGet
- found: returns
[{ type, id, attributes, references, ... }]
- not found: returns
[{ type, id, error }]
- found: returns
- resolve
- found: returns
{ saved_object: { type, id, attributes, references, ... }, outcome }
- not found: throws error
- found: returns
- bulkResolve - what to do about this? (see other comment below)
Note that the SavedObject
type requires the attributes
and references
fields and has an optional error
field that is only used for bulk responses. But the current behavior of bulkGet
(and the other bulk*
methods) doesn't return a valid SavedObject
type, because only the type
, id
, and error
fields are present, because the SOR
uses any
under the hood.
When writing the internalBulkResolve
module, I wanted to be able to reuse it for both SOR.resolve
and SOR.bulkResolve
-- that meant that I needed to return the actual DecoratedError
object so that the SOR.resolve
method could find it and throw the 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.
When writing the internalBulkResolve module, I wanted to be able to reuse it for both SOR.resolve and SOR.bulkResolve
That makes sense
that meant that I needed to return the actual DecoratedError object so that the SOR.resolve method could find it and throw the error.
Can't we just use a higher level of abstraction by having internalBulkResolve
returning the type of the error and letting the resolve
and bulkResolve
callers reformat/wrap 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.
Can't we just use a higher level of abstraction by having
internalBulkResolve
returning the type of the error and letting theresolve
andbulkResolve
callers reformat/wrap them?
Well resolve
needs to throw a DecoratedError
, while bulkResolve
needs to extract the payload.output
field from it. So I figured the least common denominator is to have the internalBulkResolve
return the DecoratedError
itself.
If you think it's better to return the error type instead that should be fine and I can make that change (we really just have two types, "unsupported" and "not found"). I'll wait til you and Rudolf get a chance to reply before doing anything else though.
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.
From a pure design standpoint, returning a real Error
(or subclass) instance as an attribute of a return value is imho bad practice. Now, honestly, if that answers our current need, that's very fine with me.
const { resolved_objects: bulkResults } = await internalBulkResolve<T>({ | ||
registry: this._registry, | ||
allowedTypes: this._allowedTypes, | ||
client: this.client, | ||
serializer: this._serializer, | ||
getIndexForType: this.getIndexForType.bind(this), | ||
incrementCounterInternal: this.incrementCounterInternal.bind(this), | ||
objects: [{ type, id }], | ||
options, | ||
}); | ||
const [result] = bulkResults; | ||
if ((result as InternalBulkResolveError).error) { | ||
throw (result as InternalBulkResolveError).error; | ||
} | ||
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); | ||
return result as SavedObjectsResolveResponse<T>; |
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.
(Part 2 of 3)
In this section, the SOR.resolve
method 1. calls internalBulkResolve
, 2. checks to see if there was an error (throwing if need be), and 3. if there was no error, returns the result, casting it as a SavedObjectsResolveResponse
type. This matches the existing behavior.
const { resolved_objects: bulkResults } = await internalBulkResolve<T>({ | ||
registry: this._registry, | ||
allowedTypes: this._allowedTypes, | ||
client: this.client, | ||
serializer: this._serializer, | ||
getIndexForType: this.getIndexForType.bind(this), | ||
incrementCounterInternal: this.incrementCounterInternal.bind(this), | ||
objects, | ||
options, | ||
}); | ||
const resolvedObjects = bulkResults.map<SavedObjectsResolveResponse<T>>((result) => { | ||
// extract payloads from saved object errors | ||
if ((result as InternalBulkResolveError).error) { | ||
const errorResult = result as InternalBulkResolveError; | ||
const { type, id, error } = errorResult; | ||
return { | ||
saved_object: ({ type, id, error: errorContent(error) } as unknown) as SavedObject<T>, | ||
outcome: 'exactMatch', | ||
}; | ||
} | ||
return result as SavedObjectsResolveResponse<T>; | ||
}); | ||
return { resolved_objects: resolvedObjects }; |
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.
(Part 3 of 3)
In this section, the SOR.bulkResolve
method 1. calls internalBulkResolve
, 2. iterates over the results, extracting error content if need be and casting it as a SavedObjectsResolveResponse
type, and 3. returns the result (which is now a valid SavedObjectsBulkResolveResponse
type).
This means that any bulkResolve errors will have the shape:
{ saved_object: { type, id, error }, outcome: 'exactMatch' }
I can see two other alternatives to this behavior:
- Add a special 'error' outcome that will only be used for
bulkResolve
responses - Omit the
outcome
field entirely for errors (casting this to aSavedObjectsResolveResponse
type anyway)
The current approach is crappy, but it seems the least crappy because of the way that existing consumers on the client side use SOC.get
; that API actually batches those calls into a single bulkGet
on behalf of the consumer, and each consumer has some way of checking if the call succeeded (either by checking to see if an expected saved object field is present [positive test], or by checking to see if the error field is present [negative test]).
I'm a bit worried that if we return an unexpected outcome, or if we don't return an outcome at all, then the impact on consumer code will be worse.
I'm curious to see what the Core team thinks of this, or if there are any other suggestions.
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 don't really like the exactMatch
outcome for errors, as it imho doesn't make a lot of sense. I would have expected something like error
or unknown
.
But that's probably not that significant, so if you think this is better and easier to handle for the client-side consumers, I' fine keeping it that way. We probably want to document this behavior in the API's tsdoc though (at least on the server-side).
{ ...CASES.HIDDEN, ...fail404() }, | ||
{ ...CASES.HIDDEN, ...fail400() }, |
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.
Changed internalBulkResolve
method to return an UnsupportedObjectType error instead of a NotFound error. See issue description; resolve
was originally modeled after get
, but there is a discrepancy between how get
and bulkGet
handle unsupported types. I figured that it made more sense for resolve
and bulkResolve
to both return an UnsupportedObjectType error.
ack: will review today/tomorrow |
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.
LGTM. A few remarks and questions, but overall nothing really significant.
Gave my opinion about the outcome
issue for bulkResolve
(see my comment), but I think it's probably fine either way.
...ugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts
Show resolved
Hide resolved
interface Right<R> { | ||
tag: 'Right'; | ||
value: R; | ||
} | ||
type Either<L = unknown, R = L> = Left<L> | Right<R>; | ||
const isLeft = <L, R>(either: Either<L, R>): either is Left<L> => either.tag === 'Left'; | ||
const isRight = <L, R>(either: Either<L, R>): either is Right<R> => either.tag === 'Right'; |
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.
We really need to extract this out of the repository file to avoid such duplication
resolveCounter.recordOutcome(REPOSITORY_RESOLVE_OUTCOME_STATS.NOT_FOUND); | ||
return { | ||
type, | ||
id, | ||
error: SavedObjectsErrorHelpers.createGenericNotFoundError(type, 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.
When writing the internalBulkResolve module, I wanted to be able to reuse it for both SOR.resolve and SOR.bulkResolve
That makes sense
that meant that I needed to return the actual DecoratedError object so that the SOR.resolve method could find it and throw the error.
Can't we just use a higher level of abstraction by having internalBulkResolve
returning the type of the error and letting the resolve
and bulkResolve
callers reformat/wrap them?
src/core/server/saved_objects/service/lib/internal_bulk_resolve.ts
Outdated
Show resolved
Hide resolved
const { resolved_objects: bulkResults } = await internalBulkResolve<T>({ | ||
registry: this._registry, | ||
allowedTypes: this._allowedTypes, | ||
client: this.client, | ||
serializer: this._serializer, | ||
getIndexForType: this.getIndexForType.bind(this), | ||
incrementCounterInternal: this.incrementCounterInternal.bind(this), | ||
objects, | ||
options, | ||
}); | ||
const resolvedObjects = bulkResults.map<SavedObjectsResolveResponse<T>>((result) => { | ||
// extract payloads from saved object errors | ||
if ((result as InternalBulkResolveError).error) { | ||
const errorResult = result as InternalBulkResolveError; | ||
const { type, id, error } = errorResult; | ||
return { | ||
saved_object: ({ type, id, error: errorContent(error) } as unknown) as SavedObject<T>, | ||
outcome: 'exactMatch', | ||
}; | ||
} | ||
return result as SavedObjectsResolveResponse<T>; | ||
}); | ||
return { resolved_objects: resolvedObjects }; |
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 don't really like the exactMatch
outcome for errors, as it imho doesn't make a lot of sense. I would have expected something like error
or unknown
.
But that's probably not that significant, so if you think this is better and easier to handle for the client-side consumers, I' fine keeping it that way. We probably want to document this behavior in the API's tsdoc though (at least on the server-side).
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.
Approving a LGTM is always better.
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.
Haven't tested locally yet, will do so before approving. No red flags, just some nits and questions from me below
* @internal | ||
*/ | ||
export function getCurrentTime() { | ||
return new Date(Date.now()).toISOString(); |
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 old implementation was return new Date().toISOString();
. What is the reason for this change?
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 only reason is so that I can easily mock the output in unit tests.
x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts
Show resolved
Hide resolved
x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts
Outdated
Show resolved
Hide resolved
expect(resolvedObject.alias_target_id).to.eql(undefined); | ||
} | ||
} | ||
// TODO: add assertions for redacted namespaces (this already exists in the bulkCreate test suite) |
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.
Is this something we want to do in this PR, or should we open an issue to track this work? If the latter, can we add a link to the issue here?
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.
Good point, I'll open an issue for follow-on work and link it here.
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.
Created an issue: #112455
Unrelated to this PR but I noticed that this needed to be changed.
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.
LGTM on green CI!
💚 Build Succeeded
Metrics [docs]Public APIs missing comments
Page load bundle
Unknown metric groupsAPI count
References to deprecated APIs
History
To update your PR or re-run it, just comment with: |
💚 Backport successful
This backport PR will be merged automatically after passing CI. |
* Add SavedObjectsClient.bulkResolve (#112025) * Fix type error Co-authored-by: Joe Portner <[email protected]>
Resolves #111201.
Testing
In addition to the included automated tests, some manual testing steps:
You should see a single
_bulk_resolve
API call that contains manylens
objects to populate the dashboard.Notes
In the saved objects core code, I added an
internalBulkResolve
module. Then I replaced the existingSavedObjectsRepository.resolve
method implementation with that, and I also used it to add a newbulkResolve
method. So nowresolve
andbulkResolve
use the same implementation, reducing our testing burden and reducing the size of the SOR itself.See also the commit message from 1e61891: