@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 +47 -29
- package/docs/README.md +3 -3
- package/docs/api-reference.md +83 -10
- package/docs/integration-guide.md +117 -82
- package/index.cjs.js +5 -3
- package/lib/helpers/create-wms-loaders.d.ts +3 -1
- package/lib/helpers/create-wms-loaders.js +5 -3
- package/package.json +1 -1
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 {
|
|
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
|
|
|
54
65
|
## DataLoader context
|
|
55
66
|
|
|
56
|
-
The module ships a `HelperModule` that provides
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
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,10 +295,10 @@ 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
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
|
-
| (
|
|
44
|
-
|
|
|
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
|
-
|
|
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 {
|
|
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
|
|
177
|
-
WmsModuleGraphqlModule.forRootAsync(
|
|
178
|
-
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
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.
|
|
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.
|
|
964
|
-
>
|
|
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**:
|
|
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
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
987
|
+
```ts
|
|
988
|
+
import {
|
|
989
|
+
WmsModuleGraphqlModule,
|
|
990
|
+
LoaderService,
|
|
991
|
+
createWmsLoaders,
|
|
992
|
+
} from '@rytass/wms-module-graphql';
|
|
985
993
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
**
|
|
992
|
-
|
|
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**:
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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):
|
|
4
|
+
export declare function createWmsLoaders(loaderService: LoaderService): {
|
|
5
|
+
loaders: WmsLoaders;
|
|
6
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
function createWmsLoaders(loaderService) {
|
|
2
2
|
return {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
loaders: {
|
|
4
|
+
batchSingle: loaderService.batchEntity(),
|
|
5
|
+
batchMany: loaderService.batchEntities(),
|
|
6
|
+
batchManyWithPagination: loaderService.batchEntitiesWithPagination(),
|
|
7
|
+
},
|
|
6
8
|
};
|
|
7
9
|
}
|
|
8
10
|
|