Query capability descriptors
QueryDescriptorClient(apiMetadata?) reads what an aggregate's query models admit: GET {aggregate}/snapshot/schema and GET {aggregate}/event/schema, which Wow 9.2.0 and later answer with a QueryModelDescriptor. The contract is the server's: every operator, sort, paging mode, group and function it lists is admitted when used on its own, and anything it does not list is rejected. constraints names the rules about combinations that no single entry shows. A value, a scope or a policy can still reject a query; that rejection carries a QueryViolation.
| Method | Endpoint | Promise result |
|---|---|---|
| describeSnapshot | GET {aggregate}/snapshot/schema | QueryDescriptorResult |
| describeEventStream | GET {aggregate}/event/schema | QueryDescriptorResult |
Each method takes (previous?, attributes?, abort?). previous is the version of a descriptor already held: descriptor.version (sha256:…), or the ETag the server sent ("sha256:…", or a weak W/"sha256:…"). The client sends it as If-None-Match: "sha256:…"; an unchanged descriptor answers 304 without a body and the method resolves to { notModified: true, version }. Otherwise it resolves to { notModified: false, descriptor, version }. A blank previous sends no header. Any other failure rejects as the query methods do, and toWowError reads it.
The schema routes have no tenant or owner segment, whatever routes the aggregate's queries take: the base path names the aggregate alone ({contextAlias}/{aggregateName}). QueryClientFactory.createQueryDescriptorClient(options?) builds it that way, leaving the factory's resourceAttribution out; an explicit basePath is used as it is. Headers such as Wow-Space-Id come from ApiMetadata as for the other clients.
A descriptor does not depend on the caller, so it can be shared and cached; its version is a hash of its content. The server reloads query schemas periodically (every 5 minutes by default), and a storage change such as a new text index can change the descriptor without a deployment, so revalidate a long-held copy instead of keeping it forever. The client keeps no cache of its own.
What the descriptor holds:
| Part | Contract |
|---|---|
record | identity (the row key), paging (PagingMode: LIST, PAGED, CURSOR), defaultScope (the deletion scope a query without one gets), rootOperators (operators that name no field of their own; EXPRESSION among them only where expensive operators are allowed) and search (modes and fields; absent when the model offers no full-text search). |
fields | Every queryable field by logical path, element fields included. Per field: types, kind, nullable, semantic (the temporal kinds; the numeric formats DECIMAL and MONEY, each with a scale and a money field with exactly one of a fixed ISO 4217 currency or a sibling currencyField; DURATION with its timeUnit; or REFERENCE to an aggregate, a fixed contextName and aggregateName or the sibling contextNameField and aggregateNameField; an array field's is its items'. A newer server may send a type this version does not list: the descriptor is plain JSON, so treat an unknown type as no semantic), enum, sensitivity (level, DISPLAY or CONFIDENTIAL, and comparable: a field that is not comparable lists no operators, has sort.paged false and is left out of record.search), project, filter.operators, sort.paged / sort.cursor, aggregate (absent when it cannot be aggregated), role for system fields (a root operator's system field, or the model's times: EVENT_TIME, FIRST_EVENT_TIME), scope, the element it lives in, aliases (other paths a query may name it by; the server replaces them with path, so results and errors use path) and deprecated ({ message? }, set when new queries should avoid it). |
elements | Array fields whose elements ELEMENT_MATCH can filter or an aggregation can run over, and search (Elasticsearch only; absent on MongoDB): the element fields a SEARCH inside ELEMENT_MATCH may name, with its modes. record.search.fields never lists a field inside an element. |
dynamic | Fields under map keys, one entry per pattern with {key}, resolved as the server resolves a concrete key (a map of arrays is one ARRAY entry); excludedKeys lists the keys declared as fields of their own, which take that field's entry instead. |
limits | The entry's effective limits: the protocol's and the HTTP budget, whichever is smaller. null is unlimited. |
analysis | The metric types; approximate, those whose results this backend estimates (PERCENTILE on MongoDB, DISTINCT_COUNT and PERCENTILE on Elasticsearch); whether expressions, having and metric sort are admitted; whether date histograms fill empty buckets; dateUnits, the AggregationDateUnits a DATE_HISTOGRAM group may bucket by; dateParts, the AggregationDateParts a DATE_PART group may group by (a time field lists both group types in aggregate.groups); and dateDiffUnits, the DateDiffUnits a DATE_DIFF may measure in (empty when expressions is false); firstLastOrderBy, the field FIRST and LAST order by when they name no orderBy (absent when the model has no event time or the storage offers neither). having.metrics never lists ANY, FIRST or LAST, and a field's aggregate.firstLast says whether FIRST / LAST may read it. |
constraints | Combination rules: CURSOR_UNIQUE_SORT (with the field it appends), COUNT_REQUIRES_FILTER, STARTS_WITH_REQUIRES_PREFIX, on MongoDB PARALLEL_ARRAY_SORT (with the array fields a sort may name only one of), and on Elasticsearch NULL_OR_EMPTY_AS_MISSING (with the fields whose null or empty value reads as missing to EXISTS, NOT_EXISTS, IS_NULL and IS_EMPTY) and ARRAY_EQUALITY (model-wide: EQ and NE take only a scalar operand). |
variants | Only on an event stream model whose payloads were inferred per event type: element (body), discriminator (bodyType) and, sorted by value (the event's bodyType), each variant's fields, full field descriptors whose path and scope are relative to the element (body.added.productId). A condition on a variant's field goes inside an ELEMENT_MATCH on the element together with one on the discriminator. |
Sets the server documents as plain strings are open in the types: QueryModel, QueryValueType, QueryFieldRole, QueryConstraintType, the aggregation groups and functions, and the metric types (approximate included) are a known union plus any string, so a newer server's value still type-checks. QueryModels, QueryValueTypes, QueryFieldRoles and QueryConstraintTypes hold the known values. Enumerations the server closes (FilterOperator, PagingMode, QueryValueKind, SensitivityLevel, SearchMode, DeletionState, AggregationDateUnit, AggregationDatePart) are enums. The descriptor types are exported from /dsl too.
Complete example
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 details
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
Open and closed value sets
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
Related topics
Client configuration and metadata · Snapshot queries · Filter expressions · Aggregation builders · Errors