@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 +34 -974
- package/README.zh-CN.md +35 -724
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +34 -0
- package/dist/index.es.js.map +1 -1
- package/dist/query/event/eventStreamQueryApi.d.ts +1 -0
- package/dist/query/event/eventStreamQueryApi.d.ts.map +1 -1
- package/dist/query/event/eventStreamQueryClient.d.ts +6 -0
- package/dist/query/event/eventStreamQueryClient.d.ts.map +1 -1
- package/dist/query/queryApi.d.ts +6 -0
- package/dist/query/queryApi.d.ts.map +1 -1
- package/dist/query/snapshot/snapshotQueryApi.d.ts +0 -6
- package/dist/query/snapshot/snapshotQueryApi.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,996 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@ahoo-wang/fetcher-wow`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://codecov.io/gh/Ahoo-Wang/fetcher)
|
|
6
|
-
[](https://github.com/Ahoo-Wang/fetcher/blob/main/LICENSE)
|
|
7
|
-
[](https://www.npmjs.com/package/@ahoo-wang/fetcher-wow)
|
|
8
|
-
[](https://www.npmjs.com/package/@ahoo-wang/fetcher-wow)
|
|
9
|
-
[](https://deepwiki.com/Ahoo-Wang/fetcher)
|
|
10
|
-
[](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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
569
|
-
const currentUserId = idGenerator.generateId();
|
|
15
|
+
## Example
|
|
570
16
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
urlParams.path['ownerId'] = currentUserId;
|
|
579
|
-
}
|
|
21
|
+
interface CartState {
|
|
22
|
+
status: 'ACTIVE' | 'CHECKED_OUT';
|
|
580
23
|
}
|
|
581
24
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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.
|
|
849
|
-
filter.eq('state.
|
|
34
|
+
filter.ownerId('u-42'),
|
|
35
|
+
filter.eq('state.status', 'ACTIVE'),
|
|
850
36
|
]),
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
995
|
-
Part of the <a href="https://github.com/Ahoo-Wang/fetcher">Fetcher</a> ecosystem
|
|
996
|
-
</p>
|
|
56
|
+
[δΈζ](./README.zh-CN.md) Β· [License](../../LICENSE)
|