@rytass/wms-module-graphql 0.1.2 → 0.1.3

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.
@@ -0,0 +1,1568 @@
1
+ # Integration Guide
2
+
3
+ How to build a Warehouse Management System on top of the `@rytass/wms-module-*` packages.
4
+
5
+ Target audience: engineers integrating WMS into a new product who want a working backend + frontend with the smallest possible amount of glue code.
6
+
7
+ > Companion document — **[API Reference](./api-reference.md)** — complete export catalog for all three packages.
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Overview & design philosophy](#1-overview--design-philosophy)
12
+ 2. [Prerequisites](#2-prerequisites)
13
+ 3. [Quick start (60 seconds)](#3-quick-start-60-seconds)
14
+ 4. [Backend setup (NestJS + GraphQL + TypeORM)](#4-backend-setup-nestjs--graphql--typeorm)
15
+ - [4.6 Version compatibility matrix](#46-version-compatibility-matrix)
16
+ 5. [Frontend setup (Next.js Pages Router)](#5-frontend-setup-nextjs-pages-router)
17
+ 6. [Pages Router shell pattern (the 19 thin shells)](#6-pages-router-shell-pattern-the-19-thin-shells)
18
+ 7. [Apollo client wiring](#7-apollo-client-wiring)
19
+ 8. [Authentication / authorization](#8-authentication--authorization)
20
+ 9. [Database support matrix](#9-database-support-matrix)
21
+ 10. [Seed data for demos](#10-seed-data-for-demos)
22
+ 11. [Deployment notes](#11-deployment-notes)
23
+ 12. [Known issues & roadmap](#12-known-issues--roadmap)
24
+ 13. [Integrating into an existing NestJS / Next.js project](#13-integrating-into-an-existing-nestjs--nextjs-project)
25
+ - [13.14 GraphQL type name conflicts](#1314-graphql-type-name-conflicts)
26
+ 14. [Reference implementation (`wms-standalone`)](#14-reference-implementation-wms-standalone)
27
+
28
+ ---
29
+
30
+ ## 1. Overview & design philosophy
31
+
32
+ WMS is packaged as three layered npm libraries:
33
+
34
+ ```
35
+ +-------------------------------+
36
+ | wms-module-react | React UI + Apollo client + GraphQL ops
37
+ | (consumer page shells) | ---------------------------------------
38
+ +--------------+----------------+ Default exports are full page bodies.
39
+ | Consumer pages are 7-12 line shells.
40
+ v
41
+ +-------------------------------+
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
45
+ +--------------+----------------+ entities + services.
46
+ v
47
+ +-------------------------------+
48
+ | wms-module-core | TypeORM entities, services, DI tokens
49
+ | (domain logic) | ---------------------------------------
50
+ +-------------------------------+ Loaded indirectly through graphql.
51
+ Consumers rarely import from core.
52
+ ```
53
+
54
+ **Design intent**: the consumer writes **wiring code only**. Domain logic — entities, services, resolvers, queries, mutations, DataLoaders, React components, Apollo wiring, GraphQL operations — all lives in the three packages.
55
+
56
+ The reference consumer ([`wms-standalone`](#13-reference-implementation-wms-standalone)) is **~570 LOC of glue code** including 19 Pages Router shells. The only file with non-trivial logic is a 79-line Navigation component that calls `useRoutes()` from the lib and renders Mezzanine UI's `<Navigation>`.
57
+
58
+ ---
59
+
60
+ ## 2. Prerequisites
61
+
62
+ | Dependency | Version | Notes |
63
+ |-----------------|--------------------------------------|----------------------------------------------|
64
+ | Node.js | 20 LTS or later | The libs build for ES2022. |
65
+ | Package manager | pnpm 9+ (recommended), Yarn 4, npm 10+| pnpm's strict hoisting is honored. |
66
+ | PostgreSQL | 14 or later | Entities use `timestamptz`. See section 9. |
67
+ | Browser runtime | Modern browsers with ES2022 support | The React package ships ESM only. |
68
+
69
+ The frontend package (`wms-module-react`) is **ESM-only** (`"type": "module"`). Bundlers that target Next.js / Vite / webpack 5 handle this transparently; classic CJS-only Node tools do not.
70
+
71
+ ---
72
+
73
+ ## 3. Quick start (60 seconds)
74
+
75
+ ```bash
76
+ # 1. Create a workspace and install the three packages plus their peers
77
+ mkdir my-wms && cd my-wms
78
+ pnpm init
79
+ pnpm add @rytass/wms-module-core @rytass/wms-module-graphql @rytass/wms-module-react
80
+ pnpm add @nestjs/common @nestjs/core @nestjs/graphql @nestjs/typeorm \
81
+ @nestjs/apollo @nestjs/config @nestjs/platform-express @apollo/server@^4 \
82
+ typeorm pg reflect-metadata graphql rxjs \
83
+ class-transformer class-validator \
84
+ @as-integrations/express5
85
+
86
+ # 2. Wire NestJS in 17 + 52 lines (apps/api/src/main.ts and app/app.module.ts)
87
+ # 3. Wire Next.js in 31 + 29 lines (apps/client/src/pages/_app.tsx and next.config.ts)
88
+ # 4. Add 19 page shells, each 7-12 lines
89
+
90
+ # That is the entire integration.
91
+ ```
92
+
93
+ Detailed steps in sections 4-7.
94
+
95
+ ---
96
+
97
+ ## 4. Backend setup (NestJS + GraphQL + TypeORM)
98
+
99
+ ### 4.1 `apps/api/src/main.ts`
100
+
101
+ Standard NestJS bootstrap. The library puts no constraints on the bootstrap file.
102
+
103
+ ```ts
104
+ import 'reflect-metadata';
105
+ import { Logger } from '@nestjs/common';
106
+ import { NestFactory } from '@nestjs/core';
107
+ import { AppModule } from './app/app.module';
108
+
109
+ async function bootstrap(): Promise<void> {
110
+ const app = await NestFactory.create(AppModule);
111
+ const globalPrefix = 'api';
112
+ app.setGlobalPrefix(globalPrefix);
113
+ const port = process.env['PORT'] || 7103;
114
+ await app.listen(port);
115
+ Logger.log(`Application is running on: http://localhost:${port}/${globalPrefix}`);
116
+ }
117
+
118
+ bootstrap();
119
+ ```
120
+
121
+ ### 4.2 `apps/api/src/app/app.module.ts`
122
+
123
+ 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.
124
+
125
+ ```ts
126
+ import { Module } from '@nestjs/common';
127
+ import { ConfigModule, ConfigService } from '@nestjs/config';
128
+ import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
129
+ import { WmsModuleGraphqlModule } from '@rytass/wms-module-graphql';
130
+ import { ApolloDriver } from '@nestjs/apollo';
131
+ import {
132
+ ApolloServerPluginLandingPageLocalDefault,
133
+ } from '@apollo/server/plugin/landingPage/default';
134
+ import { resolve } from 'path';
135
+
136
+ @Module({
137
+ imports: [
138
+ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
139
+
140
+ // PostgreSQL via TypeORM
141
+ TypeOrmModule.forRootAsync({
142
+ imports: [ConfigModule],
143
+ inject: [ConfigService],
144
+ useFactory: (cfg: ConfigService): TypeOrmModuleOptions => ({
145
+ type: 'postgres',
146
+ host: cfg.getOrThrow<string>('WMS_DB_HOST'),
147
+ port: cfg.get<number>('WMS_DB_PORT', 5432),
148
+ username: cfg.getOrThrow<string>('WMS_DB_USER'),
149
+ password: cfg.getOrThrow<string>('WMS_DB_PASS'),
150
+ database: cfg.getOrThrow<string>('WMS_DB_NAME'),
151
+ schema: cfg.getOrThrow<string>('WMS_DB_SCHEMA'),
152
+ autoLoadEntities: true,
153
+ synchronize: true, // dev/demo only; use migrations in production
154
+ logging: false,
155
+ }),
156
+ }),
157
+
158
+ // WMS GraphQL - one call wires the entire domain
159
+ WmsModuleGraphqlModule.forRootAsync(
160
+ {
161
+ useFactory: async () => ({}),
162
+ // Module-init seed values; the keys you pass here are created
163
+ // on first boot for `location`, `loaderType`, and `loader`.
164
+ defaultLoaderSerialId: 'DEFAULT',
165
+ defaultLoaderTypeKey: 'DEFAULT',
166
+ defaultLocationKey: 'DEFAULT',
167
+ },
168
+ {
169
+ driver: ApolloDriver,
170
+ useFactory: async () => ({
171
+ playground: false,
172
+ introspection: true,
173
+ cors: true,
174
+ csrfPrevention: false,
175
+ plugins: [ApolloServerPluginLandingPageLocalDefault()],
176
+ autoTransformHttpErrors: true,
177
+ autoSchemaFile: resolve(process.cwd(), 'schema.gql'),
178
+ }),
179
+ },
180
+ ),
181
+ ],
182
+ })
183
+ export class AppModule {}
184
+ ```
185
+
186
+ What you get from this single `forRootAsync`:
187
+
188
+ - 50 TypeORM entities (material, batch, loader, location, every order kind, change logs, signatures, etc.)
189
+ - ~100 GraphQL types
190
+ - ~65 queries and mutations
191
+ - DataLoader N+1 prevention across every resolveField
192
+ - Default seed rows (DEFAULT location, DEFAULT loaderType, DEFAULT loader) on first boot
193
+
194
+ See [API Reference - wms-module-graphql](./api-reference.md) for the full `forRootAsync` signature.
195
+
196
+ ### 4.3 `.env`
197
+
198
+ Keep DB credentials in `.env` (and add it to `.gitignore`).
199
+
200
+ ```
201
+ WMS_DB_HOST=staging.example.internal
202
+ WMS_DB_PORT=5432
203
+ WMS_DB_USER=wms_module
204
+ WMS_DB_PASS=...
205
+ WMS_DB_NAME=wms
206
+ WMS_DB_SCHEMA=wms_demo
207
+ ```
208
+
209
+ ### 4.4 `apps/api/package.json` essentials
210
+
211
+ ```json
212
+ {
213
+ "name": "api",
214
+ "scripts": {
215
+ "dev": "ts-node -r tsconfig-paths/register src/main.ts",
216
+ "build": "tsc -p tsconfig.json",
217
+ "start": "node dist/main.js"
218
+ },
219
+ "dependencies": {
220
+ "@apollo/server": "^4.0.0",
221
+ "@as-integrations/express5": "^1.0.0",
222
+ "@nestjs/apollo": "^13.0.0",
223
+ "@nestjs/common": "^11.0.0",
224
+ "@nestjs/config": "^4.0.0",
225
+ "@nestjs/core": "^11.0.0",
226
+ "@nestjs/graphql": "^13.0.0",
227
+ "@nestjs/platform-express": "^11.0.0",
228
+ "@nestjs/typeorm": "^11.0.0",
229
+ "@rytass/wms-module-core": "^0.1.2",
230
+ "@rytass/wms-module-graphql": "^0.1.2",
231
+ "graphql": "^16.0.0",
232
+ "pg": "^8.0.0",
233
+ "reflect-metadata": "^0.2.0",
234
+ "rxjs": "^7.0.0",
235
+ "class-transformer": "^0.5.0",
236
+ "class-validator": "^0.14.0",
237
+ "typeorm": "^0.3.0"
238
+ },
239
+ "devDependencies": {
240
+ "ts-node": "^10.0.0",
241
+ "tsconfig-paths": "^4.0.0",
242
+ "typescript": "^5.0.0"
243
+ }
244
+ }
245
+ ```
246
+
247
+ The `@as-integrations/express5` package is required at runtime by `@nestjs/apollo@13` + Express 5; it is listed as a peer of `wms-module-graphql` since `0.1.1`.
248
+
249
+ > **Apollo Server version**: `@nestjs/apollo@13` currently pairs with **Apollo Server 4** (not 5).
250
+ > Use `"@apollo/server": "^4.0.0"`. Apollo Server 5 is a breaking release — the
251
+ > `@as-integrations/express5` peer pairing and plugin import paths differ. Installing
252
+ > `^5.0.0` will cause hard runtime errors with the current NestJS Apollo adapter.
253
+
254
+ ### 4.5 Sanity check
255
+
256
+ Start the server and probe the schema:
257
+
258
+ > **Note**: NestJS's `app.setGlobalPrefix('api')` does **not** apply to the `/graphql`
259
+ > endpoint mounted by `@nestjs/apollo`. The GraphQL endpoint is always at `/graphql`,
260
+ > not `/api/graphql`, regardless of the global prefix setting.
261
+
262
+ ```bash
263
+ pnpm dev
264
+ curl -s -X POST http://localhost:7103/graphql \
265
+ -H "Content-Type: application/json" \
266
+ -d '{"query":"{ __schema { queryType { name } } }"}'
267
+ # -> {"data":{"__schema":{"queryType":{"name":"Query"}}}}
268
+
269
+ curl -s -X POST http://localhost:7103/graphql \
270
+ -H "Content-Type: application/json" \
271
+ -d '{"query":"{ locations { items { id key } } }"}'
272
+ # -> {"data":{"locations":{"items":[{"id":"<uuid>","key":"DEFAULT"}]}}}
273
+ ```
274
+
275
+ The `DEFAULT` location/loader/loaderType returned by the second query was seeded by module init using the keys you passed in section 4.2.
276
+
277
+ ---
278
+
279
+ ### 4.6 Version compatibility matrix
280
+
281
+ Tested combinations as of 0.1.2:
282
+
283
+ | Package | Tested version | Notes |
284
+ |----------------------------------|-------------------------|--------------------------------------------------------------------|
285
+ | NestJS (`@nestjs/*`) | 11.x | 10.x untested; some Express-5-related APIs differ |
286
+ | `@nestjs/graphql` | 13.x | Required for `@nestjs/apollo@13` |
287
+ | `@nestjs/apollo` | 13.x | Pairs with Apollo Server 4 + Express 5 |
288
+ | `@apollo/server` | 4.x | v5 is breaking; not supported |
289
+ | `@as-integrations/express5` | 1.x | Required runtime peer |
290
+ | TypeORM | 0.3.x | 0.2.x not supported (different DataSource API) |
291
+ | PostgreSQL | 14+ | Native dialect |
292
+ | `graphql` | 16.x | 15.x untested |
293
+ | Node.js | 20 LTS or 22 | 18 deprecated, 24 untested |
294
+ | Next.js | 15.x Pages Router | App Router unsupported (see §13.11) |
295
+ | React | 19.x | 18.x likely works but untested |
296
+ | `@mezzanine-ui/react` | 1.3.x | Earlier versions untested |
297
+ | `nuqs` | 2.x (Pages adapter) | App Router adapter untested (see §13.11) |
298
+
299
+ ---
300
+
301
+ ## 5. Frontend setup (Next.js Pages Router)
302
+
303
+ The frontend package targets **Next.js Pages Router**, not App Router. The pre-built page bodies use SSR-incompatible providers and must be loaded on the client.
304
+
305
+ ### 5.1 `apps/client/src/pages/_app.tsx`
306
+
307
+ Provider chain — order matters.
308
+
309
+ ```tsx
310
+ import '@xyflow/react/dist/style.css'; // Map view needs React Flow styles
311
+ import '../styles/mezzanine.scss'; // Mezzanine design tokens
312
+ import '../styles/global.scss'; // Your global reset
313
+ import { type AppProps } from 'next/app';
314
+ import Head from 'next/head';
315
+ import { ApolloProvider } from '@apollo/client/react';
316
+ import CalendarConfigProviderDayjs from '@mezzanine-ui/react/Calendar/CalendarConfigProviderDayjs';
317
+ import { NuqsAdapter } from 'nuqs/adapters/next/pages';
318
+ import { ApolloClient } from '@rytass/wms-module-react';
319
+ import AuthGuard from '../auth/AuthGuard';
320
+
321
+ function CustomApp({ Component, pageProps }: AppProps): React.ReactNode {
322
+ return (
323
+ <>
324
+ <Head>
325
+ <title>My WMS</title>
326
+ </Head>
327
+ <ApolloProvider client={ApolloClient}>
328
+ <CalendarConfigProviderDayjs>
329
+ <NuqsAdapter>
330
+ <AuthGuard>
331
+ <Component {...pageProps} />
332
+ </AuthGuard>
333
+ </NuqsAdapter>
334
+ </CalendarConfigProviderDayjs>
335
+ </ApolloProvider>
336
+ </>
337
+ );
338
+ }
339
+
340
+ export default CustomApp;
341
+ ```
342
+
343
+ Required CSS imports — all three matter:
344
+
345
+ | Import | Why |
346
+ |-----------------------------------|------------------------------------------------------------------------------------------------------------------|
347
+ | `@xyflow/react/dist/style.css` | Maps page and Locations map tab use React Flow. Without this, the canvas controls render unstyled. |
348
+ | `mezzanine.scss` | Sets up Mezzanine design tokens and base styles. |
349
+ | `global.scss` | Your own global reset / overrides. |
350
+
351
+ ### 5.2 `apps/client/src/styles/mezzanine.scss`
352
+
353
+ ```scss
354
+ @use '@mezzanine-ui/core' as mzn-core;
355
+ @use '@mezzanine-ui/system' as mzn-system;
356
+ @use '@mezzanine-ui/system/spacing';
357
+
358
+ :root {
359
+ @include mzn-system.common-variables('default');
360
+ @include mzn-system.colors();
361
+ }
362
+
363
+ @include mzn-core.styles();
364
+
365
+ // Overrides the WMS UI depends on:
366
+ .mzn-backdrop.mzn-backdrop--open.mzn-modal__overlay {
367
+ position: absolute;
368
+ top: 0;
369
+ right: 0;
370
+ left: 0;
371
+ bottom: 0;
372
+ }
373
+
374
+ .mzn-modal--wide {
375
+ width: calc(100% - var(--mzn-spacing-padding-horizontal-spacious) * 2);
376
+ }
377
+
378
+ .mzn-dropdown-popper--with-portal {
379
+ z-index: 1004 !important;
380
+ }
381
+
382
+ .mzn-table__cell__content .mzn-icon {
383
+ vertical-align: sub;
384
+ }
385
+
386
+ .mzn-select,
387
+ .mzn-input {
388
+ min-width: unset !important;
389
+ }
390
+
391
+ .mzn-form-field--horizontal .mzn-form-field__label-area {
392
+ min-width: fit-content !important;
393
+ }
394
+ ```
395
+
396
+ ### 5.3 `apps/client/next.config.ts`
397
+
398
+ ```ts
399
+ import type { NextConfig } from 'next';
400
+
401
+ const API_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:7103';
402
+
403
+ const nextConfig: NextConfig = {
404
+ reactStrictMode: true,
405
+ // Transpile the ESM-only packages
406
+ transpilePackages: [
407
+ '@rytass/wms-module-react',
408
+ '@rytass/wms-map-react-components',
409
+ '@mezzanine-ui/react',
410
+ '@mezzanine-ui/core',
411
+ '@mezzanine-ui/icons',
412
+ '@mezzanine-ui/system',
413
+ ],
414
+ // The default ApolloClient instance hardcodes `/graphql` as the URL.
415
+ // Proxy that to the API server so it actually reaches the backend.
416
+ async rewrites() {
417
+ return [
418
+ {
419
+ source: '/graphql',
420
+ destination: `${API_URL}/graphql`,
421
+ },
422
+ ];
423
+ },
424
+ };
425
+
426
+ export default nextConfig;
427
+ ```
428
+
429
+ Why `transpilePackages`: the React package declares `"type": "module"` and Mezzanine UI ships ESM-syntax JS without the flag. Both must run through the Next.js transformer.
430
+
431
+ `@rytass/wms-map-react-components` is a companion package that provides the warehouse-map
432
+ visualization canvas used by the **LocationsManagement** and **Maps** pages (built on React
433
+ Flow / `@xyflow/react`). It is a **required runtime dependency** for those two pages; omitting
434
+ it causes import errors when those pages load. Like the React lib, it ships ESM-only and must
435
+ be listed in `transpilePackages`. Install it as a regular production dependency:
436
+
437
+ ```bash
438
+ pnpm add @rytass/wms-map-react-components
439
+ ```
440
+
441
+ Current published version: `^0.1.0`. Registry: `https://registry.npmjs.org/`.
442
+
443
+ ### 5.4 `apps/client/package.json` essentials
444
+
445
+ ```json
446
+ {
447
+ "name": "client",
448
+ "scripts": {
449
+ "dev": "next dev -p 7101",
450
+ "build": "next build",
451
+ "start": "next start -p 7101"
452
+ },
453
+ "dependencies": {
454
+ "@apollo/client": "^4.0.0",
455
+ "@hookform/error-message": "^2.0.0",
456
+ "@hookform/resolvers": "^5.0.0",
457
+ "@mezzanine-ui/core": "^1.0.0",
458
+ "@mezzanine-ui/icons": "^1.0.0",
459
+ "@mezzanine-ui/react": "^1.0.0",
460
+ "@mezzanine-ui/system": "^1.0.0",
461
+ "@rytass/wms-map-react-components": "^0.1.0",
462
+ "@rytass/wms-module-react": "^0.1.2",
463
+ "@xyflow/react": "^12.0.0",
464
+ "graphql": "^16.0.0",
465
+ "lodash": "^4.17.0",
466
+ "next": "^15.0.0",
467
+ "nuqs": "^2.0.0",
468
+ "react": "^19.0.0",
469
+ "react-dom": "^19.0.0",
470
+ "react-hook-form": "^7.0.0",
471
+ "yup": "^1.0.0"
472
+ }
473
+ }
474
+ ```
475
+
476
+ ---
477
+
478
+ ## 6. Pages Router shell pattern (the 19 thin shells)
479
+
480
+ The React package exports each functional area as a **default-exportable page body**. The consumer's `pages/<area>/index.tsx` is a 7-12 line shell.
481
+
482
+ ### Static shells (one component, no params)
483
+
484
+ ```tsx
485
+ // apps/client/src/pages/materials-management/index.tsx
486
+ import { MaterialsManagement } from '@rytass/wms-module-react';
487
+ import { type ReactNode } from 'react';
488
+
489
+ function MaterialsManagementPage(): ReactNode {
490
+ return <MaterialsManagement />;
491
+ }
492
+
493
+ export const getServerSideProps = async () => {
494
+ return { props: {} };
495
+ };
496
+
497
+ export default MaterialsManagementPage;
498
+ ```
499
+
500
+ Required shells — one per route:
501
+
502
+ | Route | Component | Source |
503
+ |--------------------------------|----------------------------|------------------|
504
+ | `/materials-management` | `<MaterialsManagement />` | wms-module-react |
505
+ | `/loaders-management` | `<LoadersManagement />` | wms-module-react |
506
+ | `/loaders-management/create` | `<CreateLoader />` | wms-module-react |
507
+ | `/locations-management` | `<LocationsManagement />` | wms-module-react |
508
+ | `/inventory-list` | `<InventoryList />` | wms-module-react |
509
+ | `/inventory-count` | `<InventoryCount />` | wms-module-react |
510
+ | `/inventory-movement` | `<InventoryMovement />` | wms-module-react |
511
+ | `/change-history` | `<ChangeHistory />` | wms-module-react |
512
+ | `/receive-inventory-orders` | `<ReceiveInventoryOrders />`| wms-module-react|
513
+ | `/ship-inventory-orders` | `<ShipInventoryOrders />` | wms-module-react |
514
+ | `/label-search` | `<LabelSearch />` | wms-module-react |
515
+ | `/maps` | `<Maps />` | wms-module-react |
516
+
517
+ ### Dynamic shells (one route param)
518
+
519
+ The dynamic page components read `useRouter().query.id` internally — the consumer does not pass props.
520
+
521
+ ```tsx
522
+ // apps/client/src/pages/receive-inventory-orders/[id]/view.tsx
523
+ import { ViewReceiveInventoryOrder } from '@rytass/wms-module-react';
524
+ import { type ReactNode } from 'react';
525
+
526
+ function ReceiveInventoryOrderViewPage(): ReactNode {
527
+ return <ViewReceiveInventoryOrder />;
528
+ }
529
+
530
+ export const getServerSideProps = async () => {
531
+ return { props: {} };
532
+ };
533
+
534
+ export default ReceiveInventoryOrderViewPage;
535
+ ```
536
+
537
+ | Route | Component |
538
+ |------------------------------------------------|---------------------------------|
539
+ | `/receive-inventory-orders/[id]/view` | `<ViewReceiveInventoryOrder />` |
540
+ | `/receive-inventory-orders/[id]/edit` | `<EditReceiveInventoryOrder />` |
541
+ | `/ship-inventory-orders/[id]/view` | `<ViewShipInventoryOrder />` |
542
+ | `/ship-inventory-orders/[id]/edit` | `<EditShipInventoryOrder />` |
543
+ | `/inventory-movement/transfer-order/[id]` | `<EditTransferOrder />` |
544
+
545
+ ### Home page
546
+
547
+ The lib does not ship a dashboard. Provide your own or render nothing:
548
+
549
+ ```tsx
550
+ // apps/client/src/pages/index.tsx
551
+ import { type ReactNode } from 'react';
552
+
553
+ function IndexPage(): ReactNode {
554
+ return null; // Or your own landing page
555
+ }
556
+
557
+ export default IndexPage;
558
+ ```
559
+
560
+ ---
561
+
562
+ ## 7. Apollo client wiring
563
+
564
+ ### 7.1 Default `ApolloClient` instance (simple case)
565
+
566
+ The lib re-exports a configured Apollo client instance for fast wiring:
567
+
568
+ ```ts
569
+ import { ApolloClient } from '@rytass/wms-module-react';
570
+
571
+ // Use directly in ApolloProvider:
572
+ <ApolloProvider client={ApolloClient}>...</ApolloProvider>
573
+ ```
574
+
575
+ **Important caveat**: the default instance points at the relative URL `/graphql`. For this to reach the NestJS backend, the Next.js dev server must proxy `/graphql` to the API host. See the `rewrites()` in section 5.3.
576
+
577
+ In production, set `NEXT_PUBLIC_API_URL` to the public API origin so `next.config.ts` proxies to the right place.
578
+
579
+ ### 7.2 Factory pattern (recommended for custom URLs)
580
+
581
+ If you need to talk to multiple GraphQL endpoints, override the URL, add headers, or compose links, use the `createApolloClient` factory:
582
+
583
+ ```ts
584
+ import { createApolloClient } from '@rytass/wms-module-react';
585
+
586
+ const client = createApolloClient();
587
+ ```
588
+
589
+ As of **0.1.2**, `createApolloClient()` takes **no parameters** and returns a fresh
590
+ `ApolloClient<NormalizedCacheObject>` instance configured with the same `/graphql` URI as
591
+ the default singleton. A URL/header override parameter is planned for **0.2.0** (will be a
592
+ non-breaking addition). See [API Reference - createApolloClient](./api-reference.md).
593
+
594
+ ### 7.3 GraphQL operations & DocumentNode constants
595
+
596
+ All queries and mutations the lib pages need are pre-defined and pre-typed and exported from
597
+ `@rytass/wms-module-react`. You can import them directly to build custom pages on top of the
598
+ same data layer.
599
+
600
+ > **Codegen strategy**: the lib uses the `typed-document-node` GraphQL Code Generator plugin,
601
+ > which generates `DocumentNode` constants (e.g. `MaterialsDocument`) plus paired TypeScript
602
+ > types — **not** React hook wrappers. Call `useQuery`/`useMutation` from `@apollo/client/react`
603
+ > and pass the `Document` constant as the first argument.
604
+
605
+ **Query example:**
606
+
607
+ ```ts
608
+ import { useQuery } from '@apollo/client/react';
609
+ import {
610
+ MaterialsDocument,
611
+ type MaterialsQuery,
612
+ type MaterialsQueryVariables,
613
+ } from '@rytass/wms-module-react';
614
+
615
+ function MyCustomMaterialList(): ReactNode {
616
+ const { data, loading } = useQuery<MaterialsQuery, MaterialsQueryVariables>(
617
+ MaterialsDocument,
618
+ { variables: { offset: 0, limit: 20 } },
619
+ );
620
+
621
+ if (loading) return <Loading />;
622
+ return <ul>{data?.materials.items.map((m) => <li key={m.id}>{m.name}</li>)}</ul>;
623
+ }
624
+ ```
625
+
626
+ **Paginated query example:**
627
+
628
+ ```ts
629
+ import { useQuery } from '@apollo/client/react';
630
+ import {
631
+ LocationsDocument,
632
+ type LocationsQuery,
633
+ type LocationsQueryVariables,
634
+ } from '@rytass/wms-module-react';
635
+
636
+ function MyLocationPicker(): ReactNode {
637
+ const { data } = useQuery<LocationsQuery, LocationsQueryVariables>(LocationsDocument, {
638
+ variables: { offset: 0, limit: 100 },
639
+ });
640
+ const locations = data?.locations.items ?? [];
641
+ // ...
642
+ }
643
+ ```
644
+
645
+ **Mutation example:**
646
+
647
+ ```ts
648
+ import { useMutation } from '@apollo/client/react';
649
+ import {
650
+ CreateMaterialDocument,
651
+ type CreateMaterialMutation,
652
+ type CreateMaterialMutationVariables,
653
+ } from '@rytass/wms-module-react';
654
+
655
+ function CreateMaterialButton(): ReactNode {
656
+ const [createMaterial, { loading }] = useMutation<
657
+ CreateMaterialMutation,
658
+ CreateMaterialMutationVariables
659
+ >(CreateMaterialDocument);
660
+
661
+ const handleCreate = (): void => {
662
+ createMaterial({ variables: { name: 'Steel Bolt', description: 'M8' } });
663
+ };
664
+
665
+ return <button onClick={handleCreate} disabled={loading}>Create</button>;
666
+ }
667
+ ```
668
+
669
+ The full list of generated `Document` constants and their types is in
670
+ [API Reference - Generated GraphQL Operations](./api-reference.md).
671
+
672
+ ---
673
+
674
+ ## 8. Authentication / authorization
675
+
676
+ The lib **does not** ship an auth system. The page components assume they render in an authenticated context — they call queries on mount without checking session state.
677
+
678
+ ### 8.1 Stub for demos
679
+
680
+ The minimum wrapper that lets the lib run unauthenticated:
681
+
682
+ ```tsx
683
+ // apps/client/src/auth/AuthGuard.tsx
684
+ import dynamic from 'next/dynamic';
685
+ import { type ReactNode } from 'react';
686
+
687
+ const AuthorizedAdminPage = dynamic(() => import('./AuthorizedAdminPage'), {
688
+ ssr: false,
689
+ });
690
+
691
+ interface AuthGuardProps {
692
+ children: ReactNode;
693
+ }
694
+
695
+ function AuthGuard({ children }: AuthGuardProps): ReactNode {
696
+ return <AuthorizedAdminPage>{children}</AuthorizedAdminPage>;
697
+ }
698
+
699
+ export default AuthGuard;
700
+ ```
701
+
702
+ The `ssr: false` matters — the lib's providers contain client-only state (Mezzanine portal refs, Apollo subscriptions) that breaks SSR hydration if rendered server-side.
703
+
704
+ ### 8.2 Layout shell with the lib's providers
705
+
706
+ `AuthorizedAdminPage` wraps the lib's three context providers and the navigation chrome:
707
+
708
+ ```tsx
709
+ // apps/client/src/auth/AuthorizedAdminPage/index.tsx
710
+ import { type ReactNode } from 'react';
711
+ import Main from './Main';
712
+ import {
713
+ DialogProvider,
714
+ InventoryActionsProvider,
715
+ ModalProvider,
716
+ } from '@rytass/wms-module-react';
717
+
718
+ interface AuthorizedAdminPageProps {
719
+ children: ReactNode;
720
+ }
721
+
722
+ function AuthorizedAdminPage({ children }: AuthorizedAdminPageProps): ReactNode {
723
+ return (
724
+ <InventoryActionsProvider>
725
+ <DialogProvider>
726
+ <ModalProvider>
727
+ <Main>{children}</Main>
728
+ </ModalProvider>
729
+ </DialogProvider>
730
+ </InventoryActionsProvider>
731
+ );
732
+ }
733
+
734
+ export default AuthorizedAdminPage;
735
+ ```
736
+
737
+ All three providers come from the lib. They wire up cross-component modal/dialog state that the page components rely on. **Order is significant** — `InventoryActionsProvider` must wrap the dialog/modal providers because some inventory actions trigger dialogs.
738
+
739
+ ### 8.3 Navigation
740
+
741
+ The reference implementation's Navigation component uses `useRoutes()` from the lib to enumerate the menu structure:
742
+
743
+ ```tsx
744
+ // apps/client/src/auth/AuthorizedAdminPage/Navigation.tsx (excerpt)
745
+ import { useRoutes } from '@rytass/wms-module-react';
746
+ import { Navigation as MznNavigation } from '@mezzanine-ui/react';
747
+
748
+ function Navigation(): ReactNode {
749
+ const routes = useRoutes(); // returns the full RouteConfig tree
750
+ // ... render MznNavigation with routes
751
+ }
752
+ ```
753
+
754
+ `useRoutes()` returns a typed tree of `{ title, icon, href, children }` — keep that shape, render however you want. The reference implementation is 79 lines.
755
+
756
+ ### 8.4 Real auth integration
757
+
758
+ Replace the `AuthGuard` stub with your own logic that:
759
+
760
+ 1. Verifies session before rendering `AuthorizedAdminPage`.
761
+ 2. Provides member identity to GraphQL via the Apollo client's `context` link.
762
+ 3. Handles 401 responses (logout / redirect).
763
+
764
+ The lib's resolvers do not enforce auth themselves. If you need RBAC, layer your own guard/decorator on top of the resolvers or apply NestJS-level guards globally in `app.module.ts`.
765
+
766
+ ---
767
+
768
+ ## 9. Database support matrix
769
+
770
+ | Database | Status | Notes |
771
+ |------------|---------------|------------------------------------|
772
+ | PostgreSQL | Recommended | Native dialect of the entities |
773
+ | SQLite | Limited | See known issues below |
774
+
775
+ ### PostgreSQL (recommended)
776
+
777
+ The entities are designed for PostgreSQL:
778
+
779
+ - `timestamptz` columns map to native `timestamp with time zone`
780
+ - `bool` maps to PG `boolean`
781
+ - Sequence-based serial IDs (e.g. loader.serialId) use PG `CURRVAL`
782
+ - `synchronize: true` works for dev/demo; use proper migrations in production
783
+
784
+ This is the only database where the lib runs without any consumer-side workaround.
785
+
786
+ ### SQLite limitations
787
+
788
+ If you must run against SQLite (e.g. for local prototyping without a PG server):
789
+
790
+ 1. **`timestamptz` / `bool` column types** — TypeORM's `EntityMetadataValidator` rejects these for the SQLite driver. The consumer must patch the validator's `supportedDataTypes` allowlist, e.g.:
791
+
792
+ ```ts
793
+ import { AbstractSqliteDriver } from 'typeorm/driver/sqlite-abstract/AbstractSqliteDriver';
794
+ AbstractSqliteDriver.prototype.supportedDataTypes.push('timestamptz', 'bool');
795
+ ```
796
+
797
+ At the disk layer SQLite is liberal — it accepts any column type string and gives it NUMERIC affinity — so once the validator is past, schema creation works.
798
+
799
+ 2. **`CURRVAL(seq_name)` PostgreSQL function** — used internally to compute loader serial IDs. SQLite has no equivalent. All `createLoader` mutations will fail with `no such function: CURRVAL`. There is no clean consumer-side workaround; running real loader creation flows on SQLite is not supported as of 0.1.2.
800
+
801
+ 3. **DateTime serialize round-trip** — SQLite returns datetime strings in `YYYY-MM-DD HH:MM:SS` format. The GraphQL DateTime scalar may emit `null` for some columns; the consumer-visible effect is blank `createdAt` fields in some payloads.
802
+
803
+ **Recommendation**: use PostgreSQL. If you absolutely need SQLite for offline demos, drop the `createLoader` flow from your test plan or wait for a future release with driver-agnostic ID generation.
804
+
805
+ ---
806
+
807
+ ## 10. Seed data for demos
808
+
809
+ The lib seeds three rows on first boot (`DEFAULT` location, loaderType, loader). Beyond that, the consumer is responsible for any demo data.
810
+
811
+ The cleanest approach is a small fetch-based script that POSTs against the local GraphQL endpoint. Seeding via the GraphQL surface also doubles as a smoke test of every mutation.
812
+
813
+ ```js
814
+ // scripts/seed.mjs (ESM, no build step)
815
+ const GQL_URL = 'http://localhost:7103/graphql';
816
+
817
+ async function gql(query, variables = {}) {
818
+ const res = await fetch(GQL_URL, {
819
+ method: 'POST',
820
+ headers: { 'Content-Type': 'application/json' },
821
+ body: JSON.stringify({ query, variables }),
822
+ });
823
+ const json = await res.json();
824
+ if (json.errors) throw new Error(json.errors.map((e) => e.message).join('; '));
825
+ return json.data;
826
+ }
827
+
828
+ const { createLocation } = await gql(`
829
+ mutation {
830
+ createLocation(key: "ZONE-A", description: "A 區") {
831
+ id key description
832
+ }
833
+ }
834
+ `);
835
+ console.log('Created location:', createLocation.id);
836
+ ```
837
+
838
+ The reference implementation's full seed script (`scripts/seed.mjs`, 286 lines) seeds 3 materials × units, 2 loaderTypes, 5 loaders, 2 locations, and 1 completed receive order with items — enough to exercise every list page with meaningful data.
839
+
840
+ ---
841
+
842
+ ## 11. Deployment notes
843
+
844
+ ### 11.1 API server
845
+
846
+ The NestJS API can deploy to any platform that supports Node.js 20:
847
+
848
+ - Containerize with multi-stage build (see the reference repo's `Dockerfile` pattern)
849
+ - Set `WMS_DB_*` env vars in the deployment environment
850
+ - Disable `synchronize: true` in production; use TypeORM migrations
851
+ - The default port is 7103 (overridable via `PORT`)
852
+
853
+ ### 11.2 Frontend
854
+
855
+ The Next.js Pages Router app builds to `next start` output:
856
+
857
+ - Set `NEXT_PUBLIC_API_URL` to the public API origin so the `next.config.ts` proxy rewrites `/graphql` correctly
858
+ - The lib targets ES2022 — modern browsers only
859
+ - The `transpilePackages` list is required at build time, not just dev
860
+
861
+ ### 11.3 Standalone Next.js output (optional)
862
+
863
+ The lib does not require any special Next.js output mode. If your deployment platform benefits from `output: 'standalone'` (smaller Docker images), enable it in `next.config.ts`:
864
+
865
+ ```ts
866
+ const nextConfig: NextConfig = {
867
+ output: 'standalone',
868
+ // ...
869
+ };
870
+ ```
871
+
872
+ This is what the original WMS monorepo uses for its client deployment.
873
+
874
+ ---
875
+
876
+ ## 12. Known issues & roadmap
877
+
878
+ Cosmetic warnings (do not affect functionality):
879
+
880
+ - React Flow logs *"It looks like you've created a new nodeTypes object"* — internal lib optimization issue, no functional impact.
881
+ - Loader-create form numeric inputs warn about `undefined` values — minor UX cleanup.
882
+ - `MznModal` aria-hidden interaction has a known accessibility nit (Mezzanine UI issue).
883
+
884
+ Functional limitations:
885
+
886
+ - **Apollo endpoint is hardcoded** in the default `ApolloClient` instance (`/graphql`). The Next.js rewrite is the supported workaround. A future minor will widen `createApolloClient` to accept a URL parameter (breaking change -> 0.2.0).
887
+ - **SQLite support is partial** — see section 9. Driver-agnostic loader serial IDs are on the roadmap.
888
+
889
+ Tracked in this repo — see `libs/core/CHANGELOG.md`, `libs/graphql/CHANGELOG.md`, `libs/react/CHANGELOG.md` and `RELEASING.md` for cadence.
890
+
891
+ ---
892
+
893
+ ---
894
+
895
+ ## 13. Integrating into an existing NestJS / Next.js project
896
+
897
+ This section covers the real-world scenarios where the WMS modules are added to a project
898
+ that already has its own NestJS backend, GraphQL setup, or Next.js frontend. The greenfield
899
+ setup in sections 4-7 is the baseline; read this section for the delta.
900
+
901
+ > **Scope**: 0.1.x. Many limitations listed here are known gaps — roadmap notes are included
902
+ > where improvements are planned.
903
+
904
+ ### 13.1 Coexisting with an existing `GraphQLModule`
905
+
906
+ **Status**: Not supported in 0.1.x without forking the library.
907
+
908
+ `WmsModuleGraphqlModule.forRootAsync(...)` internally calls `GraphQLModule.forRootAsync(...)`.
909
+ NestJS does not allow two `GraphQLModule` registrations in the same application — the second
910
+ registration throws `Error: Cannot register 2 GraphQL modules`. If your host app already
911
+ imports `GraphQLModule`, you cannot simply add `WmsModuleGraphqlModule` to the same
912
+ `imports: [...]` array.
913
+
914
+ **Recommended workarounds** (in order of practicality):
915
+
916
+ 1. **Separate NestJS process** — run the WMS API as its own NestJS server on a different port
917
+ (e.g. 7103) and have the host API proxy or federate as needed. This is the architecture
918
+ used by `wms-standalone` and is the safest path.
919
+
920
+ 2. **NestJS sub-app (monorepo)** — if you use the Nx or NestJS CLI monorepo pattern, you can
921
+ host the WMS app as a separate application inside the same repo that shares code with your
922
+ existing app but runs as a distinct process.
923
+
924
+ 3. **Fork and merge** — advanced consumers can fork `libs/graphql` and extract the resolver
925
+ registrations, removing the internal `GraphQLModule.forRootAsync(...)` call. The host app
926
+ then registers a single `GraphQLModule` that includes the WMS resolver classes. This requires
927
+ maintaining a fork and is not recommended unless you intend to take full ownership of the lib.
928
+
929
+ **Tied to §13.1**: the `/graphql` endpoint name conflict (see §13.14 for GraphQL type name conflicts) is also
930
+ fundamentally blocked by this same single-GraphQLModule constraint when running both APIs in
931
+ the same process.
932
+
933
+ ---
934
+
935
+ ### 13.2 Sharing a TypeORM DataSource
936
+
937
+ `WmsModuleCoreModule` (loaded automatically by `WmsModuleGraphqlModule`) calls
938
+ `TypeOrmModule.forFeature(entities)` using the **default** TypeORM data source connection.
939
+ It does not create a named connection.
940
+
941
+ **Implications:**
942
+
943
+ - If your host app uses `TypeOrmModule.forRoot(...)` with the default connection name (`default`),
944
+ the WMS entities will register against the same data source — this is correct behavior and
945
+ works as-is. The `autoLoadEntities: true` flag handles entity discovery.
946
+
947
+ - If your host uses a **named connection** (e.g. `TypeOrmModule.forRoot({ name: 'main', ... })`),
948
+ the WMS entities will attempt to register against the unnamed default connection, which will
949
+ not exist. You must provide a default connection alongside your named connection, or fork the
950
+ lib to pass a connection name through `forRootAsync`.
951
+
952
+ **Schema isolation**: the WMS entities use no table prefix by default. If your database already
953
+ has tables named `material`, `loader`, `location`, `stock`, etc. you will get name collisions.
954
+ Use a dedicated PostgreSQL schema (the `schema` option in `TypeOrmModuleOptions`) to isolate
955
+ WMS tables:
956
+
957
+ ```ts
958
+ TypeOrmModule.forRoot({
959
+ // ...
960
+ schema: 'wms', // All WMS entities create tables in the 'wms' schema
961
+ autoLoadEntities: true,
962
+ });
963
+ ```
964
+
965
+ This is what `wms-standalone` does (`WMS_DB_SCHEMA=wms_demo`).
966
+
967
+ ---
968
+
969
+ ### 13.3 Entity / Table Name Conflicts
970
+
971
+ The WMS TypeORM entities use unqualified class names as table names (TypeORM default).
972
+ If your host application already defines entities named `Material`, `Location`, `Loader`,
973
+ `Stock`, `Batch`, `SaleOrder`, or any of the 50 WMS entity names, you will get either a
974
+ TypeORM registration conflict or unexpected data sharing.
975
+
976
+ **Fix**: use a dedicated PostgreSQL schema (see §13.2 above). If you cannot use a separate
977
+ schema, you must fork `libs/core` and add a table-name prefix to every entity decorator,
978
+ e.g. `@Entity('wms_material')`.
979
+
980
+ ---
981
+
982
+ ### 13.4 Production Database Migrations
983
+
984
+ `synchronize: true` is for development only. In production:
985
+
986
+ 1. Disable `synchronize`:
987
+
988
+ ```ts
989
+ TypeOrmModule.forRoot({
990
+ // ...
991
+ synchronize: false,
992
+ });
993
+ ```
994
+
995
+ 2. To generate migrations that include WMS entities, TypeORM's `migration:generate` CLI needs
996
+ access to all entity classes. Since WMS entities are bundled inside `@rytass/wms-module-core`,
997
+ you need to import them in your TypeORM data source config:
998
+
999
+ ```ts
1000
+ // data-source.ts (TypeORM CLI config)
1001
+ import { DataSource } from 'typeorm';
1002
+ // Import all entity classes from the core lib
1003
+ // (see docs/api-reference.md "TypeORM Entities" for the complete list of 50 classes)
1004
+ import {
1005
+ MaterialEntityClass,
1006
+ LoadersEntityClass,
1007
+ LocationEntityClass,
1008
+ // ... all entity classes from @rytass/wms-module-core
1009
+ } from '@rytass/wms-module-core';
1010
+ // WarehouseMapEntityClass lives in the base module, not in core
1011
+ import { WarehouseMapEntityClass } from '@rytass/wms-base-nestjs-module/models/warehouse-map.entity';
1012
+
1013
+ export default new DataSource({
1014
+ type: 'postgres',
1015
+ // ... connection config
1016
+ entities: [
1017
+ MaterialEntityClass,
1018
+ LoadersEntityClass,
1019
+ LocationEntityClass,
1020
+ // ... list every entity class from @rytass/wms-module-core
1021
+ WarehouseMapEntityClass, // required — used by LocationsManagement and Maps pages
1022
+ // Add your own entity classes here
1023
+ ],
1024
+ migrations: ['src/migrations/*.ts'],
1025
+ });
1026
+ ```
1027
+
1028
+ > **Note**: `WarehouseMapEntityClass` is imported from `@rytass/wms-base-nestjs-module`, not
1029
+ > from `@rytass/wms-module-core`. If it is omitted, the `warehouse_map` table is never
1030
+ > created; the `LocationsManagement` and `Maps` pages will throw at runtime trying to query
1031
+ > a non-existent table. Install `@rytass/wms-base-nestjs-module` as an additional dependency
1032
+ > if it is not already in your `package.json`.
1033
+ >
1034
+ > For the full list of the 50+ entity classes exported from `@rytass/wms-module-core`, refer
1035
+ > to the **TypeORM Entities** section in [docs/api-reference.md](./api-reference.md).
1036
+
1037
+ 3. Run `npx typeorm migration:generate src/migrations/wms-init -d data-source.ts`.
1038
+
1039
+ The lib does **not** ship migration files. The first time you deploy with `synchronize: false`,
1040
+ you must generate and run an initial migration that creates all WMS tables.
1041
+
1042
+ ---
1043
+
1044
+ ### 13.5 Passing Host Auth Identity into WMS Resolvers
1045
+
1046
+ The WMS lib resolvers do **not** enforce auth themselves — they execute queries against the
1047
+ database without any identity check. RBAC must be layered on top.
1048
+
1049
+ **Option A**: NestJS-level global guard. Apply a guard globally in your `AppModule`:
1050
+
1051
+ ```ts
1052
+ // app.module.ts
1053
+ import { APP_GUARD } from '@nestjs/core';
1054
+ import { YourAuthGuard } from './auth/auth.guard';
1055
+
1056
+ @Module({
1057
+ providers: [
1058
+ {
1059
+ provide: APP_GUARD,
1060
+ useClass: YourAuthGuard,
1061
+ },
1062
+ ],
1063
+ })
1064
+ export class AppModule {}
1065
+ ```
1066
+
1067
+ This applies your guard to every resolver, including WMS resolvers.
1068
+
1069
+ **Option B**: Read auth identity via `context.req.user` in custom resolvers. The Express `req`
1070
+ object is spread directly into the GraphQL context by the lib's internal DataLoader injection
1071
+ logic. Any field set on `req` by a NestJS guard or Passport strategy (e.g. `req.user`) is
1072
+ therefore accessible from resolvers as `ctx.req.user`.
1073
+
1074
+ > **Why returning `context` from `gqlOptions.useFactory` does not work**: the lib's
1075
+ > `forRootAsync` calls `gqlOptions.useFactory(…)` to collect your Apollo options, then
1076
+ > unconditionally replaces the `context` key with its own factory that injects DataLoaders
1077
+ > (`{ loaders, ...contextArg }`). Any `context` you return from `useFactory` is silently
1078
+ > dropped. The resulting GraphQL context is always `{ req, res, loaders }` — there is no
1079
+ > `user` key.
1080
+
1081
+ To access the authenticated user in a **custom** resolver, populate `req.user` in a guard
1082
+ or middleware and read it from `ctx.req.user`:
1083
+
1084
+ ```ts
1085
+ // auth.guard.ts — populate req.user before the resolver runs
1086
+ import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
1087
+ import { GqlExecutionContext } from '@nestjs/graphql';
1088
+
1089
+ @Injectable()
1090
+ export class JwtAuthGuard implements CanActivate {
1091
+ canActivate(executionContext: ExecutionContext): boolean {
1092
+ const ctx = GqlExecutionContext.create(executionContext);
1093
+ const req = ctx.getContext<{ req: Request }>().req;
1094
+ // Verify JWT and populate req.user — throw UnauthorizedException if invalid
1095
+ (req as Request & { user?: unknown }).user = verifyToken(
1096
+ req.headers['authorization'],
1097
+ );
1098
+ return true;
1099
+ }
1100
+ }
1101
+
1102
+ // my.resolver.ts — read req.user from the GraphQL context
1103
+ import { Resolver, Query, Context } from '@nestjs/graphql';
1104
+ import { Request, Response } from 'express';
1105
+
1106
+ interface GqlContext {
1107
+ req: Request & { user?: { id: string; roles: string[] } };
1108
+ res: Response;
1109
+ loaders: Record<string, unknown>;
1110
+ }
1111
+
1112
+ @Resolver()
1113
+ export class MyResolver {
1114
+ @Query(() => String)
1115
+ whoAmI(@Context() ctx: GqlContext): string {
1116
+ return ctx.req.user?.id ?? 'anonymous';
1117
+ }
1118
+ }
1119
+ ```
1120
+
1121
+ The built-in WMS lib resolvers do not read `req.user` — they execute without any identity
1122
+ check. Use **Option A** (global `APP_GUARD`) to enforce auth on WMS resolvers.
1123
+
1124
+ ---
1125
+
1126
+ ### 13.6 Apollo Client Coexistence — Using Your Host ApolloProvider
1127
+
1128
+ If your host Next.js app already has an `<ApolloProvider>`, you can use it for WMS page
1129
+ components. The WMS page components call `useQuery`/`useMutation` from `@apollo/client/react`
1130
+ internally, which reads the **nearest** `ApolloProvider` in the React tree.
1131
+
1132
+ **Recommended approach: separate `<ApolloProvider>` wrapping WMS routes only**
1133
+
1134
+ This is the only reliably correct approach. WMS page components issue `useQuery` calls
1135
+ internally — there is no mechanism in the lib to tag those operations with a custom context
1136
+ that a split-link predicate could test. A shared client with a split link will never route
1137
+ WMS operations to the WMS endpoint.
1138
+
1139
+ Create a dedicated Apollo client pointing at the WMS API and wrap only the WMS routes with
1140
+ it:
1141
+
1142
+ ```tsx
1143
+ // apollo-clients.ts
1144
+ import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
1145
+
1146
+ export const hostApolloClient = new ApolloClient({
1147
+ link: new HttpLink({ uri: '/api/graphql' }),
1148
+ cache: new InMemoryCache(),
1149
+ });
1150
+
1151
+ export const wmsApolloClient = new ApolloClient({
1152
+ link: new HttpLink({ uri: 'http://wms-api.internal/graphql' }),
1153
+ cache: new InMemoryCache(),
1154
+ });
1155
+ ```
1156
+
1157
+ ```tsx
1158
+ // In your routing layer (e.g. _app.tsx or a layout component):
1159
+ import { ApolloProvider } from '@apollo/client/react';
1160
+ import { hostApolloClient, wmsApolloClient } from './apollo-clients';
1161
+
1162
+ function App({ Component, pageProps }: AppProps) {
1163
+ const isWmsRoute = router.pathname.startsWith('/wms');
1164
+
1165
+ return (
1166
+ <ApolloProvider client={hostApolloClient}>
1167
+ {/* host routes use hostApolloClient via context */}
1168
+
1169
+ {isWmsRoute ? (
1170
+ // WMS routes get their own provider — useQuery inside WMS components
1171
+ // picks up wmsApolloClient from the nearest ApolloProvider
1172
+ <ApolloProvider client={wmsApolloClient}>
1173
+ <Component {...pageProps} />
1174
+ </ApolloProvider>
1175
+ ) : (
1176
+ <Component {...pageProps} />
1177
+ )}
1178
+ </ApolloProvider>
1179
+ );
1180
+ }
1181
+ ```
1182
+
1183
+ **Why split-link does not work for WMS pages**: a split-link predicate runs per operation
1184
+ and tests `operation.getContext()`. WMS page components call `useQuery(SomeDocument, vars)`
1185
+ without setting any operation context. The predicate `(op) => op.getContext().client === 'wms'`
1186
+ is always `false` for WMS operations. Split-link is only viable if you fork the lib and add
1187
+ an Apollo link that injects context into every WMS query — not recommended.
1188
+
1189
+ ---
1190
+
1191
+ ### 13.7 Inserting the WMS Provider Chain into an Existing `_app.tsx`
1192
+
1193
+ The three WMS providers (`InventoryActionsProvider`, `DialogProvider`, `ModalProvider`) need
1194
+ to wrap all WMS page components. In an existing `_app.tsx`, add them inside your existing
1195
+ provider stack. **Order is significant** — `InventoryActionsProvider` must be the outermost
1196
+ of the three:
1197
+
1198
+ ```tsx
1199
+ // Existing _app.tsx — add WMS providers around the relevant subtree
1200
+ import {
1201
+ DialogProvider,
1202
+ ModalProvider,
1203
+ InventoryActionsProvider,
1204
+ } from '@rytass/wms-module-react';
1205
+
1206
+ function App({ Component, pageProps }: AppProps): ReactNode {
1207
+ return (
1208
+ <YourExistingProviders>
1209
+ {/* WMS provider chain — only needed for WMS routes */}
1210
+ <InventoryActionsProvider>
1211
+ <DialogProvider>
1212
+ <ModalProvider>
1213
+ <Component {...pageProps} />
1214
+ </ModalProvider>
1215
+ </DialogProvider>
1216
+ </InventoryActionsProvider>
1217
+ </YourExistingProviders>
1218
+ );
1219
+ }
1220
+ ```
1221
+
1222
+ **Z-index and portal considerations**: `DialogProvider` and `ModalProvider` use Mezzanine UI
1223
+ portals that render at the document body. If your host app has modals or overlays with high
1224
+ `z-index` values, WMS dialogs may appear behind them. The `mezzanine.scss` overrides in §5.2
1225
+ set `.mzn-dropdown-popper--with-portal { z-index: 1004 !important }` — adjust these values
1226
+ to fit your host app's z-index scale.
1227
+
1228
+ ---
1229
+
1230
+ ### 13.8 Using `useRoutes()` with Your Own Navigation
1231
+
1232
+ `useRoutes()` returns the full WMS navigation tree as a plain data structure. You do not need
1233
+ to use the reference `Navigation.tsx` component (which uses Mezzanine UI's `<Navigation>`).
1234
+ You can render the route tree with any navigation component:
1235
+
1236
+ ```tsx
1237
+ import { useRoutes } from '@rytass/wms-module-react';
1238
+ import { YourSidebar } from '../components/YourSidebar';
1239
+
1240
+ // The route tree has two shapes:
1241
+ // - Leaf route: { title: string; href: NextPath; children?: undefined }
1242
+ // - Group node: { title: string; href?: undefined; children: LeafRoute[] }
1243
+ // Top-level entries like "庫存查詢" are groups with no href — only their
1244
+ // children have navigable hrefs.
1245
+
1246
+ function WmsLayout({ children }: { children: ReactNode }): ReactNode {
1247
+ const routes = useRoutes();
1248
+
1249
+ const navItems = routes.flatMap((r) => {
1250
+ if (r.children) {
1251
+ // Group node — render a section header + child links
1252
+ return [
1253
+ { label: r.title, href: undefined, isGroup: true as const },
1254
+ ...r.children.map((c) => ({ label: c.title, href: c.href, isGroup: false as const })),
1255
+ ];
1256
+ }
1257
+ // Leaf route
1258
+ return [{ label: r.title, href: r.href, isGroup: false as const }];
1259
+ });
1260
+
1261
+ return (
1262
+ <div>
1263
+ <YourSidebar items={navItems} />
1264
+ <main>{children}</main>
1265
+ </div>
1266
+ );
1267
+ }
1268
+ ```
1269
+
1270
+ The lib's page components do not require the WMS navigation shell to be present — they only
1271
+ require the three providers (§13.7) and an Apollo client (§13.6). You can completely replace
1272
+ the navigation chrome with your own.
1273
+
1274
+ ---
1275
+
1276
+ ### 13.9 i18n: Current Limitations and Workarounds
1277
+
1278
+ **Status**: No i18n support in 0.1.x. All user-visible strings in the lib are hardcoded
1279
+ Traditional Chinese (zh-TW).
1280
+
1281
+ Affected areas:
1282
+ - `useRoutes()` — route titles (`庫存查詢`, `載具管理`, etc.)
1283
+ - Internal button labels, form field labels, table column headers (all inside component source)
1284
+ - Status display utilities (`changeTypeName`, `shipInventoryOrderStatusName`, etc.) — these
1285
+ functions are **not** barrel-exported and cannot be replaced
1286
+
1287
+ **Workaround for route titles** (the only part accessible from outside the lib):
1288
+
1289
+ Mirror `useRoutes()` output and replace titles:
1290
+
1291
+ ```ts
1292
+ import { useRoutes, NextPath } from '@rytass/wms-module-react';
1293
+
1294
+ const TITLE_MAP: Partial<Record<NextPath, string>> = {
1295
+ [NextPath.MATERIALSMANAGEMENT]: 'Materials',
1296
+ [NextPath.LOADERSMANAGEMENT]: 'Loaders',
1297
+ // ...
1298
+ };
1299
+
1300
+ function useLocalizedRoutes() {
1301
+ const routes = useRoutes();
1302
+ return routes.map((r) => ({
1303
+ ...r,
1304
+ title: (r.href && TITLE_MAP[r.href]) ?? r.title,
1305
+ children: r.children?.map((c) => ({
1306
+ ...c,
1307
+ title: TITLE_MAP[c.href] ?? c.title,
1308
+ })),
1309
+ }));
1310
+ }
1311
+ ```
1312
+
1313
+ > **Limitation**: the `TITLE_MAP` approach can only override leaf routes that have an `href`
1314
+ > (e.g. `NextPath.MATERIALSMANAGEMENT`). Top-level navigation group labels such as `庫存查詢`
1315
+ > and `主檔管理` have no `href` and therefore cannot be keyed in `TITLE_MAP` — they remain
1316
+ > hardcoded in zh-TW. If you need to localize group titles, you must fork the lib.
1317
+
1318
+ For component-internal strings (form labels, buttons, column headers), there is currently no
1319
+ override mechanism. The only path is to fork the lib. This is a known gap targeted for 0.2.x.
1320
+
1321
+ ---
1322
+
1323
+ ### 13.10 Embedded / Sectional Mode Limitations
1324
+
1325
+ WMS page components (`MaterialsManagement`, `InventoryList`, etc.) are designed for
1326
+ **full-viewport rendering**. They use `PageWrapper` and `SectionWrapper` internally which
1327
+ set `width: 100%` and `height: 100%` of the viewport.
1328
+
1329
+ **Embedding in a tab, panel, or drawer is not officially supported in 0.1.x.** Known issues
1330
+ when embedded in a constrained container:
1331
+ - Mezzanine Table column-width calculations use the viewport width
1332
+ - Modal/dialog portals render at document body, not relative to the container
1333
+ - React Flow (used by Maps and LocationsManagement map tab) requires a fixed-height container
1334
+
1335
+ There is no clean workaround in 0.1.x. If you need sectional embedding, fork the lib and
1336
+ modify the `PageWrapper`/`SectionWrapper` styles to be container-relative.
1337
+
1338
+ ---
1339
+
1340
+ ### 13.11 App Router: Why it is Incompatible
1341
+
1342
+ **Status**: Not supported. The WMS frontend lib targets **Next.js Pages Router only**.
1343
+
1344
+ The incompatibility has three root causes that compound each other:
1345
+
1346
+ 1. **Client-only context providers**: `InventoryActionsProvider`, `DialogProvider`,
1347
+ `ModalProvider` use `useState`/`useContext` with browser-only side effects. In App Router,
1348
+ all Server Components are rendered on the server, and these providers would need to be
1349
+ wrapped in a `'use client'` boundary — but their children (the page components) also
1350
+ transitively import many client-only hooks.
1351
+
1352
+ 2. **`nuqs` adapter**: `useTablePageState(true)` and `PageTabs` (internal) use `nuqs` for
1353
+ URL query-string state sync. `nuqs` requires the `NuqsAdapter` from the Pages Router
1354
+ adapter (`import { NuqsAdapter } from 'nuqs/adapters/next/pages'`) — the lib was built
1355
+ and tested against this adapter.
1356
+
1357
+ 3. **`ssr: false` dynamic loading**: The reference `AuthGuard` wraps the entire layout with
1358
+ `dynamic(..., { ssr: false })` to prevent SSR hydration mismatches from the Mezzanine UI
1359
+ portal refs and Apollo client subscriptions. In App Router, `next/dynamic` with `ssr: false`
1360
+ still works in Client Components but eliminates the benefits of Server Components.
1361
+
1362
+ **Features that become unavailable for WMS routes if you attempt App Router:**
1363
+ - `getStaticProps` / ISR revalidate (`revalidate` option) — not available for pages rendered
1364
+ by `ssr: false` dynamic import
1365
+ - Streaming SSR for WMS routes
1366
+ - React Server Component data fetching
1367
+ - Partial Prerendering (PPR) for WMS pages
1368
+
1369
+ **Unofficial sketch workaround** (untested, unsupported):
1370
+
1371
+ If you are building an App Router project and need to embed WMS pages, the only viable
1372
+ approach would be:
1373
+
1374
+ ```tsx
1375
+ // app/wms/[...path]/page.tsx
1376
+ 'use client';
1377
+
1378
+ import dynamic from 'next/dynamic';
1379
+
1380
+ const WmsPageLoader = dynamic(() => import('../../../components/WmsPageLoader'), {
1381
+ ssr: false,
1382
+ });
1383
+
1384
+ export default function WmsPage() {
1385
+ return <WmsPageLoader />;
1386
+ }
1387
+ ```
1388
+
1389
+ ```tsx
1390
+ // components/WmsPageLoader.tsx
1391
+ 'use client';
1392
+ import CalendarConfigProviderDayjs from '@mezzanine-ui/react/Calendar/CalendarConfigProviderDayjs';
1393
+ import { ApolloProvider } from '@apollo/client/react';
1394
+ // Use nuqs/adapters/next (combined adapter), NOT nuqs/adapters/next/pages.
1395
+ // nuqs/adapters/next/pages uses next/router internally, which throws
1396
+ // "Error: NextRouter was not mounted." hard crash in App Router context.
1397
+ import { NuqsAdapter } from 'nuqs/adapters/next';
1398
+ import {
1399
+ ApolloClient,
1400
+ InventoryActionsProvider,
1401
+ DialogProvider,
1402
+ ModalProvider,
1403
+ } from '@rytass/wms-module-react';
1404
+ // ... route the path segment to the right page component
1405
+
1406
+ export default function WmsPageLoader() {
1407
+ // render the right WMS page component based on current route
1408
+ return (
1409
+ <CalendarConfigProviderDayjs>
1410
+ <ApolloProvider client={ApolloClient}>
1411
+ <NuqsAdapter>
1412
+ <InventoryActionsProvider>
1413
+ <DialogProvider>
1414
+ <ModalProvider>
1415
+ {/* WMS page component here */}
1416
+ </ModalProvider>
1417
+ </DialogProvider>
1418
+ </InventoryActionsProvider>
1419
+ </NuqsAdapter>
1420
+ </ApolloProvider>
1421
+ </CalendarConfigProviderDayjs>
1422
+ );
1423
+ }
1424
+ ```
1425
+
1426
+ > **Warning**: This is **officially unsupported and untested**. Even with the correct
1427
+ > `nuqs/adapters/next` combined adapter, the runtime behaviour of `useQueryState` inside lib
1428
+ > components may be incorrect because the lib was built and tested exclusively against the
1429
+ > Pages Router adapter. Use Pages Router for all WMS pages.
1430
+
1431
+ Use Pages Router for WMS pages.
1432
+
1433
+ ---
1434
+
1435
+ ### 13.12 Vite and Other Bundlers
1436
+
1437
+ **Status**: Partially supported. The lib is tested against Next.js / webpack 5. Vite support
1438
+ is not officially tested.
1439
+
1440
+ **For Vite consumers:**
1441
+
1442
+ 1. The `transpilePackages` mechanism (Next.js specific) is not needed. Vite handles ESM
1443
+ natively.
1444
+ 2. Mirror the Next.js `/graphql` rewrite with a Vite proxy:
1445
+
1446
+ ```ts
1447
+ // vite.config.ts
1448
+ export default defineConfig({
1449
+ server: {
1450
+ proxy: {
1451
+ '/graphql': {
1452
+ target: 'http://localhost:7103',
1453
+ changeOrigin: true,
1454
+ },
1455
+ },
1456
+ },
1457
+ });
1458
+ ```
1459
+
1460
+ 3. The `nuqs` adapter import (`nuqs/adapters/next/pages`) is Next.js specific. WMS page
1461
+ components that use `nuqs` internally will break in a Vite + React app without a
1462
+ compatible `nuqs` adapter. As of `nuqs@2`, use `nuqs/adapters/react` but you will need
1463
+ to wrap WMS components appropriately.
1464
+ 4. `@mezzanine-ui/react` imports `lodash/compact` without a `.js` extension, which may
1465
+ require `resolve.alias` configuration in Vite.
1466
+
1467
+ No official Vite setup guide exists for 0.1.x.
1468
+
1469
+ ---
1470
+
1471
+ ### 13.13 Existing Monorepo Considerations
1472
+
1473
+ If you already have an Nx or Turborepo monorepo, you can add the WMS modules without
1474
+ creating a new monorepo. Key points:
1475
+
1476
+ **What you must replicate:**
1477
+
1478
+ - `transpilePackages` list in `next.config.ts` (§5.3) — required by any Next.js app
1479
+ consuming the ESM lib, regardless of monorepo structure
1480
+ - The `/graphql` rewrite in `next.config.ts`
1481
+ - CSS imports in `_app.tsx` (§5.1)
1482
+ - The three provider wrappers (§13.7)
1483
+
1484
+ **What you can skip:**
1485
+
1486
+ - The worktree / Nx project structure — you do not need to use Nx. The lib is a plain npm
1487
+ package.
1488
+ - The `libs/*` is-NOT-a-workspace rule — only applies to the Rytass monorepo's internal
1489
+ build pipeline. Your monorepo can import `@rytass/wms-module-react` as a regular npm dep.
1490
+ - Any `tsconfig.base.json` path aliases to lib source — those are only needed if you want
1491
+ to consume lib source directly (skipping `npm install`). Normal consumers use the published
1492
+ package.
1493
+
1494
+ **TypeScript path aliases**: if your monorepo uses path aliases that overlap with
1495
+ `@rytass/wms-module-*` package names, ensure they point to `node_modules` (the installed
1496
+ package), not local source, to avoid confusion.
1497
+
1498
+ ---
1499
+
1500
+ ### 13.14 GraphQL Type Name Conflicts
1501
+
1502
+ WMS uses common DTO and type names such as `MaterialDto`, `LocationDto`, `BatchDto`,
1503
+ `LoaderDto`, `StockStatus` (enum), `ChangeType` (enum), and others. If a host
1504
+ application's existing GraphQL schema already declares types with the same names —
1505
+ common in retail and logistics domains — running both schemas in the same NestJS process
1506
+ would fail at schema registration with:
1507
+
1508
+ ```
1509
+ Error: Schema must contain uniquely named types but contains multiple types named "MaterialDto"
1510
+ ```
1511
+
1512
+ There is no name-prefixing or namespace configuration in the lib's DTOs. This constraint
1513
+ affects **all** GraphQL types in the WMS schema: ObjectTypes, InputTypes, Enums, and any
1514
+ union types.
1515
+
1516
+ The viable options are the same as those described in §13.1:
1517
+
1518
+ 1. **Run WMS as a separate NestJS process** (recommended) — isolates schemas entirely;
1519
+ no type name collisions possible.
1520
+ 2. **Fork the lib and add a type prefix** — rename all DTO classes (e.g. `WmsMaterialDto`)
1521
+ and regenerate the React lib's codegen. Requires maintaining a fork.
1522
+ 3. **Wait for a future major release** — a type prefix configuration option is a candidate
1523
+ for a future major version that introduces namespace support.
1524
+
1525
+ This is the same fundamental single-`GraphQLModule` constraint discussed in §13.1. Running
1526
+ both APIs in the same process is not supported in 0.1.x.
1527
+
1528
+
1529
+ ## 14. Reference implementation (`wms-standalone`)
1530
+
1531
+ A complete, runnable integration sample lives at `~/Downloads/wms-standalone` on the maintainers' machines; the published equivalent is the original monorepo's `apps/api` and `apps/client`.
1532
+
1533
+ Structure recap:
1534
+
1535
+ ```
1536
+ wms-standalone/
1537
+ apps/
1538
+ api/
1539
+ src/
1540
+ main.ts 17 LOC NestJS bootstrap
1541
+ app/app.module.ts 52 LOC WmsModuleGraphqlModule.forRootAsync
1542
+ .env 6 LOC PG credentials
1543
+ package.json
1544
+ client/
1545
+ src/
1546
+ pages/
1547
+ _app.tsx 31 LOC Provider chain
1548
+ index.tsx 7 LOC Home (return null)
1549
+ <area>/index.tsx 19 x ~10 LOC <Component /> shells
1550
+ auth/
1551
+ AuthGuard.tsx 27 LOC ssr:false dynamic load
1552
+ AuthorizedAdminPage/
1553
+ index.tsx 25 LOC Provider stack
1554
+ Main.tsx 19 LOC Flex layout
1555
+ Navigation.tsx 79 LOC useRoutes() + MznNavigation
1556
+ styles/
1557
+ mezzanine.scss 51 LOC Mezzanine setup + overrides
1558
+ global.scss 12 LOC Global reset
1559
+ next.config.ts 29 LOC transpilePackages + rewrites
1560
+ scripts/
1561
+ seed.mjs 286 LOC GraphQL mutation seed
1562
+ ```
1563
+
1564
+ Totals: **~860 LOC** including the seed script, **~574 LOC** of pure integration code (no test data).
1565
+
1566
+ Of those, **only the 79-line Navigation has any non-trivial logic**. Everything else is wiring boilerplate.
1567
+
1568
+ This document and the [API Reference](./api-reference.md) are the authoritative reproduction recipe.