Saved Objects 服务编辑

Saved Objects 服务在服务器端和客户端均可用。

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

通过使用 Saved Objects,您的插件可以利用以下功能

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

本文档包含想要使用 Saved Objects 的插件的开发者指南和最佳实践。

服务器端使用编辑

注册 Saved Object 类型编辑

Saved Object 类型定义应在它们自己的 my_plugin/server/saved_objects 目录中定义。

该文件夹应包含每个类型的文件,以类型的蛇形命名,以及一个导出所有类型的 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
};

由于 Saved Object 类型的名称可能构成公共 Saved Objects HTTP API 的 URL 路径的一部分,因此它们应遵循我们的 API URL 路径约定,并且始终以蛇形命名。

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

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

映射编辑

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

例如,由 search Saved Object 类型定义的映射

.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 个的默认限制,因此插件应仔细考虑添加到映射中的字段。同样,Saved Object 类型永远不应使用 dynamic: true,因为这会导致将任意数量的字段添加到 .kibana 索引中。

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

Saved Objects 支持使用 modelVersions` 进行更改。modelVersion API 是一种定义 savedObject 类型转换(`‘迁移’')的新方法,将在 Kibana 版本 8.10.0 之后取代“传统”迁移 API。传统迁移 API 已被弃用,这意味着不再可以使用传统系统注册迁移。

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

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

定义模型版本编辑

与旧迁移一样,模型版本绑定到给定的 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'],
};

注意: 目前无法从现有索引的映射中删除字段(无需重新索引到另一个索引),因此使用此更改类型标记的映射目前不会被删除,但这仍然应该用于允许我们的系统在 upstream(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 文档生命周期的各个阶段验证或转换它们。

当前可用的模式是

forwardCompatibilityedit

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

从索引中检索 savedObject 文档时,如果文档的版本高于 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);
}

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

createedit

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

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

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

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

用例示例edit

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

注意: 更复杂的场景(例如通过复制/同步进行字段变异)可能已经实现,但如果没有从 Core 公开适当的工具,与同步和兼容性相关的大部分工作将必须在类型所有者的域层中实现,这就是我们还没有记录它们的原因。

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

我们目前处于模型版本 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' },
    },
  },
};
添加没有默认值的索引字段edit

此场景与上一个场景非常接近。区别在于,使用索引字段意味着添加 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' },
    },
  },
};
添加具有默认值的索引字段edit

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

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

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 中所做的那样)。

删除现有字段edit

我们目前处于模型版本 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 仍在使用此字段,但在回滚后它将不再出现在我们的文档中。

这就是为什么我们需要执行任何字段删除作为两步操作: - 发布 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' },
    },
  },
};

测试模型版本edit

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

单元测试工具edit

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

模型版本测试迁移器edit

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);
    });
  });
});
集成测试工具edit

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

模型版本测试平台edit

该包公开了 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 存储库所需的 Core 部分,并且由于我们无法正确加载所有插件(以实现适当的隔离),因此集成测试平台目前存在一些限制

  • 没有启用扩展

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

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

无服务器环境,以及在这种环境中升级以某种方式执行,即在某个时刻,应用程序的旧版本和新版本共存,导致 SO API 的工作方式出现一些特殊情况,以及一些需要记录的限制/边缘情况。

使用 find savedObjects API 的 fields 选项编辑

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

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

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

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

使用 bulkUpdate 处理包含大型 json 块的字段编辑

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