定义与字段类型
定义是代码:它随应用发布,说清一个业务对象能被怎样观察——哪些字段、各是什么类型、能怎样筛选、排序和汇总,以及随定义一起发布的系统视图。用户保存的每个视图都按它校验。defineView 从来源的查询描述构建一份数据定义:描述给出事实,规格在其中挑选、命名、收窄。
相关指南:写好一份定义(从描述符出发逐项选择、措辞、系统视图与看板)、视图引擎的核心概念(定义在模型里的位置)、视图引擎入门(第 5 步的一份完整定义)。
defineView
descriptor 是与定义一起提交的查询描述快照(GET /<aggregate>/snapshot/schema 的回答):模块加载时定义就建好,测试建出来的也是同一份。运行时来源答出的描述还会再收窄它一次,和任何定义一样。
- 事实归描述:路径、类型、取值、敏感性、数组的条目。没列出的字段不出现。描述里没有的路径或取值是准入报告的错误(
DataViewDefinition.described),从不抛出:一处写错的定义照样加载,并说出错在哪里。 - 能力不写死:宿主不收窄的地方,定义取来源授予的全部;收窄的地方,取它的子集。超出快照的能力要求是警告(
definition.field.sort-wider等)——路径按什么排序与聚合是存储说了算,换个存储可能就授予了。 - 结果是一份普通定义:之后没有任何东西知道它是怎么写的,完全手写的定义也照样是定义。
export declare function defineView(descriptor: QueryModelDescriptor, spec: DefineViewSpec, options?: DefineViewOptions): DataViewDefinition;import { defineView, text } from '@ahoo-wang/wow-view-engine';
export const ordersDefinition = defineView(ordersDescriptor, {
id: 'orders',
// 宿主解析这份定义的来源时用的键(ViewEngine 的 resources)。
source: 'order',
title: text('orders.title'),
timeField: 'firstEventTime',
// 只出现列出的字段,按列出的顺序;字段是什么、能排序筛选聚合什么,由描述说。
fields: {
aggregateId: { label: text('orders.id'), cell: 'copyable' },
'state.status': {
label: text('orders.status'),
cell: 'status',
options: {
PAID: { label: text('orders.paid'), tone: 'warning' },
SHIPPED: { label: text('orders.shipped'), tone: 'success' },
},
},
'state.amount': { label: text('orders.amount'), summary: ['SUM', 'AVG'] },
firstEventTime: text('orders.placedAt'),
},
// 操作要读的字段即使视图不显示也要取回,见下面的 rowFields。
record: { layouts: ['table', 'card'], rowFields: ['state.status'] },
});DefineViewSpec
| 成员 | 作用 |
|---|---|
id、title、recordNoun | 定义的标识与标题;标题可以是 text(key) |
source | 宿主解析这份定义的数据来源时用的键 |
fields | 读者看到的字段,按根路径,按列出的顺序。描述里有而这里没列的不出现。值写成字符串就只是标签 |
fieldGroups | 字段选择器列出字段时用的分组,按此顺序 |
record | 记录能力:行键与分页缺省取描述的(它的标识;能分页处分页,否则用游标),以表格显示。false 不提供记录 |
analysis | 分析能力,按描述提供的;false 不提供 |
timeField | 看板的时间筛选通过它到达面板;必须是 fields 里列出的日期字段 |
views | 随定义发布的系统视图:人人可见、无人可覆盖、谁都能另存一份 |
export interface DefineViewSpec {
analysis?: AnalysisSpec | false;
fieldGroups?: FieldGroupDefinition[];
fields: Readonly<Record<string, FieldSpec | string>>;
id: string;
record?: Partial<RecordCapability> | false;
recordNoun?: string;
source: string;
timeField?: string;
title: string;
views?: SystemView[];
}FieldSpec
一个字段:宿主在它的路径之上说了什么。
| 成员 | 作用 |
|---|---|
label | 读者认识它的那个词。不写时取描述里的说明,再没有就是路径——准入会说出来(definition.field.unlabelled,一条 note) |
kind | 描述留有选择余地时的类型:可复制的引用 id、当作类别读的字符串。不写时由描述的值类型决定 |
cell | 单元格的读法:string、number、boolean、date、datetime、enum、status、tags、link、text、copyable |
operators | 提供的比较,必须是该路径比较的子集,超出是警告 definition.field.operator-wider |
sortable | false 不在能排序的路径上提供排序;true 而路径不能排序是警告 definition.field.sort-wider |
summary | 表尾提供的汇总,在路径能喂的函数之内,超出是警告 definition.field.summary-wider |
options | 类别的取值,按列出的顺序,各带措辞与色调,false 隐藏一个。描述不给取值的路径上,这就是宿主自己的封闭列表。数字取值的类别写成 [值, 措辞] 列表,因为对象的整数键会被 JavaScript 排到前面 |
elements、elementTitle | 数组的条目:它们的字段按条目内的路径写,路径必须是描述列出的元素 |
search | 一个搜索框而不是一条路径:键只是个把手,fields 是它搜的路径 |
analysis | 它怎样参与分析,收窄后的;false 不参与 |
deprecated | 描述把它标成废弃时仍保留它的原因 |
timePrecision | 表格单元格写到多细:'second' 用在秒才是重点的地方(事件流的时间);缺省到分钟 |
more | 手写字段时字段还能说的其他东西 |
export interface FieldSpec {
analysis?: FieldAnalysisSpec | false;
cell?: FieldCellId;
deprecated?: { message?: string };
elements?: Readonly<Record<string, FieldSpec | string>>;
elementTitle?: string;
kind?: FieldKindId;
label?: string;
more?: Partial<Pick<FieldDefinition, 'numberFormat' | 'numeric' | 'temporal' | 'remote' | 'stringComparison'>>;
operators?: FilterOperatorName[];
options?: Readonly<Record<string, OptionSpec>> | readonly (readonly [FieldOption['value'], OptionSpec])[];
search?: { fields: string[]; mode?: SearchModeName };
sortable?: boolean;
summary?: SummaryFunction[];
timePrecision?: TimePrecision;
}行里取回哪些字段:rowFields
一页记录只向来源要视图显示的字段(行键、可见列、卡片字段、排序字段),不要整份文档。宿主自己的代码要读、而视图不一定显示的字段——行操作的 available 判断、批量操作、自定义单元格——写进 record.rowFields,每个都必须是行里有的已声明字段,准入会检查。
这是接入时最常见的坑:一个读 state.status 判断能否发货的操作,在不显示状态列的视图里会一直禁用,直到定义写上 rowFields: ['state.status']。没有「有操作就取整份文档」这回事——整份文档正是一页失败执行曾经变成 808 KB 的原因。
export interface RecordCapability {
defaults?: Partial<RecordViewConfig>;
layouts: RecordLayout[];
maxSortFields?: number;
maxWindow?: number;
paging: PagingMode;
parallelArrays?: string[][];
requiresFilter?: boolean;
rowFields?: string[];
rowKey: string;
}DefineViewOptions
宿主登记的字段类型(与 ViewEngineOptions.kinds 相同)。定义在任何引擎之前就建好,所以宿主自定义类型的字段,只有类型在这里已知时才会写上快照的比较。不写时用内置类型。
export interface DefineViewOptions {
kinds?: FieldKindRegistry;
}SystemView
随定义发布的基线视图。id 在定义内唯一,且不含 :。timeField 是这个视图读记录时的时刻(不同于定义的 timeField 时);null 表示整体读取的视图,看板的时间筛选够不到它。
export interface SystemView {
config: ViewConfig;
id: string;
timeField?: string | null;
title: string;
}text 与键
定义写 text('orders.title'),而不是某种语言里的「订单」,这样一份定义服务所有语言,由宿主的措辞目录去说它。
- 键以字符串旅行:定义、系统视图与看板的每个标签位都是
string,键夹在两个任何措辞都不用的字符之间(私用区 U+E000 与 U+E001)。所以键也能出现在代码拼出的长字符串里,在那里同样被说出来。字面字符串仍是标签,给只有一种语言的宿主。 - 键在叶子处才说出:定义、保存的配置、每个运行时的状态和快照都保持键;只有在显示或离开引擎的地方——渲染出的标签、图表选项、导出、可访问名称、标题——才按当时生效的 Provider 的措辞说出,没有 Provider 就用引擎构建时的(
ViewEngineOptions.text)。所以一个引擎服务所有语言,换语言只是重画。 - 缺措辞的键读作键本身,并被报告(
definition.text.unknown、definition.text.fallback,见 Issue code)。测试里用admit连同措辞一起检查。
export declare function text(key: string): Text;export type Text = string & {
readonly __text: unique symbol;
};字段类型
FieldKind 是引擎的主要扩展点:一种字段类型的全部知识——支持的运算符、值的形状与校验、编译成 Wow 的 FilterExpression、编辑器描述。类型拥有它的值的形状,所以应用加一种类型时内核什么都不用学。
editor()返回的是数据,不是组件名:EditorDescriptor.input是封闭联合,成员列在EDITOR_INPUTS里,/ui的值编辑器正是对它分支。没有渲染器登记表,所以自定义类型从已有的输入里挑一个;要一个引擎没有的输入,validateFilter以filter.kind.unknown-editor拒绝,未登记的类型是filter.kind.unregistered,应用按钮被挡住。emptyValue()是叶子的起始值,通常是「还没有」:选了字段却没填值是正常的编辑状态,不校验也不编译。拿0当数字的起始值会在行一出现时悄悄套上amount = 0。compile()把一个已准入的叶子映射到 Wow 协议;compiledOperators()说明某运算符实际以哪些 Wow 运算符发送,描述必须全部允许它们,该运算符才会提供。readLeaf()把字段还不是这个类型时存下的叶子读成这个类型提供的形状;每一遍过树都先经它读每片叶子。内置的enum把字段还是字符串时存下的EQ读成一个值的IN,条件因此照常准入与编译。scalar、singleString、fieldless、nested告诉引擎这个类型的值在 Wow 那边是什么形状,以免引擎认为可用的条件被服务端拒绝。
内置类型:string、number、boolean、date、datetime、enum、reference、array、elementMatch、search,以及 Wow 元数据筛选背后的 documentId、aggregateId、tenantId、ownerId、spaceId、deletion。
export interface FieldKind {
compile(context: FieldKindCompileContext): FilterExpression;
compiledOperators?(operator: FilterOperatorName): readonly FilterOperatorName[];
defaultOperator: FilterOperatorName;
describe(context: FieldKindDescribeContext): FieldKindDescription;
editor(operator: FilterOperatorName, field: FieldDefinition, value?: unknown): EditorDescriptor;
emptyValue(operator: FilterOperatorName, field: FieldDefinition): unknown;
fieldless?: true;
id: FieldKindId;
isBlank?(context: FieldKindBlankContext): boolean;
nested?(value: unknown, field: FieldDefinition, operator: FilterOperatorName): NestedTree | null;
operators: FilterOperatorName[];
readLeaf?(leaf: FilterLeaf, field: FieldDefinition): FilterLeaf;
relations?: Partial<Record<FilterOperatorName, FilterSummaryRelation>>;
scalar?: boolean;
singleString?: boolean;
validate(context: FieldKindValidateContext): Issue[];
}登记表
登记表是按 id 索引的只读 Map。builtinFieldKinds 是现成的登记表;withFieldKinds 在它之上添加或替换类型而不改动它,结果交给 ViewEngineOptions.kinds,也交给 defineView 的 options.kinds。
import { builtinFieldKinds, withFieldKinds } from '@ahoo-wang/wow-view-engine';
export const kinds = withFieldKinds(builtinFieldKinds, [moneyKind]);export type FieldKindRegistry = ReadonlyMap<FieldKindId, FieldKind>;
export declare const builtinFieldKinds: FieldKindRegistry;
export declare const BUILTIN_FIELD_KINDS: readonly FieldKind[];
export declare function withFieldKinds(registry: FieldKindRegistry, kinds: readonly FieldKind[]): FieldKindRegistry;
export declare function createFieldKindRegistry(kinds: readonly FieldKind[]): FieldKindRegistry;编辑器描述
export interface EditorDescriptor {
input: EditorInput;
multiple?: boolean;
options?: FieldOption[];
range?: boolean;
remote?: string;
withTime?: boolean;
}
export declare const EDITOR_INPUTS: readonly ['none', 'text', 'number', 'boolean', 'deletion', 'select', 'remote', 'date', 'dateRange', 'relativeDate', 'predicate', 'duration'];完整可运行版本
- Storybook 的接入导览从一份提交的查询描述声明订单定义。
- 源文件:
ordersDefinition.ts与ordersDescriptor.json;defineView在src/runtime/define/defineView.ts,FieldKind在src/filter/fieldKind.ts。