查询能力描述
QueryDescriptorClient(apiMetadata?) 读取聚合的查询模型接受什么:GET {aggregate}/snapshot/schema 与 GET {aggregate}/event/schema,Wow 9.2.0 及以上以 QueryModelDescriptor 应答。契约由服务端给出:它列出的每个算子、排序、分页方式、分组与函数单独使用都会被接受,没列出的都会被拒绝;constraints 写明单项看不出的组合规则。值、作用域或策略仍可能拒绝一个查询,这种拒绝带 QueryViolation。
| 方法 | 端点 | Promise 结果 |
|---|---|---|
| describeSnapshot | GET {aggregate}/snapshot/schema | QueryDescriptorResult |
| describeEventStream | GET {aggregate}/event/schema | QueryDescriptorResult |
每个方法接受 (previous?, attributes?, abort?)。previous 是已持有描述的版本:descriptor.version(sha256:…),或服务端发来的 ETag("sha256:…",或弱标签 W/"sha256:…")。客户端把它作为 If-None-Match: "sha256:…" 发送;描述未变时服务端答 304、不带正文,方法得到 { notModified: true, version };否则得到 { notModified: false, descriptor, version }。空白的 previous 不发送该头。其他失败与查询方法一样拒绝,用 toWowError 读取。
无论聚合的查询走哪条路由,schema 路由都没有租户、所有者段:基础路径只含聚合({contextAlias}/{aggregateName})。QueryClientFactory.createQueryDescriptorClient(options?) 就这样拼路径,不带工厂的 resourceAttribution;显式的 basePath 原样使用。Wow-Space-Id 等请求头与其他客户端一样来自 ApiMetadata。
描述与调用者无关,可以共享和缓存;version 是其内容的哈希。服务端定期(默认每 5 分钟)重新加载查询 schema,存储的变化(例如新建全文索引)不经部署也会改变描述,所以长期持有的副本要重新验证,不要永久保留。客户端自身不做缓存。
描述包含:
| 部分 | 契约 |
|---|---|
record | identity(行键)、paging(PagingMode:LIST、PAGED、CURSOR)、defaultScope(未写删除范围的查询得到的范围)、rootOperators(自身不指明字段的算子;只有允许高代价算子时才含 EXPRESSION)与 search(模式与字段;模型不提供全文检索时缺省)。 |
fields | 按逻辑路径列出每个可查询字段,含元素内的字段。每个字段有 types、kind、nullable、semantic(时间语义;数值格式 DECIMAL 与 MONEY:都带 scale,金额字段恰有固定的 ISO 4217 currency 或同级的 currencyField 之一;带 timeUnit 的 DURATION;或指向聚合的 REFERENCE:固定的 contextName 与 aggregateName,或同级的 contextNameField 与 aggregateNameField;数组字段取其元素的语义。更新的服务端可能发来本版本未列出的 type:描述符是原样的 JSON,不认识的 type 当作没有语义)、enum、sensitivity(level 为 DISPLAY 或 CONFIDENTIAL,以及 comparable:不可比较的字段不列算子、sort.paged 为 false、不进入 record.search)、project、filter.operators、sort.paged / sort.cursor、aggregate(不能聚合时缺省)、系统字段的 role(根算子针对的系统字段,或模型的时间:EVENT_TIME、FIRST_EVENT_TIME)、所在元素 scope、aliases(查询还可以用来指代它的其他路径;服务端先换回 path,结果与错误都用 path)与 deprecated({ message? },新查询应避开时出现)。 |
elements | 可以用 ELEMENT_MATCH 逐元素过滤、或在其元素上聚合的数组字段;以及 search(仅 Elasticsearch,MongoDB 上缺省):ELEMENT_MATCH 内的 SEARCH 可指明的元素字段与可用模式。record.search.fields 不列元素内的字段。 |
dynamic | map 键下的字段,每个带 {key} 的模式一条,按服务端解析具体键的方式解析(值为数组的 map 是一条 ARRAY);excludedKeys 列出另行声明为字段的键,这些键按该字段自己的条目处理。 |
limits | 该入口的有效上限:协议上限与 HTTP 预算取较小者。null 表示不限。 |
analysis | 指标类型;approximate:该后端估算而非精确计算的指标类型(MongoDB 上是 PERCENTILE,Elasticsearch 上是 DISTINCT_COUNT 与 PERCENTILE);是否接受表达式、having 与按指标排序;日期直方图是否补空桶;dateUnits:DATE_HISTOGRAM 分组可用的 AggregationDateUnit;dateParts:DATE_PART 分组可用的 AggregationDatePart(时间字段在 aggregate.groups 里两种分组都列出);dateDiffUnits:DATE_DIFF 可用的 DateDiffUnit(expressions 为 false 时为空);firstLastOrderBy:FIRST、LAST 未写 orderBy 时排序所用的字段(模型没有事件时间或存储不支持时缺省)。having.metrics 从不列 ANY、FIRST、LAST;字段的 aggregate.firstLast 表示 FIRST / LAST 能否读取它。 |
constraints | 组合规则:CURSOR_UNIQUE_SORT(带它追加的字段)、COUNT_REQUIRES_FILTER、STARTS_WITH_REQUIRES_PREFIX,MongoDB 上的 PARALLEL_ARRAY_SORT(带 fields:一次排序只能用其中一个的列表字段),以及 Elasticsearch 上的 NULL_OR_EMPTY_AS_MISSING(带 fields:这些字段的 null 或空列表在 EXISTS、NOT_EXISTS、IS_NULL、IS_EMPTY 看来就是缺失)与 ARRAY_EQUALITY(整个模型:EQ、NE 只收单个值作操作数)。 |
variants | 仅出现在按事件类型推断了载荷的事件流模型上:element(body)、discriminator(bodyType),以及按 value(事件的 bodyType)排序的每个变体的 fields——完整的字段描述,path 与 scope 相对该元素(如 body.added.productId)。对某个变体字段的条件要与判别字段的条件一起写在该元素的 ELEMENT_MATCH 里。 |
服务端文档里是普通字符串的集合,在类型里是开放的:QueryModel、QueryValueType、QueryFieldRole、QueryConstraintType、聚合的分组与函数、指标类型(含 approximate)都是「已知联合 + 任意字符串」,更新的服务端发来的新值仍能通过类型检查;QueryModels、QueryValueTypes、QueryFieldRoles、QueryConstraintTypes 给出已知值。服务端封闭的枚举(FilterOperator、PagingMode、QueryValueKind、SensitivityLevel、SearchMode、DeletionState、AggregationDateUnit、AggregationDatePart)是 enum。描述的类型也从 /dsl 导出。
完整示例
import type { Fetcher } from '@ahoo-wang/fetcher';
import {
FilterOperator,
QueryClientFactory,
type QueryModelDescriptor,
} from '@ahoo-wang/wow-client';
let held: QueryModelDescriptor | undefined;
export async function operatorsOf(
fetcher: Fetcher,
path: string,
signal = AbortSignal.timeout(10_000),
): Promise<FilterOperator[]> {
const descriptors = new QueryClientFactory({
fetcher,
contextAlias: 'example',
aggregateName: 'cart',
}).createQueryDescriptorClient();
const result = await descriptors.describeSnapshot(
held?.version,
undefined,
signal,
);
if (!result.notModified) held = result.descriptor;
return held?.fields.find(field => field.path === path)?.filter.operators ?? [];
}API 详情
QueryDescriptorClient
export class QueryDescriptorClient implements QueryDescriptorApi, ApiMetadataCapable {
constructor(public readonly apiMetadata?: ApiMetadata);
describeSnapshot(previous?: string, attributes?: Record<string, unknown>, abort?: AbortController | AbortSignal): Promise<QueryDescriptorResult>;
describeEventStream(previous?: string, attributes?: Record<string, unknown>, abort?: AbortController | AbortSignal): Promise<QueryDescriptorResult>;
}typescript/wow-client/src/client/query/descriptor/queryDescriptorClient.ts
QueryDescriptorApi
export interface QueryDescriptorApi {
describeSnapshot(previous?: string, attributes?: Record<string, unknown>, abort?: AbortController | AbortSignal): Promise<QueryDescriptorResult>;
describeEventStream(previous?: string, attributes?: Record<string, unknown>, abort?: AbortController | AbortSignal): Promise<QueryDescriptorResult>;
}
export interface QueryDescriptorRead {
notModified: false;
descriptor: QueryModelDescriptor;
version: string;
}
export interface QueryDescriptorNotModified {
notModified: true;
version: string;
}
export type QueryDescriptorResult = QueryDescriptorRead | QueryDescriptorNotModified;typescript/wow-client/src/client/query/descriptor/queryDescriptorApi.ts
QueryModelDescriptor
export interface QueryModelDescriptor {
model: QueryModel;
version: string;
timeZone: string;
record: RecordDescriptor;
limits: LimitsDescriptor;
analysis: AnalysisDescriptor;
fields: FieldDescriptor[];
elements: ElementDescriptor[];
dynamic: DynamicFieldDescriptor[];
constraints: ConstraintDescriptor[];
variants?: VariantsDescriptor;
}
export interface VariantsDescriptor {
element: string;
discriminator: string;
values: VariantDescriptor[];
}
export interface VariantDescriptor {
value: string;
fields: FieldDescriptor[];
description?: string;
}
export interface RecordDescriptor {
identity: string;
paging: PagingMode[];
defaultScope?: DeletionState;
rootOperators: FilterOperator[];
search?: SearchDescriptor;
}
export interface SearchDescriptor {
modes: SearchMode[];
fields: string[];
}
export interface LimitsDescriptor {
maxListSize: number | null;
defaultListSize: number | null;
maxPageSize: number | null;
maxPageWindow: number | null;
maxFilterNodes: number | null;
maxFilterValues: number | null;
maxSortFields: number;
aggregation: AggregationLimitsDescriptor;
}
export interface AggregationLimitsDescriptor {
maxGroups: number;
maxMetrics: number;
maxElements: number;
maxLimit: number;
maxExpressionDepth: number;
maxExpressionNodes: number;
}
export interface AnalysisDescriptor {
metrics: (AggregationMetricType | (string & {}))[];
approximate: (AggregationMetricType | (string & {}))[];
expressions: boolean;
having: HavingDescriptor;
sort: AnalysisSortDescriptor;
dense: boolean;
dateUnits: AggregationDateUnit[];
dateParts: AggregationDatePart[];
dateDiffUnits: DateDiffUnit[];
firstLastOrderBy?: string;
}
export interface HavingDescriptor {
metrics: (AggregationMetricType | (string & {}))[];
}
export interface AnalysisSortDescriptor {
groups: boolean;
metrics: boolean;
}
export interface ConstraintDescriptor {
type: QueryConstraintType;
appended?: string;
fields?: string[];
}typescript/wow-client/src/dsl/descriptor.ts
FieldDescriptor
export interface FieldDescriptor {
path: string;
role?: QueryFieldRole;
types: QueryValueType[];
kind: QueryValueKind;
nullable: boolean;
semantic?: QuerySemanticType;
enum?: EnumValueDescriptor[];
description?: string;
sensitivity?: SensitivityDescriptor;
project: boolean;
filter: FieldFilterDescriptor;
sort: FieldSortDescriptor;
aggregate?: FieldAggregateDescriptor;
scope?: string;
deprecated?: QueryDeprecation;
aliases: string[];
}
export interface FieldFilterDescriptor {
operators: FilterOperator[];
}
export interface FieldSortDescriptor {
paged: boolean;
cursor: boolean;
}
export interface FieldAggregateDescriptor {
groups: (AggregationGroupType | (string & {}))[];
missingKey: boolean;
functions: (AggregationFunction | (string & {}))[];
distinctCount: boolean;
percentile: boolean;
any: boolean;
firstLast: boolean;
expressionInput: boolean;
inMetricFilter: boolean;
}
export interface EnumValueDescriptor {
value: unknown;
description?: string;
}
export interface QueryDeprecation {
message?: string | null;
}
export interface SensitivityDescriptor {
level: SensitivityLevel;
comparable: boolean;
}
export interface ElementDescriptor {
path: string;
filter: boolean;
aggregate: boolean;
search?: SearchDescriptor;
}
export interface DynamicFieldDescriptor {
pattern: string;
types: QueryValueType[];
kind: QueryValueKind;
filter: FieldFilterDescriptor;
excludedKeys?: string[];
}
export type QuerySemanticType = TemporalDate | TemporalEpoch | TemporalFormatted | NumericDecimal | NumericMoney | DurationSemantic | ReferenceSemantic;
export interface TemporalDate {
type: 'TEMPORAL_DATE';
}
export interface TemporalEpoch {
type: 'TEMPORAL_EPOCH';
timeUnit?: TimeUnit;
}
export interface TemporalFormatted {
type: 'TEMPORAL_FORMATTED';
pattern: string;
}
export interface NumericDecimal {
type: 'DECIMAL';
scale: number;
}
export type NumericMoney = {
type: 'MONEY';
scale: number;
} & ({
currency: string;
currencyField?: undefined;
} | {
currency?: undefined;
currencyField: string;
});
export interface DurationSemantic {
type: 'DURATION';
timeUnit: TimeUnit;
}
export type ReferenceSemantic = {
type: 'REFERENCE';
} & ({
contextName: string;
aggregateName: string;
contextNameField?: undefined;
aggregateNameField?: undefined;
} | {
contextName?: undefined;
aggregateName?: undefined;
contextNameField: string;
aggregateNameField: string;
});typescript/wow-client/src/dsl/descriptor.ts
开放与封闭的取值集合
export declare const QueryModels: Readonly<{
readonly SNAPSHOT: 'SNAPSHOT';
readonly EVENT_STREAM: 'EVENT_STREAM';
}>;
export type QueryModel = (typeof QueryModels)[keyof typeof QueryModels] | (string & {});
export declare const QueryValueTypes: Readonly<{
readonly STRING: 'STRING';
readonly INTEGER: 'INTEGER';
readonly DECIMAL: 'DECIMAL';
readonly BOOLEAN: 'BOOLEAN';
readonly OBJECT: 'OBJECT';
}>;
export type QueryValueType = (typeof QueryValueTypes)[keyof typeof QueryValueTypes] | (string & {});
export declare const QueryFieldRoles: Readonly<{
readonly IDENTITY: 'IDENTITY';
readonly AGGREGATE_ID: 'AGGREGATE_ID';
readonly TENANT_ID: 'TENANT_ID';
readonly OWNER_ID: 'OWNER_ID';
readonly SPACE_ID: 'SPACE_ID';
readonly DELETED: 'DELETED';
readonly EVENT_TIME: 'EVENT_TIME';
readonly FIRST_EVENT_TIME: 'FIRST_EVENT_TIME';
}>;
export type QueryFieldRole = (typeof QueryFieldRoles)[keyof typeof QueryFieldRoles] | (string & {});
export declare const QueryConstraintTypes: Readonly<{
readonly CURSOR_UNIQUE_SORT: 'CURSOR_UNIQUE_SORT';
readonly COUNT_REQUIRES_FILTER: 'COUNT_REQUIRES_FILTER';
readonly STARTS_WITH_REQUIRES_PREFIX: 'STARTS_WITH_REQUIRES_PREFIX';
readonly PARALLEL_ARRAY_SORT: 'PARALLEL_ARRAY_SORT';
readonly NULL_OR_EMPTY_AS_MISSING: 'NULL_OR_EMPTY_AS_MISSING';
readonly ARRAY_EQUALITY: 'ARRAY_EQUALITY';
}>;
export type QueryConstraintType = (typeof QueryConstraintTypes)[keyof typeof QueryConstraintTypes] | (string & {});
export declare enum PagingMode {
LIST = 'LIST',
PAGED = 'PAGED',
CURSOR = 'CURSOR'
}
export declare enum QueryValueKind {
UNKNOWN = 'UNKNOWN',
NULL = 'NULL',
SCALAR = 'SCALAR',
OBJECT = 'OBJECT',
ARRAY = 'ARRAY',
UNION = 'UNION'
}
export declare enum SensitivityLevel {
DISPLAY = 'DISPLAY',
CONFIDENTIAL = 'CONFIDENTIAL'
}typescript/wow-client/src/dsl/descriptor.ts