[[saved-objects-service]] == Saved Objects service NOTE: The Saved Objects service is available both server and client side. `Saved Objects service` allows {kib} plugins to use {es} like a primary database. Think of it as an Object Document Mapper for {es}. Once a plugin has registered one or more Saved Object types, the Saved Objects client can be used to query or perform create, read, update and delete operations on each type. By using Saved Objects your plugin can take advantage of the following features: * Migrations can evolve your document's schema by transforming documents and ensuring that the field mappings on the index are always up to date. * a <> is automatically exposed for each type (unless `hidden=true` is specified). * a Saved Objects client that can be used from both the server and the browser. * Users can import or export Saved Objects using the Saved Objects management UI or the Saved Objects import/export API. * By declaring `references`, an object's entire reference graph will be exported. This makes it easy for users to export e.g. a `dashboard` object and have all the `visualization` objects required to display the dashboard included in the export. * When the X-Pack security and spaces plugins are enabled these transparently provide RBAC access control and the ability to organize Saved Objects into spaces. This document contains developer guidelines and best-practices for plugins wanting to use Saved Objects. === Server side usage [[saved-objects-type-registration]] ==== Registering a Saved Object type Saved object type definitions should be defined in their own `my_plugin/server/saved_objects` directory. The folder should contain a file per type, named after the snake_case name of the type, and an `index.ts` file exporting all the types. .src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts [source,typescript] ---- import { SavedObjectsType } from 'src/core/server'; export const dashboardVisualization: SavedObjectsType = { name: 'dashboard_visualization', // <1> hidden: true, namespaceType: 'multiple-isolated', // <2> switchToModelVersionAt: '8.10.0', modelVersions: { 1: modelVersion1, 2: modelVersion2, }, mappings: { dynamic: false, properties: { description: { type: 'text', }, hits: { type: 'integer', }, }, }, // ...other mandatory properties }; ---- <1> Since the name of a Saved Object type may form part of the URL path for the public Saved Objects HTTP API, these should follow our API URL path convention and always be written in snake case. <2> This field determines "space behavior" -- whether these objects can exist in one space, multiple spaces, or all spaces. This value means that objects of this type can only exist in a single space. See <> for more information. .src/plugins/my_plugin/server/saved_objects/index.ts [source,typescript] ---- export { dashboardVisualization } from './dashboard_visualization'; export { dashboard } from './dashboard'; ---- .src/plugins/my_plugin/server/plugin.ts [source,typescript] ---- import { dashboard, dashboardVisualization } from './saved_objects'; export class MyPlugin implements Plugin { setup({ savedObjects }) { savedObjects.registerType(dashboard); savedObjects.registerType(dashboardVisualization); } } ---- ==== Mappings Each Saved Object type can define it's own {es} field mappings. Because multiple Saved Object types can share the same index, mappings defined by a type will be nested under a top-level field that matches the type name. For example, the mappings defined by the `search` Saved Object type: https://github.com/elastic/kibana/blob/main/src/platform/plugins/shared/saved_search/server/saved_objects/search.ts#L19-L70[.src/platform/plugins/shared/saved_search/server/saved_objects/search.ts] [source,typescript] ---- import { SavedObjectsType } from 'src/core/server'; // ... other imports export function getSavedSearchObjectType: SavedObjectsType = { // <1> name: 'search', hidden: false, namespaceType: 'multiple-isolated', mappings: { dynamic: false, properties: { title: { type: 'text' }, description: { type: 'text' }, }, }, modelVersions: { ... }, // ...other optional properties }; ---- <1> Simplification Will result in the following mappings being applied to the `.kibana_analytics` index: [source,json] ---- { "mappings": { "dynamic": "strict", "properties": { ... "search": { "dynamic": false, "properties": { "title": { "type": "text", }, "description": { "type": "text", }, }, } } } } ---- Do not use field mappings like you would use data types for the columns of a SQL database. Instead, field mappings are analogous to a SQL index. Only specify field mappings for the fields you wish to search on or query. By specifying `dynamic: false` in any level of your mappings, {es} will accept and store any other fields even if they are not specified in your mappings. Since {es} has a default limit of 1000 fields per index, plugins should carefully consider the fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the `.kibana` index. [[saved-objects-service-writing-migrations]] ==== Writing Migrations by defining model versions Saved Objects support changes using `modelVersions``. The modelVersion API is a new way to define transformations (_``migrations''_) for your savedObject types, and will replace the ``legacy'' migration API after Kibana version `8.10.0`. The legacy migration API has been deprecated, meaning it is no longer possible to register migrations using the legacy system. Model versions are decoupled from the stack version and satisfy the requirements for zero downtime and backward-compatibility. Each Saved Object type may define model versions for its schema and are bound to a given https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/src/core/packages/saved-objects/server/src/saved_objects_type.ts#L22-L27[savedObject type]. Changes to a saved object type are specified by defining a new model. === Defining model versions As for old migrations, model versions are bound to a given https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/src/core/packages/saved-objects/server/src/saved_objects_type.ts#L22-L27[savedObject type] When registering a SO type, a new https://github.com/elastic/kibana/blob/9a6a2ccdff619f827b31c40dd9ed30cb27203da7/src/core/packages/saved-objects/server/src/saved_objects_type.ts#L138-L177[modelVersions] property is available. This attribute is a map of https://github.com/elastic/kibana/blob/9a6a2ccdff619f827b31c40dd9ed30cb27203da7/src/core/packages/saved-objects/server/src/model_version/model_version.ts#L12-L20[SavedObjectsModelVersion] which is the top-level type/container to define model versions. This map follows a similar `{ [version number] => version definition }` format as the old migration map, however a given SO type’s model version is now identified by a single integer. The first version must be numbered as version 1, incrementing by one for each new version. That way: - SO type versions are decoupled from stack versioning - SO type versions are independent between types _a *valid* version numbering:_ [source,ts] ---- const myType: SavedObjectsType = { name: 'test', switchToModelVersionAt: '8.10.0', modelVersions: { 1: modelVersion1, // valid: start with version 1 2: modelVersion2, // valid: no gap between versions }, // ...other mandatory properties }; ---- _an *invalid* version numbering:_ [source,ts] ---- const myType: SavedObjectsType = { name: 'test', switchToModelVersionAt: '8.10.0', modelVersions: { 2: modelVersion2, // invalid: first version must be 1 4: modelVersion3, // invalid: skipped version 3 }, // ...other mandatory properties }; ---- === Structure of a model version https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/src/core/packages/saved-objects/server/src/model_version/model_version.ts#L12-L20[Model versions] are not just functions as the previous migrations were, but structured objects describing how the version behaves and what changed since the last one. _A base example of what a model version can look like:_ [source,ts] ---- const myType: SavedObjectsType = { name: 'test', switchToModelVersionAt: '8.10.0', modelVersions: { 1: { changes: [ { type: 'mappings_addition', addedMappings: { someNewField: { type: 'text' }, }, }, { type: 'data_backfill', transform: someBackfillFunction, }, ], schemas: { forwardCompatibility: fcSchema, create: createSchema, }, }, }, // ...other mandatory properties }; ---- *Note:* Having multiple changes of the same type for a given version is supported by design to allow merging different sources (to prepare for an eventual higher-level API) _This definition would be perfectly valid:_ [source,ts] ---- const version1: SavedObjectsModelVersion = { changes: [ { type: 'mappings_addition', addedMappings: { someNewField: { type: 'text' }, }, }, { type: 'mappings_addition', addedMappings: { anotherNewField: { type: 'text' }, }, }, ], }; ---- It’s currently composed of two main properties: ==== changes https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/src/core/packages/saved-objects/server/src/model_version/model_version.ts#L21-L51[link to the TS doc for `changes`] Describes the list of changes applied during this version. *Important:* This is the part that replaces the old migration system, and allows defining when a version adds new mapping, mutates the documents, or other type-related changes. The current types of changes are: ===== - mappings_addition Used to define new mappings introduced in a given version. _Usage example:_ [source,ts] ---- const change: SavedObjectsModelMappingsAdditionChange = { type: 'mappings_addition', addedMappings: { newField: { type: 'text' }, existingNestedField: { properties: { newNestedProp: { type: 'keyword' }, }, }, }, }; ---- *note:* _When adding mappings, the root `type.mappings` must also be updated accordingly (as it was done previously)._ ===== - mappings_deprecation Used to flag mappings as no longer being used and ready to be removed. _Usage example:_ [source,ts] ---- let change: SavedObjectsModelMappingsDeprecationChange = { type: 'mappings_deprecation', deprecatedMappings: ['someDeprecatedField', 'someNested.deprecatedField'], }; ---- *note:* _It is currently not possible to remove fields from an existing index’s mapping (without reindexing into another index), so the mappings flagged with this change type won’t be deleted for now, but this should still be used to allow our system to clean the mappings once upstream (ES) unblock us._ ===== - data_backfill Used to populate fields (indexed or not) added in the same version. _Usage example:_ [source,ts] ---- let change: SavedObjectsModelDataBackfillChange = { type: 'data_backfill', transform: (document) => { return { attributes: { someAddedField: 'defaultValue' } }; }, }; ---- *note:* _Even if no check is performed to ensure it, this type of model change should only be used to backfill newly introduced fields._ ===== - data_removal Used to remove data (unset fields) from all documents of the type. _Usage example:_ [source,ts] ---- let change: SavedObjectsModelDataRemovalChange = { type: 'data_removal', attributePaths: ['someRootAttributes', 'some.nested.attribute'], }; ---- *note:* _Due to backward compatibility, field utilization must be stopped in a prior release before actual data removal (in case of rollback). Please refer to the field removal migration example below in this document_ ===== - unsafe_transform Used to execute an arbitrary transformation function. _Usage example:_ [source,ts] ---- let change: SavedObjectsModelUnsafeTransformChange = { type: 'unsafe_transform', transformFn: (document) => { document.attributes.someAddedField = 'defaultValue'; return { document }; }, }; ---- *note:* _Using such transformations is potentially unsafe, given the migration system will have no knowledge of which kind of operations will effectively be executed against the documents. Those should only be used when there’s no other way to cover one’s migration needs._ *Please reach out to the development team if you think you need to use this, as you theoretically shouldn’t.* ==== schemas https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/src/core/packages/saved-objects/server/src/model_version/schemas.ts#L11-L16[link to the TS doc for `schemas`] The schemas associated with this version. Schemas are used to validate or convert SO documents at various stages of their lifecycle. The currently available schemas are: ===== forwardCompatibility This is a new concept introduced by model versions. This schema is used for inter-version compatibility. When retrieving a savedObject document from an index, if the version of the document is higher than the latest version known of the Kibana instance, the document will go through the `forwardCompatibility` schema of the associated model version. *Important:* These conversion mechanism shouldn’t assert the data itself, and only strip unknown fields to convert the document to the *shape* of the document at the given version. Basically, this schema should keep all the known fields of a given version, and remove all the unknown fields, without throwing. Forward compatibility schema can be implemented in two different ways. [arabic] . Using `config-schema` _Example of schema for a version having two fields: someField and anotherField_ [source,ts] ---- const versionSchema = schema.object( { someField: schema.maybe(schema.string()), anotherField: schema.maybe(schema.string()), }, { unknowns: 'ignore' } ); ---- *Important:* Note the `{ unknowns: 'ignore' }` in the schema’s options. This is required when using `config-schema` based schemas, as this what will evict the additional fields without throwing an error. [arabic, start=2] . Using a plain javascript function _Example of schema for a version having two fields: someField and anotherField_ [source,ts] ---- const versionSchema: SavedObjectModelVersionEvictionFn = (attributes) => { const knownFields = ['someField', 'anotherField']; return pick(attributes, knownFields); } ---- *note:* _Even if highly recommended, implementing this schema is not strictly required. Type owners can manage unknown fields and inter-version compatibility themselves in their service layer instead._ ===== create This is a direct replacement for https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/src/core/packages/saved-objects/server/src/saved_objects_type.ts#L75-L82[the old SavedObjectType.schemas] definition, now directly included in the model version definition. As a refresher the `create` schema is a `@kbn/config-schema` object-type schema, and is used to validate the properties of the document during `create` and `bulkCreate` operations. *note:* _Implementing this schema is optional, but still recommended, as otherwise there will be no validating when importing objects_ For implementation examples, refer to <>. include::saved-objects-service-use-case-examples.asciidoc[leveloffset=+1]