@rytass/wms-module-graphql 0.3.0 → 0.5.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 +55 -0
- package/adapters/event-emitter.d.ts +29 -0
- package/adapters/event-emitter.js +41 -0
- package/adapters/member-base.d.ts +35 -0
- package/adapters/member-base.js +47 -0
- package/docs/README.md +6 -0
- package/docs/api-reference.md +100 -1
- package/docs/authorization.md +349 -0
- package/docs/events.md +304 -0
- package/docs/integration-guide.md +121 -4
- package/index.cjs.js +299 -3
- package/index.d.ts +4 -0
- package/index.js +7 -0
- package/lib/auth/host-skip.constant.d.ts +27 -0
- package/lib/auth/host-skip.constant.js +29 -0
- package/lib/auth/index.d.ts +4 -0
- package/lib/auth/wms-auth.guard.d.ts +19 -0
- package/lib/auth/wms-auth.guard.js +41 -0
- package/lib/auth/wms-auth.types.d.ts +36 -0
- package/lib/auth/wms-auth.types.js +6 -0
- package/lib/auth/wms-operation.decorator.d.ts +23 -0
- package/lib/auth/wms-operation.decorator.js +29 -0
- package/lib/events/index.d.ts +3 -0
- package/lib/events/wms-mutation-event.constants.d.ts +12 -0
- package/lib/events/wms-mutation-event.constants.js +4 -0
- package/lib/events/wms-mutation-event.decorator.d.ts +15 -0
- package/lib/events/wms-mutation-event.decorator.js +20 -0
- package/lib/events/wms-mutation.interceptor.d.ts +19 -0
- package/lib/events/wms-mutation.interceptor.js +72 -0
- package/lib/graphql.module.d.ts +20 -3
- package/lib/graphql.module.js +19 -4
- package/lib/mutations/allocate-inventory-order.mutation.js +10 -0
- package/lib/mutations/hold-inventory-order.mutation.js +10 -0
- package/lib/mutations/loader.mutation.js +14 -0
- package/lib/mutations/location.mutation.js +8 -0
- package/lib/mutations/material.mutation.js +8 -0
- package/lib/mutations/merge-batch.mutation.js +6 -0
- package/lib/mutations/quality-inspection-order.mutation.js +6 -0
- package/lib/mutations/receive-inventory-order.mutation.js +10 -0
- package/lib/mutations/reclassify-inventory-order.mutation.js +6 -0
- package/lib/mutations/scrape-order.mutation.js +6 -0
- package/lib/mutations/ship-inventory-order.mutation.js +12 -0
- package/lib/mutations/split-batch.mutation.js +6 -0
- package/lib/mutations/transfer-customer-order.mutation.js +10 -0
- package/lib/mutations/transfer-order.mutation.js +12 -0
- package/lib/mutations/transfer-vendor-order.mutation.js +10 -0
- package/lib/mutations/warehouse-map.mutation.js +6 -0
- package/lib/queries/allocate-inventory-order.query.js +3 -0
- package/lib/queries/batch.query.js +3 -0
- package/lib/queries/hold-inventory-order.query.js +3 -0
- package/lib/queries/loader.query.js +8 -0
- package/lib/queries/location.query.js +4 -0
- package/lib/queries/material.query.js +4 -0
- package/lib/queries/recieve-inventory-order.query.js +4 -0
- package/lib/queries/sale-order.query.js +4 -0
- package/lib/queries/ship-inventory-order.query.js +4 -0
- package/lib/queries/stock.query.js +7 -0
- package/lib/queries/transfer-order.query.js +4 -0
- package/lib/queries/unit.query.js +3 -0
- package/lib/queries/warehouse-map.query.js +3 -0
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -62,6 +62,57 @@ You register a **single** `GraphQLModule.forRootAsync(...)` of your own, mix you
|
|
|
62
62
|
|
|
63
63
|
`WmsModuleGraphqlModule` internally wires `WmsModuleCoreModule.forRootAsync` so you do not need to import the core module separately when using this package.
|
|
64
64
|
|
|
65
|
+
## Authorization
|
|
66
|
+
|
|
67
|
+
Every WMS root `@Query` / `@Mutation` is annotated with `@WmsOperation({ resource, action })` — a **host-neutral** permission requirement enforced by the library's own route-level `WmsAuthGuard`, which delegates the allow/deny decision to an `authChecker` **you** supply. The library never imports your auth system; you map WMS's `resource`/`action` onto whatever you use (Casbin, RBAC, JWT scopes…).
|
|
68
|
+
|
|
69
|
+
> ⚠️ **Using a global guard (e.g. member-base's `CasbinGuard` as `APP_GUARD`)?** A deny-by-default global guard blankets *every* resolver in the schema, including WMS's, and rejects them all because they carry no host-specific metadata — "every WMS operation is forbidden". WMS fixes this by stamping each operation with a host-skip marker so your global guard **defers**, while `WmsAuthGuard` does the real enforcement. **You do not touch your global guard.**
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { WmsModuleGraphqlModule } from '@rytass/wms-module-graphql';
|
|
73
|
+
import { createMemberBaseWmsAuthChecker } from '@rytass/wms-module-graphql/adapters/member-base';
|
|
74
|
+
|
|
75
|
+
WmsModuleGraphqlModule.forRootAsync({
|
|
76
|
+
useFactory: async () => ({}),
|
|
77
|
+
defaultLoaderSerialId: 'DEFAULT',
|
|
78
|
+
defaultLoaderTypeKey: 'DEFAULT',
|
|
79
|
+
defaultLocationKey: 'DEFAULT',
|
|
80
|
+
// optional — omit to stay fully permissive (local dev / demos)
|
|
81
|
+
authChecker: createMemberBaseWmsAuthChecker({
|
|
82
|
+
map: ({ resource, action }) => [[`wms:${resource}`, action]],
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The member-base adapter reuses everything `CasbinGuard` already put on the request (verified `payload`, `enforcer`, your custom `casbinPermissionChecker`) so WMS enforces with the **exact same** policy engine as your own resolvers — with **zero** member-base import on the WMS side. For custom JWT/RBAC, pass your own `authChecker: (ctx, { resource, action }) => boolean`.
|
|
88
|
+
|
|
89
|
+
➡️ **Full design & recipes: [`docs/authorization.md`](./docs/authorization.md)** (the problem, execution order, member-base / custom-JWT / no-guard recipes, the single coupling point, public API, FAQ).
|
|
90
|
+
|
|
91
|
+
## Events & Hooks
|
|
92
|
+
|
|
93
|
+
Intervene in and get notified about mutations. Every root mutation carries a typed **operation event** (`material.created`, `ship-inventory-order.shipped`, …) and fans out **stock domain events** (`stock.allocated`, `stock.shipped`, …) driven by the `ChangeType` state machine. Wire two opt-in hook points via `forRootAsync`:
|
|
94
|
+
|
|
95
|
+
- **`beforeHooks`** run before any DB work — **throw to veto** the mutation (zero side effects).
|
|
96
|
+
- **`afterListeners`** run **after the outermost transaction commits** — never notified about rolled-back changes.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
WmsModuleGraphqlModule.forRootAsync({
|
|
100
|
+
...coreOptions,
|
|
101
|
+
beforeHooks: {
|
|
102
|
+
'ship-inventory-order.shipped': async ({ args }) => {
|
|
103
|
+
if (!allowed(args)) throw new ForbiddenException('blocked');
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
afterListeners: {
|
|
107
|
+
'stock.allocated': async ({ payload }) => { await erp.reserve(payload); },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
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
|
+
|
|
114
|
+
➡️ **Full design, the complete 56-event list & limitations: [`docs/events.md`](./docs/events.md)**.
|
|
115
|
+
|
|
65
116
|
## DataLoader context
|
|
66
117
|
|
|
67
118
|
The module ships a `HelperModule` (auto-registered) that provides a singleton `LoaderService`. To avoid N+1 queries, the WMS resolvers expect a per-request DataLoader trio at `context.loaders`:
|
|
@@ -97,12 +148,16 @@ Full documentation is shipped inside this package under the `docs/` directory (a
|
|
|
97
148
|
| Document | Description |
|
|
98
149
|
|---|---|
|
|
99
150
|
| [`docs/integration-guide.md`](./docs/integration-guide.md) | Step-by-step wiring guide for NestJS + Next.js consumers |
|
|
151
|
+
| [`docs/authorization.md`](./docs/authorization.md) | Complete resolver-authorization design & integration recipes (member-base / custom JWT / no-guard) |
|
|
152
|
+
| [`docs/events.md`](./docs/events.md) | Mutation event/hook system — before-hooks (veto), post-commit after-listeners, complete event list, event-emitter bridge |
|
|
100
153
|
| [`docs/api-reference.md`](./docs/api-reference.md) | Complete export catalog with signatures and usage examples |
|
|
101
154
|
|
|
102
155
|
GitHub hosted links (always up-to-date):
|
|
103
156
|
|
|
104
157
|
- [Integration Guide — §4 Backend setup](https://github.com/Rytass/WMS-Module/blob/main/docs/integration-guide.md#4-backend-setup-nestjs--graphql--typeorm)
|
|
105
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)
|
|
159
|
+
- [Authorization — design & recipes](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/authorization.md)
|
|
160
|
+
- [Events & Hooks — design & event list](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/events.md)
|
|
106
161
|
- [API Reference — @rytass/wms-module-graphql](https://github.com/Rytass/WMS-Module/blob/main/docs/api-reference.md#rytasswms-module-graphql)
|
|
107
162
|
|
|
108
163
|
## License
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Provider } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Bridges WMS domain events onto `@nestjs/event-emitter`'s `EventEmitter2`, so
|
|
4
|
+
* consumers can subscribe with `@OnEvent('wms.<event-name>')` instead of (or in
|
|
5
|
+
* addition to) the typed `afterListeners`.
|
|
6
|
+
*
|
|
7
|
+
* Pass this into `WmsModuleGraphqlModule.forRootAsync({ eventEmitterBridge })`.
|
|
8
|
+
* Importing this module is the only place `@nestjs/event-emitter` is referenced,
|
|
9
|
+
* keeping it an entirely optional peer dependency — the core graphql module
|
|
10
|
+
* never imports it.
|
|
11
|
+
*
|
|
12
|
+
* Prerequisite: register `EventEmitterModule.forRoot()` in your app so an
|
|
13
|
+
* `EventEmitter2` instance is available for injection.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { wmsEventEmitterBridgeProvider } from '@rytass/wms-module-graphql/adapters/event-emitter';
|
|
18
|
+
*
|
|
19
|
+
* WmsModuleGraphqlModule.forRootAsync({
|
|
20
|
+
* ...coreOptions,
|
|
21
|
+
* eventEmitterBridge: wmsEventEmitterBridgeProvider,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // elsewhere
|
|
25
|
+
* @OnEvent('wms.stock.allocated')
|
|
26
|
+
* handle(payload: WmsStockEventPayload) { ... }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const wmsEventEmitterBridgeProvider: Provider;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
2
|
+
import { WMS_EVENT_EMITTER_BRIDGE } from '@rytass/wms-module-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bridges WMS domain events onto `@nestjs/event-emitter`'s `EventEmitter2`, so
|
|
6
|
+
* consumers can subscribe with `@OnEvent('wms.<event-name>')` instead of (or in
|
|
7
|
+
* addition to) the typed `afterListeners`.
|
|
8
|
+
*
|
|
9
|
+
* Pass this into `WmsModuleGraphqlModule.forRootAsync({ eventEmitterBridge })`.
|
|
10
|
+
* Importing this module is the only place `@nestjs/event-emitter` is referenced,
|
|
11
|
+
* keeping it an entirely optional peer dependency — the core graphql module
|
|
12
|
+
* never imports it.
|
|
13
|
+
*
|
|
14
|
+
* Prerequisite: register `EventEmitterModule.forRoot()` in your app so an
|
|
15
|
+
* `EventEmitter2` instance is available for injection.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { wmsEventEmitterBridgeProvider } from '@rytass/wms-module-graphql/adapters/event-emitter';
|
|
20
|
+
*
|
|
21
|
+
* WmsModuleGraphqlModule.forRootAsync({
|
|
22
|
+
* ...coreOptions,
|
|
23
|
+
* eventEmitterBridge: wmsEventEmitterBridgeProvider,
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // elsewhere
|
|
27
|
+
* @OnEvent('wms.stock.allocated')
|
|
28
|
+
* handle(payload: WmsStockEventPayload) { ... }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
const wmsEventEmitterBridgeProvider = {
|
|
32
|
+
provide: WMS_EVENT_EMITTER_BRIDGE,
|
|
33
|
+
useFactory: (emitter) => ({
|
|
34
|
+
forward: (event) => {
|
|
35
|
+
emitter.emit(`wms.${event.name}`, event.payload);
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
inject: [EventEmitter2],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { wmsEventEmitterBridgeProvider };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { WmsAuthChecker, WmsOperationRequirement } from '../lib/auth';
|
|
2
|
+
/**
|
|
3
|
+
* A Casbin `[subject, action]` tuple, e.g. `['wms:material', 'read']`.
|
|
4
|
+
*/
|
|
5
|
+
export type CasbinAction = [string, string];
|
|
6
|
+
export interface MemberBaseWmsAuthCheckerOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Map a host-neutral WMS requirement onto one or more Casbin
|
|
9
|
+
* `[subject, action]` tuples. This is where your project's permission naming
|
|
10
|
+
* convention lives. Returning several tuples means "allow if *any* passes"
|
|
11
|
+
* (member-base's default checker is OR-based).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* map: ({ resource, action }) => [[`wms:${resource}`, action]]
|
|
16
|
+
* // material:read -> enforce(memberId, domain, 'wms:material', 'read')
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
readonly map: (requirement: WmsOperationRequirement) => CasbinAction[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builds a {@link WmsAuthChecker} that authorizes WMS operations against
|
|
23
|
+
* `@rytass/member-base-nestjs-module`'s Casbin setup.
|
|
24
|
+
*
|
|
25
|
+
* It reuses everything the host's global `CasbinGuard` already prepared on the
|
|
26
|
+
* request — the verified `payload`, the `enforcer`, and even the consumer's
|
|
27
|
+
* (possibly customized) `casbinPermissionChecker`. WMS therefore enforces with
|
|
28
|
+
* the *exact same* policy engine and rules as the host's own resolvers, with no
|
|
29
|
+
* member-base import on the WMS side.
|
|
30
|
+
*
|
|
31
|
+
* Prerequisite: register `MemberBaseModule` with its global guard enabled
|
|
32
|
+
* (the default), and expose the request on the GraphQL context as `req`
|
|
33
|
+
* (Apollo's default).
|
|
34
|
+
*/
|
|
35
|
+
export declare const createMemberBaseWmsAuthChecker: (options: MemberBaseWmsAuthCheckerOptions) => WmsAuthChecker;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { GqlExecutionContext } from '@nestjs/graphql';
|
|
2
|
+
|
|
3
|
+
const extractRequest = (context) => {
|
|
4
|
+
const gqlContext = GqlExecutionContext.create(context).getContext();
|
|
5
|
+
return gqlContext?.req;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Builds a {@link WmsAuthChecker} that authorizes WMS operations against
|
|
9
|
+
* `@rytass/member-base-nestjs-module`'s Casbin setup.
|
|
10
|
+
*
|
|
11
|
+
* It reuses everything the host's global `CasbinGuard` already prepared on the
|
|
12
|
+
* request — the verified `payload`, the `enforcer`, and even the consumer's
|
|
13
|
+
* (possibly customized) `casbinPermissionChecker`. WMS therefore enforces with
|
|
14
|
+
* the *exact same* policy engine and rules as the host's own resolvers, with no
|
|
15
|
+
* member-base import on the WMS side.
|
|
16
|
+
*
|
|
17
|
+
* Prerequisite: register `MemberBaseModule` with its global guard enabled
|
|
18
|
+
* (the default), and expose the request on the GraphQL context as `req`
|
|
19
|
+
* (Apollo's default).
|
|
20
|
+
*/
|
|
21
|
+
const createMemberBaseWmsAuthChecker = (options) => {
|
|
22
|
+
return async (context, requirement) => {
|
|
23
|
+
const request = extractRequest(context);
|
|
24
|
+
// No verified member → unauthenticated. CasbinGuard sets `payload` to
|
|
25
|
+
// undefined when the token is missing or invalid.
|
|
26
|
+
if (!request?.payload) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// The enforcer / checker are injected by CasbinGuard. Their absence means
|
|
30
|
+
// the host did not enable the global guard — fail closed rather than
|
|
31
|
+
// silently allowing everything.
|
|
32
|
+
if (!request.enforcer || !request.casbinPermissionChecker) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const actions = options.map(requirement);
|
|
36
|
+
if (actions.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return request.casbinPermissionChecker({
|
|
40
|
+
enforcer: request.enforcer,
|
|
41
|
+
payload: request.payload,
|
|
42
|
+
actions,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { createMemberBaseWmsAuthChecker };
|
package/docs/README.md
CHANGED
|
@@ -7,6 +7,8 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
|
|
|
7
7
|
| Document | Description |
|
|
8
8
|
|------------------------------------------------|---------------------------------------------------------------------------------------------------|
|
|
9
9
|
| [Integration Guide](./integration-guide.md) | Step-by-step instructions for wiring the three packages into a new consumer project (NestJS + Next.js + PostgreSQL). Includes ready-to-paste `_app.tsx`, `app.module.ts`, `next.config.ts`, the 19 Pages Router shell pattern, AuthGuard stub, seed script template, deployment notes, and known issues. |
|
|
10
|
+
| [Authorization](./authorization.md) | Complete design of the resolver-level authorization protocol (`@WmsOperation` + `WmsAuthGuard` + `authChecker`). Explains the global deny-by-default guard pitfall, execution order, and integration recipes for member-base / Casbin, custom JWT / RBAC, and no-guard setups. Read this if any WMS query/mutation returns *forbidden*. |
|
|
11
|
+
| [Events & Hooks](./events.md) | The mutation event/hook system — `beforeHooks` (veto before any DB work) and `afterListeners` (post-commit notify), the `AsyncLocalStorage` post-commit mechanism, the optional `@nestjs/event-emitter` bridge, and the complete list of 38 operation + 18 stock domain events with payloads. |
|
|
10
12
|
| [API Reference](./api-reference.md) | Complete export catalog for all three packages — every NestJS module, TypeORM entity, GraphQL resolver, DTO, React component, hook, and utility, organized by role with source paths and usage examples. Mirrors the actual `src/index.ts` barrel exports of `@rytass/wms-module-graphql@0.3.0` and `@rytass/wms-module-{core,react}@0.1.6`. |
|
|
11
13
|
|
|
12
14
|
## Quick links
|
|
@@ -23,6 +25,10 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
|
|
|
23
25
|
| Look up a specific export's signature | [API Reference](./api-reference.md) |
|
|
24
26
|
| Understand why the lib needs `transpilePackages` | [Integration Guide section 5.3](./integration-guide.md#53-appsclientnextconfigts) |
|
|
25
27
|
| Replace the `AuthGuard` stub with real auth | [Integration Guide section 8.4](./integration-guide.md#84-real-auth-integration) |
|
|
28
|
+
| Authorize WMS resolvers with member-base / Casbin / RBAC | [Authorization](./authorization.md) |
|
|
29
|
+
| Fix "every WMS query/mutation is forbidden" | [Authorization section 1](./authorization.md#1-the-problem) |
|
|
30
|
+
| Hook into / get notified about mutations | [Events & Hooks](./events.md) |
|
|
31
|
+
| See every event the system emits | [Events & Hooks section 5](./events.md#5-complete-event-list) |
|
|
26
32
|
| Decide between PostgreSQL and SQLite | [Integration Guide section 9](./integration-guide.md#9-database-support-matrix) |
|
|
27
33
|
| Build a custom UI on top of the lib's GraphQL operations| [Integration Guide section 7.3](./integration-guide.md#73-graphql-operations--typed-hooks) |
|
|
28
34
|
|
package/docs/api-reference.md
CHANGED
|
@@ -301,17 +301,23 @@ enum ShipInventoryOrderStatus { PENDING, PARTIAL_SHIPPED, SHIPPED, REMOVED }
|
|
|
301
301
|
**`forRootAsync(options): DynamicModule`** (since 0.3.0)
|
|
302
302
|
```ts
|
|
303
303
|
static forRootAsync(
|
|
304
|
-
options:
|
|
304
|
+
options: WmsModuleGraphqlModuleAsyncOptions,
|
|
305
|
+
// = WmsModuleCoreModuleAsyncOptions & WmsAuthModuleOptions
|
|
306
|
+
//
|
|
305
307
|
// WMSBaseModuleAsyncOptions(Vault/DB 選項)
|
|
306
308
|
// + WmsModuleCoreModuleOptions:
|
|
307
309
|
// defaultLoaderSerialId: string — 庫存作業預設載具序號
|
|
308
310
|
// defaultLoaderTypeKey: string — 預設載具類型 key
|
|
309
311
|
// defaultLocationKey: string — 預設儲存位置 key
|
|
312
|
+
// + WmsAuthModuleOptions:
|
|
313
|
+
// authChecker?: WmsAuthChecker — 選填;resolver 授權委派函式(見 Authorization)
|
|
310
314
|
): DynamicModule
|
|
311
315
|
```
|
|
312
316
|
|
|
313
317
|
> **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).
|
|
314
318
|
|
|
319
|
+
> **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).
|
|
320
|
+
|
|
315
321
|
### `LoaderService` (`@Injectable()`, since 0.3.0 publicly exported)
|
|
316
322
|
- 用途: 提供 DataLoader 工廠方法(`batchEntity()`、`batchEntities()`、`batchEntitiesWithPagination()`)。每次呼叫產生新的 DataLoader instance — 須在 GraphQL context factory **per request** 呼叫,**勿** 在 module scope 重用。
|
|
317
323
|
- 來源: `lib/helpers/loader.service.ts`
|
|
@@ -390,6 +396,99 @@ material(
|
|
|
390
396
|
|
|
391
397
|
---
|
|
392
398
|
|
|
399
|
+
## Authorization
|
|
400
|
+
|
|
401
|
+
Host-neutral, opt-in resolver authorization. Every WMS root `@Query`/`@Mutation` carries `@WmsOperation({ resource, action })`; `WmsAuthGuard` enforces it by delegating to the consumer-supplied `authChecker`. Full design & recipes: [`docs/authorization.md`](./authorization.md).
|
|
402
|
+
|
|
403
|
+
### Root exports (`@rytass/wms-module-graphql`)
|
|
404
|
+
|
|
405
|
+
| Export | Kind | Signature / shape |
|
|
406
|
+
|---|---|---|
|
|
407
|
+
| `WmsOperation` | decorator | `(requirement: WmsOperationRequirement) => MethodDecorator` — already applied across the library; add it to any new root op. |
|
|
408
|
+
| `WmsAuthGuard` | `@Injectable()` guard | route-level `CanActivate`; reads the requirement, calls `authChecker`. Auto-registered by `forRootAsync`. |
|
|
409
|
+
| `WmsAuthChecker` | type | `(context: ExecutionContext, requirement: WmsOperationRequirement) => boolean \| Promise<boolean>` |
|
|
410
|
+
| `WmsOperationRequirement` | type | `{ readonly resource: string; readonly action: string }` |
|
|
411
|
+
| `WmsAuthModuleOptions` | type | `{ readonly authChecker?: WmsAuthChecker }` |
|
|
412
|
+
| `WmsModuleGraphqlModuleAsyncOptions` | type | `WmsModuleCoreModuleAsyncOptions & WmsAuthModuleOptions` (the `forRootAsync` arg) |
|
|
413
|
+
| `WMS_AUTH_OPTIONS` | `symbol` | DI token holding the resolved auth options |
|
|
414
|
+
| `WMS_OPERATION_METADATA` | `string` (`'wms:operation'`) | Reflector key carrying the requirement |
|
|
415
|
+
| `HOST_SKIP_METADATA_KEY` | `string` (`'IS_ROUTE_PUBLIC'`) | host-skip metadata key stamped on each op so a global guard defers (default matches member-base) |
|
|
416
|
+
|
|
417
|
+
### Adapter subpath (`@rytass/wms-module-graphql/adapters/member-base`)
|
|
418
|
+
|
|
419
|
+
| Export | Kind | Signature / shape |
|
|
420
|
+
|---|---|---|
|
|
421
|
+
| `createMemberBaseWmsAuthChecker` | factory | `(options: MemberBaseWmsAuthCheckerOptions) => WmsAuthChecker` |
|
|
422
|
+
| `MemberBaseWmsAuthCheckerOptions` | type | `{ readonly map: (requirement: WmsOperationRequirement) => CasbinAction[] }` |
|
|
423
|
+
| `CasbinAction` | type | `[string, string]` — a `[subject, action]` tuple |
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
// member-base / Casbin
|
|
427
|
+
import { createMemberBaseWmsAuthChecker } from '@rytass/wms-module-graphql/adapters/member-base';
|
|
428
|
+
|
|
429
|
+
WmsModuleGraphqlModule.forRootAsync({
|
|
430
|
+
useFactory: async () => ({}),
|
|
431
|
+
defaultLoaderSerialId: 'DEFAULT',
|
|
432
|
+
defaultLoaderTypeKey: 'DEFAULT',
|
|
433
|
+
defaultLocationKey: 'DEFAULT',
|
|
434
|
+
authChecker: createMemberBaseWmsAuthChecker({
|
|
435
|
+
map: ({ resource, action }) => [[`wms:${resource}`, action]],
|
|
436
|
+
}),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// custom JWT / RBAC
|
|
440
|
+
import type { WmsAuthChecker } from '@rytass/wms-module-graphql';
|
|
441
|
+
|
|
442
|
+
const authChecker: WmsAuthChecker = (ctx, { resource, action }) => {
|
|
443
|
+
// read your session off ctx and decide
|
|
444
|
+
return true;
|
|
445
|
+
};
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
The adapter imports nothing from member-base — it reads the `payload` / `enforcer` / `casbinPermissionChecker` that `CasbinGuard` attaches to the request at runtime. member-base is therefore **not** a dependency or peer dependency of this package.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Events & Hooks
|
|
453
|
+
|
|
454
|
+
Opt-in `beforeHooks` (veto) and post-commit `afterListeners` for every mutation. Each root mutation carries `@WmsMutationEvent({ event })` (already applied); a per-mutation interceptor runs the before-hook, opens an `AsyncLocalStorage` dispatch scope, and flushes collected events after the transaction commits. Stock domain events are collected centrally by a TypeORM subscriber on `*_order_change` inserts. Full design & the complete event list: [`docs/events.md`](./events.md).
|
|
455
|
+
|
|
456
|
+
### Root exports (`@rytass/wms-module-graphql`, re-exported from `@rytass/wms-module-core`)
|
|
457
|
+
|
|
458
|
+
| Export | Kind | Signature / shape |
|
|
459
|
+
|---|---|---|
|
|
460
|
+
| `WmsMutationEvent` | decorator | `({ event: WmsOperationEventName, resolveId? }) => MethodDecorator` |
|
|
461
|
+
| `WmsEventModuleOptions` | type | `{ beforeHooks?: WmsBeforeHookMap; afterListeners?: WmsAfterListenerMap }` — 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 }` |
|
|
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 | `{ id: string \| null; result: unknown }` |
|
|
470
|
+
| `WmsStockEventPayload` | type | `{ orderId; itemId; quantityDelta; changeType: ChangeType \| null; inspectionResult? }` |
|
|
471
|
+
|
|
472
|
+
### Adapter subpath (`@rytass/wms-module-graphql/adapters/event-emitter`)
|
|
473
|
+
|
|
474
|
+
| Export | Kind | Purpose |
|
|
475
|
+
|---|---|---|
|
|
476
|
+
| `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
|
+
|
|
478
|
+
```ts
|
|
479
|
+
WmsModuleGraphqlModule.forRootAsync({
|
|
480
|
+
...coreOptions,
|
|
481
|
+
beforeHooks: { 'ship-inventory-order.shipped': async ({ args }) => { if (!ok(args)) throw new ForbiddenException(); } },
|
|
482
|
+
afterListeners: { 'stock.allocated': async ({ payload }) => { await sync(payload); } },
|
|
483
|
+
// optional @OnEvent bridge:
|
|
484
|
+
// eventEmitterBridge: wmsEventEmitterBridgeProvider,
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
`beforeHooks` / `afterListeners` are **not** part of the GraphQL schema — they are runtime hooks. With none configured the interceptor passes through with zero overhead.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
393
492
|
## Resolvers(`@Resolver()` decorator class)
|
|
394
493
|
|
|
395
494
|
以下 Resolver 類別處理 GraphQL `@ResolveField`,解析關聯欄位(防止 N+1):
|