保存对象服务

编辑

保存对象服务在服务器端和客户端都可用。

保存对象服务允许 Kibana 插件像使用主数据库一样使用 Elasticsearch。可以将其视为 Elasticsearch 的对象文档映射器。一旦插件注册了一个或多个保存对象类型,就可以使用保存对象客户端对每种类型执行查询或创建、读取、更新和删除操作。

通过使用保存对象,您的插件可以利用以下功能

  • 迁移可以通过转换文档并确保索引上的字段映射始终是最新的来发展您的文档模式。
  • 每个类型都会自动公开一个 HTTP API(除非指定 hidden=true)。
  • 一个可以从服务器和浏览器使用的保存对象客户端。
  • 用户可以使用保存对象管理 UI 或保存对象导入/导出 API 导入或导出保存对象。
  • 通过声明 references,将导出对象的整个引用图。这使得用户可以轻松导出例如 dashboard 对象,并且导出中包含显示仪表板所需的所有 visualization 对象。
  • 当启用 X-Pack 安全和空间插件时,它们会透明地提供 RBAC 访问控制以及将保存对象组织到空间中的能力。

本文档包含希望使用保存对象的插件的开发者指南和最佳实践。

服务器端用法

编辑

注册保存对象类型

编辑

保存对象类型定义应在其自己的 my_plugin/server/saved_objects 目录中定义。

该文件夹应包含每个类型的文件,以类型的 snake_case 名称命名,以及一个导出所有类型的 index.ts 文件。

src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts.

import { SavedObjectsType } from 'src/core/server';

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization', 
  hidden: true,
  namespaceType: 'multiple-isolated', 
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    1: modelVersion1,
    2: modelVersion2,
  },
  mappings: {
    dynamic: false,
    properties: {
      description: {
        type: 'text',
      },
      hits: {
        type: 'integer',
      },
    },
  },
  // ...other mandatory properties
};

由于保存对象类型的名称可能会构成公共保存对象 HTTP API 的 URL 路径的一部分,因此这些名称应遵循我们的 API URL 路径约定,并且始终以 snake case 编写。

此字段确定“空间行为”——这些对象可以存在于一个空间、多个空间还是所有空间中。此值表示此类型的对象只能存在于单个空间中。有关更多信息,请参阅 共享保存对象

src/plugins/my_plugin/server/saved_objects/index.ts.

export { dashboardVisualization } from './dashboard_visualization';
export { dashboard } from './dashboard';

src/plugins/my_plugin/server/plugin.ts.

import { dashboard, dashboardVisualization } from './saved_objects';

export class MyPlugin implements Plugin {
  setup({ savedObjects }) {
    savedObjects.registerType(dashboard);
    savedObjects.registerType(dashboardVisualization);
  }
}

映射

编辑

每个保存对象类型都可以定义自己的 Elasticsearch 字段映射。由于多个保存对象类型可以共享同一个索引,因此类型定义的映射将嵌套在与类型名称匹配的顶级字段下。

例如,search 保存对象类型定义的映射

.src/plugins/saved_search/server/saved_objects/search.ts

import { SavedObjectsType } from 'src/core/server';
// ... other imports
export function getSavedSearchObjectType: SavedObjectsType = { 
  name: 'search',
  hidden: false,
  namespaceType: 'multiple-isolated',
  mappings: {
    dynamic: false,
    properties: {
      title: { type: 'text' },
      description: { type: 'text' },
    },
  },
  modelVersions: { ... },
  // ...other optional properties
};

简化

将导致以下映射应用于 .kibana_analytics 索引

{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      ...
      "search": {
        "dynamic": false,
        "properties": {
          "title": {
            "type": "text",
          },
          "description": {
            "type": "text",
          },
        },
      }
    }
  }
}

不要像使用 SQL 数据库的列的数据类型那样使用字段映射。相反,字段映射类似于 SQL 索引。仅为要搜索或查询的字段指定字段映射。通过在映射的任何级别指定 dynamic: false,即使未在映射中指定,Elasticsearch 也会接受并存储任何其他字段。

由于 Elasticsearch 每个索引的默认字段限制为 1000 个,因此插件应仔细考虑添加到映射中的字段。同样,保存对象类型永远不应使用 dynamic: true,因为这可能会导致任意数量的字段添加到 .kibana 索引中。

通过定义模型版本编写迁移

编辑

保存对象使用 modelVersions 支持更改。modelVersion API 是一种为您的 savedObject 类型定义转换(“*迁移”*)的新方法,并且将在 Kibana 版本 8.10.0 之后取代“旧”迁移 API。旧迁移 API 已被弃用,这意味着不再可能使用旧系统注册迁移。

模型版本与堆栈版本分离,并满足零停机和向后兼容性的要求。

每个保存对象类型都可以为其模式定义模型版本,并且绑定到给定的 savedObject 类型。通过定义新模型来指定对保存对象类型的更改。

定义模型版本

编辑

对于旧的迁移,模型版本绑定到给定的 savedObject 类型

注册 SO 类型时,可以使用新的 modelVersions 属性。此属性是 SavedObjectsModelVersion 的映射,后者是定义模型版本的顶级类型/容器。

此映射遵循与旧迁移映射类似的 { [版本号] => 版本定义 } 格式,但是给定 SO 类型的模型版本现在由单个整数标识。

第一个版本必须编号为版本 1,每个新版本递增 1。

这样:- SO 类型版本与堆栈版本控制分离 - SO 类型版本在类型之间是独立的

一个有效的版本编号

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
};

一个无效的版本编号

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
};

模型版本的结构

编辑

模型版本不仅仅是以前的迁移函数,而是描述版本行为方式以及自上次版本以来所发生变化的结构化对象。

模型版本外观的基本示例

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
};

注意:从设计上来说,支持给定版本的同类型多个更改,以允许合并不同的源(为最终的更高级别的 API 做准备)

此定义将完全有效

const version1: SavedObjectsModelVersion = {
  changes: [
    {
      type: 'mappings_addition',
      addedMappings: {
        someNewField: { type: 'text' },
      },
    },
    {
      type: 'mappings_addition',
      addedMappings: {
        anotherNewField: { type: 'text' },
      },
    },
  ],
};

它目前由两个主要属性组成

更改

编辑

链接到 changes 的 TS 文档

描述此版本期间应用的更改列表。

重要提示:这是取代旧迁移系统的部分,允许定义版本何时添加新映射、更改文档或其他与类型相关的更改。

当前更改类型是

- mappings_addition
编辑

用于定义给定版本中引入的新映射。

用法示例

const change: SavedObjectsModelMappingsAdditionChange = {
  type: 'mappings_addition',
  addedMappings: {
    newField: { type: 'text' },
    existingNestedField: {
      properties: {
        newNestedProp: { type: 'keyword' },
      },
    },
  },
};

注意:添加映射时,还必须相应地更新根 type.mappings(如先前所做的那样)。

- mappings_deprecation
编辑

用于将映射标记为不再使用并且可以删除。

用法示例

let change: SavedObjectsModelMappingsDeprecationChange = {
  type: 'mappings_deprecation',
  deprecatedMappings: ['someDeprecatedField', 'someNested.deprecatedField'],
};

注意:目前无法从现有索引的映射中删除字段(无需重新索引到另一个索引),因此现在不会删除使用此更改类型标记的映射,但这仍然应该用于允许我们的系统在上游 (ES) 解除阻止我们时清理映射。

- data_backfill
编辑

用于填充在同一版本中添加的字段(已索引或未索引)。

用法示例

let change: SavedObjectsModelDataBackfillChange = {
  type: 'data_backfill',
  transform: (document) => {
    return { attributes: { someAddedField: 'defaultValue' } };
  },
};

注意:即使不执行任何检查以确保这一点,此类型的模型更改也应仅用于回填新引入的字段。

- data_removal
编辑

用于从类型的所有文档中删除数据(取消设置字段)。

用法示例

let change: SavedObjectsModelDataRemovalChange = {
  type: 'data_removal',
  attributePaths: ['someRootAttributes', 'some.nested.attribute'],
};

注意:由于向后兼容性,必须在实际数据删除之前(在回滚的情况下)在先前的版本中停止字段利用率。请参阅本文档中下面的字段删除迁移示例

- unsafe_transform
编辑

用于执行任意转换函数。

用法示例

let change: SavedObjectsModelUnsafeTransformChange = {
  type: 'unsafe_transform',
  transformFn: (document) => {
    document.attributes.someAddedField = 'defaultValue';
    return { document };
  },
};

注意:考虑到迁移系统将不知道将对文档执行哪种操作,因此使用此类转换可能是不安全的。仅当没有其他方法可以满足迁移需求时,才应使用这些转换。 如果您认为需要使用此项,请联系开发团队,因为理论上您不应该这样做。

模式

编辑

链接到 schemas 的 TS 文档

与此版本关联的模式。模式用于在 SO 文档生命周期的各个阶段验证或转换 SO 文档。

当前可用的模式是

forwardCompatibility
编辑

这是模型版本引入的新概念。此模式用于版本之间的兼容性。

当从索引检索保存的文档时,如果文档的版本高于 Kibana 实例的最新已知版本,则该文档将通过关联模型版本的 forwardCompatibility 模式。

重要提示:这些转换机制不应断言数据本身,而只应剥离未知字段以将文档转换为给定版本的文档的形状

基本上,此模式应保留给定版本的所有已知字段,并删除所有未知字段,而不抛出错误。

前向兼容性模式可以通过两种不同的方式实现。

  1. 使用 config-schema

具有两个字段的版本的模式示例:someField 和 anotherField

const versionSchema = schema.object(
  {
    someField: schema.maybe(schema.string()),
    anotherField: schema.maybe(schema.string()),
  },
  { unknowns: 'ignore' }
);

重要提示:请注意模式选项中的 { unknowns: 'ignore' }。这是在使用基于 config-schema 的模式时所必需的,因为这就是在不抛出错误的情况下清除其他字段的原因。

  1. 使用纯 JavaScript 函数

具有两个字段的版本的模式示例:someField 和 anotherField

const versionSchema: SavedObjectModelVersionEvictionFn = (attributes) => {
  const knownFields = ['someField', 'anotherField'];
  return pick(attributes, knownFields);
}

注意:即使强烈建议,也不严格要求实现此模式。类型所有者可以在其服务层中自行管理未知字段和版本之间的兼容性。

创建
编辑

这是 旧的 SavedObjectType.schemas 定义的直接替代品,现在直接包含在模型版本定义中。

作为回顾,create 模式是一个 @kbn/config-schema 对象类型模式,用于在 createbulkCreate 操作期间验证文档的属性。

注意:实现此模式是可选的,但仍然建议这样做,否则在导入对象时不会进行任何验证

有关实现示例,请参阅 用例示例

用例示例

编辑

这些是系统当前支持的(开箱即用的)迁移场景示例。

注意: 更复杂的场景(例如,通过复制/同步进行的字段突变)可能已经实现,但由于核心组件没有公开适当的工具,因此与同步和兼容性相关的大部分工作都必须在类型所有者的域层中实现,这就是我们尚未记录这些内容的原因。

添加没有默认值的非索引字段
编辑

我们目前处于模型版本 1,我们的类型定义了 2 个索引字段:foobar

版本 1 的类型定义如下所示

const myType: SavedObjectsType = {
  name: 'test',
  namespaceType: 'single',
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    // initial (and current) model version
    1: {
      changes: [],
      schemas: {
        // FC schema defining the known fields (indexed or not) for this version
        forwardCompatibility: schema.object(
          { foo: schema.string(), bar: schema.string() },
          { unknowns: 'ignore' } // note the `unknown: ignore` which is how we're evicting the unknown fields
        ),
        // schema that will be used to validate input during `create` and `bulkCreate`
        create:  schema.object(
          { foo: schema.string(), bar: schema.string() },
        )
      },
    },
  },
  mappings: {
    properties: {
      foo: { type: 'text' },
      bar: { type: 'text' },
    },
  },
};

假设我们现在要引入一个新的 dolly 字段,该字段不是索引的,并且我们不需要使用默认值填充它。

为了实现这一点,我们需要引入一个新的模型版本,唯一需要做的就是定义相关的模式以包含这个新字段。

添加的模型版本将如下所示

// the new model version adding the `dolly` field
let modelVersion2: SavedObjectsModelVersion = {
  // not an indexed field, no data backfill, so changes are actually empty
  changes: [],
  schemas: {
    // the only addition in this model version: taking the new field into account for the schemas
    forwardCompatibility: schema.object(
      { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
      { unknowns: 'ignore' } // note the `unknown: ignore` which is how we're evicting the unknown fields
    ),
    create:  schema.object(
      { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
    )
  },
};

添加新模型版本后的完整类型定义

const myType: SavedObjectsType = {
  name: 'test',
  namespaceType: 'single',
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    1: {
      changes: [],
      schemas: {
        forwardCompatibility: schema.object(
          { foo: schema.string(), bar: schema.string() },
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { foo: schema.string(), bar: schema.string() },
        )
      },
    },
    2: {
      changes: [],
      schemas: {
        forwardCompatibility: schema.object(
          { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
        )
      },
    },
  },
  mappings: {
    properties: {
      foo: { type: 'text' },
      bar: { type: 'text' },
    },
  },
};
添加没有默认值的索引字段
编辑

这种情况与前一种情况非常相似。区别在于,使用索引字段意味着添加一个 mappings_addition 更改,并且还需要相应地更新根映射。

为了重用前面的示例,假设我们想要添加的 dolly 字段需要被索引。

在这种情况下,新版本需要执行以下操作:- 添加一个 mappings_addition 类型更改以定义新的映射 - 相应地更新根 mappings - 像上一个示例一样添加更新后的模式

新版本的定义如下所示

let modelVersion2: SavedObjectsModelVersion = {
  // add a change defining the mapping for the new field
  changes: [
    {
      type: 'mappings_addition',
      addedMappings: {
        dolly: { type: 'text' },
      },
    },
  ],
  schemas: {
    // adding the new field to the forwardCompatibility schema
    forwardCompatibility: schema.object(
      { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
      { unknowns: 'ignore' }
    ),
    create:  schema.object(
      { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
    )
  },
};

如前所述,我们还需要更新根映射定义

mappings: {
  properties: {
    foo: { type: 'text' },
    bar: { type: 'text' },
    dolly: { type: 'text' },
  },
},

添加模型版本 2 后的完整类型定义将如下所示

const myType: SavedObjectsType = {
  name: 'test',
  namespaceType: 'single',
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    1: {
      changes: [
        {
          type: 'mappings_addition',
          addedMappings: {
            foo: { type: 'text' },
            bar: { type: 'text' },
          },
        },
      ],
      schemas: {
        forwardCompatibility: schema.object(
          { foo: schema.string(), bar: schema.string() },
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { foo: schema.string(), bar: schema.string() },
        )
      },
    },
    2: {
      changes: [
        {
          type: 'mappings_addition',
          addedMappings: {
            dolly: { type: 'text' },
          },
        },
      ],
      schemas: {
        forwardCompatibility: schema.object(
          { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
        )
      },
    },
  },
  mappings: {
    properties: {
      foo: { type: 'text' },
      bar: { type: 'text' },
      dolly: { type: 'text' },
    },
  },
};
添加具有默认值的索引字段
编辑

现在,我们来看一个稍微不同的场景,我们希望使用默认值填充新引入的字段。

在这种情况下,除了 mappings_addition 更改之外,我们还需要添加一个额外的 data_backfill 更改来填充新字段的值。

let modelVersion2: SavedObjectsModelVersion = {
  changes: [
    // setting the `dolly` field's default value.
    {
      type: 'data_backfill',
      transform: (document) => {
        return { attributes: { dolly: 'default_value' } };
      },
    },
    // define the mappings for the new field
    {
      type: 'mappings_addition',
      addedMappings: {
        dolly: { type: 'text' },
      },
    },
  ],
  schemas: {
    // define `dolly` as an know field in the schema
    forwardCompatibility: schema.object(
      { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
      { unknowns: 'ignore' }
    ),
    create:  schema.object(
      { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
    )
  },
};

完整的类型定义将如下所示

const myType: SavedObjectsType = {
  name: 'test',
  namespaceType: 'single',
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    1: {
      changes: [
        {
          type: 'mappings_addition',
          addedMappings: {
            foo: { type: 'text' },
            bar: { type: 'text' },
          },
        },
      ],
      schemas: {
        forwardCompatibility: schema.object(
          { foo: schema.string(), bar: schema.string() },
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { foo: schema.string(), bar: schema.string() },
        )
      },
    },
    2: {
      changes: [
        {
          type: 'data_backfill',
          transform: (document) => {
            return { attributes: { dolly: 'default_value' } };
          },
        },
        {
          type: 'mappings_addition',
          addedMappings: {
            dolly: { type: 'text' },
          },
        },
      ],
      schemas: {
        forwardCompatibility: schema.object(
          { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { foo: schema.string(), bar: schema.string(), dolly: schema.string() },
        )
      },
    },
  },
  mappings: {
    properties: {
      foo: { type: 'text' },
      bar: { type: 'text' },
      dolly: { type: 'text' },
    },
  },
};

注意: 如果该字段未被索引,我们将不会使用 mappings_addition 更改或更新映射(如示例 1 中所做的那样)

删除现有字段
编辑

我们目前处于模型版本 1,我们的类型定义了 2 个索引字段:keptremoved

版本 1 的类型定义如下所示

const myType: SavedObjectsType = {
  name: 'test',
  namespaceType: 'single',
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    // initial (and current) model version
    1: {
      changes: [],
      schemas: {
        // FC schema defining the known fields (indexed or not) for this version
        forwardCompatibility: schema.object(
          { kept: schema.string(), removed: schema.string() },
          { unknowns: 'ignore' } // note the `unknown: ignore` which is how we're evicting the unknown fields
        ),
        // schema that will be used to validate input during `create` and `bulkCreate`
        create:  schema.object(
          { kept: schema.string(), removed: schema.string() },
        )
      },
    },
  },
  mappings: {
    properties: {
      kept: { type: 'text' },
      removed: { type: 'text' },
    },
  },
};

假设我们现在要删除 removed 字段,因为我们的应用程序自最近更改以来不再需要它了。

这里首先要理解的是对向后兼容性的影响:假设 Kibana 版本 X 仍在用这个字段,而我们在版本 X+1 中停止使用该字段。

我们不能在版本 X+1 中删除数据,因为我们需要能够随时回滚到之前的版本。如果我们在升级到版本 X+1 时删除了此 removed 字段的数据,并且如果由于任何原因,我们需要回滚到版本 X,这将导致数据丢失,因为版本 X 仍在用此字段,但在回滚后它将不再存在于我们的文档中。

这就是为什么我们需要将任何字段删除作为 2 步操作执行:- 发布 X:Kibana 仍然使用该字段 - 发布 X+1:Kibana 不再使用该字段,但数据仍然存在于文档中 - 发布 X+2:数据已从文档中有效删除。

这样,任何先前版本的的回滚(X+2X+1 X+1X)在数据完整性方面都是安全的)

那么主要问题是,在版本 X+1 期间,如何让我们的应用程序层简单地忽略此 removed 字段,因为我们不希望此字段(现在未使用)从持久层返回,因为它可能会“污染”更高层,其中该字段实际上不再使用甚至未知。

这可以通过引入新版本并使用 forwardCompatibility 模式来“浅化”该字段来轻松完成。

X+1 模型版本将如下所示

// the new model version ignoring the `removed` field
let modelVersion2: SavedObjectsModelVersion = {
  changes: [],
  schemas: {
    forwardCompatibility: schema.object(
      { kept: schema.string() }, // `removed` is no longer defined here
      { unknowns: 'ignore' }
    ),
    create:  schema.object(
      { kept: schema.string() }, // `removed` is no longer defined here
    )
  },
};

添加新模型版本后的完整类型定义

const myType: SavedObjectsType = {
  name: 'test',
  namespaceType: 'single',
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    // initial (and current) model version
    1: {
      changes: [],
      schemas: {
        // FC schema defining the known fields (indexed or not) for this version
        forwardCompatibility: schema.object(
          { kept: schema.string(), removed: schema.string() },
          { unknowns: 'ignore' } // note the `unknown: ignore` which is how we're evicting the unknown fields
        ),
        // schema that will be used to validate input during `create` and `bulkCreate`
        create:  schema.object(
          { kept: schema.string(), removed: schema.string() },
        )
      },
    },
    2: {
      changes: [],
      schemas: {
        forwardCompatibility: schema.object(
          { kept: schema.string() }, // `removed` is no longer defined here
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { kept: schema.string() }, // `removed` is no longer defined here
        )
      },
    }
  },
  mappings: {
    properties: {
      kept: { type: 'text' },
      removed: { type: 'text' },
    },
  },
};

然后,在稍后的版本中,我们可以部署将有效删除文档中数据的更改

// the new model version ignoring the `removed` field
let modelVersion3: SavedObjectsModelVersion = {
  changes: [ // define a data_removal change to delete the field
    {
      type: 'data_removal',
      removedAttributePaths: ['removed']
    }
  ],
  schemas: {
    forwardCompatibility: schema.object(
      { kept: schema.string() },
      { unknowns: 'ignore' }
    ),
    create:  schema.object(
      { kept: schema.string() },
    )
  },
};

数据删除后的完整类型定义将如下所示

const myType: SavedObjectsType = {
  name: 'test',
  namespaceType: 'single',
  switchToModelVersionAt: '8.10.0',
  modelVersions: {
    // initial (and current) model version
    1: {
      changes: [],
      schemas: {
        // FC schema defining the known fields (indexed or not) for this version
        forwardCompatibility: schema.object(
          { kept: schema.string(), removed: schema.string() },
          { unknowns: 'ignore' } // note the `unknown: ignore` which is how we're evicting the unknown fields
        ),
        // schema that will be used to validate input during `create` and `bulkCreate`
        create:  schema.object(
          { kept: schema.string(), removed: schema.string() },
        )
      },
    },
    2: {
      changes: [],
      schemas: {
        forwardCompatibility: schema.object(
          { kept: schema.string() }, // `removed` is no longer defined here
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { kept: schema.string() }, // `removed` is no longer defined here
        )
      },
    },
    3: {
      changes: [ // define a data_removal change to delete the field
        {
          type: 'data_removal',
          removedAttributePaths: ['removed']
        }
      ],
      schemas: {
        forwardCompatibility: schema.object(
          { kept: schema.string() },
          { unknowns: 'ignore' }
        ),
        create:  schema.object(
          { kept: schema.string() },
        )
      },
    }
  },
  mappings: {
    properties: {
      kept: { type: 'text' },
      removed: { type: 'text' },
    },
  },
};

测试模型版本

编辑

模型版本定义比传统的迁移函数更结构化,这使得在没有适当工具的情况下很难进行测试。这就是为什么从 @kbn/core-test-helpers-model-versions 包中公开了一组测试工具和实用程序的原因,以帮助正确测试与模型版本及其相关转换相关的逻辑。

单元测试工具
编辑

对于单元测试,该包公开了实用程序,可以轻松测试将文档从一个模型版本转换为另一个模型版本(向上或向下)的影响。

模型版本测试迁移器
编辑

createModelVersionTestMigrator 帮助程序允许创建一个测试迁移器,该迁移器可用于通过以与升级期间迁移算法相同的方式转换文档来测试版本之间的模型版本更改。

示例

import {
  createModelVersionTestMigrator,
  type ModelVersionTestMigrator
} from '@kbn/core-test-helpers-model-versions';

const mySoTypeDefinition = someSoType();

describe('mySoTypeDefinition model version transformations', () => {
  let migrator: ModelVersionTestMigrator;

  beforeEach(() => {
    migrator = createModelVersionTestMigrator({ type: mySoTypeDefinition });
  });

  describe('Model version 2', () => {
    it('properly backfill the expected fields when converting from v1 to v2', () => {
      const obj = createSomeSavedObject();

      const migrated = migrator.migrate({
        document: obj,
        fromVersion: 1,
        toVersion: 2,
      });

      expect(migrated.properties).toEqual(expectedV2Properties);
    });

    it('properly removes the expected fields when converting from v2 to v1', () => {
      const obj = createSomeSavedObject();

      const migrated = migrator.migrate({
        document: obj,
        fromVersion: 2,
        toVersion: 1,
      });

      expect(migrated.properties).toEqual(expectedV1Properties);
    });
  });
});
集成测试工具
编辑

在集成测试期间,我们可以启动一个真实的 Elasticsearch 集群,允许我们以与在生产运行时几乎相似的方式操作 SO 文档。通过集成测试,我们甚至可以模拟两个具有不同模型版本的 Kibana 实例的共存,以断言它们的交互行为。

模型版本测试平台
编辑

该包公开了一个 createModelVersionTestBed 函数,该函数可用于为模型版本集成测试完全设置测试平台。它可用于启动和停止 ES 服务器,并启动我们正在测试的两个版本之间的迁移。

示例

import {
  createModelVersionTestBed,
  type ModelVersionTestKit
} from '@kbn/core-test-helpers-model-versions';

describe('myIntegrationTest', () => {
  const testbed = createModelVersionTestBed();
  let testkit: ModelVersionTestKit;

  beforeAll(async () => {
    await testbed.startES();
  });

  afterAll(async () => {
    await testbed.stopES();
  });

  beforeEach(async () => {
    // prepare the test, preparing the index and performing the SO migration
    testkit = await testbed.prepareTestKit({
      savedObjectDefinitions: [{
        definition: mySoTypeDefinition,
        // the model version that will be used for the "before" version
        modelVersionBefore: 1,
        // the model version that will be used for the "after" version
        modelVersionAfter: 2,
      }]
    })
  });

  afterEach(async () => {
    if(testkit) {
      // delete the indices between each tests to perform a migration again
      await testkit.tearDown();
    }
  });

  it('can be used to test model version cohabitation', async () => {
    // last registered version is `1` (modelVersionBefore)
    const repositoryV1 = testkit.repositoryBefore;
    // last registered version is `2` (modelVersionAfter)
    const repositoryV2 = testkit.repositoryAfter;

    // do something with the two repositories, e.g
    await repositoryV1.create(someAttrs, { id });
    const v2docReadFromV1 = await repositoryV2.get('my-type', id);
    expect(v2docReadFromV1.attributes).toEqual(whatIExpect);
  });
});

限制

由于测试平台仅创建实例化两个 SO 存储库所需的核心部分,并且由于我们无法正确加载所有插件(以实现适当的隔离),因此集成测试平台当前有一些限制

  • 未启用扩展

    • 没有安全性
    • 没有加密
    • 没有空间
  • 所有 SO 类型都将使用相同的 SO 索引

无服务器环境中的限制和边缘情况

编辑

无服务器环境以及在这种环境中以某种方式执行升级的事实(在某些时候,应用程序的旧版本和新版本共存),导致了 SO API 工作方式的一些特殊性,以及我们需要记录的一些限制/边缘情况

使用 find savedObjects API 的 fields 选项
编辑

默认情况下,find API(与返回文档的任何其他 SO API 一样)将在返回文档之前迁移所有文档,以确保在共存期间两个版本都可以使用文档(例如,旧节点搜索已经迁移的文档,或新节点搜索尚未迁移的文档)。

但是,当使用 find API 的 fields 选项时,文档无法迁移,因为某些模型版本更改无法应用于部分属性集。因此,当提供 fields 选项时,从 find 返回的文档将会被迁移。

这就是为什么在使用此选项时,API 使用者需要确保传递给 fields 选项的所有字段已经存在于先前的模型版本中。否则,可能会导致升级期间出现不一致,其中新引入或回填的字段可能不一定出现在使用该选项时从 search API 返回的文档中。

注意:Kibana 的先前版本和下一个版本都必须遵循此规则)

对具有大型 json blob 的字段使用 bulkUpdate
编辑

savedObjects bulkUpdate API 将更新客户端的文档,然后重新索引更新的文档。这些更新操作在内存中完成,并且在更新许多在某些字段中存储了大型 json blob 的对象时会导致内存约束问题。因此,我们建议不要对以下 savedObjects 使用 bulkUpdate:- 使用数组(因为这些往往是大型对象) - 在某些字段中存储大型 json blob