@rytass/wms-module-graphql 0.2.0 → 0.3.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 CHANGED
@@ -17,56 +17,74 @@ 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
 
54
65
  ## DataLoader context
55
66
 
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:
67
+ 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
68
 
58
69
  ```ts
59
- context: async (contextArg) => ({
60
- loaders: {
61
- batchSingle: ...,
62
- batchMany: ...,
63
- batchManyWithPagination: ...,
64
- },
65
- ...contextArg,
66
- })
70
+ type WmsLoaders = {
71
+ batchSingle: DataLoader<...>;
72
+ batchMany: DataLoader<...>;
73
+ batchManyWithPagination: DataLoader<...>;
74
+ };
75
+ ```
76
+
77
+ 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:
78
+
79
+ ```ts
80
+ context: ({ req }) => ({
81
+ ...createWmsLoaders(loaderService), // adds `loaders: { batchSingle, ... }`
82
+ user: req.user, // your fields preserved
83
+ csrfToken: req.headers['x-csrf'],
84
+ }),
67
85
  ```
68
86
 
69
- Custom context factories from `gqlOptions.useFactory` are merged with the loaders — your existing context fields survive the wrap.
87
+ 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
88
 
71
89
  ## Schema artefacts
72
90
 
package/docs/README.md CHANGED
@@ -7,7 +7,7 @@ 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
+ | [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
11
 
12
12
  ## Quick links
13
13
 
@@ -28,6 +28,6 @@ Reference material for consumers of `@rytass/wms-module-{core,graphql,react}`.
28
28
 
29
29
  ## Versioning
30
30
 
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.
31
+ 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
32
 
33
- When you upgrade the lib, check each package's `CHANGELOG.md` for breaking changes that may affect the snippets here.
33
+ `@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,10 +295,10 @@ 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
304
  options: WmsModuleCoreModuleAsyncOptions,
@@ -305,16 +307,87 @@ static forRootAsync(
305
307
  // defaultLoaderSerialId: string — 庫存作業預設載具序號
306
308
  // defaultLoaderTypeKey: string — 預設載具類型 key
307
309
  // 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 }>
315
310
  ): DynamicModule
316
311
  ```
317
312
 
313
+ > **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
+
315
+ ### `LoaderService` (`@Injectable()`, since 0.3.0 publicly exported)
316
+ - 用途: 提供 DataLoader 工廠方法(`batchEntity()`、`batchEntities()`、`batchEntitiesWithPagination()`)。每次呼叫產生新的 DataLoader instance — 須在 GraphQL context factory **per request** 呼叫,**勿** 在 module scope 重用。
317
+ - 來源: `lib/helpers/loader.service.ts`
318
+ - 透過 `WmsModuleGraphqlModule.forRootAsync(...)` 自動經 `HelperModule` (`@Global()`) 全域提供,可在任何模組的 factory 內 `inject: [LoaderService]`。
319
+
320
+ ### `createWmsLoaders`
321
+ - 用途: 便利 helper — 呼叫 `LoaderService` 三個 factory 並回傳 spreadable context fragment `{ loaders: { batchSingle, batchMany, batchManyWithPagination } }`。
322
+ - 來源: `lib/helpers/create-wms-loaders.ts`
323
+
324
+ ```ts
325
+ import {
326
+ createWmsLoaders,
327
+ LoaderService,
328
+ type WmsLoaders,
329
+ } from '@rytass/wms-module-graphql';
330
+
331
+ GraphQLModule.forRootAsync({
332
+ driver: ApolloDriver,
333
+ inject: [LoaderService],
334
+ useFactory: (loaderService: LoaderService) => ({
335
+ autoSchemaFile: 'schema.gql',
336
+ context: ({ req }) => ({
337
+ ...createWmsLoaders(loaderService), // ← adds `loaders` key
338
+ user: req.user, // your own fields preserved
339
+ }),
340
+ }),
341
+ });
342
+ ```
343
+
344
+ ```ts
345
+ // Type signature
346
+ export type WmsLoaders = {
347
+ batchSingle: DataLoader<LoaderOptions<unknown>, unknown>;
348
+ batchMany: DataLoader<LoaderOptions<unknown>, unknown[]>;
349
+ batchManyWithPagination: DataLoader<
350
+ LoaderOptionsWithPagination<unknown>,
351
+ { items: unknown[]; total: number }
352
+ >;
353
+ };
354
+
355
+ export function createWmsLoaders(
356
+ loaderService: LoaderService,
357
+ ): { loaders: WmsLoaders };
358
+ ```
359
+
360
+ 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.
361
+
362
+ ### `LoaderType`
363
+ - 用途: Resolver 端解構 GraphQL context 的型別契約,描述 `context.loaders` 的形狀。
364
+ - 來源: `lib/helpers/dataloader.type.ts`
365
+
366
+ ```ts
367
+ export interface LoaderType<T> {
368
+ loaders: {
369
+ batchSingle: DataLoader<LoaderOptions<T>, T>;
370
+ batchMany: DataLoader<LoaderOptions<T>, T[]>;
371
+ batchManyWithPagination: DataLoader<
372
+ LoaderOptionsWithPagination<T>,
373
+ { items: T[]; total: number }
374
+ >;
375
+ };
376
+ }
377
+
378
+ // 使用範例(resolver 內)
379
+ @ResolveField(() => MaterialEntityClass)
380
+ material(
381
+ @Parent() stock: StockDto,
382
+ @Context() { loaders }: LoaderType<MaterialEntityClass>,
383
+ ) {
384
+ return loaders.batchSingle.load({
385
+ __entity: MaterialEntity,
386
+ __from: { id: stock.materialId },
387
+ });
388
+ }
389
+ ```
390
+
318
391
  ---
319
392
 
320
393
  ## Resolvers(`@Resolver()` decorator class)
@@ -40,8 +40,8 @@ WMS is packaged as three layered npm libraries:
40
40
  v
41
41
  +-------------------------------+
42
42
  | wms-module-graphql | Resolvers, DTOs, DataLoader helpers
43
- | (one forRootAsync wires up | ---------------------------------------
44
- | the entire GraphQL surface) | Wires the schema, depends on core for
43
+ | (composable providers + your | ---------------------------------------
44
+ | own GraphQLModule.forRoot) | Exposes resolvers; depends on core for
45
45
  +--------------+----------------+ entities + services.
46
46
  v
47
47
  +-------------------------------+
@@ -138,13 +138,18 @@ bootstrap();
138
138
 
139
139
  ### 4.2 `apps/api/src/app/app.module.ts`
140
140
 
141
- The entire backend domain is wired in a single `forRootAsync` call on `WmsModuleGraphqlModule`. The module cascades into `WmsModuleCoreModule` automatically — the consumer **does not** import `wms-module-core` directly.
141
+ Two registrations: `WmsModuleGraphqlModule.forRootAsync(...)` wires the WMS resolvers, entities, services and `LoaderService`; your own `GraphQLModule.forRootAsync(...)` starts Apollo Server and owns the context factory. The two participate in **one** schema — NestJS's schema builder discovers every `@Resolver()` provider across all imported modules.
142
142
 
143
143
  ```ts
144
144
  import { Module } from '@nestjs/common';
145
145
  import { ConfigModule, ConfigService } from '@nestjs/config';
146
146
  import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
147
- import { WmsModuleGraphqlModule } from '@rytass/wms-module-graphql';
147
+ import { GraphQLModule } from '@nestjs/graphql';
148
+ import {
149
+ WmsModuleGraphqlModule,
150
+ LoaderService,
151
+ createWmsLoaders,
152
+ } from '@rytass/wms-module-graphql';
148
153
  import { ApolloDriver } from '@nestjs/apollo';
149
154
  import {
150
155
  ApolloServerPluginLandingPageLocalDefault,
@@ -173,40 +178,46 @@ import { resolve } from 'path';
173
178
  }),
174
179
  }),
175
180
 
176
- // WMS GraphQL - one call wires the entire domain
177
- WmsModuleGraphqlModule.forRootAsync(
178
- {
179
- useFactory: async () => ({}),
180
- // Module-init seed values; the keys you pass here are created
181
- // on first boot for `location`, `loaderType`, and `loader`.
182
- defaultLoaderSerialId: 'DEFAULT',
183
- defaultLoaderTypeKey: 'DEFAULT',
184
- defaultLocationKey: 'DEFAULT',
185
- },
186
- {
187
- driver: ApolloDriver,
188
- useFactory: async () => ({
189
- playground: false,
190
- introspection: true,
191
- cors: true,
192
- csrfPrevention: false,
193
- plugins: [ApolloServerPluginLandingPageLocalDefault()],
194
- autoTransformHttpErrors: true,
195
- autoSchemaFile: resolve(process.cwd(), 'schema.gql'),
181
+ // WMS resolvers + LoaderService — no Apollo Server is started here
182
+ WmsModuleGraphqlModule.forRootAsync({
183
+ useFactory: async () => ({}),
184
+ // Module-init seed values; the keys you pass here are created
185
+ // on first boot for `location`, `loaderType`, and `loader`.
186
+ defaultLoaderSerialId: 'DEFAULT',
187
+ defaultLoaderTypeKey: 'DEFAULT',
188
+ defaultLocationKey: 'DEFAULT',
189
+ }),
190
+
191
+ // YOUR Apollo / Schema / Context registration
192
+ GraphQLModule.forRootAsync({
193
+ driver: ApolloDriver,
194
+ inject: [LoaderService],
195
+ useFactory: (loaderService: LoaderService) => ({
196
+ playground: false,
197
+ introspection: true,
198
+ cors: true,
199
+ csrfPrevention: false,
200
+ plugins: [ApolloServerPluginLandingPageLocalDefault()],
201
+ autoTransformHttpErrors: true,
202
+ autoSchemaFile: resolve(process.cwd(), 'schema.gql'),
203
+ context: ({ req }) => ({
204
+ ...createWmsLoaders(loaderService), // per-request DataLoaders
205
+ // your own context fields — fully preserved
206
+ user: req.user,
196
207
  }),
197
- },
198
- ),
208
+ }),
209
+ }),
199
210
  ],
200
211
  })
201
212
  export class AppModule {}
202
213
  ```
203
214
 
204
- What you get from this single `forRootAsync`:
215
+ What this registration wires:
205
216
 
206
217
  - 50 TypeORM entities (material, batch, loader, location, every order kind, change logs, signatures, etc.)
207
218
  - ~100 GraphQL types
208
219
  - ~65 queries and mutations
209
- - DataLoader N+1 prevention across every resolveField
220
+ - DataLoader N+1 prevention across every `resolveField` (via the `loaders` key in context)
210
221
  - Default seed rows (DEFAULT location, DEFAULT loaderType, DEFAULT loader) on first boot
211
222
 
212
223
  **`forRootAsync` TypeScript signature**:
@@ -214,21 +225,16 @@ What you get from this single `forRootAsync`:
214
225
  ```ts
215
226
  WmsModuleGraphqlModule.forRootAsync(
216
227
  options: WmsModuleCoreModuleAsyncOptions,
217
- gqlOptions: GqlModuleAsyncOptions<ApolloDriverConfig>,
218
228
  ): DynamicModule
219
229
  ```
220
230
 
221
- > Note the **two positional arguments**. A common mistake is to pass a single options object
222
- > (matching NestJS's typical convention) which produces a misleading
223
- > `TypeError: Cannot read properties of undefined (reading 'imports')` from inside the lib.
224
- > The first arg configures the WMS core module (seed defaults, async factory, etc.); the
225
- > second arg configures the underlying NestJS GraphQL module (driver, autoSchemaFile, plugins).
231
+ A single argument — the WMS core module options (seed defaults, async factory). All GraphQL-server concerns (driver, schema file, context factory, plugins) live in your own `GraphQLModule.forRootAsync(...)` next to it.
226
232
 
227
- See [API Reference - wms-module-graphql](./api-reference.md) for the full `forRootAsync` signature.
233
+ See [API Reference - wms-module-graphql](./api-reference.md) for the full export catalog including `LoaderService`, `createWmsLoaders`, `LoaderType<T>` and `WmsLoaders`.
228
234
 
229
235
  ### Adding your own resolvers alongside WMS
230
236
 
231
- `WmsModuleGraphqlModule` calls `GraphQLModule.forRootAsync(...)` internally — that registration is workspace-global, so you cannot register a second `GraphQLModule` (see §13.1). What you **can** do is put your own resolvers in **sibling NestJS modules** imported in `AppModule`:
237
+ Your `GraphQLModule.forRootAsync(...)` registration **is** the single workspace-wide GraphQL module — no constraint from `WmsModuleGraphqlModule` (the lib intentionally does **not** call `GraphQLModule.forRoot` internally). Put your resolvers in normal NestJS modules and import them next to `WmsModuleGraphqlModule`:
232
238
 
233
239
  ```ts
234
240
  // apps/api/src/modules/posts/posts.module.ts
@@ -246,14 +252,14 @@ export class PostsModule {}
246
252
  // apps/api/src/app/app.module.ts
247
253
  @Module({
248
254
  imports: [
249
- /* ... TypeOrm, Config, WmsModuleGraphqlModule ... */
255
+ /* ... TypeOrm, Config, WmsModuleGraphqlModule, GraphQLModule ... */
250
256
  PostsModule, // your sibling module
251
257
  ],
252
258
  })
253
259
  export class AppModule {}
254
260
  ```
255
261
 
256
- NestJS's GraphQL module discovers every `@Resolver()` class from every imported module via DI and merges them into the **same** schema. So your `Post`/`User` resolvers coexist in one schema with the WMS resolvers automatically — no extra wiring needed. The §13.1 "one GraphQLModule" constraint only forbids a second `GraphQLModule.forRoot(...)` registration, not additional resolvers.
262
+ NestJS's GraphQL module discovers every `@Resolver()` class from every imported module via DI and merges them into the **same** schema. So your `Post`/`User` resolvers coexist in one schema with the WMS resolvers automatically — no extra wiring needed.
257
263
 
258
264
  ### 4.3 `.env`
259
265
 
@@ -960,37 +966,59 @@ This section covers the real-world scenarios where the WMS modules are added to
960
966
  that already has its own NestJS backend, GraphQL setup, or Next.js frontend. The greenfield
961
967
  setup in sections 4-7 is the baseline; read this section for the delta.
962
968
 
963
- > **Scope**: 0.1.x. Many limitations listed here are known gaps — roadmap notes are included
964
- > where improvements are planned.
969
+ > **Scope**: 0.3.0+. Earlier 0.1.x / 0.2.0 limitations around `GraphQLModule` co-existence have
970
+ > been removed by the user-driven composition refactor — see §13.1.
965
971
 
966
972
  ### 13.1 Coexisting with an existing `GraphQLModule`
967
973
 
968
- **Status**: Not supported in 0.1.x without forking the library.
969
-
970
- `WmsModuleGraphqlModule.forRootAsync(...)` internally calls `GraphQLModule.forRootAsync(...)`.
971
- NestJS does not allow two `GraphQLModule` registrations in the same application — the second
972
- registration throws `Error: Cannot register 2 GraphQL modules`. If your host app already
973
- imports `GraphQLModule`, you cannot simply add `WmsModuleGraphqlModule` to the same
974
- `imports: [...]` array.
974
+ **Status**: Fully supported since 0.3.0.
975
975
 
976
- **Recommended workarounds** (in order of practicality):
976
+ `WmsModuleGraphqlModule.forRootAsync(...)` only registers providers (resolvers, `LoaderService`,
977
+ core module) — it does **not** call `GraphQLModule.forRoot` internally. The host application
978
+ registers a **single** `GraphQLModule.forRootAsync(...)` of its own. NestJS's schema builder
979
+ walks the DI container at boot, collects every `@Resolver()` class from every imported module,
980
+ and merges them into one schema.
977
981
 
978
- 1. **Separate NestJS process** — run the WMS API as its own NestJS server on a different port
979
- (e.g. 7103) and have the host API proxy or federate as needed. This is the architecture
980
- used by `wms-standalone` and is the safest path.
982
+ This means a host app with an existing GraphQL server just adds `WmsModuleGraphqlModule` next to
983
+ its existing `GraphQLModule.forRootAsync(...)`. To preserve N+1 prevention for the WMS resolvers,
984
+ the host must inject `LoaderService` into its own context factory and call `createWmsLoaders(...)`
985
+ per request:
981
986
 
982
- 2. **NestJS sub-app (monorepo)** — if you use the Nx or NestJS CLI monorepo pattern, you can
983
- host the WMS app as a separate application inside the same repo that shares code with your
984
- existing app but runs as a distinct process.
987
+ ```ts
988
+ import {
989
+ WmsModuleGraphqlModule,
990
+ LoaderService,
991
+ createWmsLoaders,
992
+ } from '@rytass/wms-module-graphql';
985
993
 
986
- 3. **Fork and merge** — advanced consumers can fork `libs/graphql` and extract the resolver
987
- registrations, removing the internal `GraphQLModule.forRootAsync(...)` call. The host app
988
- then registers a single `GraphQLModule` that includes the WMS resolver classes. This requires
989
- maintaining a fork and is not recommended unless you intend to take full ownership of the lib.
994
+ @Module({
995
+ imports: [
996
+ // ... your existing modules ...
997
+ WmsModuleGraphqlModule.forRootAsync({
998
+ useFactory: async () => ({}),
999
+ defaultLoaderSerialId: 'DEFAULT',
1000
+ defaultLoaderTypeKey: 'DEFAULT',
1001
+ defaultLocationKey: 'DEFAULT',
1002
+ }),
1003
+ GraphQLModule.forRootAsync({
1004
+ driver: ApolloDriver,
1005
+ inject: [LoaderService, /* your existing deps */],
1006
+ useFactory: (loaderService: LoaderService /* , ... */) => ({
1007
+ autoSchemaFile: 'schema.gql',
1008
+ context: ({ req }) => ({
1009
+ ...createWmsLoaders(loaderService), // WMS DataLoader trio
1010
+ user: req.user, // your existing context fields
1011
+ // ... any other context shape you already had ...
1012
+ }),
1013
+ }),
1014
+ }),
1015
+ ],
1016
+ })
1017
+ export class AppModule {}
1018
+ ```
990
1019
 
991
- **Tied to §13.1**: the `/graphql` endpoint name conflict (see §13.14 for GraphQL type name conflicts) is also
992
- fundamentally blocked by this same single-GraphQLModule constraint when running both APIs in
993
- the same process.
1020
+ **Type name conflicts**: still a separate concern — see §13.14. The shared-schema model means
1021
+ your `type Material`, `type Location`, etc. must not collide with the WMS counterparts.
994
1022
 
995
1023
  ---
996
1024
 
@@ -1128,26 +1156,34 @@ export class AppModule {}
1128
1156
 
1129
1157
  This applies your guard to every resolver, including WMS resolvers.
1130
1158
 
1131
- **Option B**: Read auth identity via `context.req.user` in custom resolvers. The Express `req`
1132
- object is spread directly into the GraphQL context by the lib's internal DataLoader injection
1133
- logic. Any field set on `req` by a NestJS guard or Passport strategy (e.g. `req.user`) is
1134
- therefore accessible from resolvers as `ctx.req.user`.
1159
+ **Option B**: Surface the authenticated user through your **own** GraphQL context factory. Since 0.3.0 you own the entire `GraphQLModule.forRootAsync(...)` registration, including `context`. Both `context.user` (returned directly from your factory) and `context.req.user` (set by a guard / Passport on Express) work — pick whichever your resolvers prefer:
1135
1160
 
1136
- > **Why returning `context` from `gqlOptions.useFactory` does not work**: the lib's
1137
- > internal context factory replaces any `context` key you return from `gqlOptions.useFactory`,
1138
- > but it spreads the raw `contextArg` (the `{ req, res }` provided by Apollo Server). So:
1139
- >
1140
- > | Approach | Works? |
1141
- > |----------------------------------------------------|--------|
1142
- > | Return `context: () => ({ user })` from useFactory | ❌ overridden |
1143
- > | Populate `req.user` via NestJS guard / Passport | ✅ access via `context.req.user` |
1144
- >
1145
- > Always populate identity onto `req` (via a guard) and read it as `context.req.user` in
1146
- > your resolvers. The resulting GraphQL context is `{ req, res, loaders }` — `req.user` IS
1147
- > accessible as long as a guard or middleware has set it on the request object.
1161
+ ```ts
1162
+ GraphQLModule.forRootAsync({
1163
+ driver: ApolloDriver,
1164
+ inject: [LoaderService],
1165
+ useFactory: (loaderService: LoaderService) => ({
1166
+ autoSchemaFile: 'schema.gql',
1167
+ context: ({ req }) => ({
1168
+ ...createWmsLoaders(loaderService), // WMS DataLoader trio
1169
+ user: req.user, // directly accessible as ctx.user
1170
+ // ...any other fields you want on context
1171
+ }),
1172
+ }),
1173
+ })
1174
+ ```
1175
+
1176
+ | Approach | Works? |
1177
+ |---------------------------------------------------------|--------|
1178
+ | Return `context: () => ({ user, ...createWmsLoaders(svc) })` | ✅ user fully controls context |
1179
+ | Populate `req.user` via NestJS guard / Passport | ✅ access via `context.req.user` |
1180
+
1181
+ > **0.1.x behavior gone**: earlier releases of this lib silently replaced any `context` factory
1182
+ > you supplied because `WmsModuleGraphqlModule` registered Apollo Server itself. Since 0.3.0,
1183
+ > the lib only exposes providers and your `GraphQLModule.forRootAsync(...)` is the single
1184
+ > registration — there is no internal override anymore.
1148
1185
 
1149
- To access the authenticated user in a **custom** resolver, populate `req.user` in a guard
1150
- or middleware and read it from `ctx.req.user`:
1186
+ To access the authenticated user in a **custom** resolver, either read it directly from `ctx.user` (if you put it there in your factory) or from `ctx.req.user` (if a guard set it on the request):
1151
1187
 
1152
1188
  ```ts
1153
1189
  // auth.guard.ts — populate req.user before the resolver runs
@@ -1631,8 +1667,7 @@ The viable options are the same as those described in §13.1:
1631
1667
  3. **Wait for a future major release** — a type prefix configuration option is a candidate
1632
1668
  for a future major version that introduces namespace support.
1633
1669
 
1634
- This is the same fundamental single-`GraphQLModule` constraint discussed in §13.1. Running
1635
- both APIs in the same process is not supported in 0.1.x.
1670
+ Unlike the §13.1 `GraphQLModule` co-existence story (fixed in 0.3.0), the type-name collision is a **shared-schema** problem: every `@Resolver()` provider in the DI container, WMS or host-app, contributes to the same `GraphQLSchema` instance. Renaming the host app's types away from WMS-occupied names is the only fix without splitting processes.
1636
1671
 
1637
1672
 
1638
1673
  ## 14. Reference implementation (`wms-standalone`)
@@ -1647,7 +1682,7 @@ wms-standalone/
1647
1682
  api/
1648
1683
  src/
1649
1684
  main.ts 17 LOC NestJS bootstrap
1650
- app/app.module.ts 52 LOC WmsModuleGraphqlModule.forRootAsync
1685
+ app/app.module.ts 60 LOC WmsModuleGraphqlModule + GraphQLModule.forRootAsync
1651
1686
  .env 6 LOC PG credentials
1652
1687
  package.json
1653
1688
  client/
package/index.cjs.js CHANGED
@@ -204,9 +204,11 @@ common.createParamDecorator((data, context) => {
204
204
 
205
205
  function createWmsLoaders(loaderService) {
206
206
  return {
207
- batchSingle: loaderService.batchEntity(),
208
- batchMany: loaderService.batchEntities(),
209
- batchManyWithPagination: loaderService.batchEntitiesWithPagination(),
207
+ loaders: {
208
+ batchSingle: loaderService.batchEntity(),
209
+ batchMany: loaderService.batchEntities(),
210
+ batchManyWithPagination: loaderService.batchEntitiesWithPagination(),
211
+ },
210
212
  };
211
213
  }
212
214
 
@@ -1,4 +1,6 @@
1
1
  import type { LoaderType } from './dataloader.type';
2
2
  import type { LoaderService } from './loader.service';
3
3
  export type WmsLoaders = LoaderType<unknown>['loaders'];
4
- export declare function createWmsLoaders(loaderService: LoaderService): WmsLoaders;
4
+ export declare function createWmsLoaders(loaderService: LoaderService): {
5
+ loaders: WmsLoaders;
6
+ };
@@ -1,8 +1,10 @@
1
1
  function createWmsLoaders(loaderService) {
2
2
  return {
3
- batchSingle: loaderService.batchEntity(),
4
- batchMany: loaderService.batchEntities(),
5
- batchManyWithPagination: loaderService.batchEntitiesWithPagination(),
3
+ loaders: {
4
+ batchSingle: loaderService.batchEntity(),
5
+ batchMany: loaderService.batchEntities(),
6
+ batchManyWithPagination: loaderService.batchEntitiesWithPagination(),
7
+ },
6
8
  };
7
9
  }
8
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/wms-module-graphql",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Rytass WMS Module — GraphQL schema, resolvers and DTOs for warehouse management",
5
5
  "keywords": [
6
6
  "rytass",