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