@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.
- package/README.md +92 -2
- package/adapters/event-emitter.cjs.js +43 -0
- package/adapters/member-base.cjs.js +66 -0
- package/docs/api-reference.md +80 -1
- package/docs/events.md +23 -4
- package/docs/integration-guide.md +31 -17
- package/index.cjs.js +935 -1
- package/index.d.ts +3 -1
- package/index.js +2 -0
- package/lib/dto/index.d.ts +1 -0
- package/lib/dto/inventory-count-order.dto.d.ts +61 -0
- package/lib/dto/inventory-count-order.dto.js +197 -0
- package/lib/events/operation-result-map.d.ts +8 -0
- package/lib/graphql.module.d.ts +6 -2
- package/lib/graphql.module.js +18 -1
- package/lib/inputs/find-stock.args.d.ts +9 -0
- package/lib/inputs/find-stock.args.js +25 -0
- package/lib/inputs/inventory-count-order.args.d.ts +69 -0
- package/lib/inputs/inventory-count-order.args.js +257 -0
- package/lib/mutations/inventory-count-order.mutation.d.ts +19 -0
- package/lib/mutations/inventory-count-order.mutation.js +138 -0
- package/lib/mutations/loader.mutation.js +1 -0
- package/lib/mutations/material.mutation.js +1 -0
- package/lib/mutations/receive-inventory-order.mutation.js +1 -0
- package/lib/mutations/ship-inventory-order.mutation.js +1 -0
- package/lib/queries/inventory-count-order.query.d.ts +17 -0
- package/lib/queries/inventory-count-order.query.js +63 -0
- package/lib/queries/loader.query.js +1 -0
- package/lib/queries/material.query.js +1 -0
- package/lib/queries/recieve-inventory-order.query.js +1 -0
- package/lib/queries/ship-inventory-order.query.js +1 -0
- package/lib/queries/stock.query.js +1 -0
- package/lib/registry/index.d.ts +1 -0
- package/lib/registry/operation-registry.d.ts +59 -0
- package/lib/registry/operation-registry.js +136 -0
- package/lib/resolvers/aggregated-stock.resolver.js +1 -0
- package/lib/resolvers/change-log.resolver.js +1 -0
- package/lib/resolvers/inventory-count-order.resolver.d.ts +34 -0
- package/lib/resolvers/inventory-count-order.resolver.js +148 -0
- package/lib/resolvers/loader-type.resolver.js +1 -0
- package/lib/resolvers/loader.resolver.js +1 -0
- package/lib/resolvers/material.resolver.js +1 -0
- package/lib/resolvers/receive-inventory-order.resolver.js +1 -0
- package/lib/resolvers/ship-inventory-order.resolver.js +1 -0
- package/lib/resolvers/stock.resolver.js +1 -0
- package/lib/resolvers/transfer-order.resolver.js +1 -0
- 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
|
|
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;
|
package/docs/api-reference.md
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
|
358
|
-
|
|
359
|
-
| `@
|
|
360
|
-
|
|
|
361
|
-
|
|
|
362
|
-
| `
|
|
363
|
-
|
|
|
364
|
-
|
|
|
365
|
-
|
|
|
366
|
-
| `@
|
|
367
|
-
| `
|
|
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
|
|