@rytass/wms-module-graphql 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +56 -3
  2. package/adapters/event-emitter.d.ts +29 -0
  3. package/adapters/event-emitter.js +41 -0
  4. package/adapters/member-base.d.ts +11 -0
  5. package/adapters/member-base.js +17 -1
  6. package/docs/README.md +3 -0
  7. package/docs/api-reference.md +60 -0
  8. package/docs/events.md +500 -0
  9. package/docs/integration-guide.md +111 -2
  10. package/index.cjs.js +154 -1
  11. package/index.d.ts +2 -0
  12. package/index.js +3 -0
  13. package/lib/events/index.d.ts +5 -0
  14. package/lib/events/operation-result-map.d.ts +77 -0
  15. package/lib/events/typed-maps.d.ts +73 -0
  16. package/lib/events/wms-mutation-event.constants.d.ts +12 -0
  17. package/lib/events/wms-mutation-event.constants.js +4 -0
  18. package/lib/events/wms-mutation-event.decorator.d.ts +15 -0
  19. package/lib/events/wms-mutation-event.decorator.js +20 -0
  20. package/lib/events/wms-mutation.interceptor.d.ts +21 -0
  21. package/lib/events/wms-mutation.interceptor.js +90 -0
  22. package/lib/graphql.module.d.ts +24 -5
  23. package/lib/graphql.module.js +16 -2
  24. package/lib/mutations/allocate-inventory-order.mutation.js +5 -0
  25. package/lib/mutations/hold-inventory-order.mutation.js +5 -0
  26. package/lib/mutations/loader.mutation.js +7 -0
  27. package/lib/mutations/location.mutation.js +4 -0
  28. package/lib/mutations/material.mutation.js +4 -0
  29. package/lib/mutations/merge-batch.mutation.js +3 -0
  30. package/lib/mutations/quality-inspection-order.mutation.js +3 -0
  31. package/lib/mutations/receive-inventory-order.mutation.js +5 -0
  32. package/lib/mutations/reclassify-inventory-order.mutation.js +3 -0
  33. package/lib/mutations/scrape-order.mutation.js +3 -0
  34. package/lib/mutations/ship-inventory-order.mutation.js +6 -0
  35. package/lib/mutations/split-batch.mutation.js +3 -0
  36. package/lib/mutations/transfer-customer-order.mutation.js +5 -0
  37. package/lib/mutations/transfer-order.mutation.js +6 -0
  38. package/lib/mutations/transfer-vendor-order.mutation.js +5 -0
  39. package/lib/mutations/warehouse-map.mutation.js +3 -0
  40. package/package.json +13 -3
package/README.md CHANGED
@@ -88,6 +88,57 @@ The member-base adapter reuses everything `CasbinGuard` already put on the reque
88
88
 
89
89
  ➡️ **Full design & recipes: [`docs/authorization.md`](./docs/authorization.md)** (the problem, execution order, member-base / custom-JWT / no-guard recipes, the single coupling point, public API, FAQ).
90
90
 
91
+ ## Events & Hooks
92
+
93
+ Intervene in and get notified about mutations. Every root mutation carries a typed **operation event** (`material.created`, `ship-inventory-order.shipped`, …) and fans out **stock domain events** (`stock.allocated`, `stock.shipped`, …) driven by the `ChangeType` state machine. Wire two opt-in hook points via `forRootAsync`:
94
+
95
+ - **`beforeHooks`** run before any DB work — **throw to veto** the mutation (zero side effects).
96
+ - **`afterListeners`** run **after the outermost transaction commits** — never notified about rolled-back changes.
97
+
98
+ ```ts
99
+ import { memberBaseActorExtractor } from '@rytass/wms-module-graphql/adapters/member-base';
100
+
101
+ WmsModuleGraphqlModule.forRootAsync({
102
+ ...coreOptions,
103
+
104
+ // (v2) populate payload.actor on every event — reads req.payload → { id, domain }
105
+ actorExtractor: memberBaseActorExtractor,
106
+
107
+ // (v2) append materialId/batchId/locationId/loaderId to stock event payloads
108
+ enrichStockEvents: true,
109
+
110
+ beforeHooks: {
111
+ 'ship-inventory-order.shipped': async ({ args, actor }) => {
112
+ if (!allowed(args, actor)) throw new ForbiddenException('blocked');
113
+ },
114
+ },
115
+ afterListeners: {
116
+ // v2: payload.args carries items[]; payload.actor carries { id, domain }
117
+ 'ship-inventory-order.shipped': async ({ payload }) => {
118
+ await erp.markShipped(payload.id, payload.result, payload.actor);
119
+ },
120
+ // v2 with enrichStockEvents: materialId/batchId/locationId/loaderId also available
121
+ 'stock.allocated': async ({ payload }) => { await erp.reserve(payload); },
122
+ },
123
+ });
124
+ ```
125
+
126
+ **v2 payload enrichment** (upcoming minor — `core@0.3.0` / `graphql@0.6.0`):
127
+
128
+ - Operation events gain `args` (the GraphQL input that triggered the mutation) and `actor`
129
+ (`WmsActor | null`). Nine action mutations return `boolean`; their real information is in
130
+ `args` and the stock events they fan out.
131
+ - Stock events gain opt-in `materialId / batchId / locationId / loaderId` when
132
+ `enrichStockEvents: true` is set. **Caveat**: split / merge / reclassify order-items have
133
+ no `materialId` / `batchId` at the item level — only `locationId` / `loaderId` are
134
+ available for those three domains even with eager enrichment.
135
+ - `WmsOperationResultMap` provides a per-event typed overlay so subscribers receive the
136
+ concrete DTO (`MaterialDto`, `ShipInventoryOrderDto`, …) instead of `unknown`.
137
+
138
+ Post-commit delivery is guaranteed by an `AsyncLocalStorage` dispatch scope (events buffer during the request and flush only after commit). Prefer `@OnEvent`? Opt into the optional [`@nestjs/event-emitter`](https://docs.nestjs.com/techniques/events) bridge from `@rytass/wms-module-graphql/adapters/event-emitter` — every event also emits as `wms.<event>`. Nothing configured ⇒ no events, zero overhead.
139
+
140
+ ➡️ **Full design, the complete 56-event list & limitations: [`docs/events.md`](./docs/events.md)**.
141
+
91
142
  ## DataLoader context
92
143
 
93
144
  The module ships a `HelperModule` (auto-registered) that provides a singleton `LoaderService`. To avoid N+1 queries, the WMS resolvers expect a per-request DataLoader trio at `context.loaders`:
@@ -124,14 +175,16 @@ Full documentation is shipped inside this package under the `docs/` directory (a
124
175
  |---|---|
125
176
  | [`docs/integration-guide.md`](./docs/integration-guide.md) | Step-by-step wiring guide for NestJS + Next.js consumers |
126
177
  | [`docs/authorization.md`](./docs/authorization.md) | Complete resolver-authorization design & integration recipes (member-base / custom JWT / no-guard) |
178
+ | [`docs/events.md`](./docs/events.md) | Mutation event/hook system — before-hooks (veto), post-commit after-listeners, complete event list, event-emitter bridge |
127
179
  | [`docs/api-reference.md`](./docs/api-reference.md) | Complete export catalog with signatures and usage examples |
128
180
 
129
181
  GitHub hosted links (always up-to-date):
130
182
 
131
- - [Integration Guide — §4 Backend setup](https://github.com/Rytass/WMS-Module/blob/main/docs/integration-guide.md#4-backend-setup-nestjs--graphql--typeorm)
132
- - [Integration Guide — §13.1 Existing GraphQLModule co-existence](https://github.com/Rytass/WMS-Module/blob/main/docs/integration-guide.md#13-integrating-into-an-existing-nestjs--nextjs-project)
183
+ - [Integration Guide — §4 Backend setup](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/integration-guide.md#4-backend-setup-nestjs--graphql--typeorm)
184
+ - [Integration Guide — §13.1 Existing GraphQLModule co-existence](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/integration-guide.md#13-integrating-into-an-existing-nestjs--nextjs-project)
133
185
  - [Authorization — design & recipes](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/authorization.md)
134
- - [API Reference — @rytass/wms-module-graphql](https://github.com/Rytass/WMS-Module/blob/main/docs/api-reference.md#rytasswms-module-graphql)
186
+ - [Events & Hooks — design & event list](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/events.md)
187
+ - [API Reference — @rytass/wms-module-graphql](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/api-reference.md#rytasswms-module-graphql)
135
188
 
136
189
  ## License
137
190
 
@@ -0,0 +1,29 @@
1
+ import type { Provider } from '@nestjs/common';
2
+ /**
3
+ * Bridges WMS domain events onto `@nestjs/event-emitter`'s `EventEmitter2`, so
4
+ * consumers can subscribe with `@OnEvent('wms.<event-name>')` instead of (or in
5
+ * addition to) the typed `afterListeners`.
6
+ *
7
+ * Pass this into `WmsModuleGraphqlModule.forRootAsync({ eventEmitterBridge })`.
8
+ * Importing this module is the only place `@nestjs/event-emitter` is referenced,
9
+ * keeping it an entirely optional peer dependency — the core graphql module
10
+ * never imports it.
11
+ *
12
+ * Prerequisite: register `EventEmitterModule.forRoot()` in your app so an
13
+ * `EventEmitter2` instance is available for injection.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { wmsEventEmitterBridgeProvider } from '@rytass/wms-module-graphql/adapters/event-emitter';
18
+ *
19
+ * WmsModuleGraphqlModule.forRootAsync({
20
+ * ...coreOptions,
21
+ * eventEmitterBridge: wmsEventEmitterBridgeProvider,
22
+ * });
23
+ *
24
+ * // elsewhere
25
+ * @OnEvent('wms.stock.allocated')
26
+ * handle(payload: WmsStockEventPayload) { ... }
27
+ * ```
28
+ */
29
+ export declare const wmsEventEmitterBridgeProvider: Provider;
@@ -0,0 +1,41 @@
1
+ import { EventEmitter2 } from '@nestjs/event-emitter';
2
+ import { WMS_EVENT_EMITTER_BRIDGE } from '@rytass/wms-module-core';
3
+
4
+ /**
5
+ * Bridges WMS domain events onto `@nestjs/event-emitter`'s `EventEmitter2`, so
6
+ * consumers can subscribe with `@OnEvent('wms.<event-name>')` instead of (or in
7
+ * addition to) the typed `afterListeners`.
8
+ *
9
+ * Pass this into `WmsModuleGraphqlModule.forRootAsync({ eventEmitterBridge })`.
10
+ * Importing this module is the only place `@nestjs/event-emitter` is referenced,
11
+ * keeping it an entirely optional peer dependency — the core graphql module
12
+ * never imports it.
13
+ *
14
+ * Prerequisite: register `EventEmitterModule.forRoot()` in your app so an
15
+ * `EventEmitter2` instance is available for injection.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { wmsEventEmitterBridgeProvider } from '@rytass/wms-module-graphql/adapters/event-emitter';
20
+ *
21
+ * WmsModuleGraphqlModule.forRootAsync({
22
+ * ...coreOptions,
23
+ * eventEmitterBridge: wmsEventEmitterBridgeProvider,
24
+ * });
25
+ *
26
+ * // elsewhere
27
+ * @OnEvent('wms.stock.allocated')
28
+ * handle(payload: WmsStockEventPayload) { ... }
29
+ * ```
30
+ */
31
+ const wmsEventEmitterBridgeProvider = {
32
+ provide: WMS_EVENT_EMITTER_BRIDGE,
33
+ useFactory: (emitter) => ({
34
+ forward: (event) => {
35
+ emitter.emit(`wms.${event.name}`, event.payload);
36
+ },
37
+ }),
38
+ inject: [EventEmitter2],
39
+ };
40
+
41
+ export { wmsEventEmitterBridgeProvider };
@@ -1,3 +1,4 @@
1
+ import type { WmsActorExtractor } from '@rytass/wms-module-core';
1
2
  import type { WmsAuthChecker, WmsOperationRequirement } from '../lib/auth';
2
3
  /**
3
4
  * A Casbin `[subject, action]` tuple, e.g. `['wms:material', 'read']`.
@@ -33,3 +34,13 @@ export interface MemberBaseWmsAuthCheckerOptions {
33
34
  * (Apollo's default).
34
35
  */
35
36
  export declare const createMemberBaseWmsAuthChecker: (options: MemberBaseWmsAuthCheckerOptions) => WmsAuthChecker;
37
+ /**
38
+ * A {@link WmsActorExtractor} for `@rytass/member-base-nestjs-module`. Reads the
39
+ * verified JWT payload that `CasbinGuard` attaches to the request and surfaces it
40
+ * as the event/hook `actor`. Returns `null` for unauthenticated requests.
41
+ *
42
+ * Pass to `WmsModuleGraphqlModule.forRootAsync({ actorExtractor })` so every
43
+ * operation event and before-hook carries `{ id, domain }`. Imports nothing from
44
+ * member-base — it only reads the runtime request fields.
45
+ */
46
+ export declare const memberBaseActorExtractor: WmsActorExtractor;
@@ -43,5 +43,21 @@ const createMemberBaseWmsAuthChecker = (options) => {
43
43
  });
44
44
  };
45
45
  };
46
+ /**
47
+ * A {@link WmsActorExtractor} for `@rytass/member-base-nestjs-module`. Reads the
48
+ * verified JWT payload that `CasbinGuard` attaches to the request and surfaces it
49
+ * as the event/hook `actor`. Returns `null` for unauthenticated requests.
50
+ *
51
+ * Pass to `WmsModuleGraphqlModule.forRootAsync({ actorExtractor })` so every
52
+ * operation event and before-hook carries `{ id, domain }`. Imports nothing from
53
+ * member-base — it only reads the runtime request fields.
54
+ */
55
+ const memberBaseActorExtractor = (context) => {
56
+ const payload = extractRequest(context)?.payload;
57
+ if (!payload?.id) {
58
+ return null;
59
+ }
60
+ return { id: payload.id, domain: payload.domain, raw: payload };
61
+ };
46
62
 
47
- export { createMemberBaseWmsAuthChecker };
63
+ export { createMemberBaseWmsAuthChecker, memberBaseActorExtractor };
package/docs/README.md CHANGED
@@ -8,6 +8,7 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
8
8
  |------------------------------------------------|---------------------------------------------------------------------------------------------------|
9
9
  | [Integration Guide](./integration-guide.md) | Step-by-step instructions for wiring the three packages into a new consumer project (NestJS + Next.js + PostgreSQL). Includes ready-to-paste `_app.tsx`, `app.module.ts`, `next.config.ts`, the 19 Pages Router shell pattern, AuthGuard stub, seed script template, deployment notes, and known issues. |
10
10
  | [Authorization](./authorization.md) | Complete design of the resolver-level authorization protocol (`@WmsOperation` + `WmsAuthGuard` + `authChecker`). Explains the global deny-by-default guard pitfall, execution order, and integration recipes for member-base / Casbin, custom JWT / RBAC, and no-guard setups. Read this if any WMS query/mutation returns *forbidden*. |
11
+ | [Events & Hooks](./events.md) | The mutation event/hook system — `beforeHooks` (veto before any DB work) and `afterListeners` (post-commit notify), the `AsyncLocalStorage` post-commit mechanism, the optional `@nestjs/event-emitter` bridge, and the complete list of 38 operation + 18 stock domain events with payloads. |
11
12
  | [API Reference](./api-reference.md) | Complete export catalog for all three packages — every NestJS module, TypeORM entity, GraphQL resolver, DTO, React component, hook, and utility, organized by role with source paths and usage examples. Mirrors the actual `src/index.ts` barrel exports of `@rytass/wms-module-graphql@0.3.0` and `@rytass/wms-module-{core,react}@0.1.6`. |
12
13
 
13
14
  ## Quick links
@@ -26,6 +27,8 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
26
27
  | Replace the `AuthGuard` stub with real auth | [Integration Guide section 8.4](./integration-guide.md#84-real-auth-integration) |
27
28
  | Authorize WMS resolvers with member-base / Casbin / RBAC | [Authorization](./authorization.md) |
28
29
  | Fix "every WMS query/mutation is forbidden" | [Authorization section 1](./authorization.md#1-the-problem) |
30
+ | Hook into / get notified about mutations | [Events & Hooks](./events.md) |
31
+ | See every event the system emits | [Events & Hooks section 5](./events.md#5-complete-event-list) |
29
32
  | Decide between PostgreSQL and SQLite | [Integration Guide section 9](./integration-guide.md#9-database-support-matrix) |
30
33
  | Build a custom UI on top of the lib's GraphQL operations| [Integration Guide section 7.3](./integration-guide.md#73-graphql-operations--typed-hooks) |
31
34
 
@@ -449,6 +449,66 @@ The adapter imports nothing from member-base — it reads the `payload` / `enfor
449
449
 
450
450
  ---
451
451
 
452
+ ## Events & Hooks
453
+
454
+ Opt-in `beforeHooks` (veto) and post-commit `afterListeners` for every mutation. Each root mutation carries `@WmsMutationEvent({ event })` (already applied); a per-mutation interceptor runs the before-hook, opens an `AsyncLocalStorage` dispatch scope, and flushes collected events after the transaction commits. Stock domain events are collected centrally by a TypeORM subscriber on `*_order_change` inserts. Full design & the complete event list: [`docs/events.md`](./events.md).
455
+
456
+ ### Root exports (`@rytass/wms-module-graphql`, re-exported from `@rytass/wms-module-core`)
457
+
458
+ | Export | Kind | Signature / shape |
459
+ |-----------------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------|
460
+ | `WmsMutationEvent` | decorator | `({ event: WmsOperationEventName, resolveId? }) => MethodDecorator` |
461
+ | `WmsEventModuleOptions` | type | `{ beforeHooks?, afterListeners?, actorExtractor?, enrichStockEvents? }` — part of `forRootAsync` options |
462
+ | `WmsBeforeHookMap` | type | `{ [N in WmsEventName]?: (ctx: WmsBeforeHookContext<N>) => void \| Promise<void> }` |
463
+ | `WmsAfterListenerMap` | type | `{ [N in WmsEventName]?: (ctx: WmsAfterEventContext<N>) => void \| Promise<void> }` |
464
+ | `WmsBeforeHookContext` | type | `{ event; args: Record<string, unknown>; executionContext: ExecutionContext; actor: WmsActor \| null }` (v2 adds `actor`) |
465
+ | `WmsAfterEventContext` | type | `{ event; payload: WmsEventPayloadMap[event] }` |
466
+ | `WmsDomainEvent` | type | `{ name: WmsEventName; payload }` |
467
+ | `WmsEventName` / `WmsOperationEventName` / `WmsStockEventName` | type | event-name unions (38 operation + 18 stock) |
468
+ | `WmsEventPayloadMap` | type | event name → payload |
469
+ | `WmsOperationEventPayload` | type | v1: `{ id: string \| null; result: unknown }` — v2: `{ id; result; args: Readonly<Record<string, unknown>>; actor: WmsActor \| null }` |
470
+ | `WmsStockEventPayload` | type | `{ orderId; itemId; quantityDelta; changeType: ChangeType \| null; inspectionResult? }` — v2 adds optional `materialId?`, `batchId?`, `locationId?`, `loaderId?` when `enrichStockEvents: true` |
471
+ | `WmsActor` | type | `{ readonly id: string; readonly domain?: string; readonly raw?: unknown }` — actor identity (v2) |
472
+ | `WmsActorExtractor` | type | `(context: ExecutionContext) => WmsActor \| null` — injectable extractor (v2); token `WMS_ACTOR_EXTRACTOR` |
473
+ | `WmsOperationResultMap` | type | Maps each `WmsOperationEventName` to its concrete `result` DTO (e.g. `'material.created' → MaterialDto`); powers typed listeners (v2) |
474
+
475
+ ### Adapter subpath (`@rytass/wms-module-graphql/adapters/event-emitter`)
476
+
477
+ | Export | Kind | Purpose |
478
+ |-------------------------------|------------|------------------------------------------------------------------------------------------------------------------------------------------|
479
+ | `wmsEventEmitterBridgeProvider` | `Provider` | Pass to `forRootAsync({ eventEmitterBridge })`; fans every event onto `EventEmitter2` as `wms.<event>` for `@OnEvent`. Requires `@nestjs/event-emitter` (optional peer) + `EventEmitterModule.forRoot()`. |
480
+
481
+ ### Adapter subpath (`@rytass/wms-module-graphql/adapters/member-base`) — event extras (v2)
482
+
483
+ | Export | Kind | Purpose |
484
+ |-----------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------|
485
+ | `memberBaseActorExtractor` | extractor | `WmsActorExtractor` that reads `GqlExecutionContext.create(ctx).getContext().req?.payload` → `{ id, domain }`. Pass to `forRootAsync({ actorExtractor })`. |
486
+
487
+ ```ts
488
+ import { memberBaseActorExtractor } from '@rytass/wms-module-graphql/adapters/member-base';
489
+ import { wmsEventEmitterBridgeProvider } from '@rytass/wms-module-graphql/adapters/event-emitter';
490
+
491
+ WmsModuleGraphqlModule.forRootAsync({
492
+ ...coreOptions,
493
+ actorExtractor: memberBaseActorExtractor, // v2: populates payload.actor
494
+ enrichStockEvents: true, // v2: appends ids to stock payloads
495
+ beforeHooks: {
496
+ 'ship-inventory-order.shipped': async ({ args, actor }) => {
497
+ if (!ok(args, actor)) throw new ForbiddenException();
498
+ },
499
+ },
500
+ afterListeners: {
501
+ 'stock.allocated': async ({ payload }) => { await sync(payload); },
502
+ },
503
+ // optional @OnEvent bridge:
504
+ // eventEmitterBridge: wmsEventEmitterBridgeProvider,
505
+ });
506
+ ```
507
+
508
+ `beforeHooks` / `afterListeners` are **not** part of the GraphQL schema — they are runtime hooks. With none configured the interceptor passes through with zero overhead. All v2 options (`actorExtractor`, `enrichStockEvents`) are fully optional; omitting them preserves the v1 behaviour exactly.
509
+
510
+ ---
511
+
452
512
  ## Resolvers(`@Resolver()` decorator class)
453
513
 
454
514
  以下 Resolver 類別處理 GraphQL `@ResolveField`,解析關聯欄位(防止 N+1):