@rytass/wms-module-graphql 0.2.0 → 0.4.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 (53) hide show
  1. package/README.md +75 -29
  2. package/adapters/member-base.d.ts +35 -0
  3. package/adapters/member-base.js +47 -0
  4. package/docs/README.md +6 -3
  5. package/docs/api-reference.md +143 -11
  6. package/docs/authorization.md +349 -0
  7. package/docs/integration-guide.md +155 -84
  8. package/index.cjs.js +177 -6
  9. package/index.d.ts +2 -0
  10. package/index.js +4 -0
  11. package/lib/auth/host-skip.constant.d.ts +27 -0
  12. package/lib/auth/host-skip.constant.js +29 -0
  13. package/lib/auth/index.d.ts +4 -0
  14. package/lib/auth/wms-auth.guard.d.ts +19 -0
  15. package/lib/auth/wms-auth.guard.js +41 -0
  16. package/lib/auth/wms-auth.types.d.ts +36 -0
  17. package/lib/auth/wms-auth.types.js +6 -0
  18. package/lib/auth/wms-operation.decorator.d.ts +23 -0
  19. package/lib/auth/wms-operation.decorator.js +29 -0
  20. package/lib/graphql.module.d.ts +8 -1
  21. package/lib/graphql.module.js +12 -3
  22. package/lib/helpers/create-wms-loaders.d.ts +3 -1
  23. package/lib/helpers/create-wms-loaders.js +5 -3
  24. package/lib/mutations/allocate-inventory-order.mutation.js +5 -0
  25. package/lib/mutations/hold-inventory-order.mutation.js +5 -0
  26. package/lib/mutations/loader.mutation.js +7 -0
  27. package/lib/mutations/location.mutation.js +4 -0
  28. package/lib/mutations/material.mutation.js +4 -0
  29. package/lib/mutations/merge-batch.mutation.js +3 -0
  30. package/lib/mutations/quality-inspection-order.mutation.js +3 -0
  31. package/lib/mutations/receive-inventory-order.mutation.js +5 -0
  32. package/lib/mutations/reclassify-inventory-order.mutation.js +3 -0
  33. package/lib/mutations/scrape-order.mutation.js +3 -0
  34. package/lib/mutations/ship-inventory-order.mutation.js +6 -0
  35. package/lib/mutations/split-batch.mutation.js +3 -0
  36. package/lib/mutations/transfer-customer-order.mutation.js +5 -0
  37. package/lib/mutations/transfer-order.mutation.js +6 -0
  38. package/lib/mutations/transfer-vendor-order.mutation.js +5 -0
  39. package/lib/mutations/warehouse-map.mutation.js +3 -0
  40. package/lib/queries/allocate-inventory-order.query.js +3 -0
  41. package/lib/queries/batch.query.js +3 -0
  42. package/lib/queries/hold-inventory-order.query.js +3 -0
  43. package/lib/queries/loader.query.js +8 -0
  44. package/lib/queries/location.query.js +4 -0
  45. package/lib/queries/material.query.js +4 -0
  46. package/lib/queries/recieve-inventory-order.query.js +4 -0
  47. package/lib/queries/sale-order.query.js +4 -0
  48. package/lib/queries/ship-inventory-order.query.js +4 -0
  49. package/lib/queries/stock.query.js +7 -0
  50. package/lib/queries/transfer-order.query.js +4 -0
  51. package/lib/queries/unit.query.js +3 -0
  52. package/lib/queries/warehouse-map.query.js +3 -0
  53. package/package.json +5 -1
package/README.md CHANGED
@@ -17,56 +17,100 @@ yarn add @nestjs/common @nestjs/graphql @nestjs/apollo @apollo/server graphql da
17
17
  ```ts
18
18
  import { Module } from '@nestjs/common';
19
19
  import { TypeOrmModule } from '@nestjs/typeorm';
20
+ import { GraphQLModule } from '@nestjs/graphql';
20
21
  import { ApolloDriver } from '@nestjs/apollo';
21
22
  import { resolve } from 'node:path';
22
- import { WmsModuleGraphqlModule } from '@rytass/wms-module-graphql';
23
+ import {
24
+ WmsModuleGraphqlModule,
25
+ LoaderService,
26
+ createWmsLoaders,
27
+ } from '@rytass/wms-module-graphql';
23
28
 
24
29
  @Module({
25
30
  imports: [
26
31
  TypeOrmModule.forRoot({ /* ...db config... */ }),
27
- WmsModuleGraphqlModule.forRootAsync(
28
- {
29
- useFactory: async () => ({}),
30
- defaultLoaderSerialId: 'DEFAULT',
31
- defaultLoaderTypeKey: 'DEFAULT',
32
- defaultLocationKey: 'DEFAULT',
33
- },
34
- {
35
- driver: ApolloDriver,
36
- useFactory: async () => ({
37
- autoSchemaFile: resolve(process.cwd(), 'schema.gql'),
38
- playground: false,
39
- introspection: true,
40
- cors: true,
41
- autoTransformHttpErrors: true,
32
+ WmsModuleGraphqlModule.forRootAsync({
33
+ useFactory: async () => ({}),
34
+ defaultLoaderSerialId: 'DEFAULT',
35
+ defaultLoaderTypeKey: 'DEFAULT',
36
+ defaultLocationKey: 'DEFAULT',
37
+ }),
38
+ GraphQLModule.forRootAsync({
39
+ driver: ApolloDriver,
40
+ inject: [LoaderService],
41
+ useFactory: (loaderService: LoaderService) => ({
42
+ autoSchemaFile: resolve(process.cwd(), 'schema.gql'),
43
+ playground: false,
44
+ introspection: true,
45
+ cors: true,
46
+ autoTransformHttpErrors: true,
47
+ context: ({ req }) => ({
48
+ ...createWmsLoaders(loaderService),
49
+ // your own context fields stay yours — no silent overwrite
50
+ user: req.user,
42
51
  }),
43
- },
44
- ),
52
+ }),
53
+ }),
45
54
  ],
46
55
  })
47
56
  export class AppModule {}
48
57
  ```
49
58
 
50
- The first argument is `WmsModuleCoreModuleAsyncOptions` (merged from `@rytass/wms-module-core` — same shape as `WMSBaseModuleAsyncOptions` plus the three `default*` keys). The second is a standard `@nestjs/graphql` `GqlModuleAsyncOptions` — extend the `useFactory` to override the Apollo config (cors, plugins, custom scalars, etc.).
59
+ `WmsModuleGraphqlModule.forRootAsync` takes a single argument (`WmsModuleCoreModuleAsyncOptions`, re-exported from `@rytass/wms-module-core` — same shape as `WMSBaseModuleAsyncOptions` plus the three `default*` keys). It registers the resolvers/queries/mutations as composable NestJS providers and exposes `LoaderService` as a `@Global()` DI token; it does **not** start Apollo Server.
60
+
61
+ You register a **single** `GraphQLModule.forRootAsync(...)` of your own, mix your resolvers with the WMS resolvers in the same schema, and own the Apollo / context configuration end-to-end.
51
62
 
52
63
  `WmsModuleGraphqlModule` internally wires `WmsModuleCoreModule.forRootAsync` so you do not need to import the core module separately when using this package.
53
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
+
54
91
  ## DataLoader context
55
92
 
56
- The module ships a `HelperModule` that provides DataLoader instances for batched entity lookups, automatically injected into the GraphQL context so resolvers can avoid N+1 queries:
93
+ 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`:
57
94
 
58
95
  ```ts
59
- context: async (contextArg) => ({
60
- loaders: {
61
- batchSingle: ...,
62
- batchMany: ...,
63
- batchManyWithPagination: ...,
64
- },
65
- ...contextArg,
66
- })
96
+ type WmsLoaders = {
97
+ batchSingle: DataLoader<...>;
98
+ batchMany: DataLoader<...>;
99
+ batchManyWithPagination: DataLoader<...>;
100
+ };
101
+ ```
102
+
103
+ Inject `LoaderService` into your `GraphQLModule.forRootAsync` factory and call `createWmsLoaders(loaderService)` **per request** inside `context`. The helper returns a `{ loaders: WmsLoaders }` fragment that is safe to spread:
104
+
105
+ ```ts
106
+ context: ({ req }) => ({
107
+ ...createWmsLoaders(loaderService), // adds `loaders: { batchSingle, ... }`
108
+ user: req.user, // your fields preserved
109
+ csrfToken: req.headers['x-csrf'],
110
+ }),
67
111
  ```
68
112
 
69
- Custom context factories from `gqlOptions.useFactory` are merged with the loaders — your existing context fields survive the wrap.
113
+ Per-request is non-negotiable — DataLoader caches results inside the instance, so reusing one across requests leaks data between callers. `createWmsLoaders` is a thin factory you call from inside `context`, never at module scope.
70
114
 
71
115
  ## Schema artefacts
72
116
 
@@ -79,12 +123,14 @@ Full documentation is shipped inside this package under the `docs/` directory (a
79
123
  | Document | Description |
80
124
  |---|---|
81
125
  | [`docs/integration-guide.md`](./docs/integration-guide.md) | Step-by-step wiring guide for NestJS + Next.js consumers |
126
+ | [`docs/authorization.md`](./docs/authorization.md) | Complete resolver-authorization design & integration recipes (member-base / custom JWT / no-guard) |
82
127
  | [`docs/api-reference.md`](./docs/api-reference.md) | Complete export catalog with signatures and usage examples |
83
128
 
84
129
  GitHub hosted links (always up-to-date):
85
130
 
86
131
  - [Integration Guide — §4 Backend setup](https://github.com/Rytass/WMS-Module/blob/main/docs/integration-guide.md#4-backend-setup-nestjs--graphql--typeorm)
87
132
  - [Integration Guide — §13.1 Existing GraphQLModule co-existence](https://github.com/Rytass/WMS-Module/blob/main/docs/integration-guide.md#13-integrating-into-an-existing-nestjs--nextjs-project)
133
+ - [Authorization — design & recipes](https://github.com/Rytass/WMS-Module/blob/main/libs/graphql/docs/authorization.md)
88
134
  - [API Reference — @rytass/wms-module-graphql](https://github.com/Rytass/WMS-Module/blob/main/docs/api-reference.md#rytasswms-module-graphql)
89
135
 
90
136
  ## License
@@ -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,7 +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
- | [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 v0.1.2. |
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
+ | [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
12
 
12
13
  ## Quick links
13
14
 
@@ -23,11 +24,13 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
23
24
  | Look up a specific export's signature | [API Reference](./api-reference.md) |
24
25
  | Understand why the lib needs `transpilePackages` | [Integration Guide section 5.3](./integration-guide.md#53-appsclientnextconfigts) |
25
26
  | Replace the `AuthGuard` stub with real auth | [Integration Guide section 8.4](./integration-guide.md#84-real-auth-integration) |
27
+ | Authorize WMS resolvers with member-base / Casbin / RBAC | [Authorization](./authorization.md) |
28
+ | Fix "every WMS query/mutation is forbidden" | [Authorization section 1](./authorization.md#1-the-problem) |
26
29
  | Decide between PostgreSQL and SQLite | [Integration Guide section 9](./integration-guide.md#9-database-support-matrix) |
27
30
  | 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
31
 
29
32
  ## Versioning
30
33
 
31
- These documents track the **latest published `0.1.x` release**. Anything in `main` that has not yet been released is marked clearly inline; otherwise assume the documented signature matches the current npm-published library.
34
+ These documents track the **latest published versions** — `@rytass/wms-module-graphql@0.3.0` and `@rytass/wms-module-{core,react}@0.1.6` as of this writing. Anything in `main` that has not yet been released is marked clearly inline; otherwise assume the documented signature matches the current npm-published library.
32
35
 
33
- When you upgrade the lib, check each package's `CHANGELOG.md` for breaking changes that may affect the snippets here.
36
+ `@rytass/wms-module-graphql@0.3.0` introduced **breaking changes** to `WmsModuleGraphqlModule.forRootAsync` (single argument; consumer now registers their own `GraphQLModule.forRootAsync`). The integration guide and API reference reflect the post-0.3.0 surface — see each package's `CHANGELOG.md` for the full breaking change list when upgrading.
@@ -6,10 +6,12 @@ Complete export catalog for the three publishable libraries:
6
6
  - `@rytass/wms-module-graphql` — GraphQL resolvers, DTOs, DataLoader helpers
7
7
  - `@rytass/wms-module-react` — React components, hooks, Apollo client, GraphQL operations
8
8
 
9
- Current versions: **0.1.2** (all three packages).
9
+ Current versions: **0.3.0** (`@rytass/wms-module-graphql`), **0.1.6** (`@rytass/wms-module-core`, `@rytass/wms-module-react`).
10
10
 
11
11
  This document mirrors the actual `src/index.ts` barrel exports and is generated by static analysis of the lib sources. For runtime behavior changes between versions, see each package's `CHANGELOG.md`.
12
12
 
13
+ > **0.3.0 surface change**: `WmsModuleGraphqlModule.forRootAsync(...)` now takes a single argument and exposes resolvers as composable providers — see [`WmsModuleGraphqlModule`](#wmsmodulegraphqlmodule) and the new public exports [`LoaderService`](#loaderservice-injectable), [`createWmsLoaders`](#createwmsloaders) below.
14
+
13
15
  > Companion document — **[Integration Guide](./integration-guide.md)** — step-by-step instructions for wiring the three packages into a new consumer project.
14
16
 
15
17
  ---
@@ -293,28 +295,158 @@ enum ShipInventoryOrderStatus { PENDING, PARTIAL_SHIPPED, SHIPPED, REMOVED }
293
295
  ## NestJS Modules
294
296
 
295
297
  ### `WmsModuleGraphqlModule`
296
- - 用途: WMS GraphQL NestJS 動態模組,整合 Apollo GraphQL、DataLoader 與所有 Resolver/Query/Mutation。是消費端 `apps/api` 唯一需要 `imports: [...]` 的 WMS 模組。
298
+ - 用途: WMS GraphQL NestJS 動態模組,**僅註冊 providers**(Resolver / Query / Mutation 與 `LoaderService`),不啟動 Apollo Server。消費端在自己的 `AppModule` 內額外 `imports: [GraphQLModule.forRootAsync(...)]` 統一起 server。
297
299
  - 來源: `lib/graphql.module.ts`
298
300
 
299
- **`forRootAsync(options, gqlOptions): DynamicModule`**
301
+ **`forRootAsync(options): DynamicModule`** (since 0.3.0)
300
302
  ```ts
301
303
  static forRootAsync(
302
- options: WmsModuleCoreModuleAsyncOptions,
304
+ options: WmsModuleGraphqlModuleAsyncOptions,
305
+ // = WmsModuleCoreModuleAsyncOptions & WmsAuthModuleOptions
306
+ //
303
307
  // WMSBaseModuleAsyncOptions(Vault/DB 選項)
304
308
  // + WmsModuleCoreModuleOptions:
305
309
  // defaultLoaderSerialId: string — 庫存作業預設載具序號
306
310
  // defaultLoaderTypeKey: string — 預設載具類型 key
307
311
  // defaultLocationKey: string — 預設儲存位置 key
308
- gqlOptions: GqlModuleAsyncOptions,
309
- // 標準 @nestjs/graphql GqlModuleAsyncOptions,
310
- // 例如 autoSchemaFile、context factory 等
311
- // 注意:此方法會自動在 context 中注入 loaders(DataLoader instances):
312
- // loaders.batchSingle — DataLoader<LoaderOptions<T>, T>
313
- // loaders.batchMany — DataLoader<LoaderOptions<T>, T[]>
314
- // loaders.batchManyWithPagination — DataLoader<LoaderOptionsWithPagination<T>, { items: T[]; total: number }>
312
+ // + WmsAuthModuleOptions:
313
+ // authChecker?: WmsAuthChecker — 選填;resolver 授權委派函式(見 Authorization)
315
314
  ): DynamicModule
316
315
  ```
317
316
 
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).
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
+
321
+ ### `LoaderService` (`@Injectable()`, since 0.3.0 publicly exported)
322
+ - 用途: 提供 DataLoader 工廠方法(`batchEntity()`、`batchEntities()`、`batchEntitiesWithPagination()`)。每次呼叫產生新的 DataLoader instance — 須在 GraphQL context factory **per request** 呼叫,**勿** 在 module scope 重用。
323
+ - 來源: `lib/helpers/loader.service.ts`
324
+ - 透過 `WmsModuleGraphqlModule.forRootAsync(...)` 自動經 `HelperModule` (`@Global()`) 全域提供,可在任何模組的 factory 內 `inject: [LoaderService]`。
325
+
326
+ ### `createWmsLoaders`
327
+ - 用途: 便利 helper — 呼叫 `LoaderService` 三個 factory 並回傳 spreadable context fragment `{ loaders: { batchSingle, batchMany, batchManyWithPagination } }`。
328
+ - 來源: `lib/helpers/create-wms-loaders.ts`
329
+
330
+ ```ts
331
+ import {
332
+ createWmsLoaders,
333
+ LoaderService,
334
+ type WmsLoaders,
335
+ } from '@rytass/wms-module-graphql';
336
+
337
+ GraphQLModule.forRootAsync({
338
+ driver: ApolloDriver,
339
+ inject: [LoaderService],
340
+ useFactory: (loaderService: LoaderService) => ({
341
+ autoSchemaFile: 'schema.gql',
342
+ context: ({ req }) => ({
343
+ ...createWmsLoaders(loaderService), // ← adds `loaders` key
344
+ user: req.user, // your own fields preserved
345
+ }),
346
+ }),
347
+ });
348
+ ```
349
+
350
+ ```ts
351
+ // Type signature
352
+ export type WmsLoaders = {
353
+ batchSingle: DataLoader<LoaderOptions<unknown>, unknown>;
354
+ batchMany: DataLoader<LoaderOptions<unknown>, unknown[]>;
355
+ batchManyWithPagination: DataLoader<
356
+ LoaderOptionsWithPagination<unknown>,
357
+ { items: unknown[]; total: number }
358
+ >;
359
+ };
360
+
361
+ export function createWmsLoaders(
362
+ loaderService: LoaderService,
363
+ ): { loaders: WmsLoaders };
364
+ ```
365
+
366
+ Resolvers consume the loaders via the `LoaderType<T>` shape (see [`LoaderType`](#loadertype)) injected through `@Context()`. The helper's return shape (`{ loaders: ... }`) is intentional so it can be spread into a custom context without colliding with consumer fields.
367
+
368
+ ### `LoaderType`
369
+ - 用途: Resolver 端解構 GraphQL context 的型別契約,描述 `context.loaders` 的形狀。
370
+ - 來源: `lib/helpers/dataloader.type.ts`
371
+
372
+ ```ts
373
+ export interface LoaderType<T> {
374
+ loaders: {
375
+ batchSingle: DataLoader<LoaderOptions<T>, T>;
376
+ batchMany: DataLoader<LoaderOptions<T>, T[]>;
377
+ batchManyWithPagination: DataLoader<
378
+ LoaderOptionsWithPagination<T>,
379
+ { items: T[]; total: number }
380
+ >;
381
+ };
382
+ }
383
+
384
+ // 使用範例(resolver 內)
385
+ @ResolveField(() => MaterialEntityClass)
386
+ material(
387
+ @Parent() stock: StockDto,
388
+ @Context() { loaders }: LoaderType<MaterialEntityClass>,
389
+ ) {
390
+ return loaders.batchSingle.load({
391
+ __entity: MaterialEntity,
392
+ __from: { id: stock.materialId },
393
+ });
394
+ }
395
+ ```
396
+
397
+ ---
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
+
318
450
  ---
319
451
 
320
452
  ## Resolvers(`@Resolver()` decorator class)