-
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
Introduce the SO versionModel API #150149
Introduce the SO versionModel API #150149
Conversation
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 type { | ||
SavedObjectsModelVersion, | ||
SavedObjectsModelVersionMap, |
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.
So, I used a SavedObjectsModel
prefix for all the model / model version types. It leads to quite long names, but I couldn't find something sorted that stayed isolated and explicit...
If anyone has a better idea, I'll gladly take it.
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 prefer this long name
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 agree, using a descriptive name is great!
import type { SavedObjectsMigrationLogger } from '../migration'; | ||
|
||
// alias to more easily adapt later | ||
export type SavedObjectModelMigrationDoc<T = unknown> = SavedObjectSanitizedDoc<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.
We may need later to use specific document types for the purpose of the lossless migration, depending on how we decide to propagate the migration state to the migration functions, so I aliased this to be future-proof.
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.
Sounds good. We already use the somethingDoc type-name strategy, so at least it's consistent with that.
* | ||
* @public | ||
*/ | ||
export interface SavedObjectModelMigrationContext { |
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.
Re-used the concept of context
that was already used for the current migration system, but I created a distinct type for proper isolation, given the two will evolve differently. Also only added the properties that are really useful, so it was an opportunity to cleanup the thing.
export interface SavedObjectModelMigrationResult<DocAttrs = unknown> { | ||
document: SavedObjectModelMigrationDoc<DocAttrs>; | ||
} |
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.
Instead of having the migration functions only returning the migrated document, I introduced a result
type containing it.
This adds a lot of flexibility regarding how we will be able to make the API evolve.
For example, we could imagine adding a polymorphic return type with a type
to identify different migration outcomes, or support mutating the lossless migration state by returning the mutated version.
Overall, I'm just taking this opportunity to cleanup and improve the old migration APIs / types.
export type SavedObjectModelMigrationFn<InputAttributes = unknown, OutputAttributes = unknown> = ( | ||
document: SavedObjectModelMigrationDoc<InputAttributes>, | ||
context: SavedObjectModelMigrationContext | ||
) => SavedObjectModelMigrationResult<OutputAttributes>; |
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.
Not much to say here, very similar to the existing migration functions, only using the new types for input and output.
export interface SavedObjectModelBidirectionalMigration< | ||
PreviousAttributes = unknown, | ||
NewAttributes = unknown | ||
> { | ||
/** | ||
* The upward (old=>new) migration. | ||
*/ | ||
up: SavedObjectModelMigrationFn<PreviousAttributes, NewAttributes>; | ||
/** | ||
* The downward (new=>old) migration. | ||
*/ | ||
down: SavedObjectModelMigrationFn<NewAttributes, PreviousAttributes>; |
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 bidirectional migration structure.
Are we fine using up
and down
as the property names, or do we want something more explicit?
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'd be +1 for up/down as this is quite common in other frameworks
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'm ++ with up
and down
. My only nit is in the comment:
- Instead of
old=>new
, should we use something likeprevious => current
- Instead of
new=>old
, should we use something likecurrent => previous
I'm not fully convinced with my suggestions either, but I'm sure we can even find better wording here to provide a clear mental model that this applies to "the current migration jump"...
My mind is at: the user upgrades 2 versions in a go (v1 -> v2 -> v3). IIUC, when running the migrations that apply to v2
, new
is v2
and not the actual final version of the distributable that it's running v3
.
Disclaimer: it's a super-nit and shouldn't block this PR.
WDYT?
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.
++ to up
and down
as property names. As for the type and comments, wouldn't previous => next
and next => previous
work?
Naming's hard! These are nits though and TBH, I don't believe make much difference in the long term.
/** | ||
* Represents a model version of a given savedObjects type. | ||
* | ||
* Model versions supersede the {@link SavedObjectsType.migrations | migrations} (and {@link SavedObjectsType.schemas | schemas}) APIs | ||
* by exposing an unified way of describing the changes of shape or data of a type. | ||
* | ||
* @public | ||
*/ | ||
export interface SavedObjectsModelVersion { | ||
/** | ||
* The {@link SavedObjectsModelChange | changes} associated with this version. | ||
*/ | ||
modelChange: SavedObjectsModelChange; | ||
} |
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.
So, compared to the design in the doc, i decided to dissociate the model version
and the model change
. Some properties, not implemented yet, such as the schema validation for create/update, will be part of the model version, where the things related to the upgrade are contained within the model 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.
At first, I didn't quite get what the modelChange
represents after reading the type description comments. Then seeing the type definition made it clear.
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.
wouldn't validation have to be modelVersion specific? In next
a field might be required but in prev
it wasn't and even before we introduce contract you might still want to reject any values from being written into deprecated fields.
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.
oh I realise it would still be part of the model version... it's one level up, not two levels up.
/** | ||
* {@link SavedObjectsModelChange | model change} representing an expansion. | ||
* | ||
* A model expansion can do either, or both, or those: | ||
* - add new mappings | ||
* - migrate data in a backward-compatible way | ||
* | ||
* @remark when adding mappings, {@link SavedObjectsType.mappings | the type mappings} must also be updated accordingly. | ||
* Overall, the type's mapping represents the latests version of the mappings, where the model changes | ||
* represent the changes of mappings between two versions. | ||
* | ||
* @public | ||
*/ | ||
export interface SavedObjectsModelExpansionChange< | ||
PreviousAttributes = unknown, | ||
NewAttributes = unknown | ||
> { | ||
/** | ||
* The type of {@link SavedObjectsModelChange | change}, used to identify them internally. | ||
*/ | ||
type: 'expansion'; | ||
/** | ||
* (optional) The new mappings introduced in this version. | ||
*/ | ||
addedMappings?: SavedObjectsMappingProperties; | ||
/** | ||
* (optional) A bidirectional migration to migrate the data from and/or to the previous model version. | ||
*/ | ||
migration?: SavedObjectModelBidirectionalMigration<PreviousAttributes, NewAttributes>; | ||
} |
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 represents an expand
type change. As defined in the design doc, expand migration supports either, or both, of two actions: adding compatible mappings, and/or registering a bidirectional migration to migrate the data between the versions.
* @remarks All types will be forced to switch to use the new API in a later version. This switch is | ||
* allowing types owners to switch their types before the milestone. | ||
*/ | ||
switchToModelVersionAfter?: string; |
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 types will be forced to switch to the new API after a given version.
However, we will allow type owners to opt-in early to the new system via this switchToModelVersionAfter
property.
The main use case of manually opt-in to the model version API will be for testing: we will need to be able to define our types as using the new system for our different layers of tests.
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.
Since swithToModelVersionAfter
actually means changing to the new API, how about using switchToModelVersionMigrationStrategyAfter
rather? It's a much longer name but does clearly distinguish what the property refers to.
* | ||
* @alpha experimental and subject to change. | ||
*/ | ||
modelVersions?: SavedObjectsModelVersionMap | SavedObjectsModelVersionMapProvider; |
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 modelVersion API, as exposed on the SavedObjectsType
type.
Are we fine with using a map based definition as we did for the old migration registration? It's kinda inelegant given versions are only integers, but I couldn't find a better idea.
We could imagine using a list with the version number being included in the entries, but I wasn't convinced if was necessarily better, especially given internally we'll likely have to convert it to a map anyway?
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 like that the map guarantees unique keys
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.
A map is fine. It may not be elegant but it works for now.
Pinging @elastic/kibana-core (Team:Core) |
export type { | ||
SavedObjectsModelVersion, | ||
SavedObjectsModelVersionMap, |
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 prefer this long name
packages/core/test-helpers/core-test-helpers-so-type-serializer/src/get_migration_hash.ts
Show resolved
Hide resolved
export interface SavedObjectModelBidirectionalMigration< | ||
PreviousAttributes = unknown, | ||
NewAttributes = unknown | ||
> { | ||
/** | ||
* The upward (old=>new) migration. | ||
*/ | ||
up: SavedObjectModelMigrationFn<PreviousAttributes, NewAttributes>; | ||
/** | ||
* The downward (new=>old) migration. | ||
*/ | ||
down: SavedObjectModelMigrationFn<NewAttributes, PreviousAttributes>; |
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'm ++ with up
and down
. My only nit is in the comment:
- Instead of
old=>new
, should we use something likeprevious => current
- Instead of
new=>old
, should we use something likecurrent => previous
I'm not fully convinced with my suggestions either, but I'm sure we can even find better wording here to provide a clear mental model that this applies to "the current migration jump"...
My mind is at: the user upgrades 2 versions in a go (v1 -> v2 -> v3). IIUC, when running the migrations that apply to v2
, new
is v2
and not the actual final version of the distributable that it's running v3
.
Disclaimer: it's a super-nit and shouldn't block this PR.
WDYT?
* { | ||
* name: 'foo', | ||
* // other mandatory attributes... | ||
* switchToModelVersionAt: '8.8.0', |
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.
* switchToModelVersionAt: '8.8.0', | |
* switchToModelVersionAfter: '8.8.0', |
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 prefer switchToModelVersionAt
over using after
. To me, "after" is ambiguous, I don't know if the change should be in 8.9 or 8.8.1 or at the version given.
Besides, there's already a property for swithToModelVersionAfter
.
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, you're probably right, technically at
is more correct here.
@@ -158,4 +158,108 @@ describe('extractMigrationInfo', () => { | |||
}); | |||
}); | |||
}); | |||
|
|||
describe('modelVersions', () => { |
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: should we add a test for a mixed migration (legacy and new one)?
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.
a mixed migration
Is that even supported? How would that work? I can't imagine supporting BWC in that scenario would be easy.
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 that even supported?
Yeah it is supported, as we need it for on-prem (see #150301). It's just that you won't be able to down-migrate lower than model versions, but API consumers won't be able to ask for such operation anyway.
should we add a test for a mixed migration (legacy and new one)?
Yeah, probably worth it, will add
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 only added a few comments.
The types look great! Nice work!
* | ||
* @public | ||
*/ | ||
export type SavedObjectsModelChange = SavedObjectsModelExpansionChange; |
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 will one day add to this list, hopefully with a SavedObjectsModelReductionChange
🤞 . Should we bite the bullet and introduce a savedObjectModelChange
option from the get-go?
Something like:
export type SavedObjectsModelChange = SavedObjectsModelChangeOption.SavedObjectsModelExpansionChange;
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 could use an enum (if that's what you mean by option
) for the type
attribute, but not for the polymorphic TS type, AFAIK
* @example | ||
* ```typescript | ||
* const modelVersionMap: SavedObjectsModelVersionMap = { | ||
* '1': modelVersion1, |
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 missed where we decided using integers was the best approach. Sure, it doesn't leave wiggle room to squeeze a xx.5 in sometime in the future that could spiral but how would we easily track when and why model versions changed?
OTOH, nothing's preventing folks from documenting changes to a SO model either, so it doesn't really matter.
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's no minor/patch versions because model migrations are decoupled from the stack version, and just executed in order as soon as they're deployed. Patch versions wouldn't make any sense, given you wouldn't be able to run a patch from a prior version anyway.
The only use case I see for 'minor' model version would be introductions that aren't considered changes to the model. Could be mapping removal, or data bug fixes (bi-migrations for which the down one is a no-op).
We could support that eventually later by allowing minor versions (e.g 1.1: modelVersion11
), but I don't want to focus on that until we have a better vision on how frequent it could be used.
Ihmo starting with only 'majors' would be fine
* | ||
* @alpha experimental and subject to change. | ||
*/ | ||
modelVersions?: SavedObjectsModelVersionMap | SavedObjectsModelVersionMapProvider; |
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.
A map is fine. It may not be elegant but it works for now.
* { | ||
* name: 'foo', | ||
* // other mandatory attributes... | ||
* switchToModelVersionAt: '8.8.0', |
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 prefer switchToModelVersionAt
over using after
. To me, "after" is ambiguous, I don't know if the change should be in 8.9 or 8.8.1 or at the version given.
Besides, there's already a property for swithToModelVersionAfter
.
* @remarks All types will be forced to switch to use the new API in a later version. This switch is | ||
* allowing types owners to switch their types before the milestone. | ||
*/ | ||
switchToModelVersionAfter?: string; |
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.
Since swithToModelVersionAfter
actually means changing to the new API, how about using switchToModelVersionMigrationStrategyAfter
rather? It's a much longer name but does clearly distinguish what the property refers to.
@@ -158,4 +158,108 @@ describe('extractMigrationInfo', () => { | |||
}); | |||
}); | |||
}); | |||
|
|||
describe('modelVersions', () => { |
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.
a mixed migration
Is that even supported? How would that work? I can't imagine supporting BWC in that scenario would be easy.
export interface SavedObjectModelBidirectionalMigration< | ||
PreviousAttributes = unknown, | ||
NewAttributes = unknown | ||
> { | ||
/** | ||
* The upward (old=>new) migration. | ||
*/ | ||
up: SavedObjectModelMigrationFn<PreviousAttributes, NewAttributes>; | ||
/** | ||
* The downward (new=>old) migration. | ||
*/ | ||
down: SavedObjectModelMigrationFn<NewAttributes, PreviousAttributes>; |
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'd be +1 for up/down as this is quite common in other frameworks
* | ||
* @alpha experimental and subject to change. | ||
*/ | ||
modelVersions?: SavedObjectsModelVersionMap | SavedObjectsModelVersionMapProvider; |
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 like that the map guarantees unique keys
* | ||
* @public | ||
*/ | ||
export type SavedObjectModelMigrationFn<InputAttributes = unknown, OutputAttributes = unknown> = ( |
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 use "migration" in a lot of places and it means different things. A "migration" could also mean the whole process of going from one modelVersion to the next (mappings + transforms). What you think about calling these transformation functions like SavedObjectModelTransformationFn
/** | ||
* Represents a model version of a given savedObjects type. | ||
* | ||
* Model versions supersede the {@link SavedObjectsType.migrations | migrations} (and {@link SavedObjectsType.schemas | schemas}) APIs | ||
* by exposing an unified way of describing the changes of shape or data of a type. | ||
* | ||
* @public | ||
*/ | ||
export interface SavedObjectsModelVersion { | ||
/** | ||
* The {@link SavedObjectsModelChange | changes} associated with this version. | ||
*/ | ||
modelChange: SavedObjectsModelChange; | ||
} |
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.
wouldn't validation have to be modelVersion specific? In next
a field might be required but in prev
it wasn't and even before we introduce contract you might still want to reject any values from being written into deprecated fields.
💚 Build Succeeded
Metrics [docs]Public APIs missing comments
Public APIs missing exports
Unknown metric groupsAPI count
History
To update your PR or re-run it, just comment with: cc @pgayvallet |
Summary
Fix #150297
First step toward managed upgrades
This PR adds the types that will be used for the new SO model version API, and adds the new properties to the
SavedObjectsType
type.The implementation is outside of scope of the PR and will be implemented in a future PR.
The PR also adapt the
check_registered_types
test to trigger a review when the attributes ofSavedObjectsType
introduced in this PR are changed.