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