@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.
- package/README.md +75 -29
- package/adapters/member-base.d.ts +35 -0
- package/adapters/member-base.js +47 -0
- package/docs/README.md +6 -3
- package/docs/api-reference.md +143 -11
- package/docs/authorization.md +349 -0
- package/docs/integration-guide.md +155 -84
- package/index.cjs.js +177 -6
- package/index.d.ts +2 -0
- package/index.js +4 -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/graphql.module.d.ts +8 -1
- package/lib/graphql.module.js +12 -3
- package/lib/helpers/create-wms-loaders.d.ts +3 -1
- package/lib/helpers/create-wms-loaders.js +5 -3
- package/lib/mutations/allocate-inventory-order.mutation.js +5 -0
- package/lib/mutations/hold-inventory-order.mutation.js +5 -0
- package/lib/mutations/loader.mutation.js +7 -0
- package/lib/mutations/location.mutation.js +4 -0
- package/lib/mutations/material.mutation.js +4 -0
- package/lib/mutations/merge-batch.mutation.js +3 -0
- package/lib/mutations/quality-inspection-order.mutation.js +3 -0
- package/lib/mutations/receive-inventory-order.mutation.js +5 -0
- package/lib/mutations/reclassify-inventory-order.mutation.js +3 -0
- package/lib/mutations/scrape-order.mutation.js +3 -0
- package/lib/mutations/ship-inventory-order.mutation.js +6 -0
- package/lib/mutations/split-batch.mutation.js +3 -0
- package/lib/mutations/transfer-customer-order.mutation.js +5 -0
- package/lib/mutations/transfer-order.mutation.js +6 -0
- package/lib/mutations/transfer-vendor-order.mutation.js +5 -0
- package/lib/mutations/warehouse-map.mutation.js +3 -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 +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 {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
| [
|
|
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.
|
|
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
|
-
|
|
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.
|
package/docs/api-reference.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
301
|
+
**`forRootAsync(options): DynamicModule`** (since 0.3.0)
|
|
300
302
|
```ts
|
|
301
303
|
static forRootAsync(
|
|
302
|
-
options:
|
|
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
|
-
|
|
309
|
-
//
|
|
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)
|