Orisumi

实体 API

接口按身份寻址,所有类型共用一组路由,一次提交是一组有序的版本,内容按 JSON 的值比较,反查按目标实体列出直接的引用方,错误带规则编号

背景

Core 定义了实体、版本和提交的语义,HTTP 接口要把它们表示出来。读取方要能沿引用读到一个版本,写入方要能一次交出一组版本,并在中途被拒绝时知道哪些已经保存。重试时,后端还要判断两次提交的内容是否相同,而同一份 JSON 可以有多种写法。读取方还要能从一个实体出发,找到引用它的数据,例如和 VS Code 有关的全部前台时段;引用只保存在引用方(ENTITY-8),读取方自己无从得知谁引用了它。

决定

  • 接口按身份寻址:版本用 VersionId,实体用 EntityId。所有类型共用同一组路由。
  • 资源名和类别只作为列出实体时的筛选参数,不进入路径。
  • 按事实时间查询 Observation 时,只看已知的边界。两个边界都未知的 Observation 不参与时间查询。
  • 一次提交是一个按依赖顺序排列的版本数组。全部保存时,响应逐个列出每个版本的保存时间;中途被拒绝时,错误响应指出被拒绝的版本在提交中的位置。
  • 版本的 JSON 成员与 Core 的版本成员一一对应。提交时不带 recordedAt。
  • “内容相同”按 JSON 的值判断:对象不讲键的顺序,数字按数学值比较,时间换算为 UTC 并截断到微秒后再比较。
  • 反查用 GET /entities/{entityId}/referrers:按目标实体匹配,只返回直接的引用方,每一项是引用方的完整版本和其中引用目标的位置(JSON Pointer)。只匹配 content 中的引用。默认只看引用方的当前版本,versions=all 返回每一个引用过目标的版本。目标已删除时照常列出引用方。
  • 请求或版本本身有错时返回 400,与已保存的数据冲突时返回 409。被 Core 规则拒绝时,错误响应带规则编号 rule。
  • 读取从未出现过的身份返回 404,读取已删除的实体或版本返回 410。删除已删除的实体视为成功。

理由

引用只保存 VersionId,读取方沿引用读取时只知道这个 ID,所以读取不能要求先给出类型。所有类型的实体使用同一组操作(ENTITY-10),一组路由就能覆盖。

后端按版本自己的 schemaVersionId 校验内容,路径中的类型只会重复这项信息。Core 类型没有资源名,类型进入路径就要为它们保留名字。资源名的格式也允许以 a 到 f 开头的 UUID,/entities/{resourceName} 和 /entities/{entityId} 会无法区分。资源名作为筛选参数时,同一个参数在实体列表和 Observation 查询中含义相同。

未知的边界按无界处理时,一份“开始未知、结束于 10 月 5 日”的 Observation 会出现在 10 月 5 日之前任何时间范围的查询中。只看已知的边界,结果中的每份观测在查询范围内都有已知的时间。

数组的顺序就是 VERSION-13 中的依赖顺序。一次提交可以同时含有新版本和重试的版本,没有单个新建的资源,所以成功时统一返回 200,逐个列出结果。中途被拒绝时,位置之前的版本都已保存,写入方据此确认它们,处理完原因后原样重发。

JSON 对象本身无序,数字也有多种写法。按文本比较时,同一份内容换一个序列化库重试,就会被判为冲突。时间在保存时换算和截断,比较也在同一种表示上进行。

反查按目标实体匹配,直接给出 ENTITY-7 的归集:前台时段引用的是 VS Code 某一版,查询时问的是 VS Code 这个实体。每一项带上完整版本,读取方不用再逐个读取;带上位置,读取方才能区分关系,例如同一个实体在一份 Observation 中是 Observer,在另一份中是观测内容(ENTITY-5)。

只返回直接的引用方,因为每一层的关系含义不同:从 VS Code 到前台时段是“哪个应用”,从前台时段到 Observation 是“哪份观测”,读取方按需要决定沿哪些关系走多远。previousVersionId 和 schemaVersionId 已有自己的查询,版本链和按 resourceName 列出;把它们并入反查,查询一个 EntitySchema 就会返回这种类型的全部数据。

一份前台时段的 Observation 每延长一次就多一个版本,每一版都引用同一个时段。默认只看当前版本,每个引用方只出现一次;需要历史时,versions=all 也能找到后来改为引用别处的旧版本。目标被删除后,引用方仍是用户的数据,照常列出,读取方才能说明哪些追溯不完整(HISTORY-8)。

409 的版本换一个时机或在实体的另一个状态下可能被接受,写入方读取当前状态后重新处理;400 的版本本身需要修改。规则编号比状态码细,写入方和 Agent 按它判断具体原因,所以违反规则和不符合契约共用 400,由 rule 区分。

墓碑保留了已删除的身份(VERSION-9),区分 404 和 410 不需要额外的存储。读取方沿引用追溯到 410 时,可以说明这项数据已被用户删除(HISTORY-8);404 说明引用的目标从未存在,数据有误。

删除已删除的实体返回成功,删除请求的响应丢失后,重试也能得到确认。

放弃的方案:

  • 按类型分路由,例如 /entities/{resourceName}/{id}。 沿引用读取时不知道类型,Core 类型还要保留资源名。
  • 每个版本单独一个请求。 导入大量数据时请求数过多,写入方还要逐个等待依赖的版本保存完成。
  • 未知的边界按无界处理。 时间查询会返回大量时间实际不在范围内的观测。
  • 按序列化后的文本比较内容。 重试的结果会取决于写入方使用的 JSON 库。
  • 反查只返回引用行,不带版本内容。 读取方几乎总要读内容,每一项都要再发一次请求。
  • 反查一次返回全部间接的引用方。 结果混在一起,分不出每一层是什么关系,数量也难以预计。
  • 反查总是返回全部版本。 延长型的 Observation 会让同一个引用方重复出现许多次。
  • 目标已删除时返回 410。 读取方查不到还有哪些数据引用着它。
  • 违反规则时返回 422。 多一个状态码,区分的信息已经由 rule 提供。
  • 已删除和从未出现都返回 404。 读取方分不清用户的删除和数据错误。

代价

  • 读取一个版本前不知道它的类型,读取方要看 category 和 schemaVersionId 再解释内容。
  • 两个边界都未知的 Observation 只能按实体或类型找到。
  • 时间查询只匹配 Observation 的当前版本。要找过去某一版的时间,需要读取实体的版本链。
  • 读回的内容不保留键的顺序和数字的原始写法。需要保留原文的来源,把原文保存为字符串字段。
  • 写入方要按错误响应中的位置,区分已保存和未处理的版本。
  • 规则编号成为接口的一部分,写入方按编号判断原因。
  • 读取方能知道一个身份曾经存在、后来被删除。
  • 跨越多层的反查,读取方要逐层发请求。
  • 默认的反查只看当前版本,历史上引用过、现在不再引用的实体,要用 versions=all 才能找到。
  • 引用位置中的数组下标按保存时的顺序。InputSet 的成员无序,它的下标只用于定位。

规则位置

贡献者

页面历史