@ahoo-wang/fetcher-wow 3.18.0 β†’ 3.18.1

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.md CHANGED
@@ -1,996 +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-Interactive%20Docs-FF4785)](https://fetcher.ahoo.me/?path=/docs/wow-introduction--docs)
3
+ Typed Fetcher clients and contracts for Wow commands, snapshots, domain events,
4
+ filters, pagination, and aggregation. Use it only against Wow HTTP endpoints.
11
5
 
12
- Support for [Wow](https://github.com/Ahoo-Wang/Wow) framework in Fetcher. Provides TypeScript types and utilities for
13
- working with the Wow CQRS/DDD framework.
14
-
15
- ## 🌟 Features
16
-
17
- - **πŸ”„ CQRS Pattern Implementation**: First-class support for Command Query Responsibility Segregation architectural
18
- pattern
19
- - **🧱 DDD Primitives**: Essential Domain-Driven Design building blocks including aggregates, events, and value objects
20
- - **πŸ“¦ Complete TypeScript Support**: Full type definitions for all Wow framework entities including commands, events,
21
- and queries
22
- - **πŸ“‘ Real-time Event Streaming**: Built-in support for Server-Sent Events to receive real-time command results and data
23
- updates
24
- - **πŸš€ Command Client**: High-level client for sending commands to Wow services with both synchronous and streaming
25
- responses
26
- - **πŸ” Powerful Query DSL**: Typed `FilterExpression` builders with comprehensive operator support
27
- - **πŸ“Š Snapshot Aggregation**: Group and aggregate snapshot data with typed queries
28
- - **πŸ” Query Clients**: Specialized clients for querying snapshot and event stream data with comprehensive query
29
- operations:
30
- - Counting resources
31
- - Listing resources
32
- - Streaming resources as Server-Sent Events
33
- - Paging resources
34
- - Retrieving single resources
35
-
36
- ## πŸš€ Quick Start
37
-
38
- ### Installation
6
+ ## Install
39
7
 
40
8
  ```bash
41
- # Using npm
42
- npm install @ahoo-wang/fetcher-wow
43
-
44
- # Using pnpm
45
- pnpm add @ahoo-wang/fetcher-wow
46
-
47
- # Using yarn
48
- yarn add @ahoo-wang/fetcher-wow
49
- ```
50
-
51
- ## πŸ“š API Reference
52
-
53
- ### Command Module
54
-
55
- #### CommandResult
56
-
57
- Interface representing the result of command execution:
58
-
59
- ```typescript
60
- import { CommandResult, CommandStage } from '@ahoo-wang/fetcher-wow';
61
- ```
62
-
63
- #### CommandClient
64
-
65
- HTTP client for sending commands to the Wow framework. The client provides methods to send commands and receive results
66
- either synchronously or as a stream of events.
67
-
68
- ```typescript
69
- import {
70
- Fetcher,
71
- FetchExchange,
72
- HttpMethod,
73
- RequestInterceptor,
74
- URL_RESOLVE_INTERCEPTOR_ORDER,
75
- } from '@ahoo-wang/fetcher';
76
- import '@ahoo-wang/fetcher-eventstream';
77
- import {
78
- CommandClient,
79
- CommandRequest,
80
- CommandHeaders,
81
- CommandStage,
82
- } from '@ahoo-wang/fetcher-wow';
83
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
84
-
85
- // Create a fetcher instance with base configuration
86
- const exampleFetcher = new Fetcher({
87
- baseURL: 'http://localhost:8080/',
88
- });
89
-
90
- // Define current user ID
91
- const currentUserId = idGenerator.generateId();
92
-
93
- // Create an interceptor to handle URL parameters
94
- class AppendOwnerId implements RequestInterceptor {
95
- readonly name: string = 'AppendOwnerId';
96
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
97
-
98
- intercept(exchange: FetchExchange) {
99
- const urlParams = exchange.ensureRequestUrlParams();
100
- urlParams.path['ownerId'] = currentUserId;
101
- }
102
- }
103
-
104
- // Register the interceptor
105
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
106
-
107
- // Create the command client
108
- const cartCommandClient = new CommandClient({
109
- fetcher: exampleFetcher,
110
- basePath: 'owner/{ownerId}/cart',
111
- });
112
-
113
- // Define command endpoints
114
- class CartCommandEndpoints {
115
- static readonly addCartItem = 'add_cart_item';
116
- }
117
-
118
- // Define command interfaces
119
- interface AddCartItem {
120
- productId: string;
121
- quantity: number;
122
- }
123
-
124
- type AddCartItemCommand = CommandRequest<AddCartItem>;
125
-
126
- // Create a command request
127
- const addCartItemCommand: AddCartItemCommand = {
128
- method: HttpMethod.POST,
129
- headers: {
130
- [CommandHeaders.WAIT_STAGE]: CommandStage.SNAPSHOT,
131
- },
132
- body: {
133
- productId: 'productId',
134
- quantity: 1,
135
- },
136
- };
137
-
138
- // Send command and wait for result
139
- const commandResult = await cartCommandClient.send(
140
- CartCommandEndpoints.addCartItem,
141
- addCartItemCommand,
142
- );
143
-
144
- // Send command and receive results as a stream of events
145
- const commandResultStream = await cartCommandClient.sendAndWaitStream(
146
- CartCommandEndpoints.addCartItem,
147
- addCartItemCommand,
148
- );
149
- for await (const commandResultEvent of commandResultStream) {
150
- console.log('Received command result:', commandResultEvent.data);
151
- }
152
- ```
153
-
154
- ##### Methods
155
-
156
- - `send(path: string, commandRequest: CommandRequest): Promise<CommandResult>` - Sends a command and waits for the
157
- result.
158
- - `sendAndWaitStream(path: string, commandRequest: CommandRequest): Promise<CommandResultEventStream>` - Sends a command
159
- and returns a stream of results as Server-Sent Events.
160
-
161
- ### Query Module
162
-
163
- #### Filter Expression Builder
164
-
165
- Wow 8.11+ queries use `FilterExpression`:
166
-
167
- ```typescript
168
- import {
169
- DeletionState,
170
- filter,
171
- SearchMode,
172
- TimeUnit,
173
- } from '@ahoo-wang/fetcher-wow';
174
-
175
- const expression = filter.and([
176
- filter.deletion(DeletionState.ACTIVE),
177
- filter.eq('state.status', 'PAID'),
178
- filter.elementMatch('state.items', filter.gt('quantity', 0)),
179
- filter.search('event sourcing', {
180
- mode: SearchMode.PHRASE,
181
- fields: ['state.title', 'state.description'],
182
- }),
183
- filter.yesterday('state.createdAt', {
184
- zoneId: 'Asia/Shanghai',
185
- timeUnit: TimeUnit.MILLISECONDS,
186
- }),
187
- ]);
188
- ```
189
-
190
- Builders are grouped under `filter`: `matchAll`, `matchNone`, `and`, `or`,
191
- `nor`, comparisons, string/collection predicates, presence checks,
192
- `elementMatch`, `search`, deletion scope, and relative-time filters.
193
- `and`, `or`, `nor`, `ids`, `aggregateIds`, `isIn`, `notIn`, and
194
- `containsAll` accept one non-empty `readonly` array and throw `TypeError` for
195
- an empty array.
196
-
197
- #### Condition Builder (Deprecated)
198
-
199
- The legacy Condition API remains available for compatibility with older Wow
200
- servers. New code should use `FilterExpression` and `filter.*`.
201
-
202
- ```typescript
203
- import {
204
- and,
205
- or,
206
- eq,
207
- ne,
208
- gt,
209
- lt,
210
- gte,
211
- lte,
212
- contains,
213
- isIn,
214
- notIn,
215
- between,
216
- allIn,
217
- startsWith,
218
- endsWith,
219
- match,
220
- elemMatch,
221
- isNull,
222
- notNull,
223
- isTrue,
224
- isFalse,
225
- exists,
226
- raw,
227
- today,
228
- beforeToday,
229
- tomorrow,
230
- thisWeek,
231
- nextWeek,
232
- lastWeek,
233
- thisMonth,
234
- lastMonth,
235
- recentDays,
236
- earlierDays,
237
- active,
238
- all,
239
- id,
240
- ids,
241
- aggregateId,
242
- aggregateIds,
243
- tenantId,
244
- ownerId,
245
- } from '@ahoo-wang/fetcher-wow';
246
-
247
- // Simple conditions
248
- const simpleConditions = [
249
- eq('name', 'John'),
250
- ne('status', 'inactive'),
251
- gt('age', 18),
252
- lt('score', 100),
253
- gte('rating', 4.0),
254
- lte('price', 100),
255
- ];
256
-
257
- // String conditions
258
- const stringConditions = [
259
- contains('email', '@company.com'),
260
- startsWith('username', 'j'),
261
- endsWith('domain', '.com'),
262
- isIn('status', 'active', 'pending'),
263
- notIn('role', 'guest', 'banned'),
264
- match('description', 'search keywords'),
265
- ];
266
-
267
- // Null checks
268
- const nullConditions = [
269
- isNull('deletedAt'),
270
- notNull('email'),
271
- isTrue('isActive'),
272
- isFalse('isDeleted'),
273
- exists('phoneNumber'),
274
- ];
275
-
276
- // Array conditions
277
- const arrayConditions = [
278
- allIn('tags', 'react', 'typescript'),
279
- elemMatch('items', eq('quantity', 0)),
280
- ];
281
-
282
- // Date conditions
283
- const dateConditions = [
284
- today('createdAt'),
285
- beforeToday('lastLogin', '09:30'),
286
- tomorrow('scheduledDate'),
287
- thisWeek('updatedAt'),
288
- nextWeek('startDate'),
289
- lastWeek('endDate'),
290
- thisMonth('createdDate'),
291
- lastMonth('expirationDate'),
292
- recentDays('createdAt', 5), // Last 5 days including today
293
- earlierDays('createdAt', 3), // More than 3 days ago
294
- ];
295
-
296
- // Complex conditions
297
- const complexCondition = and(
298
- eq('tenantId', 'tenant-123'),
299
- or(
300
- contains('email', '@company.com'),
301
- isIn('department', 'engineering', 'marketing'),
302
- ),
303
- between('salary', 50000, 100000),
304
- today('createdAt'),
305
- active(),
306
- );
307
-
308
- // Raw condition for advanced use cases
309
- const rawCondition = raw({ $text: { $search: 'keywords' } });
310
- ```
311
-
312
- **Operator Reference:**
313
-
314
- | Category | Operators |
315
- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
316
- | Logical | `and`, `or`, `nor` |
317
- | Comparison | `eq`, `ne`, `gt`, `lt`, `gte`, `lte` |
318
- | String | `contains`, `startsWith`, `endsWith`, `match` |
319
- | Collection | `isIn`, `notIn`, `allIn`, `elemMatch` |
320
- | Null/Boolean | `isNull`, `notNull`, `isTrue`, `isFalse`, `exists` |
321
- | Date | `today`, `beforeToday(time)`, `tomorrow`, `thisWeek`, `nextWeek`, `lastWeek`, `thisMonth`, `lastMonth`, `recentDays(days)`, `earlierDays(days)` |
322
- | ID | `id`, `ids`, `aggregateId`, `aggregateIds`, `tenantId`, `ownerId` |
323
- | State | `active`, `all`, `deleted` |
324
- | Special | `raw` (for advanced database-specific queries) |
325
-
326
- #### SnapshotQueryClient
327
-
328
- Client for querying materialized snapshots with comprehensive query operations:
329
-
330
- ```typescript
331
- import {
332
- Fetcher,
333
- FetchExchange,
334
- RequestInterceptor,
335
- URL_RESOLVE_INTERCEPTOR_ORDER,
336
- } from '@ahoo-wang/fetcher';
337
- import '@ahoo-wang/fetcher-eventstream';
338
- import {
339
- aggregation,
340
- SnapshotQueryClient,
341
- filter,
342
- FilterListQuery,
343
- FilterPagedQuery,
344
- FilterSingleQuery,
345
- type AggregationQuery,
346
- Identifier,
347
- } from '@ahoo-wang/fetcher-wow';
348
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
349
-
350
- interface CartItem {
351
- productId: string;
352
- price: number;
353
- quantity: number;
354
- }
355
-
356
- interface CartState extends Identifier {
357
- status: string;
358
- items: CartItem[];
359
- }
360
-
361
- // Create a fetcher instance with base configuration
362
- const exampleFetcher = new Fetcher({
363
- baseURL: 'http://localhost:8080/',
364
- });
365
-
366
- // Define current user ID
367
- const currentUserId = idGenerator.generateId();
368
-
369
- // Create an interceptor to handle URL parameters
370
- class AppendOwnerId implements RequestInterceptor {
371
- readonly name: string = 'AppendOwnerId';
372
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
373
-
374
- intercept(exchange: FetchExchange) {
375
- const urlParams = exchange.ensureRequestUrlParams();
376
- urlParams.path['ownerId'] = currentUserId;
377
- }
378
- }
379
-
380
- // Register the interceptor
381
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
382
-
383
- // Create the snapshot query client
384
- const cartSnapshotQueryClient = new SnapshotQueryClient<CartState>({
385
- fetcher: exampleFetcher,
386
- basePath: 'owner/{ownerId}/cart',
387
- });
388
-
389
- // Count snapshots
390
- const count = await cartSnapshotQueryClient.count(filter.matchAll());
391
-
392
- // List snapshots
393
- const listQuery: FilterListQuery = {
394
- filter: filter.matchAll(),
395
- };
396
- const list = await cartSnapshotQueryClient.list(listQuery);
397
-
398
- // List snapshots as stream
399
- const listStream = await cartSnapshotQueryClient.listStream(listQuery);
400
- for await (const event of listStream) {
401
- const snapshot = event.data;
402
- console.log('Received snapshot:', snapshot);
403
- }
404
-
405
- // List snapshot states
406
- const stateList = await cartSnapshotQueryClient.listState(listQuery);
407
-
408
- // List snapshot states as stream
409
- const stateStream = await cartSnapshotQueryClient.listStateStream(listQuery);
410
- for await (const event of stateStream) {
411
- const state = event.data;
412
- console.log('Received state:', state);
413
- }
414
-
415
- // Paged snapshots
416
- const pagedQuery: FilterPagedQuery = {
417
- filter: filter.matchAll(),
418
- };
419
- const paged = await cartSnapshotQueryClient.paged(pagedQuery);
420
-
421
- // Paged snapshot states
422
- const pagedState = await cartSnapshotQueryClient.pagedState(pagedQuery);
423
-
424
- // Single snapshot
425
- const singleQuery: FilterSingleQuery = {
426
- filter: filter.matchAll(),
427
- };
428
- const single = await cartSnapshotQueryClient.single(singleQuery);
429
-
430
- // Single snapshot state
431
- const singleState = await cartSnapshotQueryClient.singleState(singleQuery);
432
-
433
- type CartFields = 'state.status' | 'state.items';
434
- type ItemFields = 'productId' | 'price' | 'quantity';
435
-
436
- type ProductSummary = {
437
- product: string;
438
- representativeProduct: string | null;
439
- itemCount: number;
440
- revenue: number;
441
- };
442
-
443
- const revenue = aggregation.multiply(
444
- aggregation.field<ItemFields>('price'),
445
- aggregation.field<ItemFields>('quantity'),
446
- );
447
-
448
- const aggregationQuery: AggregationQuery<CartFields, ItemFields> = {
449
- filter: filter.eq('state.status', 'COMPLETED'),
450
- elements: [aggregation.element('state.items', filter.gt('quantity', 0))],
451
- groupBy: [aggregation.terms('productId', 'product')],
452
- metrics: [
453
- aggregation.any('productId', 'representativeProduct'),
454
- aggregation.count('itemCount'),
455
- aggregation.sum(revenue, 'revenue'),
456
- ],
457
- };
458
-
459
- const summaries =
460
- await cartSnapshotQueryClient.aggregate<ProductSummary>(aggregationQuery);
461
- const summaryStream =
462
- await cartSnapshotQueryClient.aggregateStream<ProductSummary>(
463
- aggregationQuery,
464
- );
465
- for await (const event of summaryStream) {
466
- console.log(event.data);
467
- }
468
- ```
469
-
470
- `aggregation.any(field, alias)` adds a metric, not another group. It returns one
471
- non-null scalar from the current group, or `null` when no value exists. The
472
- selected value is intentionally unspecified and may differ across backends or
473
- executions; use it only when every candidate is interchangeable. Its field is
474
- relative to the innermost element, and Wow rejects collection or non-terms-capable
475
- fields at runtime. Sorting by an `ANY` alias is an expensive metric sort.
476
-
477
- ##### Methods
478
-
479
- - `count(filter: FilterExpression): Promise<number>` - Counts snapshots matching the filter expression.
480
- - `list(listQuery: FilterListQuery): Promise<Partial<MaterializedSnapshot<S>>[]>` - Retrieves a list of materialized
481
- snapshots.
482
- - `listStream(listQuery: FilterListQuery): Promise<ReadableStream<JsonServerSentEvent<Partial<MaterializedSnapshot<S>>>>>` -
483
- Retrieves a stream of materialized snapshots as Server-Sent Events.
484
- - `listState(listQuery: FilterListQuery): Promise<Partial<S>[]>` - Retrieves a list of snapshot states.
485
- - `listStateStream(listQuery: FilterListQuery): Promise<ReadableStream<JsonServerSentEvent<Partial<S>>>>` - Retrieves a stream
486
- of snapshot states as Server-Sent Events.
487
- - `paged(pagedQuery: FilterPagedQuery): Promise<PagedList<Partial<MaterializedSnapshot<S>>>>` - Retrieves a paged list of
488
- materialized snapshots.
489
- - `pagedState(pagedQuery: FilterPagedQuery): Promise<PagedList<Partial<S>>>` - Retrieves a paged list of snapshot states.
490
- - `single(singleQuery: FilterSingleQuery): Promise<Partial<MaterializedSnapshot<S>>>` - Retrieves a single materialized
491
- snapshot.
492
- - `singleState(singleQuery: FilterSingleQuery): Promise<Partial<S>>` - Retrieves a single snapshot state.
493
- - `aggregate<Row extends DynamicDocument = DynamicDocument, AGGREGATION_FIELDS extends string = string>(query: AggregationQuery<FIELDS, AGGREGATION_FIELDS>): Promise<Row[]>` - Runs a
494
- snapshot aggregation and requests `snapshot/aggregation`.
495
- - `aggregateStream<Row extends DynamicDocument = DynamicDocument, AGGREGATION_FIELDS extends string = string>(query: AggregationQuery<FIELDS, AGGREGATION_FIELDS>): Promise<ReadableStream<JsonServerSentEvent<Row>>>` - Runs a snapshot aggregation and requests `snapshot/aggregation` using Server-Sent Events (SSE).
496
-
497
- #### QueryClientFactory
498
-
499
- Factory for creating pre-configured query clients. Useful when you need multiple clients with shared configuration.
500
-
501
- ```typescript
502
- import {
503
- filter,
504
- QueryClientFactory,
505
- ResourceAttributionPathSpec,
506
- } from '@ahoo-wang/fetcher-wow';
507
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
508
-
509
- // Create a factory with default options
510
- const factory = new QueryClientFactory({
511
- contextAlias: 'example',
512
- aggregateName: 'cart',
513
- resourceAttribution: ResourceAttributionPathSpec.OWNER,
514
- fetcher: exampleFetcher,
515
- });
516
-
517
- // Create a snapshot query client
518
- const snapshotClient = factory.createSnapshotQueryClient({
519
- aggregateName: 'cart',
520
- });
521
- const carts = await snapshotClient.listState({ filter: filter.matchAll() });
522
-
523
- // Create a state aggregate client
524
- const stateClient = factory.createLoadStateAggregateClient({
525
- aggregateName: 'cart',
526
- });
527
- const cart = await stateClient.load('cart-123');
528
-
529
- // Create an event stream query client
530
- const eventClient = factory.createEventStreamQueryClient({
531
- aggregateName: 'cart',
532
- });
533
- const events = await eventClient.list({ filter: filter.matchAll() });
9
+ pnpm add @ahoo-wang/fetcher @ahoo-wang/fetcher-decorator \
10
+ @ahoo-wang/fetcher-eventstream @ahoo-wang/fetcher-wow
534
11
  ```
535
12
 
536
- **Methods:**
537
-
538
- - `createSnapshotQueryClient(options?: QueryClientOptions): SnapshotQueryClient` - Creates a client for querying snapshots.
539
- - `createLoadStateAggregateClient(options?: QueryClientOptions): LoadStateAggregateClient` - Creates a client for loading aggregate state by ID.
540
- - `createOwnerLoadStateAggregateClient(options?: QueryClientOptions): LoadOwnerStateAggregateClient` - Creates a client for loading the current owner's aggregate state.
541
- - `createEventStreamQueryClient(options?: QueryClientOptions): EventStreamQueryClient` - Creates a client for querying event streams.
542
-
543
- #### EventStreamQueryClient
544
-
545
- Client for querying domain event streams with comprehensive query operations:
546
-
547
- ```typescript
548
- import {
549
- Fetcher,
550
- FetchExchange,
551
- RequestInterceptor,
552
- URL_RESOLVE_INTERCEPTOR_ORDER,
553
- } from '@ahoo-wang/fetcher';
554
- import '@ahoo-wang/fetcher-eventstream';
555
- import {
556
- EventStreamQueryClient,
557
- filter,
558
- FilterListQuery,
559
- FilterPagedQuery,
560
- } from '@ahoo-wang/fetcher-wow';
561
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
562
-
563
- // Create a fetcher instance with base configuration
564
- const exampleFetcher = new Fetcher({
565
- baseURL: 'http://localhost:8080/',
566
- });
13
+ Peer dependencies: `fetcher`, `fetcher-decorator`, and `fetcher-eventstream`.
567
14
 
568
- // Define current user ID
569
- const currentUserId = idGenerator.generateId();
15
+ ## Example
570
16
 
571
- // Create an interceptor to handle URL parameters
572
- class AppendOwnerId implements RequestInterceptor {
573
- readonly name: string = 'AppendOwnerId';
574
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
17
+ ```ts
18
+ import { Fetcher } from '@ahoo-wang/fetcher';
19
+ import { SnapshotQueryClient, filter, listQuery } from '@ahoo-wang/fetcher-wow';
575
20
 
576
- intercept(exchange: FetchExchange) {
577
- const urlParams = exchange.ensureRequestUrlParams();
578
- urlParams.path['ownerId'] = currentUserId;
579
- }
21
+ interface CartState {
22
+ status: 'ACTIVE' | 'CHECKED_OUT';
580
23
  }
581
24
 
582
- // Register the interceptor
583
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
584
-
585
- // Create the event stream query client
586
- const cartEventStreamQueryClient = new EventStreamQueryClient({
587
- fetcher: exampleFetcher,
588
- 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',
589
29
  });
590
30
 
591
- // Count event streams
592
- const count = await cartEventStreamQueryClient.count(filter.matchAll());
593
-
594
- // List event streams
595
- const listQuery: FilterListQuery = {
596
- filter: filter.matchAll(),
597
- };
598
- const list = await cartEventStreamQueryClient.list(listQuery);
599
-
600
- // List event streams as stream
601
- const listStream = await cartEventStreamQueryClient.listStream(listQuery);
602
- for await (const event of listStream) {
603
- const domainEventStream = event.data;
604
- console.log('Received event stream:', domainEventStream);
605
- }
606
-
607
- // Paged event streams
608
- const pagedQuery: FilterPagedQuery = {
609
- filter: filter.matchAll(),
610
- };
611
- const paged = await cartEventStreamQueryClient.paged(pagedQuery);
612
- ```
613
-
614
- ##### Methods
615
-
616
- - `count(filter: FilterExpression): Promise<number>` - Counts domain event streams matching the filter expression.
617
- - `list(listQuery: FilterListQuery): Promise<Partial<DomainEventStream>[]>` - Retrieves a list of domain event streams.
618
- - `listStream(listQuery: FilterListQuery): Promise<ReadableStream<JsonServerSentEvent<Partial<DomainEventStream>>>>` -
619
- Retrieves a stream of domain event streams as Server-Sent Events.
620
- - `paged(pagedQuery: FilterPagedQuery): Promise<PagedList<Partial<DomainEventStream>>>` - Retrieves a paged list of domain
621
- event streams.
622
-
623
- ## πŸš€ Advanced Usage Examples
624
-
625
- ### Custom Command Builders and Validators
626
-
627
- Create type-safe command builders with validation:
628
-
629
- ```typescript
630
- import { CommandClient, CommandRequest } from '@ahoo-wang/fetcher-wow';
631
-
632
- // Command type definitions
633
- interface CreateUserCommand {
634
- commandType: 'CreateUser';
635
- commandId: string;
636
- aggregateId: string;
637
- name: string;
638
- email: string;
639
- role: 'admin' | 'user' | 'moderator';
640
- }
641
-
642
- interface UpdateUserProfileCommand {
643
- commandType: 'UpdateUserProfile';
644
- commandId: string;
645
- aggregateId: string;
646
- displayName?: string;
647
- bio?: string;
648
- avatarUrl?: string;
649
- }
650
-
651
- // Command builder with validation
652
- class UserCommandBuilder {
653
- private commandClient: CommandClient;
654
-
655
- constructor(commandClient: CommandClient) {
656
- this.commandClient = commandClient;
657
- }
658
-
659
- async createUser(params: {
660
- name: string;
661
- email: string;
662
- role: 'admin' | 'user' | 'moderator';
663
- ownerId: string;
664
- }): Promise<any> {
665
- // Validate input
666
- this.validateCreateUserParams(params);
667
-
668
- const command: CreateUserCommand = {
669
- commandType: 'CreateUser',
670
- commandId: crypto.randomUUID(),
671
- aggregateId: crypto.randomUUID(), // New user gets new aggregate ID
672
- ...params,
673
- };
674
-
675
- return this.commandClient.send(command, { ownerId: params.ownerId });
676
- }
677
-
678
- async updateProfile(params: {
679
- userId: string;
680
- displayName?: string;
681
- bio?: string;
682
- avatarUrl?: string;
683
- ownerId: string;
684
- }): Promise<any> {
685
- // Validate input
686
- this.validateUpdateProfileParams(params);
687
-
688
- const command: UpdateUserProfileCommand = {
689
- commandType: 'UpdateUserProfile',
690
- commandId: crypto.randomUUID(),
691
- aggregateId: params.userId, // User ID is the aggregate ID
692
- displayName: params.displayName,
693
- bio: params.bio,
694
- avatarUrl: params.avatarUrl,
695
- };
696
-
697
- return this.commandClient.send(command, { ownerId: params.ownerId });
698
- }
699
-
700
- private validateCreateUserParams(params: any) {
701
- if (!params.name || params.name.length < 2) {
702
- throw new Error('Name must be at least 2 characters');
703
- }
704
- if (!params.email || !this.isValidEmail(params.email)) {
705
- throw new Error('Valid email is required');
706
- }
707
- if (!['admin', 'user', 'moderator'].includes(params.role)) {
708
- throw new Error('Invalid role');
709
- }
710
- }
711
-
712
- private validateUpdateProfileParams(params: any) {
713
- if (!params.userId) {
714
- throw new Error('User ID is required');
715
- }
716
- if (params.displayName && params.displayName.length > 50) {
717
- throw new Error('Display name too long');
718
- }
719
- if (params.bio && params.bio.length > 500) {
720
- throw new Error('Bio too long');
721
- }
722
- }
723
-
724
- private isValidEmail(email: string): boolean {
725
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
726
- return emailRegex.test(email);
727
- }
728
- }
729
-
730
- // Usage
731
- const commandClient = new CommandClient({ basePath: '/api/commands' });
732
- const userCommands = new UserCommandBuilder(commandClient);
733
-
734
- try {
735
- // Create a new user
736
- const result = await userCommands.createUser({
737
- name: 'John Doe',
738
- email: 'john@example.com',
739
- role: 'user',
740
- ownerId: 'user-123',
741
- });
742
- console.log('User created:', result);
743
-
744
- // Update user profile
745
- await userCommands.updateProfile({
746
- userId: result.aggregateId,
747
- displayName: 'Johnny',
748
- bio: 'Software developer',
749
- ownerId: 'user-123',
750
- });
751
- } catch (error) {
752
- console.error('Command failed:', error);
753
- }
754
- ```
755
-
756
- ### Advanced Query Composition with Reactive Updates
757
-
758
- Create complex queries with reactive real-time updates:
759
-
760
- ```typescript
761
- import {
762
- EventStreamQueryClient,
763
- filter,
764
- SnapshotQueryClient,
765
- } from '@ahoo-wang/fetcher-wow';
766
-
767
- // Advanced query manager with reactive updates
768
- class ReactiveQueryManager {
769
- private snapshotClient: SnapshotQueryClient;
770
- private streamClient: EventStreamQueryClient;
771
- private listeners: Map<string, (data: any) => void> = new Map();
772
-
773
- constructor(basePath: string) {
774
- this.snapshotClient = new SnapshotQueryClient({ basePath });
775
- this.streamClient = new EventStreamQueryClient({ basePath });
776
- }
777
-
778
- // Subscribe to real-time updates for a query
779
- subscribeToQuery(
780
- queryId: string,
781
- initialQuery: any,
782
- callback: (data: any) => void,
783
- ) {
784
- this.listeners.set(queryId, callback);
785
-
786
- // Start streaming updates
787
- this.startStreaming(queryId, initialQuery);
788
- }
789
-
790
- // Unsubscribe from updates
791
- unsubscribe(queryId: string) {
792
- this.listeners.delete(queryId);
793
- // Close stream if needed
794
- }
795
-
796
- private async startStreaming(queryId: string, query: any) {
797
- try {
798
- const stream = await this.streamClient.listStream(query);
799
-
800
- for await (const event of stream) {
801
- const listener = this.listeners.get(queryId);
802
- if (listener) {
803
- listener(event);
804
- }
805
- }
806
- } catch (error) {
807
- console.error(`Stream error for ${queryId}:`, error);
808
- }
809
- }
810
-
811
- // Complex query with aggregations
812
- async getUserDashboardStats(userId: string) {
813
- const [userProfile, recentActivity, stats] = await Promise.all([
814
- this.snapshotClient.single({
815
- filter: filter.eq('aggregateId', userId),
816
- }),
817
- this.snapshotClient.list({
818
- filter: filter.and([
819
- filter.eq('state.userId', userId),
820
- filter.eq('state.type', 'activity'),
821
- ]),
822
- limit: 10,
823
- }),
824
- this.snapshotClient.count(filter.eq('state.userId', userId)),
825
- ]);
826
-
827
- return {
828
- profile: userProfile,
829
- recentActivity,
830
- totalActions: stats,
831
- lastActivity: recentActivity[0]?.timestamp,
832
- };
833
- }
834
- }
835
-
836
- // Usage
837
- const queryManager = new ReactiveQueryManager('/api/queries');
838
-
839
- // Get dashboard data
840
- const dashboard = await queryManager.getUserDashboardStats('user-123');
841
- console.log('Dashboard:', dashboard);
842
-
843
- // Subscribe to real-time updates
844
- queryManager.subscribeToQuery(
845
- 'user-activity',
846
- {
31
+ const carts = await snapshots.listState(
32
+ listQuery({
847
33
  filter: filter.and([
848
- filter.eq('state.userId', 'user-123'),
849
- filter.eq('state.type', 'activity'),
34
+ filter.ownerId('u-42'),
35
+ filter.eq('state.status', 'ACTIVE'),
850
36
  ]),
851
- },
852
- update => {
853
- console.log('New activity:', update);
854
- // Update UI with new data
855
- },
856
- );
857
- ```
858
-
859
- ## πŸ› οΈ Advanced Usage
860
-
861
- ```typescript
862
- import {
863
- Fetcher,
864
- FetchExchange,
865
- HttpMethod,
866
- RequestInterceptor,
867
- URL_RESOLVE_INTERCEPTOR_ORDER,
868
- } from '@ahoo-wang/fetcher';
869
- import '@ahoo-wang/fetcher-eventstream';
870
- import {
871
- CommandClient,
872
- CommandRequest,
873
- CommandHeaders,
874
- CommandStage,
875
- SnapshotQueryClient,
876
- filter,
877
- FilterListQuery,
878
- } from '@ahoo-wang/fetcher-wow';
879
- import { idGenerator } from '@ahoo-wang/fetcher-cosec';
880
-
881
- interface CartItem {
882
- productId: string;
883
- quantity: number;
884
- }
885
-
886
- interface CartState {
887
- id: string;
888
- items: CartItem[];
889
- }
890
-
891
- // Create a fetcher instance
892
- const exampleFetcher = new Fetcher({
893
- baseURL: 'http://localhost:8080/',
894
- });
895
-
896
- // Define current user ID
897
- const currentUserId = idGenerator.generateId();
898
-
899
- // Create an interceptor to handle URL parameters
900
- class AppendOwnerId implements RequestInterceptor {
901
- readonly name: string = 'AppendOwnerId';
902
- readonly order: number = URL_RESOLVE_INTERCEPTOR_ORDER - 1;
903
-
904
- intercept(exchange: FetchExchange) {
905
- const urlParams = exchange.ensureRequestUrlParams();
906
- urlParams.path['ownerId'] = currentUserId;
907
- }
908
- }
909
-
910
- // Register the interceptor
911
- exampleFetcher.interceptors.request.use(new AppendOwnerId());
912
-
913
- // Create clients
914
- const cartCommandClient = new CommandClient({
915
- fetcher: exampleFetcher,
916
- basePath: 'owner/{ownerId}/cart',
917
- });
918
-
919
- const cartSnapshotQueryClient = new SnapshotQueryClient<CartState>({
920
- fetcher: exampleFetcher,
921
- basePath: 'owner/{ownerId}/cart',
922
- });
923
-
924
- // Define command endpoints
925
- class CartCommandEndpoints {
926
- static readonly addCartItem = 'add_cart_item';
927
- }
928
-
929
- // Define command interfaces
930
- interface AddCartItem {
931
- productId: string;
932
- quantity: number;
933
- }
934
-
935
- type AddCartItemCommand = CommandRequest<AddCartItem>;
936
-
937
- // 1. Send command to add item to cart
938
- const addItemCommand: AddCartItemCommand = {
939
- method: HttpMethod.POST,
940
- headers: {
941
- [CommandHeaders.WAIT_STAGE]: CommandStage.SNAPSHOT,
942
- },
943
- body: {
944
- productId: 'product-123',
945
- quantity: 2,
946
- },
947
- };
948
-
949
- const commandResult = await cartCommandClient.send(
950
- CartCommandEndpoints.addCartItem,
951
- addItemCommand,
37
+ limit: 50,
38
+ }),
952
39
  );
953
- console.log('Command executed:', commandResult);
954
-
955
- // 2. Query the updated cart
956
- const listQuery: FilterListQuery = {
957
- filter: filter.matchAll(),
958
- };
959
- const carts = await cartSnapshotQueryClient.list(listQuery);
960
-
961
- for (const cart of carts) {
962
- console.log('Cart:', cart.state);
963
- }
964
-
965
- // 3. Stream cart updates
966
- const listStream = await cartSnapshotQueryClient.listStream(listQuery);
967
- for await (const event of listStream) {
968
- const cart = event.data;
969
- console.log('Cart updated:', cart.state);
970
- }
971
40
  ```
972
41
 
973
- ## πŸ§ͺ Testing
974
-
975
- ```bash
976
- # Run tests
977
- pnpm test
978
-
979
- # Run tests with coverage
980
- pnpm test --coverage
981
- ```
982
-
983
- ## 🀝 Contributing
984
-
985
- Contributions are welcome! Please see
986
- the [contributing guide](https://github.com/Ahoo-Wang/fetcher/blob/main/wiki/guide/contributing.md) for more details.
42
+ ## Core capabilities
987
43
 
988
- ## πŸ“„ License
44
+ - Command results and streaming wait stages.
45
+ - Snapshot, domain-event, load-state, and owner-state clients.
46
+ - Array-first `FilterExpression` builders with early validation.
47
+ - Single, list, paged, cursor, count, and stream query contracts.
48
+ - Projection, sorting, nested aggregation, modeling, ABAC, and metadata types.
989
49
 
990
- Apache-2.0
50
+ ## Documentation
991
51
 
992
- ---
52
+ - [Wow CQRS recipe](https://fetcher.ahoo.me/recipes/wow-cqrs)
53
+ - [Wow reference](https://fetcher.ahoo.me/reference/wow)
54
+ - [Interactive query stories](https://fetcher.ahoo.me/storybook/)
993
55
 
994
- <p align="center">
995
- Part of the <a href="https://github.com/Ahoo-Wang/fetcher">Fetcher</a> ecosystem
996
- </p>
56
+ [δΈ­ζ–‡](./README.zh-CN.md) Β· [License](../../LICENSE)