@rytass/wms-module-graphql 0.5.0 → 0.6.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 +31 -5
- package/adapters/member-base.d.ts +11 -0
- package/adapters/member-base.js +17 -1
- package/docs/api-reference.md +38 -18
- package/docs/events.md +263 -67
- package/docs/integration-guide.md +35 -7
- package/index.cjs.js +112 -10
- package/index.d.ts +2 -2
- package/lib/events/index.d.ts +2 -0
- package/lib/events/operation-result-map.d.ts +77 -0
- package/lib/events/typed-maps.d.ts +73 -0
- package/lib/events/wms-mutation.interceptor.d.ts +4 -2
- package/lib/events/wms-mutation.interceptor.js +24 -6
- package/lib/graphql.module.d.ts +13 -4
- package/lib/graphql.module.js +11 -3
- package/lib/helpers/loader.service.d.ts +22 -0
- package/lib/helpers/loader.service.js +64 -2
- package/lib/resolvers/location.resolver.d.ts +10 -1
- package/lib/resolvers/location.resolver.js +18 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -96,19 +96,45 @@ Intervene in and get notified about mutations. Every root mutation carries a typ
|
|
|
96
96
|
- **`afterListeners`** run **after the outermost transaction commits** — never notified about rolled-back changes.
|
|
97
97
|
|
|
98
98
|
```ts
|
|
99
|
+
import { memberBaseActorExtractor } from '@rytass/wms-module-graphql/adapters/member-base';
|
|
100
|
+
|
|
99
101
|
WmsModuleGraphqlModule.forRootAsync({
|
|
100
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
|
+
|
|
101
110
|
beforeHooks: {
|
|
102
|
-
'ship-inventory-order.shipped': async ({ args }) => {
|
|
103
|
-
if (!allowed(args)) throw new ForbiddenException('blocked');
|
|
111
|
+
'ship-inventory-order.shipped': async ({ args, actor }) => {
|
|
112
|
+
if (!allowed(args, actor)) throw new ForbiddenException('blocked');
|
|
104
113
|
},
|
|
105
114
|
},
|
|
106
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
|
|
107
121
|
'stock.allocated': async ({ payload }) => { await erp.reserve(payload); },
|
|
108
122
|
},
|
|
109
123
|
});
|
|
110
124
|
```
|
|
111
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
|
+
|
|
112
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.
|
|
113
139
|
|
|
114
140
|
➡️ **Full design, the complete 56-event list & limitations: [`docs/events.md`](./docs/events.md)**.
|
|
@@ -154,11 +180,11 @@ Full documentation is shipped inside this package under the `docs/` directory (a
|
|
|
154
180
|
|
|
155
181
|
GitHub hosted links (always up-to-date):
|
|
156
182
|
|
|
157
|
-
- [Integration Guide — §4 Backend setup](https://github.com/Rytass/WMS-Module/blob/main/docs/integration-guide.md#4-backend-setup-nestjs--graphql--typeorm)
|
|
158
|
-
- [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)
|
|
159
185
|
- [Authorization — design & recipes](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/authorization.md)
|
|
160
186
|
- [Events & Hooks — design & event list](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/events.md)
|
|
161
|
-
- [API Reference — @rytass/wms-module-graphql](https://github.com/Rytass/WMS-Module/blob/main/docs/api-reference.md#rytasswms-module-graphql)
|
|
187
|
+
- [API Reference — @rytass/wms-module-graphql](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/api-reference.md#rytasswms-module-graphql)
|
|
162
188
|
|
|
163
189
|
## License
|
|
164
190
|
|
|
@@ -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;
|
package/adapters/member-base.js
CHANGED
|
@@ -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/api-reference.md
CHANGED
|
@@ -455,37 +455,57 @@ Opt-in `beforeHooks` (veto) and post-commit `afterListeners` for every mutation.
|
|
|
455
455
|
|
|
456
456
|
### Root exports (`@rytass/wms-module-graphql`, re-exported from `@rytass/wms-module-core`)
|
|
457
457
|
|
|
458
|
-
| Export
|
|
459
|
-
|
|
460
|
-
| `WmsMutationEvent`
|
|
461
|
-
| `WmsEventModuleOptions`
|
|
462
|
-
| `WmsBeforeHookMap`
|
|
463
|
-
| `WmsAfterListenerMap`
|
|
464
|
-
| `WmsBeforeHookContext`
|
|
465
|
-
| `WmsAfterEventContext`
|
|
466
|
-
| `WmsDomainEvent`
|
|
467
|
-
| `WmsEventName` / `WmsOperationEventName` / `WmsStockEventName`
|
|
468
|
-
| `WmsEventPayloadMap`
|
|
469
|
-
| `WmsOperationEventPayload`
|
|
470
|
-
| `WmsStockEventPayload`
|
|
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) |
|
|
471
474
|
|
|
472
475
|
### Adapter subpath (`@rytass/wms-module-graphql/adapters/event-emitter`)
|
|
473
476
|
|
|
474
|
-
| Export
|
|
475
|
-
|
|
477
|
+
| Export | Kind | Purpose |
|
|
478
|
+
|-------------------------------|------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
|
476
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()`. |
|
|
477
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
|
+
|
|
478
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
|
+
|
|
479
491
|
WmsModuleGraphqlModule.forRootAsync({
|
|
480
492
|
...coreOptions,
|
|
481
|
-
|
|
482
|
-
|
|
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
|
+
},
|
|
483
503
|
// optional @OnEvent bridge:
|
|
484
504
|
// eventEmitterBridge: wmsEventEmitterBridgeProvider,
|
|
485
505
|
});
|
|
486
506
|
```
|
|
487
507
|
|
|
488
|
-
`beforeHooks` / `afterListeners` are **not** part of the GraphQL schema — they are runtime hooks. With none configured the interceptor passes through with zero overhead.
|
|
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.
|
|
489
509
|
|
|
490
510
|
---
|
|
491
511
|
|
package/docs/events.md
CHANGED
|
@@ -36,6 +36,23 @@ Two hook points per event:
|
|
|
36
36
|
Everything is optional. With nothing configured the module behaves exactly as
|
|
37
37
|
before — no events, zero overhead.
|
|
38
38
|
|
|
39
|
+
### v2 payload enrichment (upcoming minor)
|
|
40
|
+
|
|
41
|
+
Operation event payloads will gain two new fields — `args` (the GraphQL input
|
|
42
|
+
that triggered the mutation) and `actor` (who performed it, obtained via an
|
|
43
|
+
injectable `actorExtractor`) — turning the shape from `{ id, result }` into
|
|
44
|
+
`{ id, result, args, actor }`. The `result` field will also become **per-event
|
|
45
|
+
typed** via `WmsOperationResultMap` so subscribers receive the concrete DTO
|
|
46
|
+
instead of `unknown`. Nine action mutations (`received`, `deallocated`, …) still
|
|
47
|
+
return `boolean`; their real information comes from `args` and the stock events
|
|
48
|
+
they fan out.
|
|
49
|
+
|
|
50
|
+
Stock event payloads gain an opt-in `enrichStockEvents: true` flag on
|
|
51
|
+
`forRootAsync` that appends `materialId / batchId / locationId / loaderId` (one
|
|
52
|
+
indexed PK lookup per event, same transaction). Without the flag the lazy shape
|
|
53
|
+
is unchanged. See [§6 Payloads](#6-payloads) for the full shapes and the
|
|
54
|
+
split/merge/reclassify partial-enrichment caveat.
|
|
55
|
+
|
|
39
56
|
---
|
|
40
57
|
|
|
41
58
|
## 2. Architecture — post-commit guarantee
|
|
@@ -82,6 +99,7 @@ is inferred from the key.
|
|
|
82
99
|
|
|
83
100
|
```ts
|
|
84
101
|
import { WmsModuleGraphqlModule } from '@rytass/wms-module-graphql';
|
|
102
|
+
import { memberBaseActorExtractor } from '@rytass/wms-module-graphql/adapters/member-base';
|
|
85
103
|
import { ForbiddenException } from '@nestjs/common';
|
|
86
104
|
|
|
87
105
|
WmsModuleGraphqlModule.forRootAsync({
|
|
@@ -90,6 +108,12 @@ WmsModuleGraphqlModule.forRootAsync({
|
|
|
90
108
|
defaultLoaderTypeKey: 'DEFAULT',
|
|
91
109
|
defaultLocationKey: 'DEFAULT',
|
|
92
110
|
|
|
111
|
+
// (v2) extract the actor from the request; member-base reads req.payload → { id, domain }
|
|
112
|
+
actorExtractor: memberBaseActorExtractor,
|
|
113
|
+
|
|
114
|
+
// (v2) append materialId/batchId/locationId/loaderId to stock event payloads
|
|
115
|
+
enrichStockEvents: true,
|
|
116
|
+
|
|
93
117
|
// intervene — runs before any DB work; throw to veto
|
|
94
118
|
beforeHooks: {
|
|
95
119
|
'ship-inventory-order.shipped': async ({ args, executionContext }) => {
|
|
@@ -101,10 +125,12 @@ WmsModuleGraphqlModule.forRootAsync({
|
|
|
101
125
|
|
|
102
126
|
// notify — runs after commit
|
|
103
127
|
afterListeners: {
|
|
128
|
+
// v2: payload.args carries the items[] input; payload.actor carries { id, domain }
|
|
104
129
|
'ship-inventory-order.shipped': async ({ payload }) => {
|
|
105
|
-
await erp.markShipped(payload.id, payload.result);
|
|
130
|
+
await erp.markShipped(payload.id, payload.result, payload.actor);
|
|
106
131
|
},
|
|
107
132
|
'stock.allocated': async ({ payload }) => {
|
|
133
|
+
// v2 with enrichStockEvents: materialId/batchId/locationId/loaderId also available
|
|
108
134
|
await erp.reserve(payload.orderId, payload.itemId, payload.quantityDelta);
|
|
109
135
|
},
|
|
110
136
|
},
|
|
@@ -117,6 +143,39 @@ WmsModuleGraphqlModule.forRootAsync({
|
|
|
117
143
|
- An `afterListener` receives `{ event, payload }` — the committed result (see
|
|
118
144
|
[§6](#6-payloads)). It may be async; a thrown error is logged and isolated, it
|
|
119
145
|
never affects the committed mutation or other listeners.
|
|
146
|
+
- **(v2)** `payload.args` carries the full GraphQL input that triggered the mutation
|
|
147
|
+
— particularly useful for the 9 boolean action events whose `result` is
|
|
148
|
+
`true`/`false` only.
|
|
149
|
+
- **(v2)** `payload.actor` is `WmsActor | null` — populated when `actorExtractor`
|
|
150
|
+
is configured, `null` otherwise. Shape: `{ id: string; domain?: string; raw?:
|
|
151
|
+
unknown }`. The `memberBaseActorExtractor` helper (exported from the
|
|
152
|
+
`@rytass/wms-module-graphql/adapters/member-base` subpath) reads
|
|
153
|
+
`req.payload` and returns `{ id, domain }`.
|
|
154
|
+
|
|
155
|
+
### 3.1 The 9 boolean action events
|
|
156
|
+
|
|
157
|
+
Nine mutations return `boolean` and cannot carry a rich DTO `result` without a
|
|
158
|
+
breaking GraphQL schema change. For subscribers, the meaningful signal is:
|
|
159
|
+
|
|
160
|
+
1. **`args`** — the exact items input (see §5 for each event's args shape).
|
|
161
|
+
2. **Stock events** — the operation fans out one `stock.*` event per affected item
|
|
162
|
+
(see the "Fans out to stock event" column in §5).
|
|
163
|
+
|
|
164
|
+
The operation event itself only confirms success (`result: true`).
|
|
165
|
+
|
|
166
|
+
| Event | args key items shape |
|
|
167
|
+
|------------------------------------------|--------------------------------------------------------------|
|
|
168
|
+
| `receive-inventory-order.received` | `{ itemId, quantity, batchKey?, locationKey?, loaderKey?, inspectionResult? }` |
|
|
169
|
+
| `ship-inventory-order.shipped` | `{ itemId, quantity }` |
|
|
170
|
+
| `ship-inventory-order.ship-cancelled` | `{ itemId, quantity? }` |
|
|
171
|
+
| `transfer-order.transferred` | `{ itemId, quantity }` |
|
|
172
|
+
| `transfer-order.transfer-cancelled` | `{ itemId, quantity? }` |
|
|
173
|
+
| `hold-inventory-order.released` | `{ aggregatedKey, quantity, unitId? }` |
|
|
174
|
+
| `allocate-inventory-order.deallocated` | `{ aggregatedKey, quantity, unitId? }` |
|
|
175
|
+
| `transfer-customer-order.returned` | `{ aggregatedKey, quantity, unitId?, locationKey, loaderKey? }` |
|
|
176
|
+
| `transfer-vendor-order.returned` | `{ aggregatedKey, quantity, unitId?, locationKey, loaderKey? }` |
|
|
177
|
+
|
|
178
|
+
All are passed as `@Args('items', …) items: T[]`, so `args.items` is the array.
|
|
120
179
|
|
|
121
180
|
---
|
|
122
181
|
|
|
@@ -158,52 +217,62 @@ and the bridge can be used together — both fire.
|
|
|
158
217
|
|
|
159
218
|
### Operation events (38) — `<domain>.<past-tense-action>`
|
|
160
219
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| `
|
|
167
|
-
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `transfer-
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
220
|
+
The **v2 `result` type** column shows the concrete type subscribers receive via
|
|
221
|
+
`WmsOperationResultMap`. Events marked ⚠️ **boolean** are the 9 action mutations
|
|
222
|
+
whose `result` is always `true`/`false`; their real signal is `args` + the stock
|
|
223
|
+
events they fan out (see §3.1).
|
|
224
|
+
|
|
225
|
+
| Event | Mutation | v2 `result` type | Fans out to stock event |
|
|
226
|
+
|---------------------------------------------|-------------------------------|-----------------------------------|-------------------------------|
|
|
227
|
+
| `receive-inventory-order.created` | createReceiveInventoryOrder | `ReceiveInventoryOrderDto` | — |
|
|
228
|
+
| `receive-inventory-order.updated` | updateReceiveInventoryOrder | `ReceiveInventoryOrderDto` | — |
|
|
229
|
+
| `receive-inventory-order.received` | receive | ⚠️ `boolean` | `stock.received` |
|
|
230
|
+
| `ship-inventory-order.created` | createShipInventoryOrder | `ShipInventoryOrderDto` | — |
|
|
231
|
+
| `ship-inventory-order.updated` | updateShipInventoryOrder | `ShipInventoryOrderDto` | `stock.in-shipment` |
|
|
232
|
+
| `ship-inventory-order.shipped` | ship | ⚠️ `boolean` | `stock.shipped` |
|
|
233
|
+
| `ship-inventory-order.ship-cancelled` | cancelShip | ⚠️ `boolean` | `stock.shipped` (negative) |
|
|
234
|
+
| `transfer-order.created` | createTransferOrder | `TransferOrderDto` | `stock.in-transfer` |
|
|
235
|
+
| `transfer-order.updated` | updateTransferOrder | `TransferOrderDto` | `stock.in-transfer` |
|
|
236
|
+
| `transfer-order.transferred` | transfer | ⚠️ `boolean` | `stock.transferred` |
|
|
237
|
+
| `transfer-order.transfer-cancelled` | cancelTransfer | ⚠️ `boolean` | `stock.in-transfer` (reversal)|
|
|
238
|
+
| `hold-inventory-order.created` | createHoldInventoryOrder | `HoldInventoryOrderDto` | `stock.held` |
|
|
239
|
+
| `hold-inventory-order.updated` | updateHoldInventoryOrder | `HoldInventoryOrderDto` | `stock.held` |
|
|
240
|
+
| `hold-inventory-order.released` | release | ⚠️ `boolean` | `stock.released` |
|
|
241
|
+
| `allocate-inventory-order.created` | createAllocateInventoryOrder | `AllocateInventoryOrderDto` | `stock.allocated` |
|
|
242
|
+
| `allocate-inventory-order.updated` | updateAllocateInventoryOrder | `AllocateInventoryOrderDto` | `stock.allocated` |
|
|
243
|
+
| `allocate-inventory-order.deallocated` | deallocate | ⚠️ `boolean` | `stock.deallocated` |
|
|
244
|
+
| `transfer-vendor-order.created` | createTransferVendorOrder | `TransferVendorOrderDto` | `stock.vendor-transferred` |
|
|
245
|
+
| `transfer-vendor-order.updated` | updateTransferVendorOrder | `TransferVendorOrderDto` | `stock.vendor-transferred` |
|
|
246
|
+
| `transfer-vendor-order.returned` | returnTransferVendorOrder | ⚠️ `boolean` | `stock.vendor-returned` |
|
|
247
|
+
| `transfer-customer-order.created` | createTransferCustomerOrder | `TransferCustomerOrderDto` | `stock.customer-held` |
|
|
248
|
+
| `transfer-customer-order.updated` | updateTransferCustomerOrder | `TransferCustomerOrderDto` | `stock.customer-held` |
|
|
249
|
+
| `transfer-customer-order.returned` | returnTransferCustomerOrder | ⚠️ `boolean` | `stock.customer-returned` |
|
|
250
|
+
| `quality-inspection-order.inspected` | inspectMaterials | `QualityInspectionOrderDto` | `stock.inspection-resolved` |
|
|
251
|
+
| `scrape-order.scraped` | scrapeMaterials | `ScrapeOrderDto` | `stock.scraped` |
|
|
252
|
+
| `reclassify-inventory-order.reclassified` | reclassifyInventoryOrder | `ReclassifyInventoryOrderDto` | `stock.reclassified` |
|
|
253
|
+
| `batch.merged` | mergeBatch | `MergeBatchOrderDto` | `stock.merged` |
|
|
254
|
+
| `batch.splitted` | splitBatch | `SplitBatchOrderDto` | `stock.splitted` |
|
|
255
|
+
| `material.created` | createMaterial | `MaterialDto` | — |
|
|
256
|
+
| `material.updated` | updateMaterial | `MaterialDto` | — |
|
|
257
|
+
| `loader.created` | createLoader | `LoaderDto[]` | — |
|
|
258
|
+
| `loader.batch-created` | createBatchLoader | `LoaderDto[]` | — |
|
|
259
|
+
| `loader.updated` | updateLoader | `LoaderDto` | — |
|
|
260
|
+
| `loader.type-created` | createLoaderType | `LoaderTypeDto` | — |
|
|
261
|
+
| `loader.type-updated` | updateLoaderType | `LoaderTypeDto` | — |
|
|
262
|
+
| `location.created` | createLocation | `LocationDto` | — |
|
|
263
|
+
| `location.updated` | updateLocation | `LocationDto` | — |
|
|
264
|
+
| `warehouse-map.updated` | updateWarehouseMap | `MapDataDto` | — |
|
|
201
265
|
|
|
202
266
|
> The "Fans out to stock event" column is **indicative**, not a hard binding. A
|
|
203
267
|
> mutation never names a stock event directly — `StockChangeSubscriber` derives
|
|
204
268
|
> it from the `*_order_change` table that was written and the sign of
|
|
205
269
|
> `quantityChanged`. So a mutation emits a stock event only on the code paths
|
|
206
270
|
> that actually write a change row (e.g. an `update*` only when it adds items).
|
|
271
|
+
>
|
|
272
|
+
> The "v2 `result` type" column refers to the typed overlay provided by
|
|
273
|
+
> `WmsOperationResultMap` (exported from `@rytass/wms-module-graphql`). In v1
|
|
274
|
+
> `result` is typed `unknown`; consumer code using the typed maps gets the
|
|
275
|
+
> concrete DTO automatically.
|
|
207
276
|
|
|
208
277
|
### Stock domain events (18) — `stock.<change-type>`
|
|
209
278
|
|
|
@@ -227,27 +296,141 @@ with a negative `quantityDelta` (`cancelShip` -> `stock.shipped`,
|
|
|
227
296
|
|
|
228
297
|
## 6. Payloads
|
|
229
298
|
|
|
230
|
-
|
|
299
|
+
### Operation event — `WmsOperationEventPayload`
|
|
300
|
+
|
|
301
|
+
**v1 shape** (currently shipped):
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
{
|
|
305
|
+
id: string | null; // primary id of the operation's order/entity
|
|
306
|
+
result: unknown; // the mutation's return value (DTO, or boolean for actions)
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**v2 shape** (upcoming minor — `core@0.3.0` / `graphql@0.6.0`):
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
{
|
|
314
|
+
readonly id: string | null; // (unchanged) primary id
|
|
315
|
+
readonly result: TResult; // now per-event typed via WmsOperationResultMap
|
|
316
|
+
readonly args: Readonly<Record<string, unknown>>; // NEW — the GraphQL input (intent)
|
|
317
|
+
readonly actor: WmsActor | null; // NEW — who performed the mutation
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**`WmsActor`** shape:
|
|
231
322
|
|
|
232
323
|
```ts
|
|
233
|
-
|
|
234
|
-
|
|
324
|
+
interface WmsActor {
|
|
325
|
+
readonly id: string; // subject identifier
|
|
326
|
+
readonly domain?: string; // optional domain / tenant discriminator
|
|
327
|
+
readonly raw?: unknown; // host-specific extras; opaque to the lib
|
|
328
|
+
}
|
|
235
329
|
```
|
|
236
330
|
|
|
237
|
-
|
|
331
|
+
`actor` is `null` when no `actorExtractor` is configured. The
|
|
332
|
+
`memberBaseActorExtractor` helper (from
|
|
333
|
+
`@rytass/wms-module-graphql/adapters/member-base`) reads `req.payload` and
|
|
334
|
+
returns `{ id, domain }` — the same fields `CasbinGuard` already puts on the
|
|
335
|
+
JWT payload.
|
|
336
|
+
|
|
337
|
+
`WmsBeforeHookContext` also gains `actor` (so before-hooks can veto by identity):
|
|
238
338
|
|
|
239
339
|
```ts
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
340
|
+
interface WmsBeforeHookContext<N extends WmsEventName> {
|
|
341
|
+
event: N;
|
|
342
|
+
args: Record<string, unknown>;
|
|
343
|
+
executionContext: ExecutionContext;
|
|
344
|
+
actor: WmsActor | null; // v2 addition
|
|
345
|
+
}
|
|
245
346
|
```
|
|
246
347
|
|
|
348
|
+
### Typed result overlay — `WmsOperationResultMap`
|
|
349
|
+
|
|
350
|
+
The v2 graphql package exports a `WmsOperationResultMap` interface that maps
|
|
351
|
+
every operation event name to its concrete `result` type. Consumers who use the
|
|
352
|
+
typed `beforeHooks` / `afterListeners` maps automatically receive the resolved
|
|
353
|
+
DTO instead of `unknown`:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// listener receives ShipInventoryOrderDto, not unknown
|
|
357
|
+
afterListeners: {
|
|
358
|
+
'ship-inventory-order.updated': async ({ payload }) => {
|
|
359
|
+
console.log(payload.result.id); // typed as string
|
|
360
|
+
},
|
|
361
|
+
// listener receives boolean
|
|
362
|
+
'ship-inventory-order.shipped': async ({ payload }) => {
|
|
363
|
+
console.log(payload.result); // typed as boolean
|
|
364
|
+
console.log(payload.args); // { items: [{ itemId, quantity }, ...] }
|
|
365
|
+
console.log(payload.actor?.id); // actor id if actorExtractor is set
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### Stock event — `WmsStockEventPayload`
|
|
373
|
+
|
|
374
|
+
**Lazy shape** (default — unchanged):
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
{
|
|
378
|
+
readonly orderId: string; // = change.id; correlation id with the operation event
|
|
379
|
+
readonly itemId: string; // = change.itemId
|
|
380
|
+
readonly quantityDelta: number; // = change.quantityChanged (sign = direction)
|
|
381
|
+
readonly changeType: ChangeType | null; // null only for stock.inspection-resolved
|
|
382
|
+
readonly inspectionResult?: boolean | null; // present only on stock.inspection-resolved
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Enriched shape** (opt-in, `enrichStockEvents: true`):
|
|
387
|
+
|
|
388
|
+
The following fields are appended when the flag is enabled. They are loaded in
|
|
389
|
+
the **same transaction** via a single indexed PK lookup on the change's item row:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
readonly materialId?: string | null;
|
|
393
|
+
readonly batchId?: string | null;
|
|
394
|
+
readonly locationId?: string | null;
|
|
395
|
+
readonly loaderId?: string | null;
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
> All four fields are optional/nullable — a `null` value means the column is
|
|
399
|
+
> nullable in the underlying order-item row and happened to be null at the time
|
|
400
|
+
> of the change. On any lookup error the system degrades to the lazy payload.
|
|
401
|
+
|
|
402
|
+
**Per-domain enrichment completeness** (`enrichStockEvents: true`):
|
|
403
|
+
|
|
404
|
+
| Domain (stock event) | materialId | batchId | locationId | loaderId |
|
|
405
|
+
|---------------------------------------------------|:----------:|:-------:|:----------:|:--------:|
|
|
406
|
+
| receive (`stock.received`) | yes | yes * | yes * | yes * |
|
|
407
|
+
| ship (`stock.in-shipment` / `stock.shipped`) | yes | yes | yes * | yes * |
|
|
408
|
+
| transfer-order (`stock.in-transfer` / `.transferred`) | yes | yes | yes | yes * |
|
|
409
|
+
| transfer-vendor (`stock.vendor-*`) | yes | yes | yes * | yes * |
|
|
410
|
+
| transfer-customer (`stock.customer-*`) | yes | yes | yes * | yes * |
|
|
411
|
+
| hold (`stock.held` / `stock.released`) | yes | yes | yes * | yes * |
|
|
412
|
+
| allocate (`stock.allocated` / `.deallocated`) | yes | yes | yes * | yes * |
|
|
413
|
+
| scrape (`stock.scraped`) | yes | yes | yes * | yes * |
|
|
414
|
+
| quality-inspection (`stock.inspection-resolved`) | yes | yes | yes * | yes * |
|
|
415
|
+
| **split (`stock.splitted`)** | **no** | **no** | yes * | yes * |
|
|
416
|
+
| **merge (`stock.merged`)** | **no** | **no** | yes | yes * |
|
|
417
|
+
| **reclassify (`stock.reclassified`)** | **no** | **no** | yes | yes * |
|
|
418
|
+
|
|
419
|
+
`*` = column is nullable; may be `null` even when enrichment is on.
|
|
420
|
+
|
|
421
|
+
> **Important**: split / merge / reclassify order-items have **no `materialId` /
|
|
422
|
+
> `batchId`** — those domains operate at batch-aggregation level and keep that
|
|
423
|
+
> metadata on the parent order, not the item. With `enrichStockEvents: true`
|
|
424
|
+
> these three event types only receive `locationId` / `loaderId`. If you need
|
|
425
|
+
> `materialId` / `batchId` for these domains, look them up from the parent order
|
|
426
|
+
> by `orderId` inside your after-listener.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
247
430
|
`orderId` is the same on the operation event (`payload.id`) and the stock events
|
|
248
431
|
it fans out to, so listeners can correlate the two tiers. For richer
|
|
249
|
-
batch/material/location details, look them up by
|
|
250
|
-
after-listener — the data is committed by then.
|
|
432
|
+
batch/material/location details when `enrichStockEvents` is off, look them up by
|
|
433
|
+
`orderId` / `itemId` inside the after-listener — the data is committed by then.
|
|
251
434
|
|
|
252
435
|
---
|
|
253
436
|
|
|
@@ -279,25 +462,38 @@ after-listener — the data is committed by then.
|
|
|
279
462
|
From the package root (`@rytass/wms-module-graphql`, re-exported from
|
|
280
463
|
`@rytass/wms-module-core`):
|
|
281
464
|
|
|
282
|
-
| Export
|
|
283
|
-
|
|
284
|
-
| `WmsMutationEvent({ event })`
|
|
285
|
-
| `WmsBeforeHookMap` / `WmsAfterListenerMap`
|
|
286
|
-
| `WmsEventModuleOptions`
|
|
287
|
-
| `WmsBeforeHookContext` / `WmsAfterEventContext`
|
|
288
|
-
| `WmsDomainEvent`
|
|
289
|
-
| `WmsEventName` / `WmsOperationEventName` / `WmsStockEventName`
|
|
290
|
-
| `WmsEventPayloadMap`
|
|
291
|
-
| `WmsOperationEventPayload`
|
|
465
|
+
| Export | Kind | Purpose |
|
|
466
|
+
|-------------------------------------------------------------------|-----------|--------------------------------------------------------------------------------------------------------------|
|
|
467
|
+
| `WmsMutationEvent({ event })` | decorator | Annotates a mutation (already applied across the library). |
|
|
468
|
+
| `WmsBeforeHookMap` / `WmsAfterListenerMap` | type | The `beforeHooks` / `afterListeners` option shapes. |
|
|
469
|
+
| `WmsEventModuleOptions` | type | `{ beforeHooks?, afterListeners?, actorExtractor?, enrichStockEvents? }` (part of `forRootAsync`). |
|
|
470
|
+
| `WmsBeforeHookContext` / `WmsAfterEventContext` | type | Hook/listener argument shapes (v2: `WmsBeforeHookContext` gains `actor`). |
|
|
471
|
+
| `WmsDomainEvent` | type | `{ name, payload }`. |
|
|
472
|
+
| `WmsEventName` / `WmsOperationEventName` / `WmsStockEventName` | type | Event-name unions. |
|
|
473
|
+
| `WmsEventPayloadMap` | type | Event name -> payload mapping. |
|
|
474
|
+
| `WmsOperationEventPayload` | type | `{ id, result, args, actor }` (v2); `{ id, result }` in v1. |
|
|
475
|
+
| `WmsStockEventPayload` | type | `{ orderId, itemId, quantityDelta, changeType, inspectionResult? }` + optional enrich fields (v2). |
|
|
476
|
+
| `WmsActor` | type | `{ id: string; domain?: string; raw?: unknown }` — actor identity shape (v2). |
|
|
477
|
+
| `WmsActorExtractor` | type | `(context: ExecutionContext) => WmsActor \| null` — injected via `forRootAsync({ actorExtractor })` (v2). |
|
|
478
|
+
| `WmsOperationResultMap` | type | Event name → concrete `result` DTO mapping; enables per-event typed listeners (v2, lives in graphql lib). |
|
|
292
479
|
|
|
293
480
|
From the adapter subpath (`@rytass/wms-module-graphql/adapters/event-emitter`):
|
|
294
481
|
|
|
295
|
-
| Export
|
|
296
|
-
|
|
297
|
-
| `wmsEventEmitterBridgeProvider` | provider | Pass to `forRootAsync({ eventEmitterBridge })` to fan events onto `@nestjs/event-emitter`.
|
|
482
|
+
| Export | Kind | Purpose |
|
|
483
|
+
|-------------------------------|----------|-------------------------------------------------------------------------------------------------------|
|
|
484
|
+
| `wmsEventEmitterBridgeProvider` | provider | Pass to `forRootAsync({ eventEmitterBridge })` to fan events onto `@nestjs/event-emitter`. |
|
|
485
|
+
|
|
486
|
+
From the adapter subpath (`@rytass/wms-module-graphql/adapters/member-base`):
|
|
487
|
+
|
|
488
|
+
| Export | Kind | Purpose |
|
|
489
|
+
|-----------------------------|-----------|-----------------------------------------------------------------------------------------------------------|
|
|
490
|
+
| `memberBaseActorExtractor` | extractor | `WmsActorExtractor` that reads `req.payload` → `{ id, domain }` (v2). Pass to `forRootAsync({ actorExtractor })`. |
|
|
298
491
|
|
|
299
|
-
`forRootAsync` accepts `beforeHooks`, `afterListeners
|
|
300
|
-
on its single options object — all
|
|
492
|
+
`forRootAsync` accepts `beforeHooks`, `afterListeners`, `eventEmitterBridge`,
|
|
493
|
+
`actorExtractor`, and `enrichStockEvents` on its single options object — all
|
|
494
|
+
optional. Omitting `actorExtractor` leaves `actor: null` on every payload;
|
|
495
|
+
omitting `enrichStockEvents` (or setting it `false`) keeps the lazy stock
|
|
496
|
+
payload shape.
|
|
301
497
|
|
|
302
498
|
---
|
|
303
499
|
|