@ahoo-wang/fetcher-wow 3.18.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh-CN.md CHANGED
@@ -1,745 +1,56 @@
1
- # @ahoo-wang/fetcher-wow
1
+ # `@ahoo-wang/fetcher-wow`
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@ahoo-wang/fetcher-wow.svg)](https://www.npmjs.com/package/@ahoo-wang/fetcher-wow)
4
- [![Build Status](https://github.com/Ahoo-Wang/fetcher/actions/workflows/ci.yml/badge.svg)](https://github.com/Ahoo-Wang/fetcher/actions)
5
- [![codecov](https://codecov.io/gh/Ahoo-Wang/fetcher/graph/badge.svg?token=JGiWZ52CvJ)](https://codecov.io/gh/Ahoo-Wang/fetcher)
6
- [![License](https://img.shields.io/npm/l/@ahoo-wang/fetcher-wow.svg)](https://github.com/Ahoo-Wang/fetcher/blob/main/LICENSE)
7
- [![npm downloads](https://img.shields.io/npm/dm/@ahoo-wang/fetcher-wow.svg)](https://www.npmjs.com/package/@ahoo-wang/fetcher-wow)
8
- [![npm bundle size](https://img.shields.io/bundlephobia/minzip/%40ahoo-wang%2Ffetcher-wow)](https://www.npmjs.com/package/@ahoo-wang/fetcher-wow)
9
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Ahoo-Wang/fetcher)
10
- [![Storybook](https://img.shields.io/badge/Storybook-交互式文档-FF4785)](https://fetcher.ahoo.me/?path=/docs/wow-introduction--docs)
3
+ 面向 Wow 命令、快照、领域事件、过滤、分页与聚合的类型化 Fetcher 客户端和契约。只在
4
+ 对接 Wow HTTP 端点时使用。
11
5
 
12
- [Wow](https://github.com/Ahoo-Wang/Wow) 框架提供支持。提供用于与 Wow CQRS/DDD 框架配合使用的 TypeScript 类型和工具。
13
-
14
- ## 🌟 特性
15
-
16
- - **📦 完整的 TypeScript 支持**:为所有 Wow 框架实体提供完整的类型定义,包括命令、事件和查询
17
- - **🚀 命令客户端**:用于向 Wow 服务发送命令的高级客户端,支持同步和流式响应
18
- - **🔍 强大的查询 DSL**:类型安全的 `FilterExpression` 构建器,支持完整的查询操作符
19
- - **📊 快照聚合**:使用类型安全的查询对快照数据进行分组和聚合
20
- - **📡 实时事件流**:内置对服务器发送事件的支持,用于接收实时命令结果和数据更新
21
- - **🔄 CQRS 模式实现**:对命令查询责任分离架构模式的一流支持
22
- - **🧱 DDD 基础构件**:基本的领域驱动设计构建块,包括聚合、事件和值对象
23
- - **🔍 查询客户端**:专门用于查询快照和事件流数据的客户端,支持全面的查询操作:
24
- - 资源计数
25
- - 资源列表查询
26
- - 以服务器发送事件形式流式传输资源
27
- - 资源分页
28
- - 单个资源检索
29
-
30
- ## 🚀 快速开始
31
-
32
- ### 安装
6
+ ## 安装
33
7
 
34
8
  ```bash
35
- # 使用 npm
36
- npm install @ahoo-wang/fetcher-wow
37
-
38
- # 使用 pnpm
39
- pnpm add @ahoo-wang/fetcher-wow
40
-
41
- # 使用 yarn
42
- yarn add @ahoo-wang/fetcher-wow
43
- ```
44
-
45
- ## 📚 API 参考
46
-
47
- ### 命令模块
48
-
49
- #### CommandResult
50
-
51
- 表示命令执行结果的接口:
52
-
53
- ```typescript
54
- import { CommandResult, CommandStage } from '@ahoo-wang/fetcher-wow';
55
- ```
56
-
57
- #### CommandClient
58
-
59
- 用于向 Wow 框架发送命令的 HTTP 客户端。该客户端提供了同步或流式接收命令结果的方法。
60
-
61
- ```typescript
62
- import {
63
- Fetcher,
64
- FetchExchange,
65
- HttpMethod,
66
- RequestInterceptor,
67
- URL_RESOLVE_INTERCEPTOR_ORDER,
68
- } from '@ahoo-wang/fetcher';
69
- import '@ahoo-wang/fetcher-eventstream';
70
- import {
71
- CommandClient,
72
- CommandRequest,
73
- CommandHeaders,
74
- CommandStage,
75
- } from '@ahoo-wang/fetcher-wow';
76
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
77
-
78
- // 使用基础配置创建 fetcher 实例
79
- const exampleFetcher = new Fetcher({
80
- baseURL: 'http://localhost:8080/',
81
- });
82
-
83
- // 定义当前用户 ID
84
- const currentUserId = idGenerator.generateId();
85
-
86
- // 创建处理 URL 参数的拦截器
87
- class AppendOwnerId implements RequestInterceptor {
88
- readonly name: string = 'AppendOwnerId';
89
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
90
-
91
- intercept(exchange: FetchExchange) {
92
- const urlParams = exchange.ensureRequestUrlParams();
93
- urlParams.path['ownerId'] = currentUserId;
94
- }
95
- }
96
-
97
- // 注册拦截器
98
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
99
-
100
- // 创建命令客户端
101
- const cartCommandClient = new CommandClient({
102
- fetcher: exampleFetcher,
103
- basePath: 'owner/{ownerId}/cart',
104
- });
105
-
106
- // 定义命令端点
107
- class CartCommandEndpoints {
108
- static readonly addCartItem = 'add_cart_item';
109
- }
110
-
111
- // 定义命令接口
112
- interface AddCartItem {
113
- productId: string;
114
- quantity: number;
115
- }
116
-
117
- type AddCartItemCommand = CommandRequest<AddCartItem>;
118
-
119
- // 创建命令请求
120
- const addCartItemCommand: AddCartItemCommand = {
121
- method: HttpMethod.POST,
122
- headers: {
123
- [CommandHeaders.WAIT_STAGE]: CommandStage.SNAPSHOT,
124
- },
125
- body: {
126
- productId: 'productId',
127
- quantity: 1,
128
- },
129
- };
130
-
131
- // 发送命令并等待结果
132
- const commandResult = await cartCommandClient.send(
133
- CartCommandEndpoints.addCartItem,
134
- addCartItemCommand,
135
- );
136
-
137
- // 发送命令并接收流式结果
138
- const commandResultStream = await cartCommandClient.sendAndWaitStream(
139
- CartCommandEndpoints.addCartItem,
140
- addCartItemCommand,
141
- );
142
- for await (const commandResultEvent of commandResultStream) {
143
- console.log('收到命令结果:', commandResultEvent.data);
144
- }
145
- ```
146
-
147
- ##### 方法
148
-
149
- - `send(path: string, commandRequest: CommandRequest): Promise<CommandResult>` - 发送命令并等待结果。
150
- - `sendAndWaitStream(path: string, commandRequest: CommandRequest): Promise<CommandResultEventStream>` -
151
- 发送命令并以服务器发送事件的形式返回结果流。
152
-
153
- ### 查询模块
154
-
155
- #### FilterExpression 构建器
156
-
157
- Wow 8.11+ 查询使用 `FilterExpression`:
158
-
159
- ```typescript
160
- import {
161
- DeletionState,
162
- filter,
163
- SearchMode,
164
- TimeUnit,
165
- } from '@ahoo-wang/fetcher-wow';
166
-
167
- const expression = filter.and([
168
- filter.deletion(DeletionState.ACTIVE),
169
- filter.eq('state.status', 'PAID'),
170
- filter.elementMatch('state.items', filter.gt('quantity', 0)),
171
- filter.search('event sourcing', {
172
- mode: SearchMode.PHRASE,
173
- fields: ['state.title', 'state.description'],
174
- }),
175
- filter.yesterday('state.createdAt', {
176
- zoneId: 'Asia/Shanghai',
177
- timeUnit: TimeUnit.MILLISECONDS,
178
- }),
179
- ]);
180
- ```
181
-
182
- 所有构建器集中在 `filter`:`matchAll`、`matchNone`、`and`、`or`、`nor`、
183
- 比较、字符串/集合谓词、存在性检查、`elementMatch`、`search`、删除范围和相对时间过滤器。
184
- `and`、`or`、`nor`、`ids`、`aggregateIds`、`isIn`、`notIn` 和
185
- `containsAll` 接收一个非空 `readonly` 数组;传入空数组时抛出 `TypeError`。
186
-
187
- #### 条件构建器(已弃用)
188
-
189
- 旧 Condition API 仅为兼容旧版 Wow 服务保留。新代码应使用
190
- `FilterExpression` 和 `filter.*`。
191
-
192
- ```typescript
193
- import {
194
- and,
195
- or,
196
- eq,
197
- ne,
198
- gt,
199
- lt,
200
- gte,
201
- lte,
202
- contains,
203
- isIn,
204
- notIn,
205
- between,
206
- allIn,
207
- startsWith,
208
- endsWith,
209
- match,
210
- elemMatch,
211
- isNull,
212
- notNull,
213
- isTrue,
214
- isFalse,
215
- exists,
216
- raw,
217
- today,
218
- beforeToday,
219
- tomorrow,
220
- thisWeek,
221
- nextWeek,
222
- lastWeek,
223
- thisMonth,
224
- lastMonth,
225
- recentDays,
226
- earlierDays,
227
- active,
228
- all,
229
- id,
230
- ids,
231
- aggregateId,
232
- aggregateIds,
233
- tenantId,
234
- ownerId,
235
- } from '@ahoo-wang/fetcher-wow';
236
-
237
- // 简单条件
238
- const simpleConditions = [
239
- eq('name', 'John'),
240
- ne('status', 'inactive'),
241
- gt('age', 18),
242
- lt('score', 100),
243
- gte('rating', 4.0),
244
- lte('price', 100),
245
- ];
246
-
247
- // 字符串条件
248
- const stringConditions = [
249
- contains('email', '@company.com'),
250
- startsWith('username', 'j'),
251
- endsWith('domain', '.com'),
252
- isIn('status', 'active', 'pending'),
253
- notIn('role', 'guest', 'banned'),
254
- match('description', 'search keywords'),
255
- ];
256
-
257
- // 空值检查
258
- const nullConditions = [
259
- isNull('deletedAt'),
260
- notNull('email'),
261
- isTrue('isActive'),
262
- isFalse('isDeleted'),
263
- exists('phoneNumber'),
264
- ];
265
-
266
- // 数组条件
267
- const arrayConditions = [
268
- allIn('tags', 'react', 'typescript'),
269
- elemMatch('items', eq('quantity', 0)),
270
- ];
271
-
272
- // 日期条件
273
- const dateConditions = [
274
- today('createdAt'),
275
- beforeToday('lastLogin', '09:30'),
276
- tomorrow('scheduledDate'),
277
- thisWeek('updatedAt'),
278
- nextWeek('startDate'),
279
- lastWeek('endDate'),
280
- thisMonth('createdDate'),
281
- lastMonth('expirationDate'),
282
- recentDays('createdAt', 5), // 最近5天,包括今天
283
- earlierDays('createdAt', 3), // 3天之前
284
- ];
285
-
286
- // 复杂条件
287
- const complexCondition = and(
288
- eq('tenantId', 'tenant-123'),
289
- or(
290
- contains('email', '@company.com'),
291
- isIn('department', 'engineering', 'marketing'),
292
- ),
293
- between('salary', 50000, 100000),
294
- today('createdAt'),
295
- active(),
296
- );
297
-
298
- // 高级用法的原始条件
299
- const rawCondition = raw({ $text: { $search: 'keywords' } });
300
- ```
301
-
302
- **操作符参考:**
303
-
304
- | 类别 | 操作符 |
305
- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
306
- | 逻辑 | `and`, `or`, `nor` |
307
- | 比较 | `eq`, `ne`, `gt`, `lt`, `gte`, `lte` |
308
- | 字符串 | `contains`, `startsWith`, `endsWith`, `match` |
309
- | 集合 | `isIn`, `notIn`, `allIn`, `elemMatch` |
310
- | 空值/布尔 | `isNull`, `notNull`, `isTrue`, `isFalse`, `exists` |
311
- | 日期 | `today`, `beforeToday(time)`, `tomorrow`, `thisWeek`, `nextWeek`, `lastWeek`, `thisMonth`, `lastMonth`, `recentDays(days)`, `earlierDays(days)` |
312
- | ID | `id`, `ids`, `aggregateId`, `aggregateIds`, `tenantId`, `ownerId` |
313
- | 状态 | `active`, `all`, `deleted` |
314
- | 特殊 | `raw`(用于高级数据库特定查询) |
315
-
316
- #### SnapshotQueryClient
317
-
318
- 用于查询物化快照的客户端,支持全面的查询操作:
319
-
320
- ```typescript
321
- import {
322
- Fetcher,
323
- FetchExchange,
324
- RequestInterceptor,
325
- URL_RESOLVE_INTERCEPTOR_ORDER,
326
- } from '@ahoo-wang/fetcher';
327
- import '@ahoo-wang/fetcher-eventstream';
328
- import {
329
- aggregation,
330
- SnapshotQueryClient,
331
- filter,
332
- FilterListQuery,
333
- FilterPagedQuery,
334
- FilterSingleQuery,
335
- type AggregationQuery,
336
- Identifier,
337
- } from '@ahoo-wang/fetcher-wow';
338
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
339
-
340
- interface CartItem {
341
- productId: string;
342
- price: number;
343
- quantity: number;
344
- }
345
-
346
- interface CartState extends Identifier {
347
- status: string;
348
- items: CartItem[];
349
- }
350
-
351
- // 使用基础配置创建 fetcher 实例
352
- const exampleFetcher = new Fetcher({
353
- baseURL: 'http://localhost:8080/',
354
- });
355
-
356
- // 定义当前用户 ID
357
- const currentUserId = idGenerator.generateId();
358
-
359
- // 创建处理 URL 参数的拦截器
360
- class AppendOwnerId implements RequestInterceptor {
361
- readonly name: string = 'AppendOwnerId';
362
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
363
-
364
- intercept(exchange: FetchExchange) {
365
- const urlParams = exchange.ensureRequestUrlParams();
366
- urlParams.path['ownerId'] = currentUserId;
367
- }
368
- }
369
-
370
- // 注册拦截器
371
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
372
-
373
- // 创建快照查询客户端
374
- const cartSnapshotQueryClient = new SnapshotQueryClient<CartState>({
375
- fetcher: exampleFetcher,
376
- basePath: 'owner/{ownerId}/cart',
377
- });
378
-
379
- // 统计快照数量
380
- const count = await cartSnapshotQueryClient.count(filter.matchAll());
381
-
382
- // 列出快照
383
- const listQuery: FilterListQuery = {
384
- filter: filter.matchAll(),
385
- };
386
- const list = await cartSnapshotQueryClient.list(listQuery);
387
-
388
- // 以流的形式列出快照
389
- const listStream = await cartSnapshotQueryClient.listStream(listQuery);
390
- for await (const event of listStream) {
391
- const snapshot = event.data;
392
- console.log('收到快照:', snapshot);
393
- }
394
-
395
- // 列出快照状态
396
- const stateList = await cartSnapshotQueryClient.listState(listQuery);
397
-
398
- // 以流的形式列出快照状态
399
- const stateStream = await cartSnapshotQueryClient.listStateStream(listQuery);
400
- for await (const event of stateStream) {
401
- const state = event.data;
402
- console.log('收到状态:', state);
403
- }
404
-
405
- // 分页查询快照
406
- const pagedQuery: FilterPagedQuery = {
407
- filter: filter.matchAll(),
408
- };
409
- const paged = await cartSnapshotQueryClient.paged(pagedQuery);
410
-
411
- // 分页查询快照状态
412
- const pagedState = await cartSnapshotQueryClient.pagedState(pagedQuery);
413
-
414
- // 查询单个快照
415
- const singleQuery: FilterSingleQuery = {
416
- filter: filter.matchAll(),
417
- };
418
- const single = await cartSnapshotQueryClient.single(singleQuery);
419
-
420
- // 查询单个快照状态
421
- const singleState = await cartSnapshotQueryClient.singleState(singleQuery);
422
-
423
- type CartFields = 'state.status' | 'state.items';
424
- type ItemFields = 'productId' | 'price' | 'quantity';
425
-
426
- type ProductSummary = {
427
- product: string;
428
- representativeProduct: string | null;
429
- itemCount: number;
430
- revenue: number;
431
- };
432
-
433
- const revenue = aggregation.multiply(
434
- aggregation.field<ItemFields>('price'),
435
- aggregation.field<ItemFields>('quantity'),
436
- );
437
-
438
- const aggregationQuery: AggregationQuery<CartFields, ItemFields> = {
439
- filter: filter.eq('state.status', 'COMPLETED'),
440
- elements: [aggregation.element('state.items', filter.gt('quantity', 0))],
441
- groupBy: [aggregation.terms('productId', 'product')],
442
- metrics: [
443
- aggregation.any('productId', 'representativeProduct'),
444
- aggregation.count('itemCount'),
445
- aggregation.sum(revenue, 'revenue'),
446
- ],
447
- };
448
-
449
- const summaries =
450
- await cartSnapshotQueryClient.aggregate<ProductSummary>(aggregationQuery);
451
- const summaryStream =
452
- await cartSnapshotQueryClient.aggregateStream<ProductSummary>(
453
- aggregationQuery,
454
- );
455
- for await (const event of summaryStream) {
456
- console.log(event.data);
457
- }
9
+ pnpm add @ahoo-wang/fetcher @ahoo-wang/fetcher-decorator \
10
+ @ahoo-wang/fetcher-eventstream @ahoo-wang/fetcher-wow
458
11
  ```
459
12
 
460
- `aggregation.any(field, alias)` 增加的是指标,不是新的分组。它从当前分组返回一个
461
- 非空标量;没有值时返回 `null`。具体选择哪个值不属于契约,可能随后端或执行变化;
462
- 只有候选值可互换时才应使用。字段相对最内层 Element,集合字段或不具备 terms 聚合
463
- 能力的字段由 Wow 在运行时拒绝。按 `ANY` alias 排序属于昂贵的指标排序。
464
-
465
- ##### 方法
13
+ Peer 依赖:`fetcher`、`fetcher-decorator` `fetcher-eventstream`。
466
14
 
467
- - `count(filter: FilterExpression): Promise<number>` - 统计匹配过滤表达式的快照数量。
468
- - `list(listQuery: FilterListQuery): Promise<Partial<MaterializedSnapshot<S>>[]>` - 检索物化快照列表。
469
- - `listStream(listQuery: FilterListQuery): Promise<ReadableStream<JsonServerSentEvent<Partial<MaterializedSnapshot<S>>>>>` -
470
- 以服务器发送事件的形式检索物化快照流。
471
- - `listState(listQuery: FilterListQuery): Promise<Partial<S>[]>` - 检索快照状态列表。
472
- - `listStateStream(listQuery: FilterListQuery): Promise<ReadableStream<JsonServerSentEvent<Partial<S>>>>` -
473
- 以服务器发送事件的形式检索快照状态流。
474
- - `paged(pagedQuery: FilterPagedQuery): Promise<PagedList<Partial<MaterializedSnapshot<S>>>>` - 检索物化快照的分页列表。
475
- - `pagedState(pagedQuery: FilterPagedQuery): Promise<PagedList<Partial<S>>>` - 检索快照状态的分页列表。
476
- - `single(singleQuery: FilterSingleQuery): Promise<Partial<MaterializedSnapshot<S>>>` - 检索单个物化快照。
477
- - `singleState(singleQuery: FilterSingleQuery): Promise<Partial<S>>` - 检索单个快照状态。
478
- - `aggregate<Row extends DynamicDocument = DynamicDocument, AGGREGATION_FIELDS extends string = string>(query: AggregationQuery<FIELDS, AGGREGATION_FIELDS>): Promise<Row[]>` - 执行快照聚合并请求 `snapshot/aggregation`。
479
- - `aggregateStream<Row extends DynamicDocument = DynamicDocument, AGGREGATION_FIELDS extends string = string>(query: AggregationQuery<FIELDS, AGGREGATION_FIELDS>): Promise<ReadableStream<JsonServerSentEvent<Row>>>` - 执行快照聚合并通过服务器发送事件(SSE)请求 `snapshot/aggregation`。
480
-
481
- #### QueryClientFactory
482
-
483
- 用于创建预配置查询客户端的工厂。当您需要具有共享配置的多个客户端时,这非常有用。
484
-
485
- ```typescript
486
- import {
487
- filter,
488
- QueryClientFactory,
489
- ResourceAttributionPathSpec,
490
- } from '@ahoo-wang/fetcher-wow';
491
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
492
-
493
- // 使用默认选项创建工厂
494
- const factory = new QueryClientFactory({
495
- contextAlias: 'example',
496
- aggregateName: 'cart',
497
- resourceAttribution: ResourceAttributionPathSpec.OWNER,
498
- fetcher: exampleFetcher,
499
- });
15
+ ## 示例
500
16
 
501
- // 创建快照查询客户端
502
- const snapshotClient = factory.createSnapshotQueryClient({
503
- aggregateName: 'cart',
504
- });
505
- const carts = await snapshotClient.listState({ filter: filter.matchAll() });
506
-
507
- // 创建状态聚合客户端
508
- const stateClient = factory.createLoadStateAggregateClient({
509
- aggregateName: 'cart',
510
- });
511
- const cart = await stateClient.load('cart-123');
512
-
513
- // 创建事件流查询客户端
514
- const eventClient = factory.createEventStreamQueryClient({
515
- aggregateName: 'cart',
516
- });
517
- const events = await eventClient.list({ filter: filter.matchAll() });
518
- ```
519
-
520
- **方法:**
521
-
522
- - `createSnapshotQueryClient(options?: QueryClientOptions): SnapshotQueryClient` - 创建用于查询快照的客户端。
523
- - `createLoadStateAggregateClient(options?: QueryClientOptions): LoadStateAggregateClient` - 创建用于按 ID 加载聚合状态的客户端。
524
- - `createOwnerLoadStateAggregateClient(options?: QueryClientOptions): LoadOwnerStateAggregateClient` - 创建用于加载当前所有者聚合状态的客户端。
525
- - `createEventStreamQueryClient(options?: QueryClientOptions): EventStreamQueryClient` - 创建用于查询事件流的客户端。
526
-
527
- #### EventStreamQueryClient
528
-
529
- 用于查询领域事件流的客户端,支持全面的查询操作:
530
-
531
- ```typescript
532
- import {
533
- Fetcher,
534
- FetchExchange,
535
- RequestInterceptor,
536
- URL_RESOLVE_INTERCEPTOR_ORDER,
537
- } from '@ahoo-wang/fetcher';
538
- import '@ahoo-wang/fetcher-eventstream';
539
- import {
540
- EventStreamQueryClient,
541
- filter,
542
- FilterListQuery,
543
- FilterPagedQuery,
544
- } from '@ahoo-wang/fetcher-wow';
545
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
546
-
547
- // 使用基础配置创建 fetcher 实例
548
- const exampleFetcher = new Fetcher({
549
- baseURL: 'http://localhost:8080/',
550
- });
551
-
552
- // 定义当前用户 ID
553
- const currentUserId = idGenerator.generateId();
554
-
555
- // 创建处理 URL 参数的拦截器
556
- class AppendOwnerId implements RequestInterceptor {
557
- readonly name: string = 'AppendOwnerId';
558
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
559
-
560
- intercept(exchange: FetchExchange) {
561
- const urlParams = exchange.ensureRequestUrlParams();
562
- urlParams.path['ownerId'] = currentUserId;
563
- }
564
- }
565
-
566
- // 注册拦截器
567
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
568
-
569
- // 创建事件流查询客户端
570
- const cartEventStreamQueryClient = new EventStreamQueryClient({
571
- fetcher: exampleFetcher,
572
- basePath: 'owner/{ownerId}/cart',
573
- });
574
-
575
- // 统计事件流数量
576
- const count = await cartEventStreamQueryClient.count(filter.matchAll());
577
-
578
- // 列出事件流
579
- const listQuery: FilterListQuery = {
580
- filter: filter.matchAll(),
581
- };
582
- const list = await cartEventStreamQueryClient.list(listQuery);
583
-
584
- // 以流的形式列出事件流
585
- const listStream = await cartEventStreamQueryClient.listStream(listQuery);
586
- for await (const event of listStream) {
587
- const domainEventStream = event.data;
588
- console.log('收到事件流:', domainEventStream);
589
- }
590
-
591
- // 分页查询事件流
592
- const pagedQuery: FilterPagedQuery = {
593
- filter: filter.matchAll(),
594
- };
595
- const paged = await cartEventStreamQueryClient.paged(pagedQuery);
596
- ```
597
-
598
- ##### 方法
599
-
600
- - `count(filter: FilterExpression): Promise<number>` - 统计匹配过滤表达式的领域事件流数量。
601
- - `list(listQuery: FilterListQuery): Promise<Partial<DomainEventStream>[]>` - 检索领域事件流列表。
602
- - `listStream(listQuery: FilterListQuery): Promise<ReadableStream<JsonServerSentEvent<Partial<DomainEventStream>>>>` -
603
- 以服务器发送事件的形式检索领域事件流。
604
- - `paged(pagedQuery: FilterPagedQuery): Promise<PagedList<Partial<DomainEventStream>>>` - 检索领域事件流的分页列表。
605
-
606
- ## 🛠️ 高级用法
607
-
608
- ### 完整的命令和查询流程示例
609
-
610
- ```typescript
611
- import {
612
- Fetcher,
613
- FetchExchange,
614
- HttpMethod,
615
- RequestInterceptor,
616
- URL_RESOLVE_INTERCEPTOR_ORDER,
617
- } from '@ahoo-wang/fetcher';
618
- import '@ahoo-wang/fetcher-eventstream';
619
- import {
620
- CommandClient,
621
- CommandRequest,
622
- CommandHeaders,
623
- CommandStage,
624
- SnapshotQueryClient,
625
- filter,
626
- FilterListQuery,
627
- } from '@ahoo-wang/fetcher-wow';
628
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
629
-
630
- interface CartItem {
631
- productId: string;
632
- quantity: number;
633
- }
17
+ ```ts
18
+ import { Fetcher } from '@ahoo-wang/fetcher';
19
+ import { SnapshotQueryClient, filter, listQuery } from '@ahoo-wang/fetcher-wow';
634
20
 
635
21
  interface CartState {
636
- id: string;
637
- items: CartItem[];
638
- }
639
-
640
- // 创建 fetcher 实例
641
- const exampleFetcher = new Fetcher({
642
- baseURL: 'http://localhost:8080/',
643
- });
644
-
645
- // 定义当前用户 ID
646
- const currentUserId = idGenerator.generateId();
647
-
648
- // 创建处理 URL 参数的拦截器
649
- class AppendOwnerId implements RequestInterceptor {
650
- readonly name: string = 'AppendOwnerId';
651
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
652
-
653
- intercept(exchange: FetchExchange) {
654
- const urlParams = exchange.ensureRequestUrlParams();
655
- urlParams.path['ownerId'] = currentUserId;
656
- }
22
+ status: 'ACTIVE' | 'CHECKED_OUT';
657
23
  }
658
24
 
659
- // 注册拦截器
660
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
661
-
662
- // 创建客户端
663
- const cartCommandClient = new CommandClient({
664
- fetcher: exampleFetcher,
665
- basePath: 'owner/{ownerId}/cart',
25
+ const fetcher = new Fetcher({ baseURL: 'https://api.example.com' });
26
+ const snapshots = new SnapshotQueryClient<CartState>({
27
+ fetcher,
28
+ basePath: 'cart',
666
29
  });
667
30
 
668
- const cartSnapshotQueryClient = new SnapshotQueryClient<CartState>({
669
- fetcher: exampleFetcher,
670
- basePath: 'owner/{ownerId}/cart',
671
- });
672
-
673
- // 定义命令端点
674
- class CartCommandEndpoints {
675
- static readonly addCartItem = 'add_cart_item';
676
- }
677
-
678
- // 定义命令接口
679
- interface AddCartItem {
680
- productId: string;
681
- quantity: number;
682
- }
683
-
684
- type AddCartItemCommand = CommandRequest<AddCartItem>;
685
-
686
- // 1. 发送命令添加商品到购物车
687
- const addItemCommand: AddCartItemCommand = {
688
- method: HttpMethod.POST,
689
- headers: {
690
- [CommandHeaders.WAIT_STAGE]: CommandStage.SNAPSHOT,
691
- },
692
- body: {
693
- productId: 'product-123',
694
- quantity: 2,
695
- },
696
- };
697
-
698
- const commandResult = await cartCommandClient.send(
699
- CartCommandEndpoints.addCartItem,
700
- addItemCommand,
31
+ const carts = await snapshots.listState(
32
+ listQuery({
33
+ filter: filter.and([
34
+ filter.ownerId('u-42'),
35
+ filter.eq('state.status', 'ACTIVE'),
36
+ ]),
37
+ limit: 50,
38
+ }),
701
39
  );
702
- console.log('命令执行完成:', commandResult);
703
-
704
- // 2. 查询更新后的购物车
705
- const listQuery: FilterListQuery = {
706
- filter: filter.matchAll(),
707
- };
708
- const carts = await cartSnapshotQueryClient.list(listQuery);
709
-
710
- for (const cart of carts) {
711
- console.log('购物车:', cart.state);
712
- }
713
-
714
- // 3. 流式监听购物车更新
715
- const listStream = await cartSnapshotQueryClient.listStream(listQuery);
716
- for await (const event of listStream) {
717
- const cart = event.data;
718
- console.log('购物车更新:', cart.state);
719
- }
720
- ```
721
-
722
- ## 🧪 测试
723
-
724
- ```bash
725
- # 运行测试
726
- pnpm test
727
-
728
- # 运行带覆盖率的测试
729
- pnpm test --coverage
730
40
  ```
731
41
 
732
- ## 🤝 贡献
733
-
734
- 欢迎贡献!请查看
735
- [贡献指南](https://github.com/Ahoo-Wang/fetcher/blob/main/wiki/guide/contributing.md) 获取更多详情。
42
+ ## 核心能力
736
43
 
737
- ## 📄 许可证
44
+ - 命令结果与流式等待阶段。
45
+ - 快照、领域事件、状态加载与所有者状态客户端。
46
+ - 提前校验的数组优先 `FilterExpression` 构建器。
47
+ - 单条、列表、分页、游标、计数与流查询契约。
48
+ - 投影、排序、嵌套聚合、建模、ABAC 与元数据类型。
738
49
 
739
- Apache-2.0
50
+ ## 文档
740
51
 
741
- ---
52
+ - [Wow CQRS 实战](https://fetcher.ahoo.me/zh/recipes/wow-cqrs)
53
+ - [Wow 参考](https://fetcher.ahoo.me/zh/reference/wow)
54
+ - [交互式查询 Story](https://fetcher.ahoo.me/storybook/)
742
55
 
743
- <p align="center">
744
- <a href="https://github.com/Ahoo-Wang/fetcher">Fetcher</a> 生态系统的一部分
745
- </p>
56
+ [English](./README.md) · [许可证](../../LICENSE)