@rytass/wms-module-graphql 0.8.0 → 0.10.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 (47) hide show
  1. package/README.md +92 -2
  2. package/adapters/event-emitter.cjs.js +43 -0
  3. package/adapters/member-base.cjs.js +66 -0
  4. package/docs/api-reference.md +80 -1
  5. package/docs/events.md +23 -4
  6. package/docs/integration-guide.md +31 -17
  7. package/index.cjs.js +935 -1
  8. package/index.d.ts +3 -1
  9. package/index.js +2 -0
  10. package/lib/dto/index.d.ts +1 -0
  11. package/lib/dto/inventory-count-order.dto.d.ts +61 -0
  12. package/lib/dto/inventory-count-order.dto.js +197 -0
  13. package/lib/events/operation-result-map.d.ts +8 -0
  14. package/lib/graphql.module.d.ts +6 -2
  15. package/lib/graphql.module.js +18 -1
  16. package/lib/inputs/find-stock.args.d.ts +9 -0
  17. package/lib/inputs/find-stock.args.js +25 -0
  18. package/lib/inputs/inventory-count-order.args.d.ts +69 -0
  19. package/lib/inputs/inventory-count-order.args.js +257 -0
  20. package/lib/mutations/inventory-count-order.mutation.d.ts +19 -0
  21. package/lib/mutations/inventory-count-order.mutation.js +138 -0
  22. package/lib/mutations/loader.mutation.js +1 -0
  23. package/lib/mutations/material.mutation.js +1 -0
  24. package/lib/mutations/receive-inventory-order.mutation.js +1 -0
  25. package/lib/mutations/ship-inventory-order.mutation.js +1 -0
  26. package/lib/queries/inventory-count-order.query.d.ts +17 -0
  27. package/lib/queries/inventory-count-order.query.js +63 -0
  28. package/lib/queries/loader.query.js +1 -0
  29. package/lib/queries/material.query.js +1 -0
  30. package/lib/queries/recieve-inventory-order.query.js +1 -0
  31. package/lib/queries/ship-inventory-order.query.js +1 -0
  32. package/lib/queries/stock.query.js +1 -0
  33. package/lib/registry/index.d.ts +1 -0
  34. package/lib/registry/operation-registry.d.ts +59 -0
  35. package/lib/registry/operation-registry.js +136 -0
  36. package/lib/resolvers/aggregated-stock.resolver.js +1 -0
  37. package/lib/resolvers/change-log.resolver.js +1 -0
  38. package/lib/resolvers/inventory-count-order.resolver.d.ts +34 -0
  39. package/lib/resolvers/inventory-count-order.resolver.js +148 -0
  40. package/lib/resolvers/loader-type.resolver.js +1 -0
  41. package/lib/resolvers/loader.resolver.js +1 -0
  42. package/lib/resolvers/material.resolver.js +1 -0
  43. package/lib/resolvers/receive-inventory-order.resolver.js +1 -0
  44. package/lib/resolvers/ship-inventory-order.resolver.js +1 -0
  45. package/lib/resolvers/stock.resolver.js +1 -0
  46. package/lib/resolvers/transfer-order.resolver.js +1 -0
  47. package/package.json +12 -6
package/README.md CHANGED
@@ -9,7 +9,7 @@ Pair with [`@rytass/wms-module-core`](https://www.npmjs.com/package/@rytass/wms-
9
9
  ```bash
10
10
  yarn add @rytass/wms-module-graphql @rytass/wms-module-core
11
11
  # peer deps
12
- yarn add @nestjs/common @nestjs/graphql @nestjs/apollo @apollo/server graphql dataloader reflect-metadata rxjs
12
+ yarn add @nestjs/common @nestjs/graphql @nestjs/apollo @apollo/server @as-integrations/express5 graphql reflect-metadata rxjs @rytass/wms-base-nestjs-module
13
13
  ```
14
14
 
15
15
  ## Quick start
@@ -110,9 +110,99 @@ The member-base adapter reuses everything `CasbinGuard` already put on the reque
110
110
 
111
111
  ➡️ **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).
112
112
 
113
+ ## Turning operations off per deployment
114
+
115
+ `authChecker` **refuses** an operation; these options stop it being **registered at all**.
116
+ The difference matters once someone reads the schema: a refused mutation still appears in
117
+ introspection, in your frontend's codegen and in a security review, as a mutation that is
118
+ guaranteed to fail.
119
+
120
+ ```ts
121
+ WmsModuleGraphqlModule.forRootAsync({
122
+ ...coreOptions,
123
+
124
+ // deny-list — these never enter the schema
125
+ disabledOperations: ['quality-inspection-order:inspect', 'material:create'],
126
+
127
+ // or an allow-list instead (the two are mutually exclusive; passing both throws)
128
+ // enabledOperations: ['material:read', 'stock:read'],
129
+
130
+ // escape hatch by GraphQL field name, for when a requirement is too coarse
131
+ disabledFields: ['availableLoaders'],
132
+ });
133
+ ```
134
+
135
+ Names are the same `` `${resource}:${action}` `` strings `@WmsOperation` declares and your
136
+ `authChecker` receives, exported as the `WmsOperationName` union and the
137
+ `WMS_OPERATION_NAMES` array so you can iterate or diff them.
138
+
139
+ Two things to know before you use it:
140
+
141
+ - **A name is not one field.** Several queries share a requirement — `loader:read` covers
142
+ 6 fields, `stock:read` 5, `material:read` and `loader:create` 3 each. Disabling a name
143
+ disables every field carrying it; use `disabledFields` when you need one.
144
+ - **Disabling a resolver does not disable the domain.** The services stay injectable, so a
145
+ host that owns that step of the workflow itself can still drive
146
+ `ReceiveInventoryOrderService` (or any other) directly. That is the intended pattern.
147
+
148
+ Because the switches mutate process-global registration state, a process may register
149
+ `WmsModuleGraphqlModule` **once**.
150
+
151
+ ## Inventory counting
152
+
153
+ Compare the ledger against what is physically on the shelf, then adjust the ledger to
154
+ match. Twelve operations
155
+ (`inventory-count-order:{read,read-items,create,update,start,count,add-item,remove-item,reset,cancel,settle}`)
156
+ back the flow:
157
+
158
+ ```
159
+ 建立盤點 ──▶ PENDING ──開始盤點──▶ COUNTING ──執行盤差──▶ COMPLETED
160
+ │ │
161
+ └───── 取消盤點 ─────┴────────▶ CANCELLED
162
+ ```
163
+
164
+ - **Scope is by location, expanded downwards.** Counting an aisle counts every shelf under
165
+ it; `createInventoryCountOrder` stores the expansion.
166
+ - **Counting never moves stock between locations.** A line in the wrong place is a
167
+ transfer, not a count — `transfer-order:create` already does that. Counting only ever
168
+ adjusts quantity in place.
169
+ - **Nothing touches stock until `settleInventoryCount`.** Counted quantities are working
170
+ notes; settlement runs in one transaction and writes the difference as `+`/`-` rows,
171
+ which surface as `stock.count-gained` / `stock.count-lost`.
172
+ - **Uncounted lines are left alone.** "Not counted" is not "counted as zero".
173
+ - **Surpluses may name a batch that does not exist yet**; it is created during settlement.
174
+ - **`countDate` is the business date.** The variance is reported under the date the count
175
+ was created for, not the day settlement happened to run. `createdAt` keeps the real
176
+ wall-clock time.
177
+ - **The sheet has its own query.** `inventoryCountOrderItems` paginates and filters the
178
+ lines (料號 / 物料說明 / 批號 / 儲位 / 載具 as ILIKE; 供應商 / 客戶 / 銷售訂單號碼 / 銷售訂單項次
179
+ exactly; plus `onlyVariance`). A field resolver cannot take a page or a filter, and a
180
+ count covering an aisle runs to thousands of lines.
181
+ - **`startInventoryCount` snapshots eight attributes per line** — `held`, `inShipment`,
182
+ `inspectionResult`, `transferId`, `vendorId`, `customerId`, `saleId`, `saleSerialId` —
183
+ so the sheet keeps showing the world the variance is measured against, not today's.
184
+
185
+ ### ⚠️ Starting a count locks its locations
186
+
187
+ This is the one part that changes the behaviour of operations you already use. While an
188
+ order is `COUNTING`, **any** stock movement touching a location in its scope is refused
189
+ with a `409 Conflict` naming the count:
190
+
191
+ ```
192
+ Location <id> is locked by inventory count order IC2508300001;
193
+ finish or cancel that count before moving this stock.
194
+ ```
195
+
196
+ The check sits on the insert of every `*_order_change` row — the same choke point the
197
+ stock events use — so it covers resolvers **and** direct service calls, including your own
198
+ integration code. Without it the book snapshot the variance is measured against would go
199
+ stale and settlement would silently write off somebody else's legitimate movement.
200
+
201
+ Settling or cancelling the count releases the lock.
202
+
113
203
  ## Events & Hooks
114
204
 
115
- 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`:
205
+ Intervene in and get notified about mutations. Every root mutation carries a typed **operation event** (`material.created`, `ship-inventory-order.shipped`, `inventory-count-order.settled`, …) and fans out **stock domain events** (`stock.allocated`, `stock.shipped`, `stock.count-gained`, …) driven by the `ChangeType` state machine. Wire two opt-in hook points via `forRootAsync`:
116
206
 
117
207
  - **`beforeHooks`** run before any DB work — **throw to veto** the mutation (zero side effects).
118
208
  - **`afterListeners`** run **after the outermost transaction commits** — never notified about rolled-back changes.
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ var eventEmitter = require('@nestjs/event-emitter');
4
+ var wmsModuleCore = require('@rytass/wms-module-core');
5
+
6
+ /**
7
+ * Bridges WMS domain events onto `@nestjs/event-emitter`'s `EventEmitter2`, so
8
+ * consumers can subscribe with `@OnEvent('wms.<event-name>')` instead of (or in
9
+ * addition to) the typed `afterListeners`.
10
+ *
11
+ * Pass this into `WmsModuleGraphqlModule.forRootAsync({ eventEmitterBridge })`.
12
+ * Importing this module is the only place `@nestjs/event-emitter` is referenced,
13
+ * keeping it an entirely optional peer dependency — the core graphql module
14
+ * never imports it.
15
+ *
16
+ * Prerequisite: register `EventEmitterModule.forRoot()` in your app so an
17
+ * `EventEmitter2` instance is available for injection.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { wmsEventEmitterBridgeProvider } from '@rytass/wms-module-graphql/adapters/event-emitter';
22
+ *
23
+ * WmsModuleGraphqlModule.forRootAsync({
24
+ * ...coreOptions,
25
+ * eventEmitterBridge: wmsEventEmitterBridgeProvider,
26
+ * });
27
+ *
28
+ * // elsewhere
29
+ * @OnEvent('wms.stock.allocated')
30
+ * handle(payload: WmsStockEventPayload) { ... }
31
+ * ```
32
+ */
33
+ const wmsEventEmitterBridgeProvider = {
34
+ provide: wmsModuleCore.WMS_EVENT_EMITTER_BRIDGE,
35
+ useFactory: (emitter) => ({
36
+ forward: (event) => {
37
+ emitter.emit(`wms.${event.name}`, event.payload);
38
+ },
39
+ }),
40
+ inject: [eventEmitter.EventEmitter2],
41
+ };
42
+
43
+ exports.wmsEventEmitterBridgeProvider = wmsEventEmitterBridgeProvider;
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ var graphql = require('@nestjs/graphql');
4
+
5
+ const extractRequest = (context) => {
6
+ const gqlContext = graphql.GqlExecutionContext.create(context).getContext();
7
+ return gqlContext?.req;
8
+ };
9
+ /**
10
+ * Builds a {@link WmsAuthChecker} that authorizes WMS operations against
11
+ * `@rytass/member-base-nestjs-module`'s Casbin setup.
12
+ *
13
+ * It reuses everything the host's global `CasbinGuard` already prepared on the
14
+ * request — the verified `payload`, the `enforcer`, and even the consumer's
15
+ * (possibly customized) `casbinPermissionChecker`. WMS therefore enforces with
16
+ * the *exact same* policy engine and rules as the host's own resolvers, with no
17
+ * member-base import on the WMS side.
18
+ *
19
+ * Prerequisite: register `MemberBaseModule` with its global guard enabled
20
+ * (the default), and expose the request on the GraphQL context as `req`
21
+ * (Apollo's default).
22
+ */
23
+ const createMemberBaseWmsAuthChecker = (options) => {
24
+ return async (context, requirement) => {
25
+ const request = extractRequest(context);
26
+ // No verified member → unauthenticated. CasbinGuard sets `payload` to
27
+ // undefined when the token is missing or invalid.
28
+ if (!request?.payload) {
29
+ return false;
30
+ }
31
+ // The enforcer / checker are injected by CasbinGuard. Their absence means
32
+ // the host did not enable the global guard — fail closed rather than
33
+ // silently allowing everything.
34
+ if (!request.enforcer || !request.casbinPermissionChecker) {
35
+ return false;
36
+ }
37
+ const actions = options.map(requirement);
38
+ if (actions.length === 0) {
39
+ return false;
40
+ }
41
+ return request.casbinPermissionChecker({
42
+ enforcer: request.enforcer,
43
+ payload: request.payload,
44
+ actions,
45
+ });
46
+ };
47
+ };
48
+ /**
49
+ * A {@link WmsActorExtractor} for `@rytass/member-base-nestjs-module`. Reads the
50
+ * verified JWT payload that `CasbinGuard` attaches to the request and surfaces it
51
+ * as the event/hook `actor`. Returns `null` for unauthenticated requests.
52
+ *
53
+ * Pass to `WmsModuleGraphqlModule.forRootAsync({ actorExtractor })` so every
54
+ * operation event and before-hook carries `{ id, domain }`. Imports nothing from
55
+ * member-base — it only reads the runtime request fields.
56
+ */
57
+ const memberBaseActorExtractor = (context) => {
58
+ const payload = extractRequest(context)?.payload;
59
+ if (!payload?.id) {
60
+ return null;
61
+ }
62
+ return { id: payload.id, domain: payload.domain, raw: payload };
63
+ };
64
+
65
+ exports.createMemberBaseWmsAuthChecker = createMemberBaseWmsAuthChecker;
66
+ exports.memberBaseActorExtractor = memberBaseActorExtractor;
@@ -71,6 +71,14 @@ static forRootAsync(
71
71
 
72
72
  同樣每個都有 Symbol + EntityClass 雙匯出,共 37 個:
73
73
 
74
+ **盤點單 (Inventory Count Order)**
75
+ - `InventoryCountOrderEntity` / `InventoryCountOrderEntityClass` — 盤點單主檔(`countDate` 為盤差記帳基準日)
76
+ - `InventoryCountOrderLocationEntity` / `InventoryCountOrderLocationEntityClass` — 盤點範圍,已展開至所有下層儲位(`isRoot` 標示使用者實際選的節點)
77
+ - `InventoryCountOrderItemEntity` / `InventoryCountOrderItemEntityClass` — 盤點單身(`isAddition` 標示盤盈新增列)。
78
+ 開始盤點時另外快照 8 個當下屬性:`held`、`inShipment`、`inspectionResult`、`transferId`、`vendorId`、`customerId`、`saleId`、`saleSerialId`
79
+ - `InventoryCountOrderChangeEntity` / `InventoryCountOrderChangeEntityClass` — 盤差異動(正=盤盈、負=盤虧)
80
+ - 來源: `lib/models/extensions/inventory-count-order/`
81
+
74
82
  **入庫訂單 (Receive Inventory Order)**
75
83
  - `ReceiveInventoryOrderEntity` / `ReceiveInventoryOrderEntityClass` — 入庫單主檔
76
84
  - `ReceiveInventoryOrderItemEntity` / `ReceiveInventoryOrderItemEntityClass` — 入庫單明細
@@ -171,6 +179,7 @@ static forRootAsync(
171
179
  | `MergeBatchOrderService<T>` | `merge` | 批號合併 | `lib/services/merge-batch-order.service.ts` |
172
180
  | `ReclassifyInventoryOrderService<T>` | `reclassify` | 庫存轉料號 | `lib/services/reclassify-inventory-order.service.ts` |
173
181
  | `ScrapeService<T>` | `scrape` | 庫存報廢 | `lib/services/scrape.service.ts` |
182
+ | `InventoryCountOrderService<T>` | `findMany`, `findOne`, `findItems`, `create`, `update`, `start`, `count`, `addItem`, `removeItem`, `reset`, `cancel`, `settle` | 盤點單管理。`start` 會鎖定盤點範圍內的儲位,`settle` 是唯一會改動庫存的方法。`findItems` 是單身的分頁查詢 | `lib/services/inventory-count-order.service.ts` |
174
183
  | `QualityInspectionOrderService<T>` | `inspect` | 品質檢驗 | `lib/services/quality-inspection-order.service.ts` |
175
184
  | `SaleOrderService` | (DataSource 直接操作) | 銷售訂單查詢 | `lib/services/sale-order.service.ts` |
176
185
  | `HelperService` | `getSeq` | 序號產生工具 | `lib/services/helper.service.ts` |
@@ -262,11 +271,32 @@ enum ChangeType {
262
271
  RECEIVED, IN_SHIPMENT, SHIPPED, IN_TRANSFER, TRANSFERRED,
263
272
  VENDOR_TRANSFERRED, VENDOR_RETURNED, HELD, RELEASED,
264
273
  CUSTOMER_HELD, CUSTOMER_RETURNED, SPLITTED, MERGED,
265
- ALLOCATED, DEALLOCATED, RECLASSIFIED, SCRAPED
274
+ ALLOCATED, DEALLOCATED, RECLASSIFIED, SCRAPED,
275
+ COUNT_GAINED, COUNT_LOST
266
276
  }
267
277
  ```
268
278
  來源: `lib/constants/stock-status.ts`
269
279
 
280
+ ### `InventoryCountStatus` (enum)
281
+ ```ts
282
+ enum InventoryCountStatus { PENDING, COUNTING, COMPLETED, CANCELLED }
283
+ ```
284
+ 盤點單狀態:尚未執行 / 盤點中 / 已完成 / 已取消。只有 `COUNTING` 接受盤點數量,也只有
285
+ `COUNTING` 會鎖定範圍內儲位;`COMPLETED` 只能由執行盤差達成。
286
+ 來源: `lib/constants/inventory-count-status.ts`
287
+
288
+ ### `WmsOperationName` (type) / `WMS_OPERATION_NAMES` (const)
289
+ ```ts
290
+ type WmsOperationName = 'material:read' | 'material:create' | /* …59 項… */;
291
+ const WMS_OPERATION_NAMES: readonly WmsOperationName[];
292
+ ```
293
+ 本套件所有 root operation 的 `` `${resource}:${action}` `` 名稱。同一組字串有三個用途:
294
+ `authChecker` 收到的 requirement、`@rytass/wms-module-graphql` 的
295
+ `enabledOperations` / `disabledOperations`、以及
296
+ `@rytass/wms-module-react` 的 `INVENTORY_ACTION_OPERATIONS` 對照表。
297
+ **注意名稱不等於欄位**:多支 query 共用同一組 requirement(`loader:read` 對應 6 支)。
298
+ 來源: `lib/constants/operations.ts`
299
+
270
300
  ### `ReceiveInventoryOrderStatus` (enum)
271
301
  ```ts
272
302
  enum ReceiveInventoryOrderStatus { PENDING, PARTIAL_RECEIVED, RECEIVED, REMOVED }
@@ -311,6 +341,12 @@ static forRootAsync(
311
341
  // defaultLocationKey: string — 預設儲存位置 key
312
342
  // + WmsAuthModuleOptions:
313
343
  // authChecker?: WmsAuthChecker — 選填;resolver 授權委派函式(見 Authorization)
344
+ // + WmsOperationRegistrationOptions(since 0.9.0):
345
+ // enabledOperations?: readonly WmsOperationName[] — 白名單,只註冊列出的 operation
346
+ // disabledOperations?: readonly WmsOperationName[] — 黑名單;與白名單互斥,同時給會 throw
347
+ // disabledFields?: readonly string[] — 以 GraphQL 欄位名精準停用單一欄位
348
+ // 被停用者「不會進 schema」(introspection 與 SDL 皆無),
349
+ // 但對應的 service 仍可注入 — 宿主可繼續直呼。詳見下方 Operation registration。
314
350
  //
315
351
  // since 0.8.0(需 wms-base-nestjs-module >= 0.4.0):
316
352
  // WMSBaseModuleOptions 的六個 entity 選項(material / batch / location /
@@ -322,6 +358,8 @@ static forRootAsync(
322
358
 
323
359
  > **Breaking from 0.2.x**: 0.2.0 took a second `gqlOptions` argument and started Apollo Server internally; 0.3.0 dropped it. Consumers now own `GraphQLModule.forRootAsync(...)` and the GraphQL context factory. Use `createWmsLoaders(loaderService)` inside `context` to provide the WMS DataLoader trio (see below).
324
360
 
361
+ > **Operation registration (`enabledOperations` / `disabledOperations` / `disabledFields`)**: optional. Omit them and every operation is registered as before. They mutate process-global registration state, so a process may register `WmsModuleGraphqlModule` **once**.
362
+
325
363
  > **Authorization (`authChecker`)**: optional. Omit it and the module stays fully permissive. Supply it to authorize every WMS root op against your permission system — see the dedicated [**Authorization**](#authorization) section below and [`docs/authorization.md`](./authorization.md).
326
364
 
327
365
  ### `LoaderService` (`@Injectable()`, since 0.3.0 publicly exported)
@@ -455,6 +493,42 @@ The adapter imports nothing from member-base — it reads the `payload` / `enfor
455
493
 
456
494
  ---
457
495
 
496
+ ## Operation registration
497
+
498
+ 以部署為單位決定「哪些 operation 根本不進 schema」。`authChecker` 是**拒絕執行**,
499
+ 這組選項是**不註冊**——差別在於被拒絕的 mutation 仍會出現在 introspection、前端 codegen
500
+ 與安全稽核清單裡,看起來像一支保證會失敗的 API。
501
+
502
+ ### Root exports (`@rytass/wms-module-graphql`)
503
+
504
+ | 匯出 | 種類 | 說明 |
505
+ |---|---|---|
506
+ | `WmsOperationRegistrationOptions` | type | `enabledOperations` / `disabledOperations` / `disabledFields` 三個選填欄位 |
507
+ | `pruneDisabledOperations(resolverClasses, options)` | function | `forRootAsync` 內部使用;公開匯出供進階情境(自組 provider 清單)使用 |
508
+ | `PrunedOperations` | type | `{ queries, mutations }` — 實際被移除的欄位名 |
509
+ | `WmsOperationName` | type | 從 `@rytass/wms-module-core` 轉出 |
510
+ | `WMS_OPERATION_NAMES` | const | 從 `@rytass/wms-module-core` 轉出 |
511
+
512
+ ### 語意
513
+
514
+ - **名稱不等於欄位。** 多支 query 共用同一組 requirement(`loader:read` 6 支、
515
+ `stock:read` 5 支、`material:read` 與 `loader:create` 各 3 支),停用名稱會一次移除全部;
516
+ 需要更細的粒度時用 `disabledFields`。
517
+ - **停用 resolver 不會停用 domain。** service 照常可注入,這是刻意的:自有流程的宿主
518
+ 仍能直呼 `ReceiveInventoryOrderService` 等服務。
519
+ - **`@ResolveField` 不受影響**——它們只能透過已註冊的 root 欄位抵達。
520
+
521
+ ### 實作備註
522
+
523
+ resolver metadata 在 `@nestjs/graphql` 是 **lazy** 的(`@Query()` 只登記 thunk,真正寫入
524
+ 要等 schema build),所以停用同時做兩件事:`forRootAsync` 當下刪掉 resolver prototype 上
525
+ 的方法(`ResolversExplorerService` 掃 prototype,且早於 metadata 載入),並包裝
526
+ `TypeMetadataStorage.addQueryMetadata` / `addMutationMetadata` 擋掉該筆。兩者必須成對,
527
+ 否則 schema 會留下沒有 resolver 的死欄位,或 `makeExecutableSchema` 因 resolver 與
528
+ typeDefs 不符而拒絕整份 schema。
529
+
530
+ ---
531
+
458
532
  ## Events & Hooks
459
533
 
460
534
  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).
@@ -554,6 +628,7 @@ WmsModuleGraphqlModule.forRootAsync({
554
628
  | `MaterialQuery` | `material`, `materials` | `MaterialDto`, `Materials` | `lib/queries/material.query.ts` |
555
629
  | `StockQuery` | `stock`, `stocks`, `stocksGroupedByMaterial`, `aggregatedStocks`, `changeLogs` | `StockDto`, `Stocks`, `Materials`, `AggregatedStocks`, `ChangeLogs` | `lib/queries/stock.query.ts` |
556
630
  | `BatchQuery` | `batches` | `Batches` | `lib/queries/batch.query.ts` |
631
+ | `InventoryCountOrderQuery` | `inventoryCountOrders`, `inventoryCountOrder`, `inventoryCountOrderItems` | `InventoryCountOrders`, `InventoryCountOrderDto`, `InventoryCountOrderItems` | `lib/queries/inventory-count-order.query.ts` |
557
632
  | `LocationQuery` | `location`, `locations` | `LocationDto`, `Locations` | `lib/queries/location.query.ts` |
558
633
  | `ReceiveInventoryOrderQuery` | `receiveInventoryOrder`, `receiveInventoryOrders` | `ReceiveInventoryOrderDto`, `ReceiveInventoryOrders` | `lib/queries/recieve-inventory-order.query.ts` |
559
634
  | `ShipInventoryOrderQuery` | `shipInventoryOrder`, `shipInventoryOrders` | `ShipInventoryOrderDto`, `ShipInventoryOrders` | `lib/queries/ship-inventory-order.query.ts` |
@@ -572,6 +647,7 @@ WmsModuleGraphqlModule.forRootAsync({
572
647
  | `LoaderMutation` | `createLoader`, `createBatchLoader`, `updateLoader`, `createLoaderType`(方法名未確認), `updateLoaderType` | 載具與類型 | `lib/mutations/loader.mutation.ts` |
573
648
  | `LocationMutation` | `createLocation`, `updateLocation` | 庫位 | `lib/mutations/location.mutation.ts` |
574
649
  | `MaterialMutation` | `createMaterial`, `updateMaterial` | 料號 | `lib/mutations/material.mutation.ts` |
650
+ | `InventoryCountOrderMutation` | `createInventoryCountOrder`, `updateInventoryCountOrder`, `startInventoryCount`, `countInventory`, `addInventoryCountItem`, `removeInventoryCountItem`, `resetInventoryCount`, `cancelInventoryCount`, `settleInventoryCount` | 盤點單。`startInventoryCount` 會鎖定範圍內儲位;`settleInventoryCount` 是唯一會改動庫存的一支 | `lib/mutations/inventory-count-order.mutation.ts` |
575
651
  | `ReceiveInventoryOrderMutation` | `createReceiveInventoryOrder`, `updateReceiveInventoryOrder`, `receive` | 入庫單 | `lib/mutations/receive-inventory-order.mutation.ts` |
576
652
  | `ShipInventoryOrderMutation` | `createShipInventoryOrder`, `updateShipInventoryOrder`, `ship`, `cancelShip` | 出庫單 | `lib/mutations/ship-inventory-order.mutation.ts` |
577
653
  | `TransferOrderMutation` | `createTransferOrder`, `updateTransferOrder`, `transfer`, `cancelTransfer` | 調撥單 | `lib/mutations/transfer-order.mutation.ts` |
@@ -660,6 +736,9 @@ WmsModuleGraphqlModule.forRootAsync({
660
736
 
661
737
  **庫存**
662
738
  - `FindStockArgs` (extends `PaginationArgs`) — 來源: `lib/inputs/find-stock.args.ts`
739
+ - `materialKey` / `batchKey` / `locationKey` / `loaderKey` 為 ILIKE 模糊比對。
740
+ - `vendorId` / `customerId` / `saleId` / `saleSerialId` 取自批號,為**完全比對**:它們是上游系統的識別碼,
741
+ 模糊比對會讓 `SO-1` 撈到 `SO-10`。四支 stock query(`stocks`、`stocksGroupedByMaterial`、`aggregatedStocks`、`changeLogs`)皆適用。
663
742
  - `DateRangeArgs` (`@InputType`) — 來源: `lib/inputs/date-range.args.ts`
664
743
 
665
744
  **批號**
package/docs/events.md CHANGED
@@ -215,7 +215,7 @@ and the bridge can be used together — both fire.
215
215
 
216
216
  ## 5. Complete event list
217
217
 
218
- ### Operation events (38) — `<domain>.<past-tense-action>`
218
+ ### Operation events (45) — `<domain>.<past-tense-action>`
219
219
 
220
220
  The **v2 `result` type** column shows the concrete type subscribers receive via
221
221
  `WmsOperationResultMap`. Events marked ⚠️ **boolean** are the 9 action mutations
@@ -269,24 +269,43 @@ events they fan out (see §3.1).
269
269
  > `quantityChanged`. So a mutation emits a stock event only on the code paths
270
270
  > that actually write a change row (e.g. an `update*` only when it adds items).
271
271
  >
272
+
273
+ Inventory counting adds seven. Only `settled` moves stock; the rest record progress
274
+ through the count, which is what makes them useful for an audit trail of who counted
275
+ what and when.
276
+
277
+ | Event | Mutation | v2 `result` type | Fans out to stock event |
278
+ |--------------------------------------|----------------------------|--------------------------------------|---------------------------------------------|
279
+ | `inventory-count-order.created` | createInventoryCountOrder | `InventoryCountOrderDto` | — |
280
+ | `inventory-count-order.updated` | updateInventoryCountOrder / addInventoryCountItem / removeInventoryCountItem | `InventoryCountOrderDto` \| `InventoryCountOrderItemDto` | — |
281
+ | `inventory-count-order.started` | startInventoryCount | `InventoryCountOrderDto` | — (snapshots the ledger, locks the scope) |
282
+ | `inventory-count-order.counted` | countInventory | `InventoryCountOrderDto` | — |
283
+ | `inventory-count-order.reset` | resetInventoryCount | `InventoryCountOrderDto` | — |
284
+ | `inventory-count-order.cancelled` | cancelInventoryCount | `InventoryCountOrderDto` | — (releases the lock) |
285
+ | `inventory-count-order.settled` | settleInventoryCount | `InventoryCountOrderDto` | `stock.count-gained` / `stock.count-lost` |
286
+
272
287
  > The "v2 `result` type" column refers to the typed overlay provided by
273
288
  > `WmsOperationResultMap` (exported from `@rytass/wms-module-graphql`). In v1
274
289
  > `result` is typed `unknown`; consumer code using the typed maps gets the
275
290
  > concrete DTO automatically.
276
291
 
277
- ### Stock domain events (18) — `stock.<change-type>`
292
+ ### Stock domain events (20) — `stock.<change-type>`
278
293
 
279
294
  `stock.received`, `stock.in-shipment`, `stock.shipped`, `stock.in-transfer`,
280
295
  `stock.transferred`, `stock.vendor-transferred`, `stock.vendor-returned`,
281
296
  `stock.held`, `stock.released`, `stock.customer-held`,
282
297
  `stock.customer-returned`, `stock.splitted`, `stock.merged`, `stock.allocated`,
283
298
  `stock.deallocated`, `stock.reclassified`, `stock.scraped`,
284
- `stock.inspection-resolved`.
299
+ `stock.count-gained`, `stock.count-lost`, `stock.inspection-resolved`.
285
300
 
286
- The first 17 map 1:1 to `ChangeType`. `stock.inspection-resolved` has no
301
+ 19 of them map 1:1 to `ChangeType`. `stock.inspection-resolved` has no
287
302
  `ChangeType` (quality inspection only flips the `inspectionResult` flag) and
288
303
  carries `changeType: null` plus `inspectionResult`.
289
304
 
305
+ `stock.count-gained` / `stock.count-lost` are emitted only by
306
+ `settleInventoryCount`, one per line whose counted quantity differed from the
307
+ books — a line that matched writes no change row and therefore no event.
308
+
290
309
  A single operation can emit **several** stock events — one per `*_order_change`
291
310
  row written, i.e. one per affected item. Cancellations re-use the forward event
292
311
  with a negative `quantityDelta` (`cancelShip` -> `stock.shipped`,
@@ -348,23 +348,37 @@ The `DEFAULT` location/loader/loaderType returned by the second query was seeded
348
348
 
349
349
  ### 4.6 Version compatibility matrix
350
350
 
351
- Tested combinations as of 0.1.2:
352
-
353
- | Package | Tested version | Notes |
354
- |----------------------------------|-------------------------|--------------------------------------------------------------------|
355
- | NestJS (`@nestjs/*`) | 11.x | 10.x untested; some Express-5-related APIs differ |
356
- | `@nestjs/graphql` | 13.x | Required for `@nestjs/apollo@13` |
357
- | `@nestjs/apollo` | 13.0.x – 13.1.x | 13.2+ requires @apollo/server@^5 which breaks lib's v4 plugin usage |
358
- | `@apollo/server` | 4.x | v5 is breaking; not supported |
359
- | `@as-integrations/express5` | 1.x | Required runtime peer |
360
- | TypeORM | 0.3.x | 0.2.x not supported (different DataSource API) |
361
- | PostgreSQL | 14+ | Native dialect |
362
- | `graphql` | 16.x | 15.x untested |
363
- | Node.js | 20 LTS or 22 | 18 deprecated, 24 untested |
364
- | Next.js | 15.x Pages Router | App Router unsupported (see §13.11) |
365
- | React | 19.x | 18.x likely works but untested |
366
- | `@mezzanine-ui/react` | 1.3.x | Earlier versions untested |
367
- | `nuqs` | 2.x (Pages adapter) | App Router adapter untested (see §13.11) |
351
+ Two different questions, so two columns. **Accepted** is what the published
352
+ `peerDependencies` allow — a version outside it fails to install. **Tested** is the
353
+ combination CI and the integration suite actually run against; anything else is
354
+ accepted because nothing in the lib's use of that package is known to break, not
355
+ because it was exercised.
356
+
357
+ | Package | Accepted (peer range) | Tested | Notes |
358
+ |-----------------------------|---------------------------|-------------------|-----------------------------------------------------------------------------|
359
+ | `@nestjs/common` / `core` | 10.x – 11.x | 11.1.x | **12.x is not accepted**: the whole NestJS 12 line is ESM-only, which this package (dual CJS+ESM) and a webpack-bundled host cannot consume yet |
360
+ | `@nestjs/graphql` | 13.x | 13.4.5 | 14.x requires NestJS 12 (ESM-only) |
361
+ | `@nestjs/apollo` | 13.x | 13.4.5 | The lib itself imports neither `@nestjs/apollo` nor `@apollo/server`; the peer records that a driver is required |
362
+ | `@apollo/server` | not a peer | 5.5.1 | Chosen by your driver, not by this package |
363
+ | `@as-integrations/express5` | 1.x | 1.1.2 | Required runtime peer of the Express driver |
364
+ | `graphql` | 16.x | 16.12.0 | 17.x is blocked upstream — `@apollo/server@5` still peers `^16.11.0` |
365
+ | `rxjs` | 7.x | 7.8.x | One import (the mutation interceptor) |
366
+ | `@nestjs/event-emitter` | 2.x – 3.x (optional) | 3.1.0 | The next release is 12.x, which is ESM-only |
367
+ | `@rytass/wms-base-nestjs-module` | 0.4.x | 0.4.0 | Imported directly by this package (the warehouse-map mutation and the location resolver) and shared with `@rytass/wms-module-core`. Declared as a peer since 0.10.0 so both see the same copy |
368
+ | TypeORM | 0.3.x | 0.3.28 | 0.2.x not supported (different DataSource API) |
369
+ | PostgreSQL | — | 14+ | Native dialect |
370
+ | Node.js | — | 20 LTS / 22 | 18 deprecated, 24 untested |
371
+ | Next.js | 15.x – 16.x | 15.5.x Pages | Only `next/router` and `next/navigation` are used; App Router unsupported (see §13.11) |
372
+ | React / React DOM | 18.x – 19.x | 19.2.x | No React-19-only API is used, and `@mezzanine-ui/react` peers `>=18` |
373
+ | `@mezzanine-ui/react` | 1.x | 1.4.1 | `@mezzanine-ui/system` is required — 45 of the lib's SCSS files `@use` it |
374
+ | `nuqs` | 2.x | 2.8.x | Pages adapter; App Router adapter untested (see §13.11) |
375
+
376
+ > **Why `@apollo/server` v5 is fine.** An earlier release pinned `@nestjs/apollo` to
377
+ > `>=13.0.0 <13.2.0`, on the grounds that 13.2+ moved to `@apollo/server@^5` and "the lib
378
+ > uses v4 plugin APIs". That was wrong: the only `ApolloServerPluginLandingPageLocalDefault`
379
+ > usage lives in this repo's own `apps/api`, never in the published package, which imports
380
+ > nothing from `@apollo/server` at all. The pin excluded the very combination the test suite
381
+ > runs on, so it has been widened back to `^13.0.0`.
368
382
 
369
383
  ---
370
384