@k2works/claude-code-booster 3.6.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +42 -42
- package/bin/claude-code-booster +90 -90
- package/lib/assets/.claude/README.md +258 -239
- package/lib/assets/.claude/agent-memory/xp-programmer/MEMORY.md +6 -0
- package/lib/assets/.claude/agent-memory/xp-programmer/project_cargo_tracker.md +11 -0
- package/lib/assets/.claude/agent-memory/xp-programmer/project_ddd_patterns.md +27 -0
- package/lib/assets/.claude/agent-memory/xp-programmer/project_us07_route_assignment.md +19 -0
- package/lib/assets/.claude/scripts/generate-inception-deck.mjs +911 -911
- package/lib/assets/.claude/settings.json +11 -11
- package/lib/assets/.claude/skills/ai-agent-guidelines/SKILL.md +111 -111
- package/lib/assets/.claude/skills/analyzing-architecture/SKILL.md +83 -83
- package/lib/assets/.claude/skills/analyzing-business/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-data-model/SKILL.md +77 -77
- package/lib/assets/.claude/skills/analyzing-domain-model/SKILL.md +117 -117
- package/lib/assets/.claude/skills/analyzing-inception-deck/SKILL.md +84 -84
- package/lib/assets/.claude/skills/analyzing-non-functional/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-operation/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-requirements/SKILL.md +91 -91
- package/lib/assets/.claude/skills/analyzing-tech-stack/SKILL.md +101 -101
- package/lib/assets/.claude/skills/analyzing-test-strategy/SKILL.md +89 -89
- package/lib/assets/.claude/skills/analyzing-ui-design/SKILL.md +80 -80
- package/lib/assets/.claude/skills/analyzing-usecases/SKILL.md +72 -72
- package/lib/assets/.claude/skills/creating-adr/SKILL.md +113 -113
- package/lib/assets/.claude/skills/developing-backend/SKILL.md +100 -100
- package/lib/assets/.claude/skills/developing-frontend/SKILL.md +93 -93
- package/lib/assets/.claude/skills/developing-release/SKILL.md +120 -120
- package/lib/assets/.claude/skills/generating-bmc/SKILL.md +97 -0
- package/lib/assets/.claude/skills/generating-slides/SKILL.md +94 -94
- package/lib/assets/.claude/skills/git-commit/SKILL.md +81 -81
- package/lib/assets/.claude/skills/killing-processes/SKILL.md +44 -44
- package/lib/assets/.claude/skills/operating-backup/SKILL.md +59 -59
- package/lib/assets/.claude/skills/operating-cicd/SKILL.md +54 -54
- package/lib/assets/.claude/skills/operating-deploy/SKILL.md +67 -67
- package/lib/assets/.claude/skills/operating-docs/SKILL.md +219 -219
- package/lib/assets/.claude/skills/operating-provision/SKILL.md +77 -77
- package/lib/assets/.claude/skills/operating-setup/SKILL.md +63 -63
- package/lib/assets/.claude/skills/orchestrating-analysis/SKILL.md +104 -104
- package/lib/assets/.claude/skills/orchestrating-development/SKILL.md +27 -21
- package/lib/assets/.claude/skills/orchestrating-operation/SKILL.md +158 -158
- package/lib/assets/.claude/skills/orchestrating-project/SKILL.md +144 -144
- package/lib/assets/.claude/skills/planning-releases/SKILL.md +119 -119
- package/lib/assets/.claude/skills/syncing-github-project/SKILL.md +151 -151
- package/lib/assets/.claude/skills/tracking-progress/SKILL.md +91 -91
- package/lib/assets/.claude/skills/validating-iteration-plan/SKILL.md +215 -215
- package/lib/assets/.devcontainer/devcontainer.json +34 -34
- package/lib/assets/.env.example +17 -17
- package/lib/assets/.gitattributes +4 -4
- package/lib/assets/.github/workflows/docker-publish.yml +77 -77
- package/lib/assets/.github/workflows/mkdocs.yml +39 -39
- package/lib/assets/AGENTS.md +94 -94
- package/lib/assets/CLAUDE.md +1 -0
- package/lib/assets/README.md +254 -254
- package/lib/assets/docker-compose.yml +33 -33
- package/lib/assets/docs/adr/index.md +10 -10
- package/lib/assets/docs/article/functional-desgin-ppp/all/01-immutability-and-data-transformation.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/all/02-function-composition.md +519 -519
- package/lib/assets/docs/article/functional-desgin-ppp/all/03-polymorphism.md +537 -537
- package/lib/assets/docs/article/functional-desgin-ppp/all/04-data-validation.md +300 -300
- package/lib/assets/docs/article/functional-desgin-ppp/all/05-property-based-testing.md +320 -320
- package/lib/assets/docs/article/functional-desgin-ppp/all/06-tdd-and-functional.md +498 -498
- package/lib/assets/docs/article/functional-desgin-ppp/all/07-composite-pattern.md +298 -298
- package/lib/assets/docs/article/functional-desgin-ppp/all/08-decorator-pattern.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/all/09-adapter-pattern.md +336 -336
- package/lib/assets/docs/article/functional-desgin-ppp/all/10-strategy-pattern.md +303 -303
- package/lib/assets/docs/article/functional-desgin-ppp/all/11-command-pattern.md +286 -286
- package/lib/assets/docs/article/functional-desgin-ppp/all/12-visitor-pattern.md +322 -322
- package/lib/assets/docs/article/functional-desgin-ppp/all/13-abstract-factory-pattern.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/all/14-abstract-server-pattern.md +365 -365
- package/lib/assets/docs/article/functional-desgin-ppp/all/15-gossiping-bus-drivers.md +156 -156
- package/lib/assets/docs/article/functional-desgin-ppp/all/16-payroll-system.md +178 -178
- package/lib/assets/docs/article/functional-desgin-ppp/all/17-video-rental-system.md +312 -312
- package/lib/assets/docs/article/functional-desgin-ppp/all/18-concurrency-system.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/all/19-wa-tor-simulation.md +286 -286
- package/lib/assets/docs/article/functional-desgin-ppp/all/20-pattern-interactions.md +274 -274
- package/lib/assets/docs/article/functional-desgin-ppp/all/21-best-practices.md +294 -294
- package/lib/assets/docs/article/functional-desgin-ppp/all/22-oo-to-fp-migration.md +337 -337
- package/lib/assets/docs/article/functional-desgin-ppp/all/index.md +388 -388
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/01-immutability-and-data-transformation.md +273 -273
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/02-function-composition.md +380 -380
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/03-polymorphism.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/04-clojure-spec.md +350 -350
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/05-property-based-testing.md +352 -352
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/06-tdd-in-functional.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/07-composite-pattern.md +529 -529
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/08-decorator-pattern.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/09-adapter-pattern.md +399 -399
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/10-strategy-pattern.md +485 -485
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/11-command-pattern.md +566 -566
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/12-visitor-pattern.md +567 -567
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/13-abstract-factory-pattern.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/14-abstract-server-pattern.md +462 -462
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/15-gossiping-bus-drivers.md +325 -325
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/16-payroll-system.md +401 -401
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/17-video-rental-system.md +450 -450
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/18-concurrency-system.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/19-wator-simulation.md +739 -739
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/20-pattern-interactions.md +567 -567
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/21-best-practices.md +518 -518
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/22-oo-to-fp-migration.md +532 -532
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/index.md +241 -241
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/01-immutability-and-data-transformation.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/02-function-composition.md +374 -374
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/03-polymorphism.md +375 -375
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/04-data-validation.md +195 -195
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/05-property-based-testing.md +268 -268
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/06-tdd-and-fp.md +294 -294
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/07-effects-and-pure-functions.md +164 -164
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/08-error-handling-strategies.md +168 -168
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/09-io-and-external-systems.md +254 -254
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/10-concurrency-patterns.md +269 -269
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/11-command-pattern.md +148 -148
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/12-visitor-pattern.md +176 -176
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/13-abstract-factory-pattern.md +604 -604
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/14-abstract-server-pattern.md +729 -729
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/15-gossiping-bus-drivers.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/16-payroll-system.md +420 -420
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/17-video-rental-system.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/18-concurrency-system.md +466 -466
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/19-wator-simulation.md +523 -523
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/20-pattern-interactions.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/21-best-practices.md +340 -340
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/22-oo-to-fp-migration.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/index.md +248 -248
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/01-immutability-and-data-transformation.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/02-function-composition.md +452 -452
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/03-polymorphism.md +495 -495
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/04-data-validation.md +416 -416
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/05-property-based-testing.md +382 -382
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/06-tdd-functional.md +687 -687
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/07-composite-pattern.md +442 -442
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/08-decorator-pattern.md +479 -479
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/09-adapter-pattern.md +479 -479
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/10-strategy-pattern.md +427 -427
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/11-command-pattern.md +428 -428
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/12-visitor-pattern.md +339 -339
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/13-abstract-factory-pattern.md +309 -309
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/14-abstract-server-pattern.md +596 -596
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/15-gossiping-bus-drivers.md +355 -355
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/16-payroll-system.md +350 -350
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/17-video-rental-system.md +414 -414
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/18-concurrency-system.md +367 -367
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/19-wator-simulation.md +403 -403
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/20-pattern-interactions.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/21-best-practices.md +324 -324
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/22-oo-to-fp-migration.md +332 -332
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/index.md +274 -274
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/01-immutability-and-data-transformation.md +298 -298
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/02-function-composition.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/03-polymorphism.md +362 -362
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/04-data-validation.md +257 -257
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/05-property-based-testing.md +254 -254
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/06-tdd-functional.md +283 -283
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/07-composite-pattern.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/08-decorator-pattern.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/09-adapter-pattern.md +382 -382
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/10-strategy-pattern.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/11-command-pattern.md +303 -303
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/12-visitor-pattern.md +326 -326
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/13-abstract-factory-pattern.md +332 -332
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/14-abstract-server-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/15-gossiping-bus-drivers.md +177 -177
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/16-payroll-system.md +219 -219
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/17-video-rental-system.md +244 -244
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/18-concurrency-system.md +363 -363
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/19-wator-simulation.md +438 -438
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/20-pattern-interactions.md +325 -325
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/21-best-practices.md +403 -403
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/22-oo-to-fp-migration.md +469 -469
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/index.md +174 -174
- package/lib/assets/docs/article/functional-desgin-ppp/index.md +90 -90
- package/lib/assets/docs/article/functional-desgin-ppp/rust/01-immutability-and-data-transformation.md +450 -450
- package/lib/assets/docs/article/functional-desgin-ppp/rust/02-function-composition.md +463 -463
- package/lib/assets/docs/article/functional-desgin-ppp/rust/03-polymorphism.md +425 -425
- package/lib/assets/docs/article/functional-desgin-ppp/rust/04-data-validation.md +273 -273
- package/lib/assets/docs/article/functional-desgin-ppp/rust/05-property-based-testing.md +247 -247
- package/lib/assets/docs/article/functional-desgin-ppp/rust/06-tdd-and-functional.md +841 -841
- package/lib/assets/docs/article/functional-desgin-ppp/rust/07-composite-pattern.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/rust/08-decorator-pattern.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/rust/09-adapter-pattern.md +339 -339
- package/lib/assets/docs/article/functional-desgin-ppp/rust/10-strategy-pattern.md +331 -331
- package/lib/assets/docs/article/functional-desgin-ppp/rust/11-command-pattern.md +356 -356
- package/lib/assets/docs/article/functional-desgin-ppp/rust/12-visitor-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/rust/13-abstract-factory-pattern.md +361 -361
- package/lib/assets/docs/article/functional-desgin-ppp/rust/14-abstract-server-pattern.md +392 -392
- package/lib/assets/docs/article/functional-desgin-ppp/rust/15-gossiping-bus-drivers.md +300 -300
- package/lib/assets/docs/article/functional-desgin-ppp/rust/16-payroll-system.md +297 -297
- package/lib/assets/docs/article/functional-desgin-ppp/rust/17-video-rental-system.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/rust/18-concurrency-system.md +315 -315
- package/lib/assets/docs/article/functional-desgin-ppp/rust/19-wator-simulation.md +311 -311
- package/lib/assets/docs/article/functional-desgin-ppp/rust/20-pattern-interactions.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/rust/21-best-practices.md +336 -336
- package/lib/assets/docs/article/functional-desgin-ppp/rust/22-oo-to-fp-migration.md +349 -349
- package/lib/assets/docs/article/functional-desgin-ppp/rust/index.md +243 -243
- package/lib/assets/docs/article/functional-desgin-ppp/scala/01-immutability-and-data-transformation.md +328 -328
- package/lib/assets/docs/article/functional-desgin-ppp/scala/02-function-composition.md +348 -348
- package/lib/assets/docs/article/functional-desgin-ppp/scala/03-polymorphism.md +357 -357
- package/lib/assets/docs/article/functional-desgin-ppp/scala/04-data-validation.md +364 -364
- package/lib/assets/docs/article/functional-desgin-ppp/scala/05-property-based-testing.md +515 -515
- package/lib/assets/docs/article/functional-desgin-ppp/scala/06-tdd-functional.md +557 -557
- package/lib/assets/docs/article/functional-desgin-ppp/scala/07-composite-pattern.md +363 -363
- package/lib/assets/docs/article/functional-desgin-ppp/scala/08-decorator-pattern.md +327 -327
- package/lib/assets/docs/article/functional-desgin-ppp/scala/09-adapter-pattern.md +517 -517
- package/lib/assets/docs/article/functional-desgin-ppp/scala/10-strategy-pattern.md +441 -441
- package/lib/assets/docs/article/functional-desgin-ppp/scala/11-command-pattern.md +407 -407
- package/lib/assets/docs/article/functional-desgin-ppp/scala/12-visitor-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/scala/13-abstract-factory-pattern.md +398 -398
- package/lib/assets/docs/article/functional-desgin-ppp/scala/14-abstract-server-pattern.md +476 -476
- package/lib/assets/docs/article/functional-desgin-ppp/scala/15-gossiping-bus-drivers.md +391 -391
- package/lib/assets/docs/article/functional-desgin-ppp/scala/16-payroll-system.md +342 -342
- package/lib/assets/docs/article/functional-desgin-ppp/scala/17-video-rental-system.md +324 -324
- package/lib/assets/docs/article/functional-desgin-ppp/scala/18-concurrency-system.md +730 -730
- package/lib/assets/docs/article/functional-desgin-ppp/scala/19-wator-simulation.md +624 -624
- package/lib/assets/docs/article/functional-desgin-ppp/scala/20-pattern-interactions.md +512 -512
- package/lib/assets/docs/article/functional-desgin-ppp/scala/21-best-practices.md +433 -433
- package/lib/assets/docs/article/functional-desgin-ppp/scala/22-oo-to-fp-migration.md +688 -688
- package/lib/assets/docs/article/functional-desgin-ppp/scala/index.md +243 -243
- package/lib/assets/docs/article/getting-start-tdd/clojure/01-todo-list-and-first-test.md +166 -166
- package/lib/assets/docs/article/getting-start-tdd/clojure/02-fake-it-and-triangulation.md +162 -162
- package/lib/assets/docs/article/getting-start-tdd/clojure/03-obvious-implementation-and-refactoring.md +135 -135
- package/lib/assets/docs/article/getting-start-tdd/clojure/04-version-control-and-conventional-commits.md +88 -88
- package/lib/assets/docs/article/getting-start-tdd/clojure/05-package-management-and-static-analysis.md +299 -299
- package/lib/assets/docs/article/getting-start-tdd/clojure/06-task-runner-and-ci-cd.md +241 -241
- package/lib/assets/docs/article/getting-start-tdd/clojure/07-protocols-and-records.md +131 -131
- package/lib/assets/docs/article/getting-start-tdd/clojure/08-multimethods-and-design-patterns.md +130 -130
- package/lib/assets/docs/article/getting-start-tdd/clojure/09-namespaces-and-module-design.md +127 -127
- package/lib/assets/docs/article/getting-start-tdd/clojure/10-higher-order-functions-and-composition.md +114 -114
- package/lib/assets/docs/article/getting-start-tdd/clojure/11-persistent-data-and-pipeline.md +138 -138
- package/lib/assets/docs/article/getting-start-tdd/clojure/12-error-handling-and-spec.md +161 -161
- package/lib/assets/docs/article/getting-start-tdd/clojure/index.md +65 -65
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter01.md +232 -232
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter02.md +244 -244
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter03.md +202 -202
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter04.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter05.md +256 -256
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter06.md +195 -195
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter07.md +214 -214
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter08.md +249 -249
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter09.md +174 -174
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter10.md +166 -166
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter11.md +192 -192
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter12.md +211 -211
- package/lib/assets/docs/article/getting-start-tdd/csharp/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/elixir/01-todo-list-and-first-test.md +87 -87
- package/lib/assets/docs/article/getting-start-tdd/elixir/02-fake-it-and-triangulation.md +95 -95
- package/lib/assets/docs/article/getting-start-tdd/elixir/03-obvious-implementation-and-refactoring.md +109 -109
- package/lib/assets/docs/article/getting-start-tdd/elixir/04-version-control-and-conventional-commits.md +96 -96
- package/lib/assets/docs/article/getting-start-tdd/elixir/05-package-management-and-static-analysis.md +88 -88
- package/lib/assets/docs/article/getting-start-tdd/elixir/06-task-runner-and-ci-cd.md +71 -71
- package/lib/assets/docs/article/getting-start-tdd/elixir/07-structs-and-protocols.md +110 -110
- package/lib/assets/docs/article/getting-start-tdd/elixir/08-pattern-matching-and-guards.md +108 -108
- package/lib/assets/docs/article/getting-start-tdd/elixir/09-module-design-and-behaviours.md +104 -104
- package/lib/assets/docs/article/getting-start-tdd/elixir/10-higher-order-functions-and-pipeline.md +178 -178
- package/lib/assets/docs/article/getting-start-tdd/elixir/11-stream-and-lazy-evaluation.md +142 -142
- package/lib/assets/docs/article/getting-start-tdd/elixir/12-error-handling-and-with.md +145 -145
- package/lib/assets/docs/article/getting-start-tdd/elixir/index.md +35 -35
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter01.md +202 -202
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter02.md +246 -246
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter03.md +218 -218
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter04.md +179 -179
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter05.md +267 -267
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter06.md +190 -190
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter07.md +161 -161
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter08.md +175 -175
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter09.md +222 -222
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter10.md +189 -189
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter11.md +212 -212
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter12.md +215 -215
- package/lib/assets/docs/article/getting-start-tdd/fsharp/index.md +71 -71
- package/lib/assets/docs/article/getting-start-tdd/go/01-todo-list-and-first-test.md +213 -213
- package/lib/assets/docs/article/getting-start-tdd/go/02-fake-it-and-triangulation.md +302 -302
- package/lib/assets/docs/article/getting-start-tdd/go/03-obvious-implementation-and-refactoring.md +339 -339
- package/lib/assets/docs/article/getting-start-tdd/go/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/go/05-package-management-and-static-analysis.md +272 -272
- package/lib/assets/docs/article/getting-start-tdd/go/06-task-runner-and-ci-cd.md +233 -233
- package/lib/assets/docs/article/getting-start-tdd/go/07-encapsulation-and-polymorphism.md +394 -394
- package/lib/assets/docs/article/getting-start-tdd/go/08-design-patterns.md +422 -422
- package/lib/assets/docs/article/getting-start-tdd/go/09-solid-principles-and-module-design.md +400 -400
- package/lib/assets/docs/article/getting-start-tdd/go/10-higher-order-functions-and-composition.md +226 -226
- package/lib/assets/docs/article/getting-start-tdd/go/11-immutable-data-and-pipeline.md +296 -296
- package/lib/assets/docs/article/getting-start-tdd/go/12-error-handling-and-type-safety.md +411 -411
- package/lib/assets/docs/article/getting-start-tdd/go/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/haskell/01-todo-list-and-first-test.md +279 -279
- package/lib/assets/docs/article/getting-start-tdd/haskell/02-fake-it-and-triangulation.md +337 -337
- package/lib/assets/docs/article/getting-start-tdd/haskell/03-obvious-implementation-and-refactoring.md +257 -257
- package/lib/assets/docs/article/getting-start-tdd/haskell/04-version-control-and-conventional-commits.md +182 -182
- package/lib/assets/docs/article/getting-start-tdd/haskell/05-package-management-and-static-analysis.md +313 -313
- package/lib/assets/docs/article/getting-start-tdd/haskell/06-task-runner-and-ci-cd.md +309 -309
- package/lib/assets/docs/article/getting-start-tdd/haskell/07-algebraic-data-types-and-type-classes.md +412 -412
- package/lib/assets/docs/article/getting-start-tdd/haskell/08-pattern-matching-and-guards.md +390 -390
- package/lib/assets/docs/article/getting-start-tdd/haskell/09-module-design-and-smart-constructors.md +461 -461
- package/lib/assets/docs/article/getting-start-tdd/haskell/10-higher-order-functions-and-currying.md +434 -434
- package/lib/assets/docs/article/getting-start-tdd/haskell/11-function-composition-and-point-free.md +392 -392
- package/lib/assets/docs/article/getting-start-tdd/haskell/12-monad-and-error-handling.md +631 -631
- package/lib/assets/docs/article/getting-start-tdd/haskell/index.md +49 -49
- package/lib/assets/docs/article/getting-start-tdd/index.md +93 -93
- package/lib/assets/docs/article/getting-start-tdd/integration/01-language-overview.md +375 -375
- package/lib/assets/docs/article/getting-start-tdd/integration/02-test-framework-comparison.md +349 -349
- package/lib/assets/docs/article/getting-start-tdd/integration/03-tdd-pattern-comparison.md +445 -445
- package/lib/assets/docs/article/getting-start-tdd/integration/04-type-system-comparison.md +409 -409
- package/lib/assets/docs/article/getting-start-tdd/integration/05-dev-environment-comparison.md +330 -330
- package/lib/assets/docs/article/getting-start-tdd/integration/06-learning-roadmap.md +290 -290
- package/lib/assets/docs/article/getting-start-tdd/integration/index.md +69 -69
- package/lib/assets/docs/article/getting-start-tdd/java/01-todo-list-and-first-test.md +234 -234
- package/lib/assets/docs/article/getting-start-tdd/java/02-fake-it-and-triangulation.md +261 -261
- package/lib/assets/docs/article/getting-start-tdd/java/03-obvious-implementation-and-refactoring.md +185 -185
- package/lib/assets/docs/article/getting-start-tdd/java/04-version-control-and-conventional-commits.md +115 -115
- package/lib/assets/docs/article/getting-start-tdd/java/05-package-management-and-static-analysis.md +382 -382
- package/lib/assets/docs/article/getting-start-tdd/java/06-task-runner-and-ci-cd.md +272 -272
- package/lib/assets/docs/article/getting-start-tdd/java/07-encapsulation-and-polymorphism.md +626 -626
- package/lib/assets/docs/article/getting-start-tdd/java/08-design-patterns.md +393 -393
- package/lib/assets/docs/article/getting-start-tdd/java/09-solid-principles-and-module-design.md +310 -310
- package/lib/assets/docs/article/getting-start-tdd/java/10-higher-order-functions-and-composition.md +188 -188
- package/lib/assets/docs/article/getting-start-tdd/java/11-immutable-data-and-pipeline.md +167 -167
- package/lib/assets/docs/article/getting-start-tdd/java/12-error-handling-and-type-safety.md +205 -205
- package/lib/assets/docs/article/getting-start-tdd/java/index.md +61 -61
- package/lib/assets/docs/article/getting-start-tdd/node/01-todo-list-and-first-test.md +244 -244
- package/lib/assets/docs/article/getting-start-tdd/node/02-fake-it-and-triangulation.md +262 -262
- package/lib/assets/docs/article/getting-start-tdd/node/03-obvious-implementation-and-refactoring.md +169 -169
- package/lib/assets/docs/article/getting-start-tdd/node/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/node/05-package-management-and-static-analysis.md +314 -314
- package/lib/assets/docs/article/getting-start-tdd/node/06-task-runner-and-ci-cd.md +235 -235
- package/lib/assets/docs/article/getting-start-tdd/node/07-encapsulation-and-polymorphism.md +327 -327
- package/lib/assets/docs/article/getting-start-tdd/node/08-design-patterns.md +322 -322
- package/lib/assets/docs/article/getting-start-tdd/node/09-solid-principles-and-module-design.md +285 -285
- package/lib/assets/docs/article/getting-start-tdd/node/10-higher-order-functions-and-composition.md +199 -199
- package/lib/assets/docs/article/getting-start-tdd/node/11-immutable-data-and-pipeline.md +207 -207
- package/lib/assets/docs/article/getting-start-tdd/node/12-error-handling-and-type-safety.md +295 -295
- package/lib/assets/docs/article/getting-start-tdd/node/index.md +56 -56
- package/lib/assets/docs/article/getting-start-tdd/php/01-todo-list-and-first-test.md +259 -259
- package/lib/assets/docs/article/getting-start-tdd/php/02-fake-it-and-triangulation.md +200 -200
- package/lib/assets/docs/article/getting-start-tdd/php/03-obvious-implementation-and-refactoring.md +248 -248
- package/lib/assets/docs/article/getting-start-tdd/php/04-version-control-and-conventional-commits.md +141 -141
- package/lib/assets/docs/article/getting-start-tdd/php/05-package-management-and-static-analysis.md +410 -410
- package/lib/assets/docs/article/getting-start-tdd/php/06-task-runner-and-ci-cd.md +321 -321
- package/lib/assets/docs/article/getting-start-tdd/php/07-encapsulation-and-polymorphism.md +372 -372
- package/lib/assets/docs/article/getting-start-tdd/php/08-design-patterns.md +453 -453
- package/lib/assets/docs/article/getting-start-tdd/php/09-solid-principles-and-module-design.md +460 -460
- package/lib/assets/docs/article/getting-start-tdd/php/10-higher-order-functions-and-composition.md +182 -182
- package/lib/assets/docs/article/getting-start-tdd/php/11-immutable-data-and-pipeline.md +266 -266
- package/lib/assets/docs/article/getting-start-tdd/php/12-error-handling-and-type-safety.md +308 -308
- package/lib/assets/docs/article/getting-start-tdd/php/index.md +84 -84
- package/lib/assets/docs/article/getting-start-tdd/python/01-todo-list-and-first-test.md +201 -201
- package/lib/assets/docs/article/getting-start-tdd/python/02-fake-it-and-triangulation.md +247 -247
- package/lib/assets/docs/article/getting-start-tdd/python/03-obvious-implementation-and-refactoring.md +199 -199
- package/lib/assets/docs/article/getting-start-tdd/python/04-version-control-and-conventional-commits.md +87 -87
- package/lib/assets/docs/article/getting-start-tdd/python/05-package-management-and-static-analysis.md +274 -274
- package/lib/assets/docs/article/getting-start-tdd/python/06-task-runner-and-ci-cd.md +190 -190
- package/lib/assets/docs/article/getting-start-tdd/python/07-encapsulation-and-polymorphism.md +208 -208
- package/lib/assets/docs/article/getting-start-tdd/python/08-design-patterns.md +172 -172
- package/lib/assets/docs/article/getting-start-tdd/python/09-solid-principles-and-module-design.md +130 -130
- package/lib/assets/docs/article/getting-start-tdd/python/10-higher-order-functions-and-composition.md +122 -122
- package/lib/assets/docs/article/getting-start-tdd/python/11-immutable-data-and-pipeline.md +116 -116
- package/lib/assets/docs/article/getting-start-tdd/python/12-error-handling-and-type-safety.md +126 -126
- package/lib/assets/docs/article/getting-start-tdd/python/index.md +55 -55
- package/lib/assets/docs/article/getting-start-tdd/ruby/01-todo-list-and-first-test.md +231 -231
- package/lib/assets/docs/article/getting-start-tdd/ruby/02-fake-it-and-triangulation.md +238 -238
- package/lib/assets/docs/article/getting-start-tdd/ruby/03-obvious-implementation-and-refactoring.md +228 -228
- package/lib/assets/docs/article/getting-start-tdd/ruby/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/ruby/05-package-management-and-static-analysis.md +287 -287
- package/lib/assets/docs/article/getting-start-tdd/ruby/06-task-runner-and-ci-cd.md +248 -248
- package/lib/assets/docs/article/getting-start-tdd/ruby/07-encapsulation-and-polymorphism.md +279 -279
- package/lib/assets/docs/article/getting-start-tdd/ruby/08-design-patterns.md +329 -329
- package/lib/assets/docs/article/getting-start-tdd/ruby/09-solid-principles-and-module-design.md +196 -196
- package/lib/assets/docs/article/getting-start-tdd/ruby/10-higher-order-functions-and-composition.md +175 -175
- package/lib/assets/docs/article/getting-start-tdd/ruby/11-immutable-data-and-pipeline.md +237 -237
- package/lib/assets/docs/article/getting-start-tdd/ruby/12-error-handling-and-type-safety.md +398 -398
- package/lib/assets/docs/article/getting-start-tdd/ruby/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/rust/01-todo-list-and-first-test.md +211 -211
- package/lib/assets/docs/article/getting-start-tdd/rust/02-fake-it-and-triangulation.md +264 -264
- package/lib/assets/docs/article/getting-start-tdd/rust/03-obvious-implementation-and-refactoring.md +233 -233
- package/lib/assets/docs/article/getting-start-tdd/rust/04-version-control-and-conventional-commits.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/rust/05-package-management-and-static-analysis.md +212 -212
- package/lib/assets/docs/article/getting-start-tdd/rust/06-task-runner-and-ci-cd.md +164 -164
- package/lib/assets/docs/article/getting-start-tdd/rust/07-encapsulation-and-polymorphism.md +142 -142
- package/lib/assets/docs/article/getting-start-tdd/rust/08-design-patterns.md +145 -145
- package/lib/assets/docs/article/getting-start-tdd/rust/09-solid-principles-and-module-design.md +110 -110
- package/lib/assets/docs/article/getting-start-tdd/rust/10-higher-order-functions-and-composition.md +94 -94
- package/lib/assets/docs/article/getting-start-tdd/rust/11-immutable-data-and-pipeline.md +105 -105
- package/lib/assets/docs/article/getting-start-tdd/rust/12-error-handling-and-type-safety.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/rust/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/scala/01-todo-list-and-first-test.md +111 -111
- package/lib/assets/docs/article/getting-start-tdd/scala/02-fake-it-and-triangulation.md +107 -107
- package/lib/assets/docs/article/getting-start-tdd/scala/03-obvious-implementation-and-refactoring.md +99 -99
- package/lib/assets/docs/article/getting-start-tdd/scala/04-version-control-and-conventional-commits.md +123 -123
- package/lib/assets/docs/article/getting-start-tdd/scala/05-package-management-and-static-analysis.md +196 -196
- package/lib/assets/docs/article/getting-start-tdd/scala/06-task-runner-and-ci-cd.md +186 -186
- package/lib/assets/docs/article/getting-start-tdd/scala/07-case-classes-and-traits.md +139 -139
- package/lib/assets/docs/article/getting-start-tdd/scala/08-pattern-matching-and-sealed-traits.md +106 -106
- package/lib/assets/docs/article/getting-start-tdd/scala/09-packages-and-module-design.md +75 -75
- package/lib/assets/docs/article/getting-start-tdd/scala/10-higher-order-functions-and-composition.md +104 -104
- package/lib/assets/docs/article/getting-start-tdd/scala/11-collections-and-lazy-evaluation.md +94 -94
- package/lib/assets/docs/article/getting-start-tdd/scala/12-error-handling-and-type-safety.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/scala/index.md +65 -65
- package/lib/assets/docs/article/grokking-concurrency/all/index.md +404 -404
- package/lib/assets/docs/article/grokking-concurrency/all/part-1-ch02-sequential.md +554 -554
- package/lib/assets/docs/article/grokking-concurrency/all/part-2-ch04-05-threads.md +469 -469
- package/lib/assets/docs/article/grokking-concurrency/all/part-3-ch06-multitasking.md +520 -520
- package/lib/assets/docs/article/grokking-concurrency/all/part-4-ch07-parallel-patterns.md +420 -420
- package/lib/assets/docs/article/grokking-concurrency/all/part-5-ch08-09-synchronization.md +510 -510
- package/lib/assets/docs/article/grokking-concurrency/all/part-6-ch10-11-nonblocking-io.md +435 -435
- package/lib/assets/docs/article/grokking-concurrency/all/part-7-ch12-async.md +465 -465
- package/lib/assets/docs/article/grokking-concurrency/all/part-8-ch13-mapreduce.md +377 -377
- package/lib/assets/docs/article/grokking-concurrency/clojure/index.md +116 -116
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-1.md +108 -108
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-2.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-3.md +122 -122
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-4.md +123 -123
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-5.md +118 -118
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-6.md +89 -89
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-7.md +100 -100
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-8.md +120 -120
- package/lib/assets/docs/article/grokking-concurrency/csharp/index.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-1.md +97 -97
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-2.md +123 -123
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-3.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-4.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-5.md +99 -99
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-6.md +61 -61
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-7.md +84 -84
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-8.md +92 -92
- package/lib/assets/docs/article/grokking-concurrency/fsharp/index.md +65 -65
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-1.md +80 -80
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-2.md +103 -103
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-3.md +94 -94
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-4.md +110 -110
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-5.md +104 -104
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-6.md +93 -93
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-7.md +121 -121
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-8.md +107 -107
- package/lib/assets/docs/article/grokking-concurrency/haskell/index.md +248 -248
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-1.md +96 -96
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-2.md +96 -96
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-3.md +91 -91
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-4.md +106 -106
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-5.md +99 -99
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-6.md +95 -95
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-7.md +111 -111
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-8.md +118 -118
- package/lib/assets/docs/article/grokking-concurrency/index.md +66 -66
- package/lib/assets/docs/article/grokking-concurrency/java/index.md +102 -102
- package/lib/assets/docs/article/grokking-concurrency/java/part-1.md +308 -308
- package/lib/assets/docs/article/grokking-concurrency/java/part-2.md +334 -334
- package/lib/assets/docs/article/grokking-concurrency/java/part-3.md +221 -221
- package/lib/assets/docs/article/grokking-concurrency/java/part-4.md +213 -213
- package/lib/assets/docs/article/grokking-concurrency/java/part-5.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/java/part-6.md +69 -69
- package/lib/assets/docs/article/grokking-concurrency/java/part-7.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/java/part-8.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/python/index.md +313 -313
- package/lib/assets/docs/article/grokking-concurrency/python/part-1.md +239 -239
- package/lib/assets/docs/article/grokking-concurrency/python/part-2.md +418 -418
- package/lib/assets/docs/article/grokking-concurrency/python/part-3.md +227 -227
- package/lib/assets/docs/article/grokking-concurrency/python/part-4.md +299 -299
- package/lib/assets/docs/article/grokking-concurrency/python/part-5.md +315 -315
- package/lib/assets/docs/article/grokking-concurrency/python/part-6.md +297 -297
- package/lib/assets/docs/article/grokking-concurrency/python/part-7.md +314 -314
- package/lib/assets/docs/article/grokking-concurrency/python/part-8.md +360 -360
- package/lib/assets/docs/article/grokking-concurrency/rust/index.md +270 -270
- package/lib/assets/docs/article/grokking-concurrency/rust/part-1.md +108 -108
- package/lib/assets/docs/article/grokking-concurrency/rust/part-2.md +120 -120
- package/lib/assets/docs/article/grokking-concurrency/rust/part-3.md +126 -126
- package/lib/assets/docs/article/grokking-concurrency/rust/part-4.md +175 -175
- package/lib/assets/docs/article/grokking-concurrency/rust/part-5.md +158 -158
- package/lib/assets/docs/article/grokking-concurrency/rust/part-6.md +94 -94
- package/lib/assets/docs/article/grokking-concurrency/rust/part-7.md +133 -133
- package/lib/assets/docs/article/grokking-concurrency/rust/part-8.md +155 -155
- package/lib/assets/docs/article/grokking-concurrency/scala/index.md +69 -69
- package/lib/assets/docs/article/grokking-concurrency/scala/part-1.md +78 -78
- package/lib/assets/docs/article/grokking-concurrency/scala/part-2.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/scala/part-3.md +93 -93
- package/lib/assets/docs/article/grokking-concurrency/scala/part-4.md +110 -110
- package/lib/assets/docs/article/grokking-concurrency/scala/part-5.md +119 -119
- package/lib/assets/docs/article/grokking-concurrency/scala/part-6.md +83 -83
- package/lib/assets/docs/article/grokking-concurrency/scala/part-7.md +131 -131
- package/lib/assets/docs/article/grokking-concurrency/scala/part-8.md +129 -129
- package/lib/assets/docs/article/grokkingfp/all/index.md +368 -368
- package/lib/assets/docs/article/grokkingfp/all/part-1-ch01-fp-introduction.md +530 -530
- package/lib/assets/docs/article/grokkingfp/all/part-1-ch02-pure-functions.md +923 -923
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch03-immutable-data.md +1128 -1128
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch04-higher-order-functions.md +1104 -1104
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch05-flatmap.md +1026 -1026
- package/lib/assets/docs/article/grokkingfp/all/part-3-ch06-option.md +785 -785
- package/lib/assets/docs/article/grokkingfp/all/part-3-ch07-either-adt.md +871 -871
- package/lib/assets/docs/article/grokkingfp/all/part-4-ch08-io-monad.md +972 -972
- package/lib/assets/docs/article/grokkingfp/all/part-4-ch09-streams.md +926 -926
- package/lib/assets/docs/article/grokkingfp/all/part-5-ch10-concurrency.md +870 -870
- package/lib/assets/docs/article/grokkingfp/all/part-6-ch11-application.md +715 -715
- package/lib/assets/docs/article/grokkingfp/all/part-6-ch12-testing.md +626 -626
- package/lib/assets/docs/article/grokkingfp/all/writing-plan.md +712 -712
- package/lib/assets/docs/article/grokkingfp/clojure/index.md +276 -276
- package/lib/assets/docs/article/grokkingfp/clojure/part-1.md +667 -667
- package/lib/assets/docs/article/grokkingfp/clojure/part-2.md +643 -643
- package/lib/assets/docs/article/grokkingfp/clojure/part-3.md +620 -620
- package/lib/assets/docs/article/grokkingfp/clojure/part-4.md +697 -697
- package/lib/assets/docs/article/grokkingfp/clojure/part-5.md +751 -751
- package/lib/assets/docs/article/grokkingfp/clojure/part-6.md +721 -721
- package/lib/assets/docs/article/grokkingfp/csharp/index.md +246 -246
- package/lib/assets/docs/article/grokkingfp/csharp/part-1.md +811 -811
- package/lib/assets/docs/article/grokkingfp/csharp/part-2.md +971 -971
- package/lib/assets/docs/article/grokkingfp/csharp/part-3.md +981 -981
- package/lib/assets/docs/article/grokkingfp/csharp/part-4.md +949 -949
- package/lib/assets/docs/article/grokkingfp/csharp/part-5.md +947 -947
- package/lib/assets/docs/article/grokkingfp/csharp/part-6.md +739 -739
- package/lib/assets/docs/article/grokkingfp/elixir/index.md +203 -203
- package/lib/assets/docs/article/grokkingfp/elixir/part-1.md +712 -712
- package/lib/assets/docs/article/grokkingfp/elixir/part-2.md +838 -838
- package/lib/assets/docs/article/grokkingfp/elixir/part-3.md +985 -985
- package/lib/assets/docs/article/grokkingfp/elixir/part-4.md +974 -974
- package/lib/assets/docs/article/grokkingfp/elixir/part-5.md +1286 -1286
- package/lib/assets/docs/article/grokkingfp/elixir/part-6.md +1049 -1049
- package/lib/assets/docs/article/grokkingfp/fsharp/index.md +210 -210
- package/lib/assets/docs/article/grokkingfp/fsharp/part-1.md +714 -714
- package/lib/assets/docs/article/grokkingfp/fsharp/part-2.md +961 -961
- package/lib/assets/docs/article/grokkingfp/fsharp/part-3.md +972 -972
- package/lib/assets/docs/article/grokkingfp/fsharp/part-4.md +832 -832
- package/lib/assets/docs/article/grokkingfp/fsharp/part-5.md +911 -911
- package/lib/assets/docs/article/grokkingfp/fsharp/part-6.md +922 -922
- package/lib/assets/docs/article/grokkingfp/haskell/index.md +234 -234
- package/lib/assets/docs/article/grokkingfp/haskell/part-1.md +591 -591
- package/lib/assets/docs/article/grokkingfp/haskell/part-2.md +866 -866
- package/lib/assets/docs/article/grokkingfp/haskell/part-3.md +915 -915
- package/lib/assets/docs/article/grokkingfp/haskell/part-4.md +878 -878
- package/lib/assets/docs/article/grokkingfp/haskell/part-5.md +845 -845
- package/lib/assets/docs/article/grokkingfp/haskell/part-6.md +844 -844
- package/lib/assets/docs/article/grokkingfp/index.md +143 -143
- package/lib/assets/docs/article/grokkingfp/java/index.md +211 -211
- package/lib/assets/docs/article/grokkingfp/java/part-1.md +648 -648
- package/lib/assets/docs/article/grokkingfp/java/part-2.md +675 -675
- package/lib/assets/docs/article/grokkingfp/java/part-3.md +672 -672
- package/lib/assets/docs/article/grokkingfp/java/part-4.md +771 -771
- package/lib/assets/docs/article/grokkingfp/java/part-5.md +959 -959
- package/lib/assets/docs/article/grokkingfp/java/part-6.md +1328 -1328
- package/lib/assets/docs/article/grokkingfp/python/index.md +258 -258
- package/lib/assets/docs/article/grokkingfp/python/part-1.md +443 -443
- package/lib/assets/docs/article/grokkingfp/python/part-2.md +958 -958
- package/lib/assets/docs/article/grokkingfp/python/part-3.md +1004 -1004
- package/lib/assets/docs/article/grokkingfp/python/part-4.md +765 -765
- package/lib/assets/docs/article/grokkingfp/python/part-5.md +747 -747
- package/lib/assets/docs/article/grokkingfp/python/part-6.md +861 -861
- package/lib/assets/docs/article/grokkingfp/ruby/index.md +330 -330
- package/lib/assets/docs/article/grokkingfp/ruby/part-1.md +755 -755
- package/lib/assets/docs/article/grokkingfp/ruby/part-2.md +938 -938
- package/lib/assets/docs/article/grokkingfp/ruby/part-3.md +946 -946
- package/lib/assets/docs/article/grokkingfp/ruby/part-4.md +921 -921
- package/lib/assets/docs/article/grokkingfp/ruby/part-5.md +908 -908
- package/lib/assets/docs/article/grokkingfp/ruby/part-6.md +1412 -1412
- package/lib/assets/docs/article/grokkingfp/rust/index.md +242 -242
- package/lib/assets/docs/article/grokkingfp/rust/part-1.md +634 -634
- package/lib/assets/docs/article/grokkingfp/rust/part-2.md +1060 -1060
- package/lib/assets/docs/article/grokkingfp/rust/part-3.md +994 -994
- package/lib/assets/docs/article/grokkingfp/rust/part-4.md +573 -573
- package/lib/assets/docs/article/grokkingfp/rust/part-5.md +705 -705
- package/lib/assets/docs/article/grokkingfp/rust/part-6.md +508 -508
- package/lib/assets/docs/article/grokkingfp/scala/index.md +171 -171
- package/lib/assets/docs/article/grokkingfp/scala/part-1.md +543 -543
- package/lib/assets/docs/article/grokkingfp/scala/part-2.md +946 -946
- package/lib/assets/docs/article/grokkingfp/scala/part-3.md +919 -919
- package/lib/assets/docs/article/grokkingfp/scala/part-4.md +742 -742
- package/lib/assets/docs/article/grokkingfp/scala/part-5.md +722 -722
- package/lib/assets/docs/article/grokkingfp/scala/part-6.md +867 -867
- package/lib/assets/docs/article/grokkingfp/typescript/index.md +273 -273
- package/lib/assets/docs/article/grokkingfp/typescript/part-1.md +561 -561
- package/lib/assets/docs/article/grokkingfp/typescript/part-2.md +1129 -1129
- package/lib/assets/docs/article/grokkingfp/typescript/part-3.md +842 -842
- package/lib/assets/docs/article/grokkingfp/typescript/part-4.md +1087 -1087
- package/lib/assets/docs/article/grokkingfp/typescript/part-5.md +717 -717
- package/lib/assets/docs/article/grokkingfp/typescript/part-6.md +982 -982
- package/lib/assets/docs/article/practical-database-design/index.md +121 -121
- package/lib/assets/docs/article/practical-database-design/part1/chapter01.md +288 -288
- package/lib/assets/docs/article/practical-database-design/part1/chapter02.md +518 -518
- package/lib/assets/docs/article/practical-database-design/part1/chapter03.md +557 -557
- package/lib/assets/docs/article/practical-database-design/part2/chapter04.md +924 -924
- package/lib/assets/docs/article/practical-database-design/part2/chapter05.md +1627 -1627
- package/lib/assets/docs/article/practical-database-design/part2/chapter06.md +2716 -2716
- package/lib/assets/docs/article/practical-database-design/part2/chapter07.md +2082 -2082
- package/lib/assets/docs/article/practical-database-design/part2/chapter08.md +2105 -2105
- package/lib/assets/docs/article/practical-database-design/part2/chapter09.md +2031 -2031
- package/lib/assets/docs/article/practical-database-design/part2/chapter10.md +1387 -1387
- package/lib/assets/docs/article/practical-database-design/part2/chapter11.md +1677 -1677
- package/lib/assets/docs/article/practical-database-design/part2/chapter12.md +1417 -1417
- package/lib/assets/docs/article/practical-database-design/part2/chapter13.md +1434 -1434
- package/lib/assets/docs/article/practical-database-design/part3/chapter14.md +667 -667
- package/lib/assets/docs/article/practical-database-design/part3/chapter15.md +1625 -1625
- package/lib/assets/docs/article/practical-database-design/part3/chapter16.md +1915 -1915
- package/lib/assets/docs/article/practical-database-design/part3/chapter17.md +1708 -1708
- package/lib/assets/docs/article/practical-database-design/part3/chapter18.md +2095 -2095
- package/lib/assets/docs/article/practical-database-design/part3/chapter19.md +1123 -1123
- package/lib/assets/docs/article/practical-database-design/part3/chapter20.md +1031 -1031
- package/lib/assets/docs/article/practical-database-design/part3/chapter21.md +1382 -1382
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter14-orm.md +991 -991
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter15-orm.md +1300 -1300
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter16-orm.md +1166 -1166
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter17-orm.md +1584 -1584
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter18-orm.md +1183 -1183
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter19-orm.md +1016 -1016
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter20-orm.md +1753 -1753
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter21-orm.md +1447 -1447
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter22-orm.md +1878 -1878
- package/lib/assets/docs/article/practical-database-design/part4/chapter22.md +965 -965
- package/lib/assets/docs/article/practical-database-design/part4/chapter23.md +2069 -2069
- package/lib/assets/docs/article/practical-database-design/part4/chapter24.md +2439 -2439
- package/lib/assets/docs/article/practical-database-design/part4/chapter25.md +3661 -3661
- package/lib/assets/docs/article/practical-database-design/part4/chapter26.md +2916 -2916
- package/lib/assets/docs/article/practical-database-design/part4/chapter27.md +3105 -3105
- package/lib/assets/docs/article/practical-database-design/part4/chapter28.md +2697 -2697
- package/lib/assets/docs/article/practical-database-design/part4/chapter29.md +2544 -2544
- package/lib/assets/docs/article/practical-database-design/part4/chapter30.md +2180 -2180
- package/lib/assets/docs/article/practical-database-design/part4/chapter31.md +1192 -1192
- package/lib/assets/docs/article/practical-database-design/part4/chapter32.md +2101 -2101
- package/lib/assets/docs/article/practical-database-design/part5/chapter33.md +1032 -1032
- package/lib/assets/docs/article/practical-database-design/part5/chapter34.md +1609 -1609
- package/lib/assets/docs/article/practical-database-design/part5/chapter35.md +1453 -1453
- package/lib/assets/docs/article/practical-database-design/part5/chapter36.md +1292 -1292
- package/lib/assets/docs/article/practical-database-design/part5/chapter37.md +1470 -1470
- package/lib/assets/docs/article/practical-database-design/part5/chapter38.md +1698 -1698
- package/lib/assets/docs/article/practical-database-design/part5/chapter39.md +2334 -2334
- package/lib/assets/docs/article/practical-database-design/study/study2-1.md +1693 -1693
- package/lib/assets/docs/article/practical-database-design/study/study2-2.md +1347 -1347
- package/lib/assets/docs/article/practical-database-design/study/study2-3.md +2044 -2044
- package/lib/assets/docs/article/practical-database-design/study/study2-4.md +2229 -2229
- package/lib/assets/docs/article/practical-database-design/study/study2-5.md +2418 -2418
- package/lib/assets/docs/article/practical-database-design/study/study3-1.md +2205 -2205
- package/lib/assets/docs/article/practical-database-design/study/study3-2.md +2221 -2221
- package/lib/assets/docs/article/practical-database-design/study/study3-3.md +2253 -2253
- package/lib/assets/docs/article/practical-database-design/study/study3-4.md +2106 -2106
- package/lib/assets/docs/article/practical-database-design/study/study3-5.md +2507 -2507
- package/lib/assets/docs/article/practical-database-design/study/study4-1.md +2587 -2587
- package/lib/assets/docs/article/practical-database-design/study/study4-2.md +2075 -2075
- package/lib/assets/docs/article/practical-database-design/study/study4-3.md +1805 -1805
- package/lib/assets/docs/article/practical-database-design/study/study4-4.md +1895 -1895
- package/lib/assets/docs/article/practical-database-design/study/study4-5.md +2878 -2878
- package/lib/assets/docs/assets/css/extra.css +29 -29
- package/lib/assets/docs/assets/js/extra.js +44 -44
- package/lib/assets/docs/development/index.md +39 -39
- package/lib/assets/docs/operation/index.md +11 -11
- package/lib/assets/docs/reference/CodexCLIMCP/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/351/226/213/347/231/272/343/203/225/343/203/255/343/203/274.md +19 -5
- package/lib/assets/docs/reference/CodexCLIMCP/343/202/265/343/203/274/343/203/220/343/203/274/350/250/255/345/256/232/346/211/213/351/240/206.md +341 -341
- package/lib/assets/docs/reference/Java/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/346/247/213/347/257/211/343/202/254/343/202/244/343/203/211.md +581 -581
- package/lib/assets/docs/reference/SonarQube/343/203/255/343/203/274/343/202/253/343/203/253/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +642 -642
- package/lib/assets/docs/reference/TypeScript/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/346/247/213/347/257/211/343/202/254/343/202/244/343/203/211.md +465 -465
- package/lib/assets/docs/reference/UI/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +450 -450
- package/lib/assets/docs/reference/images/Ansoff.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandBasicStrategy.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandCategorization.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandRecurutementStrategy.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandValue.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BusinessActivitiy.svg +3 -3
- package/lib/assets/docs/reference/images/HRM.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/MarketingStructure.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/OrganizationElemnts.svg +3 -3
- package/lib/assets/docs/reference/images/PPM.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/PositioningMap.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ProductLayer.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ProductMix.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/SWOT.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/TargetMarket.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ThreeGenericStrategies.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/VRIO.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ValueChain.drawio.svg +3 -3
- package/lib/assets/docs/reference/index.md +52 -52
- package/lib/assets/docs/reference//343/202/210/343/201/204/343/202/275/343/203/225/343/203/210/343/202/246/343/202/247/343/202/242/343/201/250/343/201/257.md +250 -250
- package/lib/assets/docs/reference//343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +2216 -2216
- package/lib/assets/docs/reference//343/202/244/343/203/263/343/203/225/343/203/251/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +1878 -1878
- package/lib/assets/docs/reference//343/202/250/343/202/257/343/202/271/343/203/210/343/203/252/343/203/274/343/203/240/343/203/227/343/203/255/343/202/260/343/203/251/343/203/237/343/203/263/343/202/260.md +550 -550
- package/lib/assets/docs/reference//343/202/263/343/203/274/343/203/207/343/202/243/343/203/263/343/202/260/343/201/250/343/203/206/343/202/271/343/203/210/343/202/254/343/202/244/343/203/211.md +705 -705
- package/lib/assets/docs/reference//343/203/206/343/202/271/343/203/210/346/210/246/347/225/245/343/202/254/343/202/244/343/203/211.md +1313 -1313
- package/lib/assets/docs/reference//343/203/207/343/203/274/343/202/277/343/203/242/343/203/207/343/203/253/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +311 -311
- package/lib/assets/docs/reference//343/203/211/343/203/241/343/202/244/343/203/263/343/203/242/343/203/207/343/203/253/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +599 -599
- package/lib/assets/docs/reference//343/203/223/343/202/270/343/203/215/343/202/271/343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243/345/210/206/346/236/220/343/202/254/343/202/244/343/203/211.md +528 -528
- package/lib/assets/docs/reference//343/203/246/343/203/274/343/202/271/343/202/261/343/203/274/343/202/271/344/275/234/346/210/220/343/202/254/343/202/244/343/203/211.md +689 -689
- package/lib/assets/docs/reference//343/203/252/343/203/252/343/203/274/343/202/271/343/202/254/343/202/244/343/203/211.md +461 -461
- package/lib/assets/docs/reference//343/203/252/343/203/252/343/203/274/343/202/271/343/203/273/343/202/244/343/203/206/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263/350/250/210/347/224/273/343/202/254/343/202/244/343/203/211.md +580 -580
- package/lib/assets/docs/reference//343/203/255/343/202/270/343/202/253/343/203/253/343/202/267/343/203/263/343/202/255/343/203/263/343/202/260.md +1367 -1367
- package/lib/assets/docs/reference//344/274/201/346/245/255/347/265/214/345/226/266/350/253/226.md +2637 -2637
- package/lib/assets/docs/reference//347/222/260/345/242/203/345/244/211/346/225/260/347/256/241/347/220/206/343/202/254/343/202/244/343/203/211.md +665 -665
- package/lib/assets/docs/reference//350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +1248 -1248
- package/lib/assets/docs/reference//350/250/200/350/252/236/345/210/245/351/226/213/347/231/272/343/202/254/343/202/244/343/203/211.md +518 -518
- package/lib/assets/docs/reference//351/201/213/345/226/266/347/256/241/347/220/206.md +1482 -1482
- package/lib/assets/docs/reference//351/201/213/347/224/250/343/202/271/343/202/257/343/203/252/343/203/227/343/203/210/344/275/234/346/210/220/343/202/254/343/202/244/343/203/211.md +421 -421
- package/lib/assets/docs/reference//351/201/213/347/224/250/350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +392 -392
- package/lib/assets/docs/reference//351/226/213/347/231/272/343/202/254/343/202/244/343/203/211.md +299 -299
- package/lib/assets/docs/reference//351/235/236/346/251/237/350/203/275/350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +1236 -1236
- package/lib/assets/docs/review/index.md +5 -5
- package/lib/assets/docs/strategy/index.md +1 -1
- package/lib/assets/docs/template/ADR.md +30 -30
- package/lib/assets/docs/template/AWS/343/202/271/343/203/206/343/203/274/343/202/270/343/203/263/343/202/260/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +1366 -1366
- package/lib/assets/docs/template/AWS/343/203/227/343/203/255/343/203/200/343/202/257/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +634 -634
- package/lib/assets/docs/template/README.md +50 -50
- package/lib/assets/docs/template/index.md +23 -23
- package/lib/assets/docs/template//343/201/276/343/201/232/343/201/223/343/202/214/343/202/222/350/252/255/343/202/202/343/201/206/343/203/252/343/202/271/343/203/210.md +12 -12
- package/lib/assets/docs/template//343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/351/226/213/347/231/272/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +547 -547
- package/lib/assets/docs/template//343/202/244/343/203/206/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263/345/256/214/344/272/206/345/240/261/345/221/212/346/233/270.md +58 -58
- package/lib/assets/docs/template//343/202/244/343/203/263/343/202/273/343/203/227/343/202/267/343/203/247/343/203/263/343/203/207/343/203/203/343/202/255.md +13 -13
- package/lib/assets/docs/template//343/203/223/343/202/270/343/203/215/343/202/271/343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243.md +379 -379
- package/lib/assets/docs/template//344/274/201/346/245/255/345/210/206/346/236/220.md +573 -573
- package/lib/assets/docs/template//345/256/214/345/205/250/345/275/242/345/274/217/343/201/256/343/203/246/343/203/274/343/202/271/343/202/261/343/203/274/343/202/271.md +69 -69
- package/lib/assets/docs/template//350/246/201/344/273/266/345/256/232/347/276/251.md +669 -669
- package/lib/assets/docs/template//350/250/255/350/250/210.md +173 -173
- package/lib/assets/docs/template//351/226/213/347/231/272/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +688 -688
- package/lib/assets/gulpfile.js +25 -25
- package/lib/assets/mkdocs.yml +136 -136
- package/lib/assets/ops/docker/mkdoc/Dockerfile +19 -19
- package/lib/assets/ops/scripts/journal.js +180 -180
- package/lib/assets/ops/scripts/mkdocs.js +82 -82
- package/lib/assets/ops/scripts/release.js +431 -431
- package/lib/assets/ops/scripts/sonar_local.js +726 -726
- package/lib/assets/ops/scripts/ssh.js +190 -190
- package/lib/assets/ops/scripts/vault.js +299 -299
- package/lib/assets/package-lock.json +1653 -1653
- package/lib/assets/package.json +40 -40
- package/lib/gulpfile.js +37 -37
- package/package.json +41 -41
|
@@ -1,1895 +1,1895 @@
|
|
|
1
|
-
# 実践データベース設計:生産管理システム 研究 4 - GraphQL サービスの実装
|
|
2
|
-
|
|
3
|
-
## はじめに
|
|
4
|
-
|
|
5
|
-
本研究では、API サーバー構成(第32章)とは異なるアプローチとして、**GraphQL** による生産管理システムを実装します。クライアントが必要なデータを正確に指定できる柔軟なクエリと、リアルタイム更新を実現する Subscription を活用します。
|
|
6
|
-
|
|
7
|
-
第32章で構築したヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 第46章:GraphQL サーバーの基礎
|
|
12
|
-
|
|
13
|
-
### 46.1 GraphQL とは
|
|
14
|
-
|
|
15
|
-
GraphQL は Facebook が開発したクエリ言語および実行エンジンです。クライアントが必要なデータの形状を指定でき、Over-fetching や Under-fetching を防ぎます。
|
|
16
|
-
|
|
17
|
-
```plantuml
|
|
18
|
-
@startuml graphql_architecture
|
|
19
|
-
!define RECTANGLE class
|
|
20
|
-
|
|
21
|
-
skinparam backgroundColor #FEFEFE
|
|
22
|
-
|
|
23
|
-
package "GraphQL Architecture (生産管理システム)" {
|
|
24
|
-
|
|
25
|
-
package "Client Side" {
|
|
26
|
-
RECTANGLE "GraphQL Client\n(Apollo/Relay/urql)" as client {
|
|
27
|
-
- Query (読み取り)
|
|
28
|
-
- Mutation (書き込み)
|
|
29
|
-
- Subscription (リアルタイム)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
package "Server Side" {
|
|
34
|
-
RECTANGLE "GraphQL Server\n(Spring for GraphQL)" as server {
|
|
35
|
-
- ItemResolver
|
|
36
|
-
- PurchaseOrderResolver
|
|
37
|
-
- MrpResolver
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
package "Shared" {
|
|
42
|
-
RECTANGLE "GraphQL Schema\n(.graphqls files)" as schema {
|
|
43
|
-
- schema.graphqls
|
|
44
|
-
- item.graphqls
|
|
45
|
-
- purchase_order.graphqls
|
|
46
|
-
- mrp.graphqls
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
client --> schema : "スキーマに基づいて\nクエリを構築"
|
|
52
|
-
server --> schema : "スキーマに基づいて\nリゾルバを実装"
|
|
53
|
-
client <--> server : "HTTP/WebSocket\n(JSON)"
|
|
54
|
-
|
|
55
|
-
note bottom of schema
|
|
56
|
-
スキーマ駆動開発
|
|
57
|
-
クライアント主導のデータ取得
|
|
58
|
-
型安全な API
|
|
59
|
-
end note
|
|
60
|
-
|
|
61
|
-
@enduml
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**REST API / gRPC との比較:**
|
|
65
|
-
|
|
66
|
-
| 特徴 | REST API | gRPC | GraphQL |
|
|
67
|
-
|------|----------|------|---------|
|
|
68
|
-
| プロトコル | HTTP/1.1 | HTTP/2 | HTTP/1.1 or HTTP/2 |
|
|
69
|
-
| データ形式 | JSON | Protocol Buffers | JSON |
|
|
70
|
-
| スキーマ | OpenAPI (任意) | .proto (必須) | .graphqls (必須) |
|
|
71
|
-
| データ取得 | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
72
|
-
| エンドポイント | 複数 | 複数 | 単一 |
|
|
73
|
-
| リアルタイム | WebSocket 別実装 | ストリーミング | Subscription |
|
|
74
|
-
| 主な用途 | 汎用 API | マイクロサービス | フロントエンド向け |
|
|
75
|
-
|
|
76
|
-
### 46.2 3つの操作タイプ
|
|
77
|
-
|
|
78
|
-
GraphQL は 3 つの操作タイプをサポートします:
|
|
79
|
-
|
|
80
|
-
```plantuml
|
|
81
|
-
@startuml graphql_operations
|
|
82
|
-
skinparam backgroundColor #FEFEFE
|
|
83
|
-
|
|
84
|
-
rectangle "1. Query\n(読み取り)" as query {
|
|
85
|
-
(Client) --> (Server) : "{ items { code name } }"
|
|
86
|
-
(Server) --> (Client) : "{ items: [...] }"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
rectangle "2. Mutation\n(書き込み)" as mutation {
|
|
90
|
-
(Client2) --> (Server2) : "mutation { createItem(...) }"
|
|
91
|
-
(Server2) --> (Client2) : "{ createItem: {...} }"
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
rectangle "3. Subscription\n(リアルタイム)" as subscription {
|
|
95
|
-
(Client3) --> (Server3) : "subscription { orderUpdated }"
|
|
96
|
-
(Server3) --> (Client3) : "{ orderUpdated: {...} }"
|
|
97
|
-
(Server3) --> (Client3) : "{ orderUpdated: {...} }"
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@enduml
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**用途:**
|
|
104
|
-
|
|
105
|
-
1. **Query**: データ取得(品目一覧、BOM 展開)
|
|
106
|
-
2. **Mutation**: データ更新(品目登録、発注確定)
|
|
107
|
-
3. **Subscription**: リアルタイム通知(MRP 進捗、在庫変動)
|
|
108
|
-
|
|
109
|
-
### 46.3 GraphQL におけるヘキサゴナルアーキテクチャ
|
|
110
|
-
|
|
111
|
-
GraphQL を導入しても、第32章で構築したヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
112
|
-
|
|
113
|
-
```plantuml
|
|
114
|
-
@startuml hexagonal_graphql
|
|
115
|
-
!define RECTANGLE class
|
|
116
|
-
|
|
117
|
-
package "Hexagonal Architecture (GraphQL版)" {
|
|
118
|
-
|
|
119
|
-
RECTANGLE "Application Core\n(Domain + Use Cases)" as core {
|
|
120
|
-
- Item (品目)
|
|
121
|
-
- Bom (部品構成表)
|
|
122
|
-
- Order (オーダ)
|
|
123
|
-
- Stock (在庫)
|
|
124
|
-
- ItemUseCase
|
|
125
|
-
- OrderUseCase
|
|
126
|
-
- InventoryUseCase
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
RECTANGLE "Input Adapters\n(Driving Side)" as input {
|
|
130
|
-
- REST Controller(既存)
|
|
131
|
-
- gRPC Service(既存)
|
|
132
|
-
- GraphQL Resolver(新規追加)
|
|
133
|
-
- DataFetcher
|
|
134
|
-
- DataLoader
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
RECTANGLE "Output Adapters\n(Driven Side)" as output {
|
|
138
|
-
- MyBatis Repository
|
|
139
|
-
- Database Access
|
|
140
|
-
- Entity Mapping
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
input --> core : "Input Ports\n(Use Cases)"
|
|
145
|
-
core --> output : "Output Ports\n(Repository Interfaces)"
|
|
146
|
-
|
|
147
|
-
note top of core
|
|
148
|
-
既存のビジネスロジック
|
|
149
|
-
REST API / gRPC 版と完全に共有
|
|
150
|
-
GraphQL 固有のコードは含まない
|
|
151
|
-
end note
|
|
152
|
-
|
|
153
|
-
note left of input
|
|
154
|
-
GraphQL リゾルバを
|
|
155
|
-
Input Adapter として追加
|
|
156
|
-
既存の REST/gRPC と共存可能
|
|
157
|
-
end note
|
|
158
|
-
|
|
159
|
-
note right of output
|
|
160
|
-
既存の Repository を
|
|
161
|
-
そのまま使用
|
|
162
|
-
変更不要
|
|
163
|
-
end note
|
|
164
|
-
|
|
165
|
-
@enduml
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
**GraphQL でもヘキサゴナルアーキテクチャを維持する理由:**
|
|
169
|
-
|
|
170
|
-
1. **再利用性**: 既存の UseCase/Repository をそのまま活用
|
|
171
|
-
2. **並行運用**: REST API、gRPC、GraphQL を同時提供可能
|
|
172
|
-
3. **テスト容易性**: ドメインロジックは通信プロトコルに依存しない
|
|
173
|
-
4. **移行容易性**: 段階的に API 形式を追加・変更可能
|
|
174
|
-
|
|
175
|
-
### 46.4 ディレクトリ構成
|
|
176
|
-
|
|
177
|
-
既存の構成に `infrastructure/graphql/` を追加するだけです。
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
src/main/java/com/example/production/
|
|
181
|
-
├── domain/ # ドメイン層(API版と共通)
|
|
182
|
-
│ ├── model/
|
|
183
|
-
│ │ ├── item/
|
|
184
|
-
│ │ ├── bom/
|
|
185
|
-
│ │ ├── order/
|
|
186
|
-
│ │ └── inventory/
|
|
187
|
-
│ └── exception/
|
|
188
|
-
│
|
|
189
|
-
├── application/ # アプリケーション層(API版と共通)
|
|
190
|
-
│ ├── port/
|
|
191
|
-
│ │ ├── in/ # Input Port(ユースケース)
|
|
192
|
-
│ │ └── out/ # Output Port(リポジトリ)
|
|
193
|
-
│ └── service/
|
|
194
|
-
│
|
|
195
|
-
├── infrastructure/
|
|
196
|
-
│ ├── persistence/ # Output Adapter(DB実装)- 既存
|
|
197
|
-
│ │ ├── mapper/
|
|
198
|
-
│ │ └── repository/
|
|
199
|
-
│ ├── rest/ # Input Adapter(REST実装)- 既存
|
|
200
|
-
│ ├── grpc/ # Input Adapter(gRPC実装)- 既存
|
|
201
|
-
│ └── graphql/ # Input Adapter(GraphQL実装)- 新規追加
|
|
202
|
-
│ ├── resolver/ # Query/Mutation リゾルバ
|
|
203
|
-
│ ├── dataloader/ # N+1 問題対策
|
|
204
|
-
│ ├── scalar/ # カスタムスカラー型
|
|
205
|
-
│ └── subscription/ # Subscription ハンドラ
|
|
206
|
-
│
|
|
207
|
-
├── config/
|
|
208
|
-
│
|
|
209
|
-
└── src/main/resources/
|
|
210
|
-
└── graphql/ # GraphQL スキーマ定義
|
|
211
|
-
├── schema.graphqls
|
|
212
|
-
├── item.graphqls
|
|
213
|
-
├── bom.graphqls
|
|
214
|
-
├── purchase_order.graphqls
|
|
215
|
-
└── mrp.graphqls
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### 46.5 技術スタックの追加
|
|
219
|
-
|
|
220
|
-
既存の `build.gradle.kts` に GraphQL 関連の依存関係を追加します。
|
|
221
|
-
|
|
222
|
-
<details>
|
|
223
|
-
<summary>build.gradle.kts(差分)</summary>
|
|
224
|
-
|
|
225
|
-
```kotlin
|
|
226
|
-
dependencies {
|
|
227
|
-
// 既存の依存関係(Spring Boot, MyBatis, PostgreSQL等)はそのまま
|
|
228
|
-
|
|
229
|
-
// GraphQL 関連を追加
|
|
230
|
-
implementation("org.springframework.boot:spring-boot-starter-graphql")
|
|
231
|
-
implementation("org.springframework.boot:spring-boot-starter-websocket") // Subscription 用
|
|
232
|
-
|
|
233
|
-
// GraphQL 拡張
|
|
234
|
-
implementation("com.graphql-java:graphql-java-extended-scalars:21.0")
|
|
235
|
-
|
|
236
|
-
// Test
|
|
237
|
-
testImplementation("org.springframework.graphql:spring-graphql-test")
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
</details>
|
|
242
|
-
|
|
243
|
-
**追加パッケージの説明:**
|
|
244
|
-
|
|
245
|
-
| パッケージ | 用途 |
|
|
246
|
-
|-----------|------|
|
|
247
|
-
| spring-boot-starter-graphql | Spring Boot GraphQL 統合 |
|
|
248
|
-
| spring-boot-starter-websocket | Subscription (WebSocket) |
|
|
249
|
-
| graphql-java-extended-scalars | DateTime, BigDecimal 等のスカラー型 |
|
|
250
|
-
| spring-graphql-test | GraphQL テストサポート |
|
|
251
|
-
|
|
252
|
-
### 42.6 GraphQL スキーマ定義
|
|
253
|
-
|
|
254
|
-
<details>
|
|
255
|
-
<summary>src/main/resources/graphql/schema.graphqls</summary>
|
|
256
|
-
|
|
257
|
-
```graphql
|
|
258
|
-
# ルートスキーマ
|
|
259
|
-
type Query {
|
|
260
|
-
# 品目
|
|
261
|
-
item(itemCode: ID!): Item
|
|
262
|
-
items(category: ItemCategory, page: Int, size: Int): ItemConnection!
|
|
263
|
-
|
|
264
|
-
# BOM
|
|
265
|
-
bomTree(itemCode: ID!): BomNode
|
|
266
|
-
whereUsed(itemCode: ID!): [WhereUsedResult!]!
|
|
267
|
-
|
|
268
|
-
# 発注
|
|
269
|
-
purchaseOrder(orderNumber: ID!): PurchaseOrder
|
|
270
|
-
purchaseOrders(status: OrderStatus, page: Int, size: Int): PurchaseOrderConnection!
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
type Mutation {
|
|
274
|
-
# 品目
|
|
275
|
-
createItem(input: CreateItemInput!): Item!
|
|
276
|
-
updateItem(input: UpdateItemInput!): Item!
|
|
277
|
-
deleteItem(itemCode: ID!): Boolean!
|
|
278
|
-
|
|
279
|
-
# 発注
|
|
280
|
-
createPurchaseOrder(input: CreatePurchaseOrderInput!): PurchaseOrder!
|
|
281
|
-
confirmPurchaseOrder(orderNumber: ID!): PurchaseOrder!
|
|
282
|
-
cancelPurchaseOrder(orderNumber: ID!): Boolean!
|
|
283
|
-
recordReceiving(input: RecordReceivingInput!): ReceivingResult!
|
|
284
|
-
|
|
285
|
-
# MRP
|
|
286
|
-
executeMrp(input: ExecuteMrpInput!): MrpResult!
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
type Subscription {
|
|
290
|
-
# MRP 進捗
|
|
291
|
-
mrpProgress(executionId: ID!): MrpProgress!
|
|
292
|
-
|
|
293
|
-
# 発注ステータス変更
|
|
294
|
-
orderStatusChanged(orderNumber: ID): PurchaseOrder!
|
|
295
|
-
|
|
296
|
-
# 在庫変動
|
|
297
|
-
stockChanged(itemCode: ID): StockChange!
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
# ページネーション
|
|
301
|
-
type PageInfo {
|
|
302
|
-
hasNextPage: Boolean!
|
|
303
|
-
hasPreviousPage: Boolean!
|
|
304
|
-
totalElements: Int!
|
|
305
|
-
totalPages: Int!
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
</details>
|
|
310
|
-
|
|
311
|
-
<details>
|
|
312
|
-
<summary>src/main/resources/graphql/item.graphqls</summary>
|
|
313
|
-
|
|
314
|
-
```graphql
|
|
315
|
-
# 品目区分
|
|
316
|
-
enum ItemCategory {
|
|
317
|
-
PRODUCT # 製品
|
|
318
|
-
SEMI_PRODUCT # 半製品
|
|
319
|
-
PART # 部品
|
|
320
|
-
MATERIAL # 材料
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
# 品目
|
|
324
|
-
type Item {
|
|
325
|
-
itemCode: ID!
|
|
326
|
-
effectiveDate: Date!
|
|
327
|
-
itemName: String!
|
|
328
|
-
category: ItemCategory!
|
|
329
|
-
itemGroupCode: String
|
|
330
|
-
unitCode: String
|
|
331
|
-
locationCode: String
|
|
332
|
-
leadTime: Int
|
|
333
|
-
safetyStock: Int
|
|
334
|
-
|
|
335
|
-
# 関連データ(必要な場合のみ取得)
|
|
336
|
-
bom: [BomEntry!]!
|
|
337
|
-
stock: Stock
|
|
338
|
-
purchaseOrders: [PurchaseOrder!]!
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
# 品目一覧(ページネーション付き)
|
|
342
|
-
type ItemConnection {
|
|
343
|
-
edges: [ItemEdge!]!
|
|
344
|
-
pageInfo: PageInfo!
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
type ItemEdge {
|
|
348
|
-
node: Item!
|
|
349
|
-
cursor: String!
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
# 入力型
|
|
353
|
-
input CreateItemInput {
|
|
354
|
-
itemCode: ID!
|
|
355
|
-
itemName: String!
|
|
356
|
-
category: ItemCategory!
|
|
357
|
-
itemGroupCode: String
|
|
358
|
-
unitCode: String
|
|
359
|
-
locationCode: String
|
|
360
|
-
leadTime: Int
|
|
361
|
-
safetyStock: Int
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
input UpdateItemInput {
|
|
365
|
-
itemCode: ID!
|
|
366
|
-
itemName: String
|
|
367
|
-
category: ItemCategory
|
|
368
|
-
leadTime: Int
|
|
369
|
-
safetyStock: Int
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
# カスタムスカラー
|
|
373
|
-
scalar Date
|
|
374
|
-
scalar DateTime
|
|
375
|
-
scalar BigDecimal
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
</details>
|
|
379
|
-
|
|
380
|
-
<details>
|
|
381
|
-
<summary>src/main/resources/graphql/bom.graphqls</summary>
|
|
382
|
-
|
|
383
|
-
```graphql
|
|
384
|
-
# BOM エントリ
|
|
385
|
-
type BomEntry {
|
|
386
|
-
parentItemCode: ID!
|
|
387
|
-
childItemCode: ID!
|
|
388
|
-
requiredQuantity: BigDecimal!
|
|
389
|
-
effectiveFrom: Date!
|
|
390
|
-
effectiveTo: Date
|
|
391
|
-
|
|
392
|
-
# 子品目の詳細
|
|
393
|
-
childItem: Item!
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
# BOM ツリーノード
|
|
397
|
-
type BomNode {
|
|
398
|
-
itemCode: ID!
|
|
399
|
-
itemName: String!
|
|
400
|
-
requiredQuantity: BigDecimal!
|
|
401
|
-
level: Int!
|
|
402
|
-
children: [BomNode!]!
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
# 逆展開(使用先照会)
|
|
406
|
-
type WhereUsedResult {
|
|
407
|
-
parentItemCode: ID!
|
|
408
|
-
itemName: String!
|
|
409
|
-
requiredQuantity: BigDecimal!
|
|
410
|
-
level: Int!
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
</details>
|
|
415
|
-
|
|
416
|
-
<details>
|
|
417
|
-
<summary>src/main/resources/graphql/purchase_order.graphqls</summary>
|
|
418
|
-
|
|
419
|
-
```graphql
|
|
420
|
-
# 発注ステータス
|
|
421
|
-
enum OrderStatus {
|
|
422
|
-
DRAFT
|
|
423
|
-
CONFIRMED
|
|
424
|
-
CANCELLED
|
|
425
|
-
COMPLETED
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
# 発注
|
|
429
|
-
type PurchaseOrder {
|
|
430
|
-
orderNumber: ID!
|
|
431
|
-
supplierCode: String!
|
|
432
|
-
orderDate: Date!
|
|
433
|
-
status: OrderStatus!
|
|
434
|
-
totalAmount: BigDecimal!
|
|
435
|
-
details: [PurchaseOrderDetail!]!
|
|
436
|
-
|
|
437
|
-
# 関連データ
|
|
438
|
-
supplier: Supplier
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
type PurchaseOrderDetail {
|
|
442
|
-
lineNumber: Int!
|
|
443
|
-
itemCode: ID!
|
|
444
|
-
orderQuantity: BigDecimal!
|
|
445
|
-
unitPrice: BigDecimal!
|
|
446
|
-
deliveryDate: Date!
|
|
447
|
-
receivedQuantity: BigDecimal!
|
|
448
|
-
|
|
449
|
-
# 関連データ
|
|
450
|
-
item: Item!
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
type PurchaseOrderConnection {
|
|
454
|
-
edges: [PurchaseOrderEdge!]!
|
|
455
|
-
pageInfo: PageInfo!
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
type PurchaseOrderEdge {
|
|
459
|
-
node: PurchaseOrder!
|
|
460
|
-
cursor: String!
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
# 入力型
|
|
464
|
-
input CreatePurchaseOrderInput {
|
|
465
|
-
supplierCode: String!
|
|
466
|
-
details: [CreateOrderDetailInput!]!
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
input CreateOrderDetailInput {
|
|
470
|
-
itemCode: ID!
|
|
471
|
-
orderQuantity: BigDecimal!
|
|
472
|
-
unitPrice: BigDecimal!
|
|
473
|
-
deliveryDate: Date!
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
input RecordReceivingInput {
|
|
477
|
-
orderNumber: ID!
|
|
478
|
-
lineNumber: Int!
|
|
479
|
-
receivedQuantity: BigDecimal!
|
|
480
|
-
receivingDate: Date!
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
type ReceivingResult {
|
|
484
|
-
orderNumber: ID!
|
|
485
|
-
lineNumber: Int!
|
|
486
|
-
totalReceived: BigDecimal!
|
|
487
|
-
isCompleted: Boolean!
|
|
488
|
-
}
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
</details>
|
|
492
|
-
|
|
493
|
-
<details>
|
|
494
|
-
<summary>src/main/resources/graphql/mrp.graphqls</summary>
|
|
495
|
-
|
|
496
|
-
```graphql
|
|
497
|
-
# MRP 入力
|
|
498
|
-
input ExecuteMrpInput {
|
|
499
|
-
startDate: Date!
|
|
500
|
-
endDate: Date!
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
# MRP 結果
|
|
504
|
-
type MrpResult {
|
|
505
|
-
executionId: ID!
|
|
506
|
-
periodStart: Date!
|
|
507
|
-
periodEnd: Date!
|
|
508
|
-
plannedOrders: [PlannedOrder!]!
|
|
509
|
-
shortageItems: [ShortageItem!]!
|
|
510
|
-
statistics: MrpStatistics!
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
type PlannedOrder {
|
|
514
|
-
itemCode: ID!
|
|
515
|
-
itemName: String!
|
|
516
|
-
quantity: BigDecimal!
|
|
517
|
-
dueDate: Date!
|
|
518
|
-
orderType: String!
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
type ShortageItem {
|
|
522
|
-
itemCode: ID!
|
|
523
|
-
itemName: String!
|
|
524
|
-
shortageQuantity: BigDecimal!
|
|
525
|
-
recommendedOrderDate: Date!
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
type MrpStatistics {
|
|
529
|
-
totalItemsProcessed: Int!
|
|
530
|
-
purchaseOrderCount: Int!
|
|
531
|
-
productionOrderCount: Int!
|
|
532
|
-
shortageItemCount: Int!
|
|
533
|
-
executionTimeMs: Long!
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
# MRP 進捗(Subscription)
|
|
537
|
-
type MrpProgress {
|
|
538
|
-
executionId: ID!
|
|
539
|
-
phase: String!
|
|
540
|
-
current: Int!
|
|
541
|
-
total: Int!
|
|
542
|
-
message: String!
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
scalar Long
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
</details>
|
|
549
|
-
|
|
550
|
-
### 42.7 API サーバー版との Controller の違い
|
|
551
|
-
|
|
552
|
-
**API サーバー版(REST Controller)**
|
|
553
|
-
|
|
554
|
-
```java
|
|
555
|
-
@RestController // JSON を返す
|
|
556
|
-
@RequestMapping("/api/items")
|
|
557
|
-
public class ItemApiController {
|
|
558
|
-
|
|
559
|
-
private final ItemUseCase itemUseCase; // 共有
|
|
560
|
-
|
|
561
|
-
@GetMapping("/{itemCode}")
|
|
562
|
-
public ResponseEntity<ItemResponse> getItem(@PathVariable String itemCode) {
|
|
563
|
-
Item item = itemUseCase.getItemByCode(itemCode);
|
|
564
|
-
return ResponseEntity.ok(ItemResponse.from(item));
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
**GraphQL 版(QueryResolver)**
|
|
570
|
-
|
|
571
|
-
```java
|
|
572
|
-
@Controller // Spring for GraphQL
|
|
573
|
-
public class ItemResolver {
|
|
574
|
-
|
|
575
|
-
private final ItemUseCase itemUseCase; // 共有(同じインスタンス)
|
|
576
|
-
|
|
577
|
-
@QueryMapping
|
|
578
|
-
public Item item(@Argument String itemCode) {
|
|
579
|
-
// 同じ UseCase を呼び出し
|
|
580
|
-
return itemUseCase.getItemByCode(itemCode);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
**共通点と相違点:**
|
|
586
|
-
|
|
587
|
-
| 観点 | REST Controller | GraphQL Resolver |
|
|
588
|
-
|------|-----------------|------------------|
|
|
589
|
-
| **UseCase** | 共有(同一インスタンス) | 共有(同一インスタンス) |
|
|
590
|
-
| **アノテーション** | `@RestController` | `@Controller` |
|
|
591
|
-
| **メソッド** | `@GetMapping` 等 | `@QueryMapping` / `@MutationMapping` |
|
|
592
|
-
| **引数** | `@PathVariable` / `@RequestBody` | `@Argument` |
|
|
593
|
-
| **レスポンス** | DTO に変換 | ドメインモデル直接(推奨) |
|
|
594
|
-
| **データ形式** | 固定 JSON | クライアント指定 |
|
|
595
|
-
|
|
596
|
-
---
|
|
597
|
-
|
|
598
|
-
## 第47章:マスタ API の実装
|
|
599
|
-
|
|
600
|
-
### 47.1 カスタムスカラーの実装
|
|
601
|
-
|
|
602
|
-
<details>
|
|
603
|
-
<summary>ScalarConfig.java</summary>
|
|
604
|
-
|
|
605
|
-
```java
|
|
606
|
-
package com.example.production.infrastructure.graphql.scalar;
|
|
607
|
-
|
|
608
|
-
import graphql.scalars.ExtendedScalars;
|
|
609
|
-
import org.springframework.context.annotation.Bean;
|
|
610
|
-
import org.springframework.context.annotation.Configuration;
|
|
611
|
-
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* カスタムスカラー設定
|
|
615
|
-
*/
|
|
616
|
-
@Configuration
|
|
617
|
-
public class ScalarConfig {
|
|
618
|
-
|
|
619
|
-
@Bean
|
|
620
|
-
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
|
|
621
|
-
return wiringBuilder -> wiringBuilder
|
|
622
|
-
.scalar(ExtendedScalars.Date)
|
|
623
|
-
.scalar(ExtendedScalars.DateTime)
|
|
624
|
-
.scalar(ExtendedScalars.GraphQLBigDecimal)
|
|
625
|
-
.scalar(ExtendedScalars.GraphQLLong);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
</details>
|
|
631
|
-
|
|
632
|
-
### 47.2 品目 GraphQL リゾルバの TDD 実装
|
|
633
|
-
|
|
634
|
-
#### Red: 失敗するテストを書く
|
|
635
|
-
|
|
636
|
-
<details>
|
|
637
|
-
<summary>ItemResolverTest.java</summary>
|
|
638
|
-
|
|
639
|
-
```java
|
|
640
|
-
package com.example.production.infrastructure.graphql.resolver;
|
|
641
|
-
|
|
642
|
-
import com.example.production.domain.model.item.Item;
|
|
643
|
-
import com.example.production.domain.model.item.ItemCategory;
|
|
644
|
-
import com.example.production.infrastructure.persistence.mapper.ItemMapper;
|
|
645
|
-
import org.junit.jupiter.api.*;
|
|
646
|
-
import org.springframework.beans.factory.annotation.Autowired;
|
|
647
|
-
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester;
|
|
648
|
-
import org.springframework.boot.test.context.SpringBootTest;
|
|
649
|
-
import org.springframework.graphql.test.tester.GraphQlTester;
|
|
650
|
-
import org.springframework.test.context.DynamicPropertyRegistry;
|
|
651
|
-
import org.springframework.test.context.DynamicPropertySource;
|
|
652
|
-
import org.testcontainers.containers.PostgreSQLContainer;
|
|
653
|
-
import org.testcontainers.junit.jupiter.Container;
|
|
654
|
-
import org.testcontainers.junit.jupiter.Testcontainers;
|
|
655
|
-
|
|
656
|
-
import java.time.LocalDate;
|
|
657
|
-
|
|
658
|
-
import static org.assertj.core.api.Assertions.*;
|
|
659
|
-
|
|
660
|
-
@SpringBootTest
|
|
661
|
-
@AutoConfigureGraphQlTester
|
|
662
|
-
@Testcontainers
|
|
663
|
-
@DisplayName("品目 GraphQL リゾルバ")
|
|
664
|
-
class ItemResolverTest {
|
|
665
|
-
|
|
666
|
-
@Container
|
|
667
|
-
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
|
|
668
|
-
.withDatabaseName("production_test")
|
|
669
|
-
.withUsername("test")
|
|
670
|
-
.withPassword("test");
|
|
671
|
-
|
|
672
|
-
@DynamicPropertySource
|
|
673
|
-
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
674
|
-
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
675
|
-
registry.add("spring.datasource.username", postgres::getUsername);
|
|
676
|
-
registry.add("spring.datasource.password", postgres::getPassword);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
@Autowired
|
|
680
|
-
private GraphQlTester graphQlTester;
|
|
681
|
-
|
|
682
|
-
@Autowired
|
|
683
|
-
private ItemMapper itemMapper;
|
|
684
|
-
|
|
685
|
-
@BeforeEach
|
|
686
|
-
void setUp() {
|
|
687
|
-
itemMapper.deleteAll();
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
@Nested
|
|
691
|
-
@DisplayName("Query: item")
|
|
692
|
-
class ItemQueryTests {
|
|
693
|
-
|
|
694
|
-
@Test
|
|
695
|
-
@DisplayName("品目コードで品目を取得できる")
|
|
696
|
-
void shouldGetItemByCode() {
|
|
697
|
-
// Arrange
|
|
698
|
-
insertTestItem("PROD-001", "テスト製品", ItemCategory.PRODUCT);
|
|
699
|
-
|
|
700
|
-
// Act & Assert
|
|
701
|
-
graphQlTester.document("""
|
|
702
|
-
query {
|
|
703
|
-
item(itemCode: "PROD-001") {
|
|
704
|
-
itemCode
|
|
705
|
-
itemName
|
|
706
|
-
category
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
""")
|
|
710
|
-
.execute()
|
|
711
|
-
.path("item.itemCode").entity(String.class).isEqualTo("PROD-001")
|
|
712
|
-
.path("item.itemName").entity(String.class).isEqualTo("テスト製品")
|
|
713
|
-
.path("item.category").entity(String.class).isEqualTo("PRODUCT");
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
@Test
|
|
717
|
-
@DisplayName("存在しない品目は null を返す")
|
|
718
|
-
void shouldReturnNullForNonExistentItem() {
|
|
719
|
-
graphQlTester.document("""
|
|
720
|
-
query {
|
|
721
|
-
item(itemCode: "NOT-EXIST") {
|
|
722
|
-
itemCode
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
""")
|
|
726
|
-
.execute()
|
|
727
|
-
.path("item").valueIsNull();
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
@Test
|
|
731
|
-
@DisplayName("関連データを同時に取得できる")
|
|
732
|
-
void shouldGetItemWithRelatedData() {
|
|
733
|
-
insertTestItem("PROD-001", "テスト製品", ItemCategory.PRODUCT);
|
|
734
|
-
|
|
735
|
-
graphQlTester.document("""
|
|
736
|
-
query {
|
|
737
|
-
item(itemCode: "PROD-001") {
|
|
738
|
-
itemCode
|
|
739
|
-
itemName
|
|
740
|
-
bom {
|
|
741
|
-
childItemCode
|
|
742
|
-
requiredQuantity
|
|
743
|
-
}
|
|
744
|
-
stock {
|
|
745
|
-
quantity
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
""")
|
|
750
|
-
.execute()
|
|
751
|
-
.path("item.itemCode").entity(String.class).isEqualTo("PROD-001")
|
|
752
|
-
.path("item.bom").entityList(Object.class).hasSize(0);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
@Nested
|
|
757
|
-
@DisplayName("Query: items")
|
|
758
|
-
class ItemsQueryTests {
|
|
759
|
-
|
|
760
|
-
@Test
|
|
761
|
-
@DisplayName("品目一覧を取得できる")
|
|
762
|
-
void shouldGetItems() {
|
|
763
|
-
for (int i = 1; i <= 5; i++) {
|
|
764
|
-
insertTestItem("ITEM-" + String.format("%03d", i),
|
|
765
|
-
"品目" + i, ItemCategory.PRODUCT);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
graphQlTester.document("""
|
|
769
|
-
query {
|
|
770
|
-
items(page: 0, size: 10) {
|
|
771
|
-
edges {
|
|
772
|
-
node {
|
|
773
|
-
itemCode
|
|
774
|
-
itemName
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
pageInfo {
|
|
778
|
-
totalElements
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
""")
|
|
783
|
-
.execute()
|
|
784
|
-
.path("items.edges").entityList(Object.class).hasSize(5)
|
|
785
|
-
.path("items.pageInfo.totalElements").entity(Integer.class).isEqualTo(5);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
@Test
|
|
789
|
-
@DisplayName("カテゴリでフィルタリングできる")
|
|
790
|
-
void shouldFilterByCategory() {
|
|
791
|
-
insertTestItem("PROD-001", "製品1", ItemCategory.PRODUCT);
|
|
792
|
-
insertTestItem("PART-001", "部品1", ItemCategory.PART);
|
|
793
|
-
insertTestItem("PROD-002", "製品2", ItemCategory.PRODUCT);
|
|
794
|
-
|
|
795
|
-
graphQlTester.document("""
|
|
796
|
-
query {
|
|
797
|
-
items(category: PRODUCT, page: 0, size: 10) {
|
|
798
|
-
edges {
|
|
799
|
-
node {
|
|
800
|
-
itemCode
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
pageInfo {
|
|
804
|
-
totalElements
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
""")
|
|
809
|
-
.execute()
|
|
810
|
-
.path("items.pageInfo.totalElements").entity(Integer.class).isEqualTo(2);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
@Nested
|
|
815
|
-
@DisplayName("Mutation: createItem")
|
|
816
|
-
class CreateItemMutationTests {
|
|
817
|
-
|
|
818
|
-
@Test
|
|
819
|
-
@DisplayName("品目を登録できる")
|
|
820
|
-
void shouldCreateItem() {
|
|
821
|
-
graphQlTester.document("""
|
|
822
|
-
mutation {
|
|
823
|
-
createItem(input: {
|
|
824
|
-
itemCode: "NEW-001"
|
|
825
|
-
itemName: "新規品目"
|
|
826
|
-
category: PRODUCT
|
|
827
|
-
leadTime: 5
|
|
828
|
-
}) {
|
|
829
|
-
itemCode
|
|
830
|
-
itemName
|
|
831
|
-
category
|
|
832
|
-
leadTime
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
""")
|
|
836
|
-
.execute()
|
|
837
|
-
.path("createItem.itemCode").entity(String.class).isEqualTo("NEW-001")
|
|
838
|
-
.path("createItem.itemName").entity(String.class).isEqualTo("新規品目")
|
|
839
|
-
.path("createItem.category").entity(String.class).isEqualTo("PRODUCT")
|
|
840
|
-
.path("createItem.leadTime").entity(Integer.class).isEqualTo(5);
|
|
841
|
-
|
|
842
|
-
// DB に保存されていることを確認
|
|
843
|
-
var saved = itemMapper.findByCode("NEW-001");
|
|
844
|
-
assertThat(saved).isNotNull();
|
|
845
|
-
assertThat(saved.getItemName()).isEqualTo("新規品目");
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
@Test
|
|
849
|
-
@DisplayName("重複する品目コードはエラーになる")
|
|
850
|
-
void shouldFailForDuplicateCode() {
|
|
851
|
-
insertTestItem("DUP-001", "既存品目", ItemCategory.PRODUCT);
|
|
852
|
-
|
|
853
|
-
graphQlTester.document("""
|
|
854
|
-
mutation {
|
|
855
|
-
createItem(input: {
|
|
856
|
-
itemCode: "DUP-001"
|
|
857
|
-
itemName: "重複品目"
|
|
858
|
-
category: PRODUCT
|
|
859
|
-
}) {
|
|
860
|
-
itemCode
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
""")
|
|
864
|
-
.execute()
|
|
865
|
-
.errors()
|
|
866
|
-
.satisfy(errors -> assertThat(errors).hasSize(1));
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
private void insertTestItem(String code, String name, ItemCategory category) {
|
|
871
|
-
itemMapper.insert(Item.builder()
|
|
872
|
-
.itemCode(code)
|
|
873
|
-
.effectiveDate(LocalDate.of(2025, 1, 1))
|
|
874
|
-
.itemName(name)
|
|
875
|
-
.category(category)
|
|
876
|
-
.build());
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
</details>
|
|
882
|
-
|
|
883
|
-
#### Green: GraphQL リゾルバの実装
|
|
884
|
-
|
|
885
|
-
<details>
|
|
886
|
-
<summary>ItemResolver.java</summary>
|
|
887
|
-
|
|
888
|
-
```java
|
|
889
|
-
package com.example.production.infrastructure.graphql.resolver;
|
|
890
|
-
|
|
891
|
-
import com.example.production.application.port.in.*;
|
|
892
|
-
import com.example.production.domain.exception.DuplicateItemException;
|
|
893
|
-
import com.example.production.domain.exception.ItemNotFoundException;
|
|
894
|
-
import com.example.production.domain.model.item.Item;
|
|
895
|
-
import com.example.production.domain.model.item.ItemCategory;
|
|
896
|
-
import graphql.GraphQLError;
|
|
897
|
-
import graphql.GraphqlErrorBuilder;
|
|
898
|
-
import graphql.schema.DataFetchingEnvironment;
|
|
899
|
-
import org.springframework.graphql.data.method.annotation.*;
|
|
900
|
-
import org.springframework.graphql.execution.ErrorType;
|
|
901
|
-
import org.springframework.stereotype.Controller;
|
|
902
|
-
|
|
903
|
-
import java.util.List;
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* 品目 GraphQL リゾルバ実装
|
|
907
|
-
* 既存の ItemUseCase を Input Adapter として呼び出す
|
|
908
|
-
*/
|
|
909
|
-
@Controller
|
|
910
|
-
public class ItemResolver {
|
|
911
|
-
|
|
912
|
-
private final ItemUseCase itemUseCase; // 既存の UseCase を注入
|
|
913
|
-
|
|
914
|
-
public ItemResolver(ItemUseCase itemUseCase) {
|
|
915
|
-
this.itemUseCase = itemUseCase;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// ========== Query ==========
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* 品目取得
|
|
922
|
-
*/
|
|
923
|
-
@QueryMapping
|
|
924
|
-
public Item item(@Argument String itemCode) {
|
|
925
|
-
try {
|
|
926
|
-
return itemUseCase.getItemByCode(itemCode);
|
|
927
|
-
} catch (ItemNotFoundException e) {
|
|
928
|
-
return null; // GraphQL では null を返す
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
/**
|
|
933
|
-
* 品目一覧(ページネーション付き)
|
|
934
|
-
*/
|
|
935
|
-
@QueryMapping
|
|
936
|
-
public ItemConnection items(
|
|
937
|
-
@Argument ItemCategory category,
|
|
938
|
-
@Argument Integer page,
|
|
939
|
-
@Argument Integer size) {
|
|
940
|
-
|
|
941
|
-
int pageNum = page != null ? page : 0;
|
|
942
|
-
int pageSize = size != null ? size : 20;
|
|
943
|
-
|
|
944
|
-
List<Item> items;
|
|
945
|
-
int totalElements;
|
|
946
|
-
|
|
947
|
-
if (category != null) {
|
|
948
|
-
items = itemUseCase.getItemsByCategory(category);
|
|
949
|
-
} else {
|
|
950
|
-
items = itemUseCase.getAllItems();
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
totalElements = items.size();
|
|
954
|
-
|
|
955
|
-
// ページング処理
|
|
956
|
-
int start = pageNum * pageSize;
|
|
957
|
-
int end = Math.min(start + pageSize, items.size());
|
|
958
|
-
List<Item> pagedItems = items.subList(start, end);
|
|
959
|
-
|
|
960
|
-
return ItemConnection.builder()
|
|
961
|
-
.edges(pagedItems.stream()
|
|
962
|
-
.map(item -> ItemEdge.builder()
|
|
963
|
-
.node(item)
|
|
964
|
-
.cursor(item.getItemCode())
|
|
965
|
-
.build())
|
|
966
|
-
.toList())
|
|
967
|
-
.pageInfo(PageInfo.builder()
|
|
968
|
-
.hasNextPage(end < totalElements)
|
|
969
|
-
.hasPreviousPage(pageNum > 0)
|
|
970
|
-
.totalElements(totalElements)
|
|
971
|
-
.totalPages((int) Math.ceil((double) totalElements / pageSize))
|
|
972
|
-
.build())
|
|
973
|
-
.build();
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// ========== Mutation ==========
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* 品目登録
|
|
980
|
-
*/
|
|
981
|
-
@MutationMapping
|
|
982
|
-
public Item createItem(@Argument CreateItemInput input) {
|
|
983
|
-
CreateItemCommand command = CreateItemCommand.builder()
|
|
984
|
-
.itemCode(input.getItemCode())
|
|
985
|
-
.itemName(input.getItemName())
|
|
986
|
-
.category(input.getCategory())
|
|
987
|
-
.itemGroupCode(input.getItemGroupCode())
|
|
988
|
-
.unitCode(input.getUnitCode())
|
|
989
|
-
.locationCode(input.getLocationCode())
|
|
990
|
-
.leadTime(input.getLeadTime())
|
|
991
|
-
.safetyStock(input.getSafetyStock())
|
|
992
|
-
.build();
|
|
993
|
-
|
|
994
|
-
return itemUseCase.createItem(command);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
/**
|
|
998
|
-
* 品目更新
|
|
999
|
-
*/
|
|
1000
|
-
@MutationMapping
|
|
1001
|
-
public Item updateItem(@Argument UpdateItemInput input) {
|
|
1002
|
-
UpdateItemCommand command = UpdateItemCommand.builder()
|
|
1003
|
-
.itemCode(input.getItemCode())
|
|
1004
|
-
.itemName(input.getItemName())
|
|
1005
|
-
.category(input.getCategory())
|
|
1006
|
-
.leadTime(input.getLeadTime())
|
|
1007
|
-
.safetyStock(input.getSafetyStock())
|
|
1008
|
-
.build();
|
|
1009
|
-
|
|
1010
|
-
return itemUseCase.updateItem(command);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* 品目削除
|
|
1015
|
-
*/
|
|
1016
|
-
@MutationMapping
|
|
1017
|
-
public boolean deleteItem(@Argument String itemCode) {
|
|
1018
|
-
try {
|
|
1019
|
-
itemUseCase.deleteItem(itemCode);
|
|
1020
|
-
return true;
|
|
1021
|
-
} catch (ItemNotFoundException e) {
|
|
1022
|
-
return false;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// ========== 例外ハンドリング ==========
|
|
1027
|
-
|
|
1028
|
-
@GraphQlExceptionHandler
|
|
1029
|
-
public GraphQLError handleDuplicateItem(DuplicateItemException ex, DataFetchingEnvironment env) {
|
|
1030
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1031
|
-
.errorType(ErrorType.BAD_REQUEST)
|
|
1032
|
-
.message(ex.getMessage())
|
|
1033
|
-
.build();
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
@GraphQlExceptionHandler
|
|
1037
|
-
public GraphQLError handleItemNotFound(ItemNotFoundException ex, DataFetchingEnvironment env) {
|
|
1038
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1039
|
-
.errorType(ErrorType.NOT_FOUND)
|
|
1040
|
-
.message(ex.getMessage())
|
|
1041
|
-
.build();
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
</details>
|
|
1047
|
-
|
|
1048
|
-
<details>
|
|
1049
|
-
<summary>補助クラス(ItemConnection, PageInfo 等)</summary>
|
|
1050
|
-
|
|
1051
|
-
```java
|
|
1052
|
-
package com.example.production.infrastructure.graphql.resolver;
|
|
1053
|
-
|
|
1054
|
-
import com.example.production.domain.model.item.Item;
|
|
1055
|
-
import com.example.production.domain.model.item.ItemCategory;
|
|
1056
|
-
import lombok.Builder;
|
|
1057
|
-
import lombok.Data;
|
|
1058
|
-
|
|
1059
|
-
import java.util.List;
|
|
1060
|
-
|
|
1061
|
-
@Data
|
|
1062
|
-
@Builder
|
|
1063
|
-
public class ItemConnection {
|
|
1064
|
-
private List<ItemEdge> edges;
|
|
1065
|
-
private PageInfo pageInfo;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
@Data
|
|
1069
|
-
@Builder
|
|
1070
|
-
public class ItemEdge {
|
|
1071
|
-
private Item node;
|
|
1072
|
-
private String cursor;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
@Data
|
|
1076
|
-
@Builder
|
|
1077
|
-
public class PageInfo {
|
|
1078
|
-
private boolean hasNextPage;
|
|
1079
|
-
private boolean hasPreviousPage;
|
|
1080
|
-
private int totalElements;
|
|
1081
|
-
private int totalPages;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
@Data
|
|
1085
|
-
public class CreateItemInput {
|
|
1086
|
-
private String itemCode;
|
|
1087
|
-
private String itemName;
|
|
1088
|
-
private ItemCategory category;
|
|
1089
|
-
private String itemGroupCode;
|
|
1090
|
-
private String unitCode;
|
|
1091
|
-
private String locationCode;
|
|
1092
|
-
private Integer leadTime;
|
|
1093
|
-
private Integer safetyStock;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
@Data
|
|
1097
|
-
public class UpdateItemInput {
|
|
1098
|
-
private String itemCode;
|
|
1099
|
-
private String itemName;
|
|
1100
|
-
private ItemCategory category;
|
|
1101
|
-
private Integer leadTime;
|
|
1102
|
-
private Integer safetyStock;
|
|
1103
|
-
}
|
|
1104
|
-
```
|
|
1105
|
-
|
|
1106
|
-
</details>
|
|
1107
|
-
|
|
1108
|
-
### 47.3 N+1 問題対策(DataLoader)
|
|
1109
|
-
|
|
1110
|
-
GraphQL では関連データの取得時に N+1 問題が発生しやすいため、DataLoader を使用します。
|
|
1111
|
-
|
|
1112
|
-
<details>
|
|
1113
|
-
<summary>DataLoaderConfig.java</summary>
|
|
1114
|
-
|
|
1115
|
-
```java
|
|
1116
|
-
package com.example.production.infrastructure.graphql.dataloader;
|
|
1117
|
-
|
|
1118
|
-
import com.example.production.application.port.in.ItemUseCase;
|
|
1119
|
-
import com.example.production.domain.model.item.Item;
|
|
1120
|
-
import org.dataloader.DataLoader;
|
|
1121
|
-
import org.dataloader.DataLoaderFactory;
|
|
1122
|
-
import org.springframework.context.annotation.Bean;
|
|
1123
|
-
import org.springframework.context.annotation.Configuration;
|
|
1124
|
-
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
|
1125
|
-
|
|
1126
|
-
import java.util.List;
|
|
1127
|
-
import java.util.Map;
|
|
1128
|
-
import java.util.function.Function;
|
|
1129
|
-
import java.util.stream.Collectors;
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* DataLoader 設定
|
|
1133
|
-
* N+1 問題を解決するためのバッチローディング
|
|
1134
|
-
*/
|
|
1135
|
-
@Configuration
|
|
1136
|
-
public class DataLoaderConfig {
|
|
1137
|
-
|
|
1138
|
-
private final ItemUseCase itemUseCase;
|
|
1139
|
-
|
|
1140
|
-
public DataLoaderConfig(ItemUseCase itemUseCase) {
|
|
1141
|
-
this.itemUseCase = itemUseCase;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
@Bean
|
|
1145
|
-
public BatchLoaderRegistry batchLoaderRegistry(BatchLoaderRegistry registry) {
|
|
1146
|
-
// 品目 DataLoader
|
|
1147
|
-
registry.forTypePair(String.class, Item.class)
|
|
1148
|
-
.registerBatchLoader((itemCodes, env) -> {
|
|
1149
|
-
// バッチで品目を取得(1回のクエリで複数取得)
|
|
1150
|
-
List<Item> items = itemUseCase.getItemsByCodes(itemCodes);
|
|
1151
|
-
|
|
1152
|
-
Map<String, Item> itemMap = items.stream()
|
|
1153
|
-
.collect(Collectors.toMap(Item::getItemCode, Function.identity()));
|
|
1154
|
-
|
|
1155
|
-
return reactor.core.publisher.Flux.fromIterable(itemCodes)
|
|
1156
|
-
.map(code -> itemMap.get(code));
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
return registry;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
</details>
|
|
1165
|
-
|
|
1166
|
-
<details>
|
|
1167
|
-
<summary>PurchaseOrderDetailResolver.java(DataLoader 使用)</summary>
|
|
1168
|
-
|
|
1169
|
-
```java
|
|
1170
|
-
package com.example.production.infrastructure.graphql.resolver;
|
|
1171
|
-
|
|
1172
|
-
import com.example.production.domain.model.item.Item;
|
|
1173
|
-
import com.example.production.domain.model.purchase.PurchaseOrderDetail;
|
|
1174
|
-
import org.dataloader.DataLoader;
|
|
1175
|
-
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
|
1176
|
-
import org.springframework.stereotype.Controller;
|
|
1177
|
-
|
|
1178
|
-
import java.util.concurrent.CompletableFuture;
|
|
1179
|
-
|
|
1180
|
-
/**
|
|
1181
|
-
* 発注明細リゾルバ
|
|
1182
|
-
* DataLoader を使って品目を効率的に取得
|
|
1183
|
-
*/
|
|
1184
|
-
@Controller
|
|
1185
|
-
public class PurchaseOrderDetailResolver {
|
|
1186
|
-
|
|
1187
|
-
/**
|
|
1188
|
-
* 発注明細から品目を取得(DataLoader 使用)
|
|
1189
|
-
*/
|
|
1190
|
-
@SchemaMapping(typeName = "PurchaseOrderDetail", field = "item")
|
|
1191
|
-
public CompletableFuture<Item> item(
|
|
1192
|
-
PurchaseOrderDetail detail,
|
|
1193
|
-
DataLoader<String, Item> itemDataLoader) {
|
|
1194
|
-
|
|
1195
|
-
// DataLoader 経由で取得(バッチ処理される)
|
|
1196
|
-
return itemDataLoader.load(detail.getItemCode());
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
```
|
|
1200
|
-
|
|
1201
|
-
</details>
|
|
1202
|
-
|
|
1203
|
-
### 47.4 BOM GraphQL リゾルバ
|
|
1204
|
-
|
|
1205
|
-
<details>
|
|
1206
|
-
<summary>BomResolver.java</summary>
|
|
1207
|
-
|
|
1208
|
-
```java
|
|
1209
|
-
package com.example.production.infrastructure.graphql.resolver;
|
|
1210
|
-
|
|
1211
|
-
import com.example.production.application.service.BomNode;
|
|
1212
|
-
import com.example.production.application.service.BomService;
|
|
1213
|
-
import com.example.production.application.service.WhereUsedResult;
|
|
1214
|
-
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1215
|
-
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
1216
|
-
import org.springframework.stereotype.Controller;
|
|
1217
|
-
|
|
1218
|
-
import java.util.List;
|
|
1219
|
-
|
|
1220
|
-
/**
|
|
1221
|
-
* BOM GraphQL リゾルバ実装
|
|
1222
|
-
* 既存の BomService を Input Adapter として呼び出す
|
|
1223
|
-
*/
|
|
1224
|
-
@Controller
|
|
1225
|
-
public class BomResolver {
|
|
1226
|
-
|
|
1227
|
-
private final BomService bomService; // 既存のサービスを注入
|
|
1228
|
-
|
|
1229
|
-
public BomResolver(BomService bomService) {
|
|
1230
|
-
this.bomService = bomService;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
/**
|
|
1234
|
-
* 部品展開(BOM ツリー)
|
|
1235
|
-
*/
|
|
1236
|
-
@QueryMapping
|
|
1237
|
-
public BomNode bomTree(@Argument String itemCode) {
|
|
1238
|
-
return bomService.explodeBom(itemCode);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
/**
|
|
1242
|
-
* 使用先照会
|
|
1243
|
-
*/
|
|
1244
|
-
@QueryMapping
|
|
1245
|
-
public List<WhereUsedResult> whereUsed(@Argument String itemCode) {
|
|
1246
|
-
return bomService.whereUsed(itemCode);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
```
|
|
1250
|
-
|
|
1251
|
-
</details>
|
|
1252
|
-
|
|
1253
|
-
---
|
|
1254
|
-
|
|
1255
|
-
## 第48章:トランザクション API の実装
|
|
1256
|
-
|
|
1257
|
-
### 48.1 発注 GraphQL リゾルバ
|
|
1258
|
-
|
|
1259
|
-
<details>
|
|
1260
|
-
<summary>PurchaseOrderResolver.java</summary>
|
|
1261
|
-
|
|
1262
|
-
```java
|
|
1263
|
-
package com.example.production.infrastructure.graphql.resolver;
|
|
1264
|
-
|
|
1265
|
-
import com.example.production.application.port.in.*;
|
|
1266
|
-
import com.example.production.domain.model.purchase.PurchaseOrder;
|
|
1267
|
-
import org.springframework.graphql.data.method.annotation.*;
|
|
1268
|
-
import org.springframework.stereotype.Controller;
|
|
1269
|
-
|
|
1270
|
-
import java.math.BigDecimal;
|
|
1271
|
-
import java.time.LocalDate;
|
|
1272
|
-
import java.util.List;
|
|
1273
|
-
|
|
1274
|
-
/**
|
|
1275
|
-
* 発注 GraphQL リゾルバ実装
|
|
1276
|
-
* 既存の PurchaseOrderUseCase を Input Adapter として呼び出す
|
|
1277
|
-
*/
|
|
1278
|
-
@Controller
|
|
1279
|
-
public class PurchaseOrderResolver {
|
|
1280
|
-
|
|
1281
|
-
private final PurchaseOrderUseCase useCase; // 既存の UseCase
|
|
1282
|
-
|
|
1283
|
-
public PurchaseOrderResolver(PurchaseOrderUseCase useCase) {
|
|
1284
|
-
this.useCase = useCase;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// ========== Query ==========
|
|
1288
|
-
|
|
1289
|
-
@QueryMapping
|
|
1290
|
-
public PurchaseOrder purchaseOrder(@Argument String orderNumber) {
|
|
1291
|
-
return useCase.getOrder(orderNumber);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
@QueryMapping
|
|
1295
|
-
public PurchaseOrderConnection purchaseOrders(
|
|
1296
|
-
@Argument String status,
|
|
1297
|
-
@Argument Integer page,
|
|
1298
|
-
@Argument Integer size) {
|
|
1299
|
-
|
|
1300
|
-
List<PurchaseOrder> orders = useCase.getAllOrders();
|
|
1301
|
-
|
|
1302
|
-
// ページネーション処理(省略)
|
|
1303
|
-
return PurchaseOrderConnection.builder()
|
|
1304
|
-
.edges(orders.stream()
|
|
1305
|
-
.map(order -> PurchaseOrderEdge.builder()
|
|
1306
|
-
.node(order)
|
|
1307
|
-
.cursor(order.getOrderNumber())
|
|
1308
|
-
.build())
|
|
1309
|
-
.toList())
|
|
1310
|
-
.pageInfo(PageInfo.builder()
|
|
1311
|
-
.totalElements(orders.size())
|
|
1312
|
-
.build())
|
|
1313
|
-
.build();
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// ========== Mutation ==========
|
|
1317
|
-
|
|
1318
|
-
@MutationMapping
|
|
1319
|
-
public PurchaseOrder createPurchaseOrder(@Argument CreatePurchaseOrderInput input) {
|
|
1320
|
-
CreatePurchaseOrderCommand command = CreatePurchaseOrderCommand.builder()
|
|
1321
|
-
.supplierCode(input.getSupplierCode())
|
|
1322
|
-
.details(input.getDetails().stream()
|
|
1323
|
-
.map(d -> CreatePurchaseOrderCommand.PurchaseOrderDetailCommand.builder()
|
|
1324
|
-
.itemCode(d.getItemCode())
|
|
1325
|
-
.orderQuantity(d.getOrderQuantity())
|
|
1326
|
-
.unitPrice(d.getUnitPrice())
|
|
1327
|
-
.deliveryDate(d.getDeliveryDate())
|
|
1328
|
-
.build())
|
|
1329
|
-
.toList())
|
|
1330
|
-
.build();
|
|
1331
|
-
|
|
1332
|
-
return useCase.createOrder(command);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
@MutationMapping
|
|
1336
|
-
public PurchaseOrder confirmPurchaseOrder(@Argument String orderNumber) {
|
|
1337
|
-
return useCase.confirmOrder(orderNumber);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
@MutationMapping
|
|
1341
|
-
public boolean cancelPurchaseOrder(@Argument String orderNumber) {
|
|
1342
|
-
useCase.cancelOrder(orderNumber);
|
|
1343
|
-
return true;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
@MutationMapping
|
|
1347
|
-
public ReceivingResult recordReceiving(@Argument RecordReceivingInput input) {
|
|
1348
|
-
var result = useCase.recordReceiving(
|
|
1349
|
-
input.getOrderNumber(),
|
|
1350
|
-
input.getLineNumber(),
|
|
1351
|
-
input.getReceivedQuantity(),
|
|
1352
|
-
input.getReceivingDate()
|
|
1353
|
-
);
|
|
1354
|
-
|
|
1355
|
-
return ReceivingResult.builder()
|
|
1356
|
-
.orderNumber(input.getOrderNumber())
|
|
1357
|
-
.lineNumber(input.getLineNumber())
|
|
1358
|
-
.totalReceived(result.getTotalReceived())
|
|
1359
|
-
.isCompleted(result.isCompleted())
|
|
1360
|
-
.build();
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// 入力型
|
|
1365
|
-
@lombok.Data
|
|
1366
|
-
class CreatePurchaseOrderInput {
|
|
1367
|
-
private String supplierCode;
|
|
1368
|
-
private List<CreateOrderDetailInput> details;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
@lombok.Data
|
|
1372
|
-
class CreateOrderDetailInput {
|
|
1373
|
-
private String itemCode;
|
|
1374
|
-
private BigDecimal orderQuantity;
|
|
1375
|
-
private BigDecimal unitPrice;
|
|
1376
|
-
private LocalDate deliveryDate;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
@lombok.Data
|
|
1380
|
-
class RecordReceivingInput {
|
|
1381
|
-
private String orderNumber;
|
|
1382
|
-
private int lineNumber;
|
|
1383
|
-
private BigDecimal receivedQuantity;
|
|
1384
|
-
private LocalDate receivingDate;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
@lombok.Data
|
|
1388
|
-
@lombok.Builder
|
|
1389
|
-
class ReceivingResult {
|
|
1390
|
-
private String orderNumber;
|
|
1391
|
-
private int lineNumber;
|
|
1392
|
-
private BigDecimal totalReceived;
|
|
1393
|
-
private boolean isCompleted;
|
|
1394
|
-
}
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
</details>
|
|
1398
|
-
|
|
1399
|
-
### 48.2 MRP GraphQL リゾルバ
|
|
1400
|
-
|
|
1401
|
-
<details>
|
|
1402
|
-
<summary>MrpResolver.java</summary>
|
|
1403
|
-
|
|
1404
|
-
```java
|
|
1405
|
-
package com.example.production.infrastructure.graphql.resolver;
|
|
1406
|
-
|
|
1407
|
-
import com.example.production.application.service.MrpService;
|
|
1408
|
-
import org.springframework.graphql.data.method.annotation.*;
|
|
1409
|
-
import org.springframework.stereotype.Controller;
|
|
1410
|
-
|
|
1411
|
-
import java.time.LocalDate;
|
|
1412
|
-
import java.util.UUID;
|
|
1413
|
-
|
|
1414
|
-
/**
|
|
1415
|
-
* MRP GraphQL リゾルバ実装
|
|
1416
|
-
* 既存の MrpService を Input Adapter として呼び出す
|
|
1417
|
-
*/
|
|
1418
|
-
@Controller
|
|
1419
|
-
public class MrpResolver {
|
|
1420
|
-
|
|
1421
|
-
private final MrpService mrpService; // 既存のサービス
|
|
1422
|
-
|
|
1423
|
-
public MrpResolver(MrpService mrpService) {
|
|
1424
|
-
this.mrpService = mrpService;
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
/**
|
|
1428
|
-
* MRP 実行
|
|
1429
|
-
*/
|
|
1430
|
-
@MutationMapping
|
|
1431
|
-
public MrpResult executeMrp(@Argument ExecuteMrpInput input) {
|
|
1432
|
-
String executionId = UUID.randomUUID().toString();
|
|
1433
|
-
|
|
1434
|
-
long startTime = System.currentTimeMillis();
|
|
1435
|
-
var result = mrpService.execute(input.getStartDate(), input.getEndDate());
|
|
1436
|
-
long executionTime = System.currentTimeMillis() - startTime;
|
|
1437
|
-
|
|
1438
|
-
return MrpResult.builder()
|
|
1439
|
-
.executionId(executionId)
|
|
1440
|
-
.periodStart(input.getStartDate())
|
|
1441
|
-
.periodEnd(input.getEndDate())
|
|
1442
|
-
.plannedOrders(result.getPlannedOrders())
|
|
1443
|
-
.shortageItems(result.getShortageItems())
|
|
1444
|
-
.statistics(MrpStatistics.builder()
|
|
1445
|
-
.totalItemsProcessed(result.getPlannedOrders().size())
|
|
1446
|
-
.shortageItemCount(result.getShortageItems().size())
|
|
1447
|
-
.executionTimeMs(executionTime)
|
|
1448
|
-
.build())
|
|
1449
|
-
.build();
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
@lombok.Data
|
|
1454
|
-
class ExecuteMrpInput {
|
|
1455
|
-
private LocalDate startDate;
|
|
1456
|
-
private LocalDate endDate;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
@lombok.Data
|
|
1460
|
-
@lombok.Builder
|
|
1461
|
-
class MrpResult {
|
|
1462
|
-
private String executionId;
|
|
1463
|
-
private LocalDate periodStart;
|
|
1464
|
-
private LocalDate periodEnd;
|
|
1465
|
-
private java.util.List<?> plannedOrders;
|
|
1466
|
-
private java.util.List<?> shortageItems;
|
|
1467
|
-
private MrpStatistics statistics;
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
@lombok.Data
|
|
1471
|
-
@lombok.Builder
|
|
1472
|
-
class MrpStatistics {
|
|
1473
|
-
private int totalItemsProcessed;
|
|
1474
|
-
private int purchaseOrderCount;
|
|
1475
|
-
private int productionOrderCount;
|
|
1476
|
-
private int shortageItemCount;
|
|
1477
|
-
private long executionTimeMs;
|
|
1478
|
-
}
|
|
1479
|
-
```
|
|
1480
|
-
|
|
1481
|
-
</details>
|
|
1482
|
-
|
|
1483
|
-
### 48.3 Subscription(リアルタイム更新)
|
|
1484
|
-
|
|
1485
|
-
<details>
|
|
1486
|
-
<summary>SubscriptionResolver.java</summary>
|
|
1487
|
-
|
|
1488
|
-
```java
|
|
1489
|
-
package com.example.production.infrastructure.graphql.subscription;
|
|
1490
|
-
|
|
1491
|
-
import com.example.production.domain.model.purchase.PurchaseOrder;
|
|
1492
|
-
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1493
|
-
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
|
1494
|
-
import org.springframework.stereotype.Controller;
|
|
1495
|
-
import reactor.core.publisher.Flux;
|
|
1496
|
-
|
|
1497
|
-
import java.time.Duration;
|
|
1498
|
-
|
|
1499
|
-
/**
|
|
1500
|
-
* GraphQL Subscription リゾルバ
|
|
1501
|
-
*/
|
|
1502
|
-
@Controller
|
|
1503
|
-
public class SubscriptionResolver {
|
|
1504
|
-
|
|
1505
|
-
private final OrderEventPublisher orderEventPublisher;
|
|
1506
|
-
private final MrpProgressPublisher mrpProgressPublisher;
|
|
1507
|
-
|
|
1508
|
-
public SubscriptionResolver(
|
|
1509
|
-
OrderEventPublisher orderEventPublisher,
|
|
1510
|
-
MrpProgressPublisher mrpProgressPublisher) {
|
|
1511
|
-
this.orderEventPublisher = orderEventPublisher;
|
|
1512
|
-
this.mrpProgressPublisher = mrpProgressPublisher;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
/**
|
|
1516
|
-
* MRP 進捗の購読
|
|
1517
|
-
*/
|
|
1518
|
-
@SubscriptionMapping
|
|
1519
|
-
public Flux<MrpProgress> mrpProgress(@Argument String executionId) {
|
|
1520
|
-
return mrpProgressPublisher.subscribe(executionId);
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
/**
|
|
1524
|
-
* 発注ステータス変更の購読
|
|
1525
|
-
*/
|
|
1526
|
-
@SubscriptionMapping
|
|
1527
|
-
public Flux<PurchaseOrder> orderStatusChanged(@Argument String orderNumber) {
|
|
1528
|
-
return orderEventPublisher.subscribe(orderNumber);
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
/**
|
|
1532
|
-
* 在庫変動の購読
|
|
1533
|
-
*/
|
|
1534
|
-
@SubscriptionMapping
|
|
1535
|
-
public Flux<StockChange> stockChanged(@Argument String itemCode) {
|
|
1536
|
-
// ダミー実装(5秒ごとに在庫変動をシミュレート)
|
|
1537
|
-
return Flux.interval(Duration.ofSeconds(5))
|
|
1538
|
-
.map(i -> StockChange.builder()
|
|
1539
|
-
.itemCode(itemCode != null ? itemCode : "ITEM-001")
|
|
1540
|
-
.changeType("RECEIVE")
|
|
1541
|
-
.quantity(java.math.BigDecimal.valueOf(10))
|
|
1542
|
-
.build());
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
@lombok.Data
|
|
1547
|
-
@lombok.Builder
|
|
1548
|
-
class StockChange {
|
|
1549
|
-
private String itemCode;
|
|
1550
|
-
private String changeType;
|
|
1551
|
-
private java.math.BigDecimal quantity;
|
|
1552
|
-
}
|
|
1553
|
-
```
|
|
1554
|
-
|
|
1555
|
-
</details>
|
|
1556
|
-
|
|
1557
|
-
<details>
|
|
1558
|
-
<summary>MrpProgressPublisher.java(イベントパブリッシャー)</summary>
|
|
1559
|
-
|
|
1560
|
-
```java
|
|
1561
|
-
package com.example.production.infrastructure.graphql.subscription;
|
|
1562
|
-
|
|
1563
|
-
import org.springframework.stereotype.Component;
|
|
1564
|
-
import reactor.core.publisher.Flux;
|
|
1565
|
-
import reactor.core.publisher.Sinks;
|
|
1566
|
-
|
|
1567
|
-
import java.util.concurrent.ConcurrentHashMap;
|
|
1568
|
-
|
|
1569
|
-
/**
|
|
1570
|
-
* MRP 進捗イベントパブリッシャー
|
|
1571
|
-
*/
|
|
1572
|
-
@Component
|
|
1573
|
-
public class MrpProgressPublisher {
|
|
1574
|
-
|
|
1575
|
-
private final ConcurrentHashMap<String, Sinks.Many<MrpProgress>> sinks =
|
|
1576
|
-
new ConcurrentHashMap<>();
|
|
1577
|
-
|
|
1578
|
-
/**
|
|
1579
|
-
* 進捗を購読
|
|
1580
|
-
*/
|
|
1581
|
-
public Flux<MrpProgress> subscribe(String executionId) {
|
|
1582
|
-
Sinks.Many<MrpProgress> sink = sinks.computeIfAbsent(
|
|
1583
|
-
executionId,
|
|
1584
|
-
k -> Sinks.many().multicast().onBackpressureBuffer()
|
|
1585
|
-
);
|
|
1586
|
-
return sink.asFlux();
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
/**
|
|
1590
|
-
* 進捗を発行
|
|
1591
|
-
*/
|
|
1592
|
-
public void publish(String executionId, MrpProgress progress) {
|
|
1593
|
-
Sinks.Many<MrpProgress> sink = sinks.get(executionId);
|
|
1594
|
-
if (sink != null) {
|
|
1595
|
-
sink.tryEmitNext(progress);
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
/**
|
|
1600
|
-
* 完了
|
|
1601
|
-
*/
|
|
1602
|
-
public void complete(String executionId) {
|
|
1603
|
-
Sinks.Many<MrpProgress> sink = sinks.remove(executionId);
|
|
1604
|
-
if (sink != null) {
|
|
1605
|
-
sink.tryEmitComplete();
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
@lombok.Data
|
|
1611
|
-
@lombok.Builder
|
|
1612
|
-
class MrpProgress {
|
|
1613
|
-
private String executionId;
|
|
1614
|
-
private String phase;
|
|
1615
|
-
private int current;
|
|
1616
|
-
private int total;
|
|
1617
|
-
private String message;
|
|
1618
|
-
}
|
|
1619
|
-
```
|
|
1620
|
-
|
|
1621
|
-
</details>
|
|
1622
|
-
|
|
1623
|
-
---
|
|
1624
|
-
|
|
1625
|
-
## 第49章:エラーハンドリングとベストプラクティス
|
|
1626
|
-
|
|
1627
|
-
### 49.1 グローバル例外ハンドラ
|
|
1628
|
-
|
|
1629
|
-
<details>
|
|
1630
|
-
<summary>GraphQLExceptionHandler.java</summary>
|
|
1631
|
-
|
|
1632
|
-
```java
|
|
1633
|
-
package com.example.production.infrastructure.graphql;
|
|
1634
|
-
|
|
1635
|
-
import com.example.production.domain.exception.*;
|
|
1636
|
-
import graphql.GraphQLError;
|
|
1637
|
-
import graphql.GraphqlErrorBuilder;
|
|
1638
|
-
import graphql.schema.DataFetchingEnvironment;
|
|
1639
|
-
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
|
|
1640
|
-
import org.springframework.graphql.execution.ErrorType;
|
|
1641
|
-
import org.springframework.stereotype.Component;
|
|
1642
|
-
|
|
1643
|
-
/**
|
|
1644
|
-
* GraphQL グローバル例外ハンドラ
|
|
1645
|
-
*/
|
|
1646
|
-
@Component
|
|
1647
|
-
public class GraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {
|
|
1648
|
-
|
|
1649
|
-
@Override
|
|
1650
|
-
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
|
|
1651
|
-
if (ex instanceof ItemNotFoundException) {
|
|
1652
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1653
|
-
.errorType(ErrorType.NOT_FOUND)
|
|
1654
|
-
.message(ex.getMessage())
|
|
1655
|
-
.build();
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
if (ex instanceof DuplicateItemException) {
|
|
1659
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1660
|
-
.errorType(ErrorType.BAD_REQUEST)
|
|
1661
|
-
.message(ex.getMessage())
|
|
1662
|
-
.build();
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
if (ex instanceof OrderAlreadyConfirmedException) {
|
|
1666
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1667
|
-
.errorType(ErrorType.BAD_REQUEST)
|
|
1668
|
-
.message(ex.getMessage())
|
|
1669
|
-
.build();
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if (ex instanceof IllegalArgumentException) {
|
|
1673
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1674
|
-
.errorType(ErrorType.BAD_REQUEST)
|
|
1675
|
-
.message(ex.getMessage())
|
|
1676
|
-
.build();
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// 未知のエラー
|
|
1680
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1681
|
-
.errorType(ErrorType.INTERNAL_ERROR)
|
|
1682
|
-
.message("内部エラーが発生しました")
|
|
1683
|
-
.build();
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
```
|
|
1687
|
-
|
|
1688
|
-
</details>
|
|
1689
|
-
|
|
1690
|
-
### 49.2 入力バリデーション
|
|
1691
|
-
|
|
1692
|
-
<details>
|
|
1693
|
-
<summary>ValidationExceptionHandler.java</summary>
|
|
1694
|
-
|
|
1695
|
-
```java
|
|
1696
|
-
package com.example.production.infrastructure.graphql.validation;
|
|
1697
|
-
|
|
1698
|
-
import graphql.GraphQLError;
|
|
1699
|
-
import graphql.GraphqlErrorBuilder;
|
|
1700
|
-
import graphql.schema.DataFetchingEnvironment;
|
|
1701
|
-
import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler;
|
|
1702
|
-
import org.springframework.graphql.execution.ErrorType;
|
|
1703
|
-
import org.springframework.stereotype.Controller;
|
|
1704
|
-
|
|
1705
|
-
import jakarta.validation.ConstraintViolation;
|
|
1706
|
-
import jakarta.validation.ConstraintViolationException;
|
|
1707
|
-
import java.util.List;
|
|
1708
|
-
import java.util.stream.Collectors;
|
|
1709
|
-
|
|
1710
|
-
/**
|
|
1711
|
-
* バリデーション例外ハンドラ
|
|
1712
|
-
*/
|
|
1713
|
-
@Controller
|
|
1714
|
-
public class ValidationExceptionHandler {
|
|
1715
|
-
|
|
1716
|
-
@GraphQlExceptionHandler
|
|
1717
|
-
public List<GraphQLError> handleValidation(
|
|
1718
|
-
ConstraintViolationException ex,
|
|
1719
|
-
DataFetchingEnvironment env) {
|
|
1720
|
-
|
|
1721
|
-
return ex.getConstraintViolations().stream()
|
|
1722
|
-
.map(violation -> toGraphQLError(violation, env))
|
|
1723
|
-
.collect(Collectors.toList());
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
private GraphQLError toGraphQLError(
|
|
1727
|
-
ConstraintViolation<?> violation,
|
|
1728
|
-
DataFetchingEnvironment env) {
|
|
1729
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1730
|
-
.errorType(ErrorType.BAD_REQUEST)
|
|
1731
|
-
.message(String.format("%s: %s",
|
|
1732
|
-
violation.getPropertyPath(),
|
|
1733
|
-
violation.getMessage()))
|
|
1734
|
-
.build();
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
```
|
|
1738
|
-
|
|
1739
|
-
</details>
|
|
1740
|
-
|
|
1741
|
-
### 49.3 設定ファイル
|
|
1742
|
-
|
|
1743
|
-
<details>
|
|
1744
|
-
<summary>application.yml(GraphQL 追加設定)</summary>
|
|
1745
|
-
|
|
1746
|
-
```yaml
|
|
1747
|
-
spring:
|
|
1748
|
-
graphql:
|
|
1749
|
-
path: /graphql
|
|
1750
|
-
graphiql:
|
|
1751
|
-
enabled: true # 開発時に GraphiQL UI を有効化
|
|
1752
|
-
path: /graphiql
|
|
1753
|
-
websocket:
|
|
1754
|
-
path: /graphql # Subscription 用 WebSocket
|
|
1755
|
-
schema:
|
|
1756
|
-
locations: classpath:graphql/
|
|
1757
|
-
printer:
|
|
1758
|
-
enabled: true # スキーマ出力
|
|
1759
|
-
|
|
1760
|
-
# 既存の設定はそのまま
|
|
1761
|
-
datasource:
|
|
1762
|
-
url: jdbc:postgresql://localhost:5432/production
|
|
1763
|
-
username: postgres
|
|
1764
|
-
password: postgres
|
|
1765
|
-
|
|
1766
|
-
mybatis:
|
|
1767
|
-
mapper-locations: classpath:mapper/*.xml
|
|
1768
|
-
```
|
|
1769
|
-
|
|
1770
|
-
</details>
|
|
1771
|
-
|
|
1772
|
-
### 49.4 GraphQL Playground からの呼び出し
|
|
1773
|
-
|
|
1774
|
-
```graphql
|
|
1775
|
-
# 品目取得
|
|
1776
|
-
query {
|
|
1777
|
-
item(itemCode: "PROD-001") {
|
|
1778
|
-
itemCode
|
|
1779
|
-
itemName
|
|
1780
|
-
category
|
|
1781
|
-
leadTime
|
|
1782
|
-
bom {
|
|
1783
|
-
childItemCode
|
|
1784
|
-
requiredQuantity
|
|
1785
|
-
childItem {
|
|
1786
|
-
itemName
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
# 品目一覧(ページネーション)
|
|
1793
|
-
query {
|
|
1794
|
-
items(category: PRODUCT, page: 0, size: 10) {
|
|
1795
|
-
edges {
|
|
1796
|
-
node {
|
|
1797
|
-
itemCode
|
|
1798
|
-
itemName
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
pageInfo {
|
|
1802
|
-
totalElements
|
|
1803
|
-
hasNextPage
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
# 品目登録
|
|
1809
|
-
mutation {
|
|
1810
|
-
createItem(input: {
|
|
1811
|
-
itemCode: "NEW-001"
|
|
1812
|
-
itemName: "新規製品"
|
|
1813
|
-
category: PRODUCT
|
|
1814
|
-
leadTime: 7
|
|
1815
|
-
}) {
|
|
1816
|
-
itemCode
|
|
1817
|
-
itemName
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
# MRP 進捗購読
|
|
1822
|
-
subscription {
|
|
1823
|
-
mrpProgress(executionId: "exec-123") {
|
|
1824
|
-
phase
|
|
1825
|
-
current
|
|
1826
|
-
total
|
|
1827
|
-
message
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
|
-
### 49.5 GraphQL vs REST vs gRPC の使い分け
|
|
1833
|
-
|
|
1834
|
-
| 用途 | 推奨 | 理由 |
|
|
1835
|
-
|------|------|------|
|
|
1836
|
-
| **フロントエンド API** | GraphQL | 柔軟なデータ取得、Over-fetching 防止 |
|
|
1837
|
-
| **モバイルアプリ** | GraphQL | 帯域制限、必要なデータのみ取得 |
|
|
1838
|
-
| **マイクロサービス間** | gRPC | 高パフォーマンス、型安全 |
|
|
1839
|
-
| **サードパーティ連携** | REST | 広く普及、シンプル |
|
|
1840
|
-
| **リアルタイム更新** | GraphQL Subscription / gRPC Streaming | ネイティブサポート |
|
|
1841
|
-
| **バッチ処理** | gRPC | ストリーミング効率 |
|
|
1842
|
-
|
|
1843
|
-
---
|
|
1844
|
-
|
|
1845
|
-
## まとめ
|
|
1846
|
-
|
|
1847
|
-
本研究では、GraphQL による生産管理システム API を実装しました。
|
|
1848
|
-
|
|
1849
|
-
### API サーバー版との比較
|
|
1850
|
-
|
|
1851
|
-
| 観点 | REST API(第32章) | gRPC(研究2) | GraphQL(本研究) |
|
|
1852
|
-
|------|-------------------|-----------------|-------------------|
|
|
1853
|
-
| **プロトコル** | HTTP/1.1 + JSON | HTTP/2 + Protobuf | HTTP + JSON |
|
|
1854
|
-
| **スキーマ** | OpenAPI(任意) | .proto(必須) | .graphqls(必須) |
|
|
1855
|
-
| **データ取得** | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
1856
|
-
| **エンドポイント** | 複数 | 複数 | 単一 |
|
|
1857
|
-
| **リアルタイム** | WebSocket 別実装 | ストリーミング | Subscription |
|
|
1858
|
-
| **ドメイン層** | 共有 | 共有 | 共有 |
|
|
1859
|
-
| **アプリケーション層** | 共有 | 共有 | 共有 |
|
|
1860
|
-
| **Input Adapter** | REST Controller | gRPC Service | GraphQL Resolver |
|
|
1861
|
-
|
|
1862
|
-
### 実装した GraphQL 操作
|
|
1863
|
-
|
|
1864
|
-
| 操作タイプ | 操作名 | 説明 |
|
|
1865
|
-
|-----------|--------|------|
|
|
1866
|
-
| **Query** | item, items | 品目取得 |
|
|
1867
|
-
| | bomTree, whereUsed | BOM 展開・逆展開 |
|
|
1868
|
-
| | purchaseOrder, purchaseOrders | 発注取得 |
|
|
1869
|
-
| **Mutation** | createItem, updateItem, deleteItem | 品目 CRUD |
|
|
1870
|
-
| | createPurchaseOrder, confirmPurchaseOrder | 発注操作 |
|
|
1871
|
-
| | recordReceiving | 入荷登録 |
|
|
1872
|
-
| | executeMrp | MRP 実行 |
|
|
1873
|
-
| **Subscription** | mrpProgress | MRP 進捗通知 |
|
|
1874
|
-
| | orderStatusChanged | 発注ステータス変更 |
|
|
1875
|
-
| | stockChanged | 在庫変動 |
|
|
1876
|
-
|
|
1877
|
-
### 技術スタック
|
|
1878
|
-
|
|
1879
|
-
- **Spring for GraphQL**: Spring Boot 統合
|
|
1880
|
-
- **GraphQL Java**: GraphQL 実行エンジン
|
|
1881
|
-
- **Extended Scalars**: Date, BigDecimal 等のカスタム型
|
|
1882
|
-
- **DataLoader**: N+1 問題対策
|
|
1883
|
-
- **WebSocket**: Subscription サポート
|
|
1884
|
-
|
|
1885
|
-
### GraphQL を選択すべき場面
|
|
1886
|
-
|
|
1887
|
-
1. **フロントエンド主導の開発**: クライアントがデータ形状を決定
|
|
1888
|
-
2. **モバイルアプリ**: 帯域制限下での効率的なデータ取得
|
|
1889
|
-
3. **複雑な関連データ**: 1回のクエリで関連データを取得
|
|
1890
|
-
4. **リアルタイム更新**: Subscription でプッシュ通知
|
|
1891
|
-
5. **API 統合**: 複数のバックエンドを単一エンドポイントで提供
|
|
1892
|
-
|
|
1893
|
-
---
|
|
1894
|
-
|
|
1895
|
-
[目次へ戻る](../index.md)
|
|
1
|
+
# 実践データベース設計:生産管理システム 研究 4 - GraphQL サービスの実装
|
|
2
|
+
|
|
3
|
+
## はじめに
|
|
4
|
+
|
|
5
|
+
本研究では、API サーバー構成(第32章)とは異なるアプローチとして、**GraphQL** による生産管理システムを実装します。クライアントが必要なデータを正確に指定できる柔軟なクエリと、リアルタイム更新を実現する Subscription を活用します。
|
|
6
|
+
|
|
7
|
+
第32章で構築したヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 第46章:GraphQL サーバーの基礎
|
|
12
|
+
|
|
13
|
+
### 46.1 GraphQL とは
|
|
14
|
+
|
|
15
|
+
GraphQL は Facebook が開発したクエリ言語および実行エンジンです。クライアントが必要なデータの形状を指定でき、Over-fetching や Under-fetching を防ぎます。
|
|
16
|
+
|
|
17
|
+
```plantuml
|
|
18
|
+
@startuml graphql_architecture
|
|
19
|
+
!define RECTANGLE class
|
|
20
|
+
|
|
21
|
+
skinparam backgroundColor #FEFEFE
|
|
22
|
+
|
|
23
|
+
package "GraphQL Architecture (生産管理システム)" {
|
|
24
|
+
|
|
25
|
+
package "Client Side" {
|
|
26
|
+
RECTANGLE "GraphQL Client\n(Apollo/Relay/urql)" as client {
|
|
27
|
+
- Query (読み取り)
|
|
28
|
+
- Mutation (書き込み)
|
|
29
|
+
- Subscription (リアルタイム)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
package "Server Side" {
|
|
34
|
+
RECTANGLE "GraphQL Server\n(Spring for GraphQL)" as server {
|
|
35
|
+
- ItemResolver
|
|
36
|
+
- PurchaseOrderResolver
|
|
37
|
+
- MrpResolver
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
package "Shared" {
|
|
42
|
+
RECTANGLE "GraphQL Schema\n(.graphqls files)" as schema {
|
|
43
|
+
- schema.graphqls
|
|
44
|
+
- item.graphqls
|
|
45
|
+
- purchase_order.graphqls
|
|
46
|
+
- mrp.graphqls
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
client --> schema : "スキーマに基づいて\nクエリを構築"
|
|
52
|
+
server --> schema : "スキーマに基づいて\nリゾルバを実装"
|
|
53
|
+
client <--> server : "HTTP/WebSocket\n(JSON)"
|
|
54
|
+
|
|
55
|
+
note bottom of schema
|
|
56
|
+
スキーマ駆動開発
|
|
57
|
+
クライアント主導のデータ取得
|
|
58
|
+
型安全な API
|
|
59
|
+
end note
|
|
60
|
+
|
|
61
|
+
@enduml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**REST API / gRPC との比較:**
|
|
65
|
+
|
|
66
|
+
| 特徴 | REST API | gRPC | GraphQL |
|
|
67
|
+
|------|----------|------|---------|
|
|
68
|
+
| プロトコル | HTTP/1.1 | HTTP/2 | HTTP/1.1 or HTTP/2 |
|
|
69
|
+
| データ形式 | JSON | Protocol Buffers | JSON |
|
|
70
|
+
| スキーマ | OpenAPI (任意) | .proto (必須) | .graphqls (必須) |
|
|
71
|
+
| データ取得 | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
72
|
+
| エンドポイント | 複数 | 複数 | 単一 |
|
|
73
|
+
| リアルタイム | WebSocket 別実装 | ストリーミング | Subscription |
|
|
74
|
+
| 主な用途 | 汎用 API | マイクロサービス | フロントエンド向け |
|
|
75
|
+
|
|
76
|
+
### 46.2 3つの操作タイプ
|
|
77
|
+
|
|
78
|
+
GraphQL は 3 つの操作タイプをサポートします:
|
|
79
|
+
|
|
80
|
+
```plantuml
|
|
81
|
+
@startuml graphql_operations
|
|
82
|
+
skinparam backgroundColor #FEFEFE
|
|
83
|
+
|
|
84
|
+
rectangle "1. Query\n(読み取り)" as query {
|
|
85
|
+
(Client) --> (Server) : "{ items { code name } }"
|
|
86
|
+
(Server) --> (Client) : "{ items: [...] }"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
rectangle "2. Mutation\n(書き込み)" as mutation {
|
|
90
|
+
(Client2) --> (Server2) : "mutation { createItem(...) }"
|
|
91
|
+
(Server2) --> (Client2) : "{ createItem: {...} }"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
rectangle "3. Subscription\n(リアルタイム)" as subscription {
|
|
95
|
+
(Client3) --> (Server3) : "subscription { orderUpdated }"
|
|
96
|
+
(Server3) --> (Client3) : "{ orderUpdated: {...} }"
|
|
97
|
+
(Server3) --> (Client3) : "{ orderUpdated: {...} }"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@enduml
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**用途:**
|
|
104
|
+
|
|
105
|
+
1. **Query**: データ取得(品目一覧、BOM 展開)
|
|
106
|
+
2. **Mutation**: データ更新(品目登録、発注確定)
|
|
107
|
+
3. **Subscription**: リアルタイム通知(MRP 進捗、在庫変動)
|
|
108
|
+
|
|
109
|
+
### 46.3 GraphQL におけるヘキサゴナルアーキテクチャ
|
|
110
|
+
|
|
111
|
+
GraphQL を導入しても、第32章で構築したヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
112
|
+
|
|
113
|
+
```plantuml
|
|
114
|
+
@startuml hexagonal_graphql
|
|
115
|
+
!define RECTANGLE class
|
|
116
|
+
|
|
117
|
+
package "Hexagonal Architecture (GraphQL版)" {
|
|
118
|
+
|
|
119
|
+
RECTANGLE "Application Core\n(Domain + Use Cases)" as core {
|
|
120
|
+
- Item (品目)
|
|
121
|
+
- Bom (部品構成表)
|
|
122
|
+
- Order (オーダ)
|
|
123
|
+
- Stock (在庫)
|
|
124
|
+
- ItemUseCase
|
|
125
|
+
- OrderUseCase
|
|
126
|
+
- InventoryUseCase
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
RECTANGLE "Input Adapters\n(Driving Side)" as input {
|
|
130
|
+
- REST Controller(既存)
|
|
131
|
+
- gRPC Service(既存)
|
|
132
|
+
- GraphQL Resolver(新規追加)
|
|
133
|
+
- DataFetcher
|
|
134
|
+
- DataLoader
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
RECTANGLE "Output Adapters\n(Driven Side)" as output {
|
|
138
|
+
- MyBatis Repository
|
|
139
|
+
- Database Access
|
|
140
|
+
- Entity Mapping
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
input --> core : "Input Ports\n(Use Cases)"
|
|
145
|
+
core --> output : "Output Ports\n(Repository Interfaces)"
|
|
146
|
+
|
|
147
|
+
note top of core
|
|
148
|
+
既存のビジネスロジック
|
|
149
|
+
REST API / gRPC 版と完全に共有
|
|
150
|
+
GraphQL 固有のコードは含まない
|
|
151
|
+
end note
|
|
152
|
+
|
|
153
|
+
note left of input
|
|
154
|
+
GraphQL リゾルバを
|
|
155
|
+
Input Adapter として追加
|
|
156
|
+
既存の REST/gRPC と共存可能
|
|
157
|
+
end note
|
|
158
|
+
|
|
159
|
+
note right of output
|
|
160
|
+
既存の Repository を
|
|
161
|
+
そのまま使用
|
|
162
|
+
変更不要
|
|
163
|
+
end note
|
|
164
|
+
|
|
165
|
+
@enduml
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**GraphQL でもヘキサゴナルアーキテクチャを維持する理由:**
|
|
169
|
+
|
|
170
|
+
1. **再利用性**: 既存の UseCase/Repository をそのまま活用
|
|
171
|
+
2. **並行運用**: REST API、gRPC、GraphQL を同時提供可能
|
|
172
|
+
3. **テスト容易性**: ドメインロジックは通信プロトコルに依存しない
|
|
173
|
+
4. **移行容易性**: 段階的に API 形式を追加・変更可能
|
|
174
|
+
|
|
175
|
+
### 46.4 ディレクトリ構成
|
|
176
|
+
|
|
177
|
+
既存の構成に `infrastructure/graphql/` を追加するだけです。
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
src/main/java/com/example/production/
|
|
181
|
+
├── domain/ # ドメイン層(API版と共通)
|
|
182
|
+
│ ├── model/
|
|
183
|
+
│ │ ├── item/
|
|
184
|
+
│ │ ├── bom/
|
|
185
|
+
│ │ ├── order/
|
|
186
|
+
│ │ └── inventory/
|
|
187
|
+
│ └── exception/
|
|
188
|
+
│
|
|
189
|
+
├── application/ # アプリケーション層(API版と共通)
|
|
190
|
+
│ ├── port/
|
|
191
|
+
│ │ ├── in/ # Input Port(ユースケース)
|
|
192
|
+
│ │ └── out/ # Output Port(リポジトリ)
|
|
193
|
+
│ └── service/
|
|
194
|
+
│
|
|
195
|
+
├── infrastructure/
|
|
196
|
+
│ ├── persistence/ # Output Adapter(DB実装)- 既存
|
|
197
|
+
│ │ ├── mapper/
|
|
198
|
+
│ │ └── repository/
|
|
199
|
+
│ ├── rest/ # Input Adapter(REST実装)- 既存
|
|
200
|
+
│ ├── grpc/ # Input Adapter(gRPC実装)- 既存
|
|
201
|
+
│ └── graphql/ # Input Adapter(GraphQL実装)- 新規追加
|
|
202
|
+
│ ├── resolver/ # Query/Mutation リゾルバ
|
|
203
|
+
│ ├── dataloader/ # N+1 問題対策
|
|
204
|
+
│ ├── scalar/ # カスタムスカラー型
|
|
205
|
+
│ └── subscription/ # Subscription ハンドラ
|
|
206
|
+
│
|
|
207
|
+
├── config/
|
|
208
|
+
│
|
|
209
|
+
└── src/main/resources/
|
|
210
|
+
└── graphql/ # GraphQL スキーマ定義
|
|
211
|
+
├── schema.graphqls
|
|
212
|
+
├── item.graphqls
|
|
213
|
+
├── bom.graphqls
|
|
214
|
+
├── purchase_order.graphqls
|
|
215
|
+
└── mrp.graphqls
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 46.5 技術スタックの追加
|
|
219
|
+
|
|
220
|
+
既存の `build.gradle.kts` に GraphQL 関連の依存関係を追加します。
|
|
221
|
+
|
|
222
|
+
<details>
|
|
223
|
+
<summary>build.gradle.kts(差分)</summary>
|
|
224
|
+
|
|
225
|
+
```kotlin
|
|
226
|
+
dependencies {
|
|
227
|
+
// 既存の依存関係(Spring Boot, MyBatis, PostgreSQL等)はそのまま
|
|
228
|
+
|
|
229
|
+
// GraphQL 関連を追加
|
|
230
|
+
implementation("org.springframework.boot:spring-boot-starter-graphql")
|
|
231
|
+
implementation("org.springframework.boot:spring-boot-starter-websocket") // Subscription 用
|
|
232
|
+
|
|
233
|
+
// GraphQL 拡張
|
|
234
|
+
implementation("com.graphql-java:graphql-java-extended-scalars:21.0")
|
|
235
|
+
|
|
236
|
+
// Test
|
|
237
|
+
testImplementation("org.springframework.graphql:spring-graphql-test")
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
</details>
|
|
242
|
+
|
|
243
|
+
**追加パッケージの説明:**
|
|
244
|
+
|
|
245
|
+
| パッケージ | 用途 |
|
|
246
|
+
|-----------|------|
|
|
247
|
+
| spring-boot-starter-graphql | Spring Boot GraphQL 統合 |
|
|
248
|
+
| spring-boot-starter-websocket | Subscription (WebSocket) |
|
|
249
|
+
| graphql-java-extended-scalars | DateTime, BigDecimal 等のスカラー型 |
|
|
250
|
+
| spring-graphql-test | GraphQL テストサポート |
|
|
251
|
+
|
|
252
|
+
### 42.6 GraphQL スキーマ定義
|
|
253
|
+
|
|
254
|
+
<details>
|
|
255
|
+
<summary>src/main/resources/graphql/schema.graphqls</summary>
|
|
256
|
+
|
|
257
|
+
```graphql
|
|
258
|
+
# ルートスキーマ
|
|
259
|
+
type Query {
|
|
260
|
+
# 品目
|
|
261
|
+
item(itemCode: ID!): Item
|
|
262
|
+
items(category: ItemCategory, page: Int, size: Int): ItemConnection!
|
|
263
|
+
|
|
264
|
+
# BOM
|
|
265
|
+
bomTree(itemCode: ID!): BomNode
|
|
266
|
+
whereUsed(itemCode: ID!): [WhereUsedResult!]!
|
|
267
|
+
|
|
268
|
+
# 発注
|
|
269
|
+
purchaseOrder(orderNumber: ID!): PurchaseOrder
|
|
270
|
+
purchaseOrders(status: OrderStatus, page: Int, size: Int): PurchaseOrderConnection!
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
type Mutation {
|
|
274
|
+
# 品目
|
|
275
|
+
createItem(input: CreateItemInput!): Item!
|
|
276
|
+
updateItem(input: UpdateItemInput!): Item!
|
|
277
|
+
deleteItem(itemCode: ID!): Boolean!
|
|
278
|
+
|
|
279
|
+
# 発注
|
|
280
|
+
createPurchaseOrder(input: CreatePurchaseOrderInput!): PurchaseOrder!
|
|
281
|
+
confirmPurchaseOrder(orderNumber: ID!): PurchaseOrder!
|
|
282
|
+
cancelPurchaseOrder(orderNumber: ID!): Boolean!
|
|
283
|
+
recordReceiving(input: RecordReceivingInput!): ReceivingResult!
|
|
284
|
+
|
|
285
|
+
# MRP
|
|
286
|
+
executeMrp(input: ExecuteMrpInput!): MrpResult!
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
type Subscription {
|
|
290
|
+
# MRP 進捗
|
|
291
|
+
mrpProgress(executionId: ID!): MrpProgress!
|
|
292
|
+
|
|
293
|
+
# 発注ステータス変更
|
|
294
|
+
orderStatusChanged(orderNumber: ID): PurchaseOrder!
|
|
295
|
+
|
|
296
|
+
# 在庫変動
|
|
297
|
+
stockChanged(itemCode: ID): StockChange!
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# ページネーション
|
|
301
|
+
type PageInfo {
|
|
302
|
+
hasNextPage: Boolean!
|
|
303
|
+
hasPreviousPage: Boolean!
|
|
304
|
+
totalElements: Int!
|
|
305
|
+
totalPages: Int!
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
</details>
|
|
310
|
+
|
|
311
|
+
<details>
|
|
312
|
+
<summary>src/main/resources/graphql/item.graphqls</summary>
|
|
313
|
+
|
|
314
|
+
```graphql
|
|
315
|
+
# 品目区分
|
|
316
|
+
enum ItemCategory {
|
|
317
|
+
PRODUCT # 製品
|
|
318
|
+
SEMI_PRODUCT # 半製品
|
|
319
|
+
PART # 部品
|
|
320
|
+
MATERIAL # 材料
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# 品目
|
|
324
|
+
type Item {
|
|
325
|
+
itemCode: ID!
|
|
326
|
+
effectiveDate: Date!
|
|
327
|
+
itemName: String!
|
|
328
|
+
category: ItemCategory!
|
|
329
|
+
itemGroupCode: String
|
|
330
|
+
unitCode: String
|
|
331
|
+
locationCode: String
|
|
332
|
+
leadTime: Int
|
|
333
|
+
safetyStock: Int
|
|
334
|
+
|
|
335
|
+
# 関連データ(必要な場合のみ取得)
|
|
336
|
+
bom: [BomEntry!]!
|
|
337
|
+
stock: Stock
|
|
338
|
+
purchaseOrders: [PurchaseOrder!]!
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# 品目一覧(ページネーション付き)
|
|
342
|
+
type ItemConnection {
|
|
343
|
+
edges: [ItemEdge!]!
|
|
344
|
+
pageInfo: PageInfo!
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
type ItemEdge {
|
|
348
|
+
node: Item!
|
|
349
|
+
cursor: String!
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# 入力型
|
|
353
|
+
input CreateItemInput {
|
|
354
|
+
itemCode: ID!
|
|
355
|
+
itemName: String!
|
|
356
|
+
category: ItemCategory!
|
|
357
|
+
itemGroupCode: String
|
|
358
|
+
unitCode: String
|
|
359
|
+
locationCode: String
|
|
360
|
+
leadTime: Int
|
|
361
|
+
safetyStock: Int
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
input UpdateItemInput {
|
|
365
|
+
itemCode: ID!
|
|
366
|
+
itemName: String
|
|
367
|
+
category: ItemCategory
|
|
368
|
+
leadTime: Int
|
|
369
|
+
safetyStock: Int
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# カスタムスカラー
|
|
373
|
+
scalar Date
|
|
374
|
+
scalar DateTime
|
|
375
|
+
scalar BigDecimal
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
</details>
|
|
379
|
+
|
|
380
|
+
<details>
|
|
381
|
+
<summary>src/main/resources/graphql/bom.graphqls</summary>
|
|
382
|
+
|
|
383
|
+
```graphql
|
|
384
|
+
# BOM エントリ
|
|
385
|
+
type BomEntry {
|
|
386
|
+
parentItemCode: ID!
|
|
387
|
+
childItemCode: ID!
|
|
388
|
+
requiredQuantity: BigDecimal!
|
|
389
|
+
effectiveFrom: Date!
|
|
390
|
+
effectiveTo: Date
|
|
391
|
+
|
|
392
|
+
# 子品目の詳細
|
|
393
|
+
childItem: Item!
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# BOM ツリーノード
|
|
397
|
+
type BomNode {
|
|
398
|
+
itemCode: ID!
|
|
399
|
+
itemName: String!
|
|
400
|
+
requiredQuantity: BigDecimal!
|
|
401
|
+
level: Int!
|
|
402
|
+
children: [BomNode!]!
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# 逆展開(使用先照会)
|
|
406
|
+
type WhereUsedResult {
|
|
407
|
+
parentItemCode: ID!
|
|
408
|
+
itemName: String!
|
|
409
|
+
requiredQuantity: BigDecimal!
|
|
410
|
+
level: Int!
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
</details>
|
|
415
|
+
|
|
416
|
+
<details>
|
|
417
|
+
<summary>src/main/resources/graphql/purchase_order.graphqls</summary>
|
|
418
|
+
|
|
419
|
+
```graphql
|
|
420
|
+
# 発注ステータス
|
|
421
|
+
enum OrderStatus {
|
|
422
|
+
DRAFT
|
|
423
|
+
CONFIRMED
|
|
424
|
+
CANCELLED
|
|
425
|
+
COMPLETED
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# 発注
|
|
429
|
+
type PurchaseOrder {
|
|
430
|
+
orderNumber: ID!
|
|
431
|
+
supplierCode: String!
|
|
432
|
+
orderDate: Date!
|
|
433
|
+
status: OrderStatus!
|
|
434
|
+
totalAmount: BigDecimal!
|
|
435
|
+
details: [PurchaseOrderDetail!]!
|
|
436
|
+
|
|
437
|
+
# 関連データ
|
|
438
|
+
supplier: Supplier
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
type PurchaseOrderDetail {
|
|
442
|
+
lineNumber: Int!
|
|
443
|
+
itemCode: ID!
|
|
444
|
+
orderQuantity: BigDecimal!
|
|
445
|
+
unitPrice: BigDecimal!
|
|
446
|
+
deliveryDate: Date!
|
|
447
|
+
receivedQuantity: BigDecimal!
|
|
448
|
+
|
|
449
|
+
# 関連データ
|
|
450
|
+
item: Item!
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
type PurchaseOrderConnection {
|
|
454
|
+
edges: [PurchaseOrderEdge!]!
|
|
455
|
+
pageInfo: PageInfo!
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type PurchaseOrderEdge {
|
|
459
|
+
node: PurchaseOrder!
|
|
460
|
+
cursor: String!
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
# 入力型
|
|
464
|
+
input CreatePurchaseOrderInput {
|
|
465
|
+
supplierCode: String!
|
|
466
|
+
details: [CreateOrderDetailInput!]!
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
input CreateOrderDetailInput {
|
|
470
|
+
itemCode: ID!
|
|
471
|
+
orderQuantity: BigDecimal!
|
|
472
|
+
unitPrice: BigDecimal!
|
|
473
|
+
deliveryDate: Date!
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
input RecordReceivingInput {
|
|
477
|
+
orderNumber: ID!
|
|
478
|
+
lineNumber: Int!
|
|
479
|
+
receivedQuantity: BigDecimal!
|
|
480
|
+
receivingDate: Date!
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
type ReceivingResult {
|
|
484
|
+
orderNumber: ID!
|
|
485
|
+
lineNumber: Int!
|
|
486
|
+
totalReceived: BigDecimal!
|
|
487
|
+
isCompleted: Boolean!
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
</details>
|
|
492
|
+
|
|
493
|
+
<details>
|
|
494
|
+
<summary>src/main/resources/graphql/mrp.graphqls</summary>
|
|
495
|
+
|
|
496
|
+
```graphql
|
|
497
|
+
# MRP 入力
|
|
498
|
+
input ExecuteMrpInput {
|
|
499
|
+
startDate: Date!
|
|
500
|
+
endDate: Date!
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# MRP 結果
|
|
504
|
+
type MrpResult {
|
|
505
|
+
executionId: ID!
|
|
506
|
+
periodStart: Date!
|
|
507
|
+
periodEnd: Date!
|
|
508
|
+
plannedOrders: [PlannedOrder!]!
|
|
509
|
+
shortageItems: [ShortageItem!]!
|
|
510
|
+
statistics: MrpStatistics!
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
type PlannedOrder {
|
|
514
|
+
itemCode: ID!
|
|
515
|
+
itemName: String!
|
|
516
|
+
quantity: BigDecimal!
|
|
517
|
+
dueDate: Date!
|
|
518
|
+
orderType: String!
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
type ShortageItem {
|
|
522
|
+
itemCode: ID!
|
|
523
|
+
itemName: String!
|
|
524
|
+
shortageQuantity: BigDecimal!
|
|
525
|
+
recommendedOrderDate: Date!
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
type MrpStatistics {
|
|
529
|
+
totalItemsProcessed: Int!
|
|
530
|
+
purchaseOrderCount: Int!
|
|
531
|
+
productionOrderCount: Int!
|
|
532
|
+
shortageItemCount: Int!
|
|
533
|
+
executionTimeMs: Long!
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# MRP 進捗(Subscription)
|
|
537
|
+
type MrpProgress {
|
|
538
|
+
executionId: ID!
|
|
539
|
+
phase: String!
|
|
540
|
+
current: Int!
|
|
541
|
+
total: Int!
|
|
542
|
+
message: String!
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
scalar Long
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
</details>
|
|
549
|
+
|
|
550
|
+
### 42.7 API サーバー版との Controller の違い
|
|
551
|
+
|
|
552
|
+
**API サーバー版(REST Controller)**
|
|
553
|
+
|
|
554
|
+
```java
|
|
555
|
+
@RestController // JSON を返す
|
|
556
|
+
@RequestMapping("/api/items")
|
|
557
|
+
public class ItemApiController {
|
|
558
|
+
|
|
559
|
+
private final ItemUseCase itemUseCase; // 共有
|
|
560
|
+
|
|
561
|
+
@GetMapping("/{itemCode}")
|
|
562
|
+
public ResponseEntity<ItemResponse> getItem(@PathVariable String itemCode) {
|
|
563
|
+
Item item = itemUseCase.getItemByCode(itemCode);
|
|
564
|
+
return ResponseEntity.ok(ItemResponse.from(item));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**GraphQL 版(QueryResolver)**
|
|
570
|
+
|
|
571
|
+
```java
|
|
572
|
+
@Controller // Spring for GraphQL
|
|
573
|
+
public class ItemResolver {
|
|
574
|
+
|
|
575
|
+
private final ItemUseCase itemUseCase; // 共有(同じインスタンス)
|
|
576
|
+
|
|
577
|
+
@QueryMapping
|
|
578
|
+
public Item item(@Argument String itemCode) {
|
|
579
|
+
// 同じ UseCase を呼び出し
|
|
580
|
+
return itemUseCase.getItemByCode(itemCode);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**共通点と相違点:**
|
|
586
|
+
|
|
587
|
+
| 観点 | REST Controller | GraphQL Resolver |
|
|
588
|
+
|------|-----------------|------------------|
|
|
589
|
+
| **UseCase** | 共有(同一インスタンス) | 共有(同一インスタンス) |
|
|
590
|
+
| **アノテーション** | `@RestController` | `@Controller` |
|
|
591
|
+
| **メソッド** | `@GetMapping` 等 | `@QueryMapping` / `@MutationMapping` |
|
|
592
|
+
| **引数** | `@PathVariable` / `@RequestBody` | `@Argument` |
|
|
593
|
+
| **レスポンス** | DTO に変換 | ドメインモデル直接(推奨) |
|
|
594
|
+
| **データ形式** | 固定 JSON | クライアント指定 |
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## 第47章:マスタ API の実装
|
|
599
|
+
|
|
600
|
+
### 47.1 カスタムスカラーの実装
|
|
601
|
+
|
|
602
|
+
<details>
|
|
603
|
+
<summary>ScalarConfig.java</summary>
|
|
604
|
+
|
|
605
|
+
```java
|
|
606
|
+
package com.example.production.infrastructure.graphql.scalar;
|
|
607
|
+
|
|
608
|
+
import graphql.scalars.ExtendedScalars;
|
|
609
|
+
import org.springframework.context.annotation.Bean;
|
|
610
|
+
import org.springframework.context.annotation.Configuration;
|
|
611
|
+
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* カスタムスカラー設定
|
|
615
|
+
*/
|
|
616
|
+
@Configuration
|
|
617
|
+
public class ScalarConfig {
|
|
618
|
+
|
|
619
|
+
@Bean
|
|
620
|
+
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
|
|
621
|
+
return wiringBuilder -> wiringBuilder
|
|
622
|
+
.scalar(ExtendedScalars.Date)
|
|
623
|
+
.scalar(ExtendedScalars.DateTime)
|
|
624
|
+
.scalar(ExtendedScalars.GraphQLBigDecimal)
|
|
625
|
+
.scalar(ExtendedScalars.GraphQLLong);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
</details>
|
|
631
|
+
|
|
632
|
+
### 47.2 品目 GraphQL リゾルバの TDD 実装
|
|
633
|
+
|
|
634
|
+
#### Red: 失敗するテストを書く
|
|
635
|
+
|
|
636
|
+
<details>
|
|
637
|
+
<summary>ItemResolverTest.java</summary>
|
|
638
|
+
|
|
639
|
+
```java
|
|
640
|
+
package com.example.production.infrastructure.graphql.resolver;
|
|
641
|
+
|
|
642
|
+
import com.example.production.domain.model.item.Item;
|
|
643
|
+
import com.example.production.domain.model.item.ItemCategory;
|
|
644
|
+
import com.example.production.infrastructure.persistence.mapper.ItemMapper;
|
|
645
|
+
import org.junit.jupiter.api.*;
|
|
646
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
647
|
+
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester;
|
|
648
|
+
import org.springframework.boot.test.context.SpringBootTest;
|
|
649
|
+
import org.springframework.graphql.test.tester.GraphQlTester;
|
|
650
|
+
import org.springframework.test.context.DynamicPropertyRegistry;
|
|
651
|
+
import org.springframework.test.context.DynamicPropertySource;
|
|
652
|
+
import org.testcontainers.containers.PostgreSQLContainer;
|
|
653
|
+
import org.testcontainers.junit.jupiter.Container;
|
|
654
|
+
import org.testcontainers.junit.jupiter.Testcontainers;
|
|
655
|
+
|
|
656
|
+
import java.time.LocalDate;
|
|
657
|
+
|
|
658
|
+
import static org.assertj.core.api.Assertions.*;
|
|
659
|
+
|
|
660
|
+
@SpringBootTest
|
|
661
|
+
@AutoConfigureGraphQlTester
|
|
662
|
+
@Testcontainers
|
|
663
|
+
@DisplayName("品目 GraphQL リゾルバ")
|
|
664
|
+
class ItemResolverTest {
|
|
665
|
+
|
|
666
|
+
@Container
|
|
667
|
+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
|
|
668
|
+
.withDatabaseName("production_test")
|
|
669
|
+
.withUsername("test")
|
|
670
|
+
.withPassword("test");
|
|
671
|
+
|
|
672
|
+
@DynamicPropertySource
|
|
673
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
674
|
+
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
675
|
+
registry.add("spring.datasource.username", postgres::getUsername);
|
|
676
|
+
registry.add("spring.datasource.password", postgres::getPassword);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
@Autowired
|
|
680
|
+
private GraphQlTester graphQlTester;
|
|
681
|
+
|
|
682
|
+
@Autowired
|
|
683
|
+
private ItemMapper itemMapper;
|
|
684
|
+
|
|
685
|
+
@BeforeEach
|
|
686
|
+
void setUp() {
|
|
687
|
+
itemMapper.deleteAll();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@Nested
|
|
691
|
+
@DisplayName("Query: item")
|
|
692
|
+
class ItemQueryTests {
|
|
693
|
+
|
|
694
|
+
@Test
|
|
695
|
+
@DisplayName("品目コードで品目を取得できる")
|
|
696
|
+
void shouldGetItemByCode() {
|
|
697
|
+
// Arrange
|
|
698
|
+
insertTestItem("PROD-001", "テスト製品", ItemCategory.PRODUCT);
|
|
699
|
+
|
|
700
|
+
// Act & Assert
|
|
701
|
+
graphQlTester.document("""
|
|
702
|
+
query {
|
|
703
|
+
item(itemCode: "PROD-001") {
|
|
704
|
+
itemCode
|
|
705
|
+
itemName
|
|
706
|
+
category
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
""")
|
|
710
|
+
.execute()
|
|
711
|
+
.path("item.itemCode").entity(String.class).isEqualTo("PROD-001")
|
|
712
|
+
.path("item.itemName").entity(String.class).isEqualTo("テスト製品")
|
|
713
|
+
.path("item.category").entity(String.class).isEqualTo("PRODUCT");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
@Test
|
|
717
|
+
@DisplayName("存在しない品目は null を返す")
|
|
718
|
+
void shouldReturnNullForNonExistentItem() {
|
|
719
|
+
graphQlTester.document("""
|
|
720
|
+
query {
|
|
721
|
+
item(itemCode: "NOT-EXIST") {
|
|
722
|
+
itemCode
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
""")
|
|
726
|
+
.execute()
|
|
727
|
+
.path("item").valueIsNull();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
@Test
|
|
731
|
+
@DisplayName("関連データを同時に取得できる")
|
|
732
|
+
void shouldGetItemWithRelatedData() {
|
|
733
|
+
insertTestItem("PROD-001", "テスト製品", ItemCategory.PRODUCT);
|
|
734
|
+
|
|
735
|
+
graphQlTester.document("""
|
|
736
|
+
query {
|
|
737
|
+
item(itemCode: "PROD-001") {
|
|
738
|
+
itemCode
|
|
739
|
+
itemName
|
|
740
|
+
bom {
|
|
741
|
+
childItemCode
|
|
742
|
+
requiredQuantity
|
|
743
|
+
}
|
|
744
|
+
stock {
|
|
745
|
+
quantity
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
""")
|
|
750
|
+
.execute()
|
|
751
|
+
.path("item.itemCode").entity(String.class).isEqualTo("PROD-001")
|
|
752
|
+
.path("item.bom").entityList(Object.class).hasSize(0);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
@Nested
|
|
757
|
+
@DisplayName("Query: items")
|
|
758
|
+
class ItemsQueryTests {
|
|
759
|
+
|
|
760
|
+
@Test
|
|
761
|
+
@DisplayName("品目一覧を取得できる")
|
|
762
|
+
void shouldGetItems() {
|
|
763
|
+
for (int i = 1; i <= 5; i++) {
|
|
764
|
+
insertTestItem("ITEM-" + String.format("%03d", i),
|
|
765
|
+
"品目" + i, ItemCategory.PRODUCT);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
graphQlTester.document("""
|
|
769
|
+
query {
|
|
770
|
+
items(page: 0, size: 10) {
|
|
771
|
+
edges {
|
|
772
|
+
node {
|
|
773
|
+
itemCode
|
|
774
|
+
itemName
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
pageInfo {
|
|
778
|
+
totalElements
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
""")
|
|
783
|
+
.execute()
|
|
784
|
+
.path("items.edges").entityList(Object.class).hasSize(5)
|
|
785
|
+
.path("items.pageInfo.totalElements").entity(Integer.class).isEqualTo(5);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
@Test
|
|
789
|
+
@DisplayName("カテゴリでフィルタリングできる")
|
|
790
|
+
void shouldFilterByCategory() {
|
|
791
|
+
insertTestItem("PROD-001", "製品1", ItemCategory.PRODUCT);
|
|
792
|
+
insertTestItem("PART-001", "部品1", ItemCategory.PART);
|
|
793
|
+
insertTestItem("PROD-002", "製品2", ItemCategory.PRODUCT);
|
|
794
|
+
|
|
795
|
+
graphQlTester.document("""
|
|
796
|
+
query {
|
|
797
|
+
items(category: PRODUCT, page: 0, size: 10) {
|
|
798
|
+
edges {
|
|
799
|
+
node {
|
|
800
|
+
itemCode
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
pageInfo {
|
|
804
|
+
totalElements
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
""")
|
|
809
|
+
.execute()
|
|
810
|
+
.path("items.pageInfo.totalElements").entity(Integer.class).isEqualTo(2);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
@Nested
|
|
815
|
+
@DisplayName("Mutation: createItem")
|
|
816
|
+
class CreateItemMutationTests {
|
|
817
|
+
|
|
818
|
+
@Test
|
|
819
|
+
@DisplayName("品目を登録できる")
|
|
820
|
+
void shouldCreateItem() {
|
|
821
|
+
graphQlTester.document("""
|
|
822
|
+
mutation {
|
|
823
|
+
createItem(input: {
|
|
824
|
+
itemCode: "NEW-001"
|
|
825
|
+
itemName: "新規品目"
|
|
826
|
+
category: PRODUCT
|
|
827
|
+
leadTime: 5
|
|
828
|
+
}) {
|
|
829
|
+
itemCode
|
|
830
|
+
itemName
|
|
831
|
+
category
|
|
832
|
+
leadTime
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
""")
|
|
836
|
+
.execute()
|
|
837
|
+
.path("createItem.itemCode").entity(String.class).isEqualTo("NEW-001")
|
|
838
|
+
.path("createItem.itemName").entity(String.class).isEqualTo("新規品目")
|
|
839
|
+
.path("createItem.category").entity(String.class).isEqualTo("PRODUCT")
|
|
840
|
+
.path("createItem.leadTime").entity(Integer.class).isEqualTo(5);
|
|
841
|
+
|
|
842
|
+
// DB に保存されていることを確認
|
|
843
|
+
var saved = itemMapper.findByCode("NEW-001");
|
|
844
|
+
assertThat(saved).isNotNull();
|
|
845
|
+
assertThat(saved.getItemName()).isEqualTo("新規品目");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
@Test
|
|
849
|
+
@DisplayName("重複する品目コードはエラーになる")
|
|
850
|
+
void shouldFailForDuplicateCode() {
|
|
851
|
+
insertTestItem("DUP-001", "既存品目", ItemCategory.PRODUCT);
|
|
852
|
+
|
|
853
|
+
graphQlTester.document("""
|
|
854
|
+
mutation {
|
|
855
|
+
createItem(input: {
|
|
856
|
+
itemCode: "DUP-001"
|
|
857
|
+
itemName: "重複品目"
|
|
858
|
+
category: PRODUCT
|
|
859
|
+
}) {
|
|
860
|
+
itemCode
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
""")
|
|
864
|
+
.execute()
|
|
865
|
+
.errors()
|
|
866
|
+
.satisfy(errors -> assertThat(errors).hasSize(1));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private void insertTestItem(String code, String name, ItemCategory category) {
|
|
871
|
+
itemMapper.insert(Item.builder()
|
|
872
|
+
.itemCode(code)
|
|
873
|
+
.effectiveDate(LocalDate.of(2025, 1, 1))
|
|
874
|
+
.itemName(name)
|
|
875
|
+
.category(category)
|
|
876
|
+
.build());
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
</details>
|
|
882
|
+
|
|
883
|
+
#### Green: GraphQL リゾルバの実装
|
|
884
|
+
|
|
885
|
+
<details>
|
|
886
|
+
<summary>ItemResolver.java</summary>
|
|
887
|
+
|
|
888
|
+
```java
|
|
889
|
+
package com.example.production.infrastructure.graphql.resolver;
|
|
890
|
+
|
|
891
|
+
import com.example.production.application.port.in.*;
|
|
892
|
+
import com.example.production.domain.exception.DuplicateItemException;
|
|
893
|
+
import com.example.production.domain.exception.ItemNotFoundException;
|
|
894
|
+
import com.example.production.domain.model.item.Item;
|
|
895
|
+
import com.example.production.domain.model.item.ItemCategory;
|
|
896
|
+
import graphql.GraphQLError;
|
|
897
|
+
import graphql.GraphqlErrorBuilder;
|
|
898
|
+
import graphql.schema.DataFetchingEnvironment;
|
|
899
|
+
import org.springframework.graphql.data.method.annotation.*;
|
|
900
|
+
import org.springframework.graphql.execution.ErrorType;
|
|
901
|
+
import org.springframework.stereotype.Controller;
|
|
902
|
+
|
|
903
|
+
import java.util.List;
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* 品目 GraphQL リゾルバ実装
|
|
907
|
+
* 既存の ItemUseCase を Input Adapter として呼び出す
|
|
908
|
+
*/
|
|
909
|
+
@Controller
|
|
910
|
+
public class ItemResolver {
|
|
911
|
+
|
|
912
|
+
private final ItemUseCase itemUseCase; // 既存の UseCase を注入
|
|
913
|
+
|
|
914
|
+
public ItemResolver(ItemUseCase itemUseCase) {
|
|
915
|
+
this.itemUseCase = itemUseCase;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ========== Query ==========
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* 品目取得
|
|
922
|
+
*/
|
|
923
|
+
@QueryMapping
|
|
924
|
+
public Item item(@Argument String itemCode) {
|
|
925
|
+
try {
|
|
926
|
+
return itemUseCase.getItemByCode(itemCode);
|
|
927
|
+
} catch (ItemNotFoundException e) {
|
|
928
|
+
return null; // GraphQL では null を返す
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* 品目一覧(ページネーション付き)
|
|
934
|
+
*/
|
|
935
|
+
@QueryMapping
|
|
936
|
+
public ItemConnection items(
|
|
937
|
+
@Argument ItemCategory category,
|
|
938
|
+
@Argument Integer page,
|
|
939
|
+
@Argument Integer size) {
|
|
940
|
+
|
|
941
|
+
int pageNum = page != null ? page : 0;
|
|
942
|
+
int pageSize = size != null ? size : 20;
|
|
943
|
+
|
|
944
|
+
List<Item> items;
|
|
945
|
+
int totalElements;
|
|
946
|
+
|
|
947
|
+
if (category != null) {
|
|
948
|
+
items = itemUseCase.getItemsByCategory(category);
|
|
949
|
+
} else {
|
|
950
|
+
items = itemUseCase.getAllItems();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
totalElements = items.size();
|
|
954
|
+
|
|
955
|
+
// ページング処理
|
|
956
|
+
int start = pageNum * pageSize;
|
|
957
|
+
int end = Math.min(start + pageSize, items.size());
|
|
958
|
+
List<Item> pagedItems = items.subList(start, end);
|
|
959
|
+
|
|
960
|
+
return ItemConnection.builder()
|
|
961
|
+
.edges(pagedItems.stream()
|
|
962
|
+
.map(item -> ItemEdge.builder()
|
|
963
|
+
.node(item)
|
|
964
|
+
.cursor(item.getItemCode())
|
|
965
|
+
.build())
|
|
966
|
+
.toList())
|
|
967
|
+
.pageInfo(PageInfo.builder()
|
|
968
|
+
.hasNextPage(end < totalElements)
|
|
969
|
+
.hasPreviousPage(pageNum > 0)
|
|
970
|
+
.totalElements(totalElements)
|
|
971
|
+
.totalPages((int) Math.ceil((double) totalElements / pageSize))
|
|
972
|
+
.build())
|
|
973
|
+
.build();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ========== Mutation ==========
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* 品目登録
|
|
980
|
+
*/
|
|
981
|
+
@MutationMapping
|
|
982
|
+
public Item createItem(@Argument CreateItemInput input) {
|
|
983
|
+
CreateItemCommand command = CreateItemCommand.builder()
|
|
984
|
+
.itemCode(input.getItemCode())
|
|
985
|
+
.itemName(input.getItemName())
|
|
986
|
+
.category(input.getCategory())
|
|
987
|
+
.itemGroupCode(input.getItemGroupCode())
|
|
988
|
+
.unitCode(input.getUnitCode())
|
|
989
|
+
.locationCode(input.getLocationCode())
|
|
990
|
+
.leadTime(input.getLeadTime())
|
|
991
|
+
.safetyStock(input.getSafetyStock())
|
|
992
|
+
.build();
|
|
993
|
+
|
|
994
|
+
return itemUseCase.createItem(command);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* 品目更新
|
|
999
|
+
*/
|
|
1000
|
+
@MutationMapping
|
|
1001
|
+
public Item updateItem(@Argument UpdateItemInput input) {
|
|
1002
|
+
UpdateItemCommand command = UpdateItemCommand.builder()
|
|
1003
|
+
.itemCode(input.getItemCode())
|
|
1004
|
+
.itemName(input.getItemName())
|
|
1005
|
+
.category(input.getCategory())
|
|
1006
|
+
.leadTime(input.getLeadTime())
|
|
1007
|
+
.safetyStock(input.getSafetyStock())
|
|
1008
|
+
.build();
|
|
1009
|
+
|
|
1010
|
+
return itemUseCase.updateItem(command);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* 品目削除
|
|
1015
|
+
*/
|
|
1016
|
+
@MutationMapping
|
|
1017
|
+
public boolean deleteItem(@Argument String itemCode) {
|
|
1018
|
+
try {
|
|
1019
|
+
itemUseCase.deleteItem(itemCode);
|
|
1020
|
+
return true;
|
|
1021
|
+
} catch (ItemNotFoundException e) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ========== 例外ハンドリング ==========
|
|
1027
|
+
|
|
1028
|
+
@GraphQlExceptionHandler
|
|
1029
|
+
public GraphQLError handleDuplicateItem(DuplicateItemException ex, DataFetchingEnvironment env) {
|
|
1030
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1031
|
+
.errorType(ErrorType.BAD_REQUEST)
|
|
1032
|
+
.message(ex.getMessage())
|
|
1033
|
+
.build();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
@GraphQlExceptionHandler
|
|
1037
|
+
public GraphQLError handleItemNotFound(ItemNotFoundException ex, DataFetchingEnvironment env) {
|
|
1038
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1039
|
+
.errorType(ErrorType.NOT_FOUND)
|
|
1040
|
+
.message(ex.getMessage())
|
|
1041
|
+
.build();
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
</details>
|
|
1047
|
+
|
|
1048
|
+
<details>
|
|
1049
|
+
<summary>補助クラス(ItemConnection, PageInfo 等)</summary>
|
|
1050
|
+
|
|
1051
|
+
```java
|
|
1052
|
+
package com.example.production.infrastructure.graphql.resolver;
|
|
1053
|
+
|
|
1054
|
+
import com.example.production.domain.model.item.Item;
|
|
1055
|
+
import com.example.production.domain.model.item.ItemCategory;
|
|
1056
|
+
import lombok.Builder;
|
|
1057
|
+
import lombok.Data;
|
|
1058
|
+
|
|
1059
|
+
import java.util.List;
|
|
1060
|
+
|
|
1061
|
+
@Data
|
|
1062
|
+
@Builder
|
|
1063
|
+
public class ItemConnection {
|
|
1064
|
+
private List<ItemEdge> edges;
|
|
1065
|
+
private PageInfo pageInfo;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
@Data
|
|
1069
|
+
@Builder
|
|
1070
|
+
public class ItemEdge {
|
|
1071
|
+
private Item node;
|
|
1072
|
+
private String cursor;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
@Data
|
|
1076
|
+
@Builder
|
|
1077
|
+
public class PageInfo {
|
|
1078
|
+
private boolean hasNextPage;
|
|
1079
|
+
private boolean hasPreviousPage;
|
|
1080
|
+
private int totalElements;
|
|
1081
|
+
private int totalPages;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
@Data
|
|
1085
|
+
public class CreateItemInput {
|
|
1086
|
+
private String itemCode;
|
|
1087
|
+
private String itemName;
|
|
1088
|
+
private ItemCategory category;
|
|
1089
|
+
private String itemGroupCode;
|
|
1090
|
+
private String unitCode;
|
|
1091
|
+
private String locationCode;
|
|
1092
|
+
private Integer leadTime;
|
|
1093
|
+
private Integer safetyStock;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
@Data
|
|
1097
|
+
public class UpdateItemInput {
|
|
1098
|
+
private String itemCode;
|
|
1099
|
+
private String itemName;
|
|
1100
|
+
private ItemCategory category;
|
|
1101
|
+
private Integer leadTime;
|
|
1102
|
+
private Integer safetyStock;
|
|
1103
|
+
}
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
</details>
|
|
1107
|
+
|
|
1108
|
+
### 47.3 N+1 問題対策(DataLoader)
|
|
1109
|
+
|
|
1110
|
+
GraphQL では関連データの取得時に N+1 問題が発生しやすいため、DataLoader を使用します。
|
|
1111
|
+
|
|
1112
|
+
<details>
|
|
1113
|
+
<summary>DataLoaderConfig.java</summary>
|
|
1114
|
+
|
|
1115
|
+
```java
|
|
1116
|
+
package com.example.production.infrastructure.graphql.dataloader;
|
|
1117
|
+
|
|
1118
|
+
import com.example.production.application.port.in.ItemUseCase;
|
|
1119
|
+
import com.example.production.domain.model.item.Item;
|
|
1120
|
+
import org.dataloader.DataLoader;
|
|
1121
|
+
import org.dataloader.DataLoaderFactory;
|
|
1122
|
+
import org.springframework.context.annotation.Bean;
|
|
1123
|
+
import org.springframework.context.annotation.Configuration;
|
|
1124
|
+
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
|
1125
|
+
|
|
1126
|
+
import java.util.List;
|
|
1127
|
+
import java.util.Map;
|
|
1128
|
+
import java.util.function.Function;
|
|
1129
|
+
import java.util.stream.Collectors;
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* DataLoader 設定
|
|
1133
|
+
* N+1 問題を解決するためのバッチローディング
|
|
1134
|
+
*/
|
|
1135
|
+
@Configuration
|
|
1136
|
+
public class DataLoaderConfig {
|
|
1137
|
+
|
|
1138
|
+
private final ItemUseCase itemUseCase;
|
|
1139
|
+
|
|
1140
|
+
public DataLoaderConfig(ItemUseCase itemUseCase) {
|
|
1141
|
+
this.itemUseCase = itemUseCase;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
@Bean
|
|
1145
|
+
public BatchLoaderRegistry batchLoaderRegistry(BatchLoaderRegistry registry) {
|
|
1146
|
+
// 品目 DataLoader
|
|
1147
|
+
registry.forTypePair(String.class, Item.class)
|
|
1148
|
+
.registerBatchLoader((itemCodes, env) -> {
|
|
1149
|
+
// バッチで品目を取得(1回のクエリで複数取得)
|
|
1150
|
+
List<Item> items = itemUseCase.getItemsByCodes(itemCodes);
|
|
1151
|
+
|
|
1152
|
+
Map<String, Item> itemMap = items.stream()
|
|
1153
|
+
.collect(Collectors.toMap(Item::getItemCode, Function.identity()));
|
|
1154
|
+
|
|
1155
|
+
return reactor.core.publisher.Flux.fromIterable(itemCodes)
|
|
1156
|
+
.map(code -> itemMap.get(code));
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
return registry;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
</details>
|
|
1165
|
+
|
|
1166
|
+
<details>
|
|
1167
|
+
<summary>PurchaseOrderDetailResolver.java(DataLoader 使用)</summary>
|
|
1168
|
+
|
|
1169
|
+
```java
|
|
1170
|
+
package com.example.production.infrastructure.graphql.resolver;
|
|
1171
|
+
|
|
1172
|
+
import com.example.production.domain.model.item.Item;
|
|
1173
|
+
import com.example.production.domain.model.purchase.PurchaseOrderDetail;
|
|
1174
|
+
import org.dataloader.DataLoader;
|
|
1175
|
+
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
|
1176
|
+
import org.springframework.stereotype.Controller;
|
|
1177
|
+
|
|
1178
|
+
import java.util.concurrent.CompletableFuture;
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* 発注明細リゾルバ
|
|
1182
|
+
* DataLoader を使って品目を効率的に取得
|
|
1183
|
+
*/
|
|
1184
|
+
@Controller
|
|
1185
|
+
public class PurchaseOrderDetailResolver {
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* 発注明細から品目を取得(DataLoader 使用)
|
|
1189
|
+
*/
|
|
1190
|
+
@SchemaMapping(typeName = "PurchaseOrderDetail", field = "item")
|
|
1191
|
+
public CompletableFuture<Item> item(
|
|
1192
|
+
PurchaseOrderDetail detail,
|
|
1193
|
+
DataLoader<String, Item> itemDataLoader) {
|
|
1194
|
+
|
|
1195
|
+
// DataLoader 経由で取得(バッチ処理される)
|
|
1196
|
+
return itemDataLoader.load(detail.getItemCode());
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
</details>
|
|
1202
|
+
|
|
1203
|
+
### 47.4 BOM GraphQL リゾルバ
|
|
1204
|
+
|
|
1205
|
+
<details>
|
|
1206
|
+
<summary>BomResolver.java</summary>
|
|
1207
|
+
|
|
1208
|
+
```java
|
|
1209
|
+
package com.example.production.infrastructure.graphql.resolver;
|
|
1210
|
+
|
|
1211
|
+
import com.example.production.application.service.BomNode;
|
|
1212
|
+
import com.example.production.application.service.BomService;
|
|
1213
|
+
import com.example.production.application.service.WhereUsedResult;
|
|
1214
|
+
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1215
|
+
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
1216
|
+
import org.springframework.stereotype.Controller;
|
|
1217
|
+
|
|
1218
|
+
import java.util.List;
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* BOM GraphQL リゾルバ実装
|
|
1222
|
+
* 既存の BomService を Input Adapter として呼び出す
|
|
1223
|
+
*/
|
|
1224
|
+
@Controller
|
|
1225
|
+
public class BomResolver {
|
|
1226
|
+
|
|
1227
|
+
private final BomService bomService; // 既存のサービスを注入
|
|
1228
|
+
|
|
1229
|
+
public BomResolver(BomService bomService) {
|
|
1230
|
+
this.bomService = bomService;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* 部品展開(BOM ツリー)
|
|
1235
|
+
*/
|
|
1236
|
+
@QueryMapping
|
|
1237
|
+
public BomNode bomTree(@Argument String itemCode) {
|
|
1238
|
+
return bomService.explodeBom(itemCode);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* 使用先照会
|
|
1243
|
+
*/
|
|
1244
|
+
@QueryMapping
|
|
1245
|
+
public List<WhereUsedResult> whereUsed(@Argument String itemCode) {
|
|
1246
|
+
return bomService.whereUsed(itemCode);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
</details>
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
## 第48章:トランザクション API の実装
|
|
1256
|
+
|
|
1257
|
+
### 48.1 発注 GraphQL リゾルバ
|
|
1258
|
+
|
|
1259
|
+
<details>
|
|
1260
|
+
<summary>PurchaseOrderResolver.java</summary>
|
|
1261
|
+
|
|
1262
|
+
```java
|
|
1263
|
+
package com.example.production.infrastructure.graphql.resolver;
|
|
1264
|
+
|
|
1265
|
+
import com.example.production.application.port.in.*;
|
|
1266
|
+
import com.example.production.domain.model.purchase.PurchaseOrder;
|
|
1267
|
+
import org.springframework.graphql.data.method.annotation.*;
|
|
1268
|
+
import org.springframework.stereotype.Controller;
|
|
1269
|
+
|
|
1270
|
+
import java.math.BigDecimal;
|
|
1271
|
+
import java.time.LocalDate;
|
|
1272
|
+
import java.util.List;
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* 発注 GraphQL リゾルバ実装
|
|
1276
|
+
* 既存の PurchaseOrderUseCase を Input Adapter として呼び出す
|
|
1277
|
+
*/
|
|
1278
|
+
@Controller
|
|
1279
|
+
public class PurchaseOrderResolver {
|
|
1280
|
+
|
|
1281
|
+
private final PurchaseOrderUseCase useCase; // 既存の UseCase
|
|
1282
|
+
|
|
1283
|
+
public PurchaseOrderResolver(PurchaseOrderUseCase useCase) {
|
|
1284
|
+
this.useCase = useCase;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// ========== Query ==========
|
|
1288
|
+
|
|
1289
|
+
@QueryMapping
|
|
1290
|
+
public PurchaseOrder purchaseOrder(@Argument String orderNumber) {
|
|
1291
|
+
return useCase.getOrder(orderNumber);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
@QueryMapping
|
|
1295
|
+
public PurchaseOrderConnection purchaseOrders(
|
|
1296
|
+
@Argument String status,
|
|
1297
|
+
@Argument Integer page,
|
|
1298
|
+
@Argument Integer size) {
|
|
1299
|
+
|
|
1300
|
+
List<PurchaseOrder> orders = useCase.getAllOrders();
|
|
1301
|
+
|
|
1302
|
+
// ページネーション処理(省略)
|
|
1303
|
+
return PurchaseOrderConnection.builder()
|
|
1304
|
+
.edges(orders.stream()
|
|
1305
|
+
.map(order -> PurchaseOrderEdge.builder()
|
|
1306
|
+
.node(order)
|
|
1307
|
+
.cursor(order.getOrderNumber())
|
|
1308
|
+
.build())
|
|
1309
|
+
.toList())
|
|
1310
|
+
.pageInfo(PageInfo.builder()
|
|
1311
|
+
.totalElements(orders.size())
|
|
1312
|
+
.build())
|
|
1313
|
+
.build();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ========== Mutation ==========
|
|
1317
|
+
|
|
1318
|
+
@MutationMapping
|
|
1319
|
+
public PurchaseOrder createPurchaseOrder(@Argument CreatePurchaseOrderInput input) {
|
|
1320
|
+
CreatePurchaseOrderCommand command = CreatePurchaseOrderCommand.builder()
|
|
1321
|
+
.supplierCode(input.getSupplierCode())
|
|
1322
|
+
.details(input.getDetails().stream()
|
|
1323
|
+
.map(d -> CreatePurchaseOrderCommand.PurchaseOrderDetailCommand.builder()
|
|
1324
|
+
.itemCode(d.getItemCode())
|
|
1325
|
+
.orderQuantity(d.getOrderQuantity())
|
|
1326
|
+
.unitPrice(d.getUnitPrice())
|
|
1327
|
+
.deliveryDate(d.getDeliveryDate())
|
|
1328
|
+
.build())
|
|
1329
|
+
.toList())
|
|
1330
|
+
.build();
|
|
1331
|
+
|
|
1332
|
+
return useCase.createOrder(command);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
@MutationMapping
|
|
1336
|
+
public PurchaseOrder confirmPurchaseOrder(@Argument String orderNumber) {
|
|
1337
|
+
return useCase.confirmOrder(orderNumber);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
@MutationMapping
|
|
1341
|
+
public boolean cancelPurchaseOrder(@Argument String orderNumber) {
|
|
1342
|
+
useCase.cancelOrder(orderNumber);
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
@MutationMapping
|
|
1347
|
+
public ReceivingResult recordReceiving(@Argument RecordReceivingInput input) {
|
|
1348
|
+
var result = useCase.recordReceiving(
|
|
1349
|
+
input.getOrderNumber(),
|
|
1350
|
+
input.getLineNumber(),
|
|
1351
|
+
input.getReceivedQuantity(),
|
|
1352
|
+
input.getReceivingDate()
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
return ReceivingResult.builder()
|
|
1356
|
+
.orderNumber(input.getOrderNumber())
|
|
1357
|
+
.lineNumber(input.getLineNumber())
|
|
1358
|
+
.totalReceived(result.getTotalReceived())
|
|
1359
|
+
.isCompleted(result.isCompleted())
|
|
1360
|
+
.build();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// 入力型
|
|
1365
|
+
@lombok.Data
|
|
1366
|
+
class CreatePurchaseOrderInput {
|
|
1367
|
+
private String supplierCode;
|
|
1368
|
+
private List<CreateOrderDetailInput> details;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
@lombok.Data
|
|
1372
|
+
class CreateOrderDetailInput {
|
|
1373
|
+
private String itemCode;
|
|
1374
|
+
private BigDecimal orderQuantity;
|
|
1375
|
+
private BigDecimal unitPrice;
|
|
1376
|
+
private LocalDate deliveryDate;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
@lombok.Data
|
|
1380
|
+
class RecordReceivingInput {
|
|
1381
|
+
private String orderNumber;
|
|
1382
|
+
private int lineNumber;
|
|
1383
|
+
private BigDecimal receivedQuantity;
|
|
1384
|
+
private LocalDate receivingDate;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
@lombok.Data
|
|
1388
|
+
@lombok.Builder
|
|
1389
|
+
class ReceivingResult {
|
|
1390
|
+
private String orderNumber;
|
|
1391
|
+
private int lineNumber;
|
|
1392
|
+
private BigDecimal totalReceived;
|
|
1393
|
+
private boolean isCompleted;
|
|
1394
|
+
}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
</details>
|
|
1398
|
+
|
|
1399
|
+
### 48.2 MRP GraphQL リゾルバ
|
|
1400
|
+
|
|
1401
|
+
<details>
|
|
1402
|
+
<summary>MrpResolver.java</summary>
|
|
1403
|
+
|
|
1404
|
+
```java
|
|
1405
|
+
package com.example.production.infrastructure.graphql.resolver;
|
|
1406
|
+
|
|
1407
|
+
import com.example.production.application.service.MrpService;
|
|
1408
|
+
import org.springframework.graphql.data.method.annotation.*;
|
|
1409
|
+
import org.springframework.stereotype.Controller;
|
|
1410
|
+
|
|
1411
|
+
import java.time.LocalDate;
|
|
1412
|
+
import java.util.UUID;
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* MRP GraphQL リゾルバ実装
|
|
1416
|
+
* 既存の MrpService を Input Adapter として呼び出す
|
|
1417
|
+
*/
|
|
1418
|
+
@Controller
|
|
1419
|
+
public class MrpResolver {
|
|
1420
|
+
|
|
1421
|
+
private final MrpService mrpService; // 既存のサービス
|
|
1422
|
+
|
|
1423
|
+
public MrpResolver(MrpService mrpService) {
|
|
1424
|
+
this.mrpService = mrpService;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* MRP 実行
|
|
1429
|
+
*/
|
|
1430
|
+
@MutationMapping
|
|
1431
|
+
public MrpResult executeMrp(@Argument ExecuteMrpInput input) {
|
|
1432
|
+
String executionId = UUID.randomUUID().toString();
|
|
1433
|
+
|
|
1434
|
+
long startTime = System.currentTimeMillis();
|
|
1435
|
+
var result = mrpService.execute(input.getStartDate(), input.getEndDate());
|
|
1436
|
+
long executionTime = System.currentTimeMillis() - startTime;
|
|
1437
|
+
|
|
1438
|
+
return MrpResult.builder()
|
|
1439
|
+
.executionId(executionId)
|
|
1440
|
+
.periodStart(input.getStartDate())
|
|
1441
|
+
.periodEnd(input.getEndDate())
|
|
1442
|
+
.plannedOrders(result.getPlannedOrders())
|
|
1443
|
+
.shortageItems(result.getShortageItems())
|
|
1444
|
+
.statistics(MrpStatistics.builder()
|
|
1445
|
+
.totalItemsProcessed(result.getPlannedOrders().size())
|
|
1446
|
+
.shortageItemCount(result.getShortageItems().size())
|
|
1447
|
+
.executionTimeMs(executionTime)
|
|
1448
|
+
.build())
|
|
1449
|
+
.build();
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
@lombok.Data
|
|
1454
|
+
class ExecuteMrpInput {
|
|
1455
|
+
private LocalDate startDate;
|
|
1456
|
+
private LocalDate endDate;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
@lombok.Data
|
|
1460
|
+
@lombok.Builder
|
|
1461
|
+
class MrpResult {
|
|
1462
|
+
private String executionId;
|
|
1463
|
+
private LocalDate periodStart;
|
|
1464
|
+
private LocalDate periodEnd;
|
|
1465
|
+
private java.util.List<?> plannedOrders;
|
|
1466
|
+
private java.util.List<?> shortageItems;
|
|
1467
|
+
private MrpStatistics statistics;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
@lombok.Data
|
|
1471
|
+
@lombok.Builder
|
|
1472
|
+
class MrpStatistics {
|
|
1473
|
+
private int totalItemsProcessed;
|
|
1474
|
+
private int purchaseOrderCount;
|
|
1475
|
+
private int productionOrderCount;
|
|
1476
|
+
private int shortageItemCount;
|
|
1477
|
+
private long executionTimeMs;
|
|
1478
|
+
}
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
</details>
|
|
1482
|
+
|
|
1483
|
+
### 48.3 Subscription(リアルタイム更新)
|
|
1484
|
+
|
|
1485
|
+
<details>
|
|
1486
|
+
<summary>SubscriptionResolver.java</summary>
|
|
1487
|
+
|
|
1488
|
+
```java
|
|
1489
|
+
package com.example.production.infrastructure.graphql.subscription;
|
|
1490
|
+
|
|
1491
|
+
import com.example.production.domain.model.purchase.PurchaseOrder;
|
|
1492
|
+
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1493
|
+
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
|
1494
|
+
import org.springframework.stereotype.Controller;
|
|
1495
|
+
import reactor.core.publisher.Flux;
|
|
1496
|
+
|
|
1497
|
+
import java.time.Duration;
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* GraphQL Subscription リゾルバ
|
|
1501
|
+
*/
|
|
1502
|
+
@Controller
|
|
1503
|
+
public class SubscriptionResolver {
|
|
1504
|
+
|
|
1505
|
+
private final OrderEventPublisher orderEventPublisher;
|
|
1506
|
+
private final MrpProgressPublisher mrpProgressPublisher;
|
|
1507
|
+
|
|
1508
|
+
public SubscriptionResolver(
|
|
1509
|
+
OrderEventPublisher orderEventPublisher,
|
|
1510
|
+
MrpProgressPublisher mrpProgressPublisher) {
|
|
1511
|
+
this.orderEventPublisher = orderEventPublisher;
|
|
1512
|
+
this.mrpProgressPublisher = mrpProgressPublisher;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* MRP 進捗の購読
|
|
1517
|
+
*/
|
|
1518
|
+
@SubscriptionMapping
|
|
1519
|
+
public Flux<MrpProgress> mrpProgress(@Argument String executionId) {
|
|
1520
|
+
return mrpProgressPublisher.subscribe(executionId);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* 発注ステータス変更の購読
|
|
1525
|
+
*/
|
|
1526
|
+
@SubscriptionMapping
|
|
1527
|
+
public Flux<PurchaseOrder> orderStatusChanged(@Argument String orderNumber) {
|
|
1528
|
+
return orderEventPublisher.subscribe(orderNumber);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* 在庫変動の購読
|
|
1533
|
+
*/
|
|
1534
|
+
@SubscriptionMapping
|
|
1535
|
+
public Flux<StockChange> stockChanged(@Argument String itemCode) {
|
|
1536
|
+
// ダミー実装(5秒ごとに在庫変動をシミュレート)
|
|
1537
|
+
return Flux.interval(Duration.ofSeconds(5))
|
|
1538
|
+
.map(i -> StockChange.builder()
|
|
1539
|
+
.itemCode(itemCode != null ? itemCode : "ITEM-001")
|
|
1540
|
+
.changeType("RECEIVE")
|
|
1541
|
+
.quantity(java.math.BigDecimal.valueOf(10))
|
|
1542
|
+
.build());
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
@lombok.Data
|
|
1547
|
+
@lombok.Builder
|
|
1548
|
+
class StockChange {
|
|
1549
|
+
private String itemCode;
|
|
1550
|
+
private String changeType;
|
|
1551
|
+
private java.math.BigDecimal quantity;
|
|
1552
|
+
}
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
</details>
|
|
1556
|
+
|
|
1557
|
+
<details>
|
|
1558
|
+
<summary>MrpProgressPublisher.java(イベントパブリッシャー)</summary>
|
|
1559
|
+
|
|
1560
|
+
```java
|
|
1561
|
+
package com.example.production.infrastructure.graphql.subscription;
|
|
1562
|
+
|
|
1563
|
+
import org.springframework.stereotype.Component;
|
|
1564
|
+
import reactor.core.publisher.Flux;
|
|
1565
|
+
import reactor.core.publisher.Sinks;
|
|
1566
|
+
|
|
1567
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* MRP 進捗イベントパブリッシャー
|
|
1571
|
+
*/
|
|
1572
|
+
@Component
|
|
1573
|
+
public class MrpProgressPublisher {
|
|
1574
|
+
|
|
1575
|
+
private final ConcurrentHashMap<String, Sinks.Many<MrpProgress>> sinks =
|
|
1576
|
+
new ConcurrentHashMap<>();
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* 進捗を購読
|
|
1580
|
+
*/
|
|
1581
|
+
public Flux<MrpProgress> subscribe(String executionId) {
|
|
1582
|
+
Sinks.Many<MrpProgress> sink = sinks.computeIfAbsent(
|
|
1583
|
+
executionId,
|
|
1584
|
+
k -> Sinks.many().multicast().onBackpressureBuffer()
|
|
1585
|
+
);
|
|
1586
|
+
return sink.asFlux();
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* 進捗を発行
|
|
1591
|
+
*/
|
|
1592
|
+
public void publish(String executionId, MrpProgress progress) {
|
|
1593
|
+
Sinks.Many<MrpProgress> sink = sinks.get(executionId);
|
|
1594
|
+
if (sink != null) {
|
|
1595
|
+
sink.tryEmitNext(progress);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* 完了
|
|
1601
|
+
*/
|
|
1602
|
+
public void complete(String executionId) {
|
|
1603
|
+
Sinks.Many<MrpProgress> sink = sinks.remove(executionId);
|
|
1604
|
+
if (sink != null) {
|
|
1605
|
+
sink.tryEmitComplete();
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
@lombok.Data
|
|
1611
|
+
@lombok.Builder
|
|
1612
|
+
class MrpProgress {
|
|
1613
|
+
private String executionId;
|
|
1614
|
+
private String phase;
|
|
1615
|
+
private int current;
|
|
1616
|
+
private int total;
|
|
1617
|
+
private String message;
|
|
1618
|
+
}
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
</details>
|
|
1622
|
+
|
|
1623
|
+
---
|
|
1624
|
+
|
|
1625
|
+
## 第49章:エラーハンドリングとベストプラクティス
|
|
1626
|
+
|
|
1627
|
+
### 49.1 グローバル例外ハンドラ
|
|
1628
|
+
|
|
1629
|
+
<details>
|
|
1630
|
+
<summary>GraphQLExceptionHandler.java</summary>
|
|
1631
|
+
|
|
1632
|
+
```java
|
|
1633
|
+
package com.example.production.infrastructure.graphql;
|
|
1634
|
+
|
|
1635
|
+
import com.example.production.domain.exception.*;
|
|
1636
|
+
import graphql.GraphQLError;
|
|
1637
|
+
import graphql.GraphqlErrorBuilder;
|
|
1638
|
+
import graphql.schema.DataFetchingEnvironment;
|
|
1639
|
+
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
|
|
1640
|
+
import org.springframework.graphql.execution.ErrorType;
|
|
1641
|
+
import org.springframework.stereotype.Component;
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* GraphQL グローバル例外ハンドラ
|
|
1645
|
+
*/
|
|
1646
|
+
@Component
|
|
1647
|
+
public class GraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {
|
|
1648
|
+
|
|
1649
|
+
@Override
|
|
1650
|
+
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
|
|
1651
|
+
if (ex instanceof ItemNotFoundException) {
|
|
1652
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1653
|
+
.errorType(ErrorType.NOT_FOUND)
|
|
1654
|
+
.message(ex.getMessage())
|
|
1655
|
+
.build();
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (ex instanceof DuplicateItemException) {
|
|
1659
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1660
|
+
.errorType(ErrorType.BAD_REQUEST)
|
|
1661
|
+
.message(ex.getMessage())
|
|
1662
|
+
.build();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (ex instanceof OrderAlreadyConfirmedException) {
|
|
1666
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1667
|
+
.errorType(ErrorType.BAD_REQUEST)
|
|
1668
|
+
.message(ex.getMessage())
|
|
1669
|
+
.build();
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (ex instanceof IllegalArgumentException) {
|
|
1673
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1674
|
+
.errorType(ErrorType.BAD_REQUEST)
|
|
1675
|
+
.message(ex.getMessage())
|
|
1676
|
+
.build();
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// 未知のエラー
|
|
1680
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1681
|
+
.errorType(ErrorType.INTERNAL_ERROR)
|
|
1682
|
+
.message("内部エラーが発生しました")
|
|
1683
|
+
.build();
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
</details>
|
|
1689
|
+
|
|
1690
|
+
### 49.2 入力バリデーション
|
|
1691
|
+
|
|
1692
|
+
<details>
|
|
1693
|
+
<summary>ValidationExceptionHandler.java</summary>
|
|
1694
|
+
|
|
1695
|
+
```java
|
|
1696
|
+
package com.example.production.infrastructure.graphql.validation;
|
|
1697
|
+
|
|
1698
|
+
import graphql.GraphQLError;
|
|
1699
|
+
import graphql.GraphqlErrorBuilder;
|
|
1700
|
+
import graphql.schema.DataFetchingEnvironment;
|
|
1701
|
+
import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler;
|
|
1702
|
+
import org.springframework.graphql.execution.ErrorType;
|
|
1703
|
+
import org.springframework.stereotype.Controller;
|
|
1704
|
+
|
|
1705
|
+
import jakarta.validation.ConstraintViolation;
|
|
1706
|
+
import jakarta.validation.ConstraintViolationException;
|
|
1707
|
+
import java.util.List;
|
|
1708
|
+
import java.util.stream.Collectors;
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* バリデーション例外ハンドラ
|
|
1712
|
+
*/
|
|
1713
|
+
@Controller
|
|
1714
|
+
public class ValidationExceptionHandler {
|
|
1715
|
+
|
|
1716
|
+
@GraphQlExceptionHandler
|
|
1717
|
+
public List<GraphQLError> handleValidation(
|
|
1718
|
+
ConstraintViolationException ex,
|
|
1719
|
+
DataFetchingEnvironment env) {
|
|
1720
|
+
|
|
1721
|
+
return ex.getConstraintViolations().stream()
|
|
1722
|
+
.map(violation -> toGraphQLError(violation, env))
|
|
1723
|
+
.collect(Collectors.toList());
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
private GraphQLError toGraphQLError(
|
|
1727
|
+
ConstraintViolation<?> violation,
|
|
1728
|
+
DataFetchingEnvironment env) {
|
|
1729
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1730
|
+
.errorType(ErrorType.BAD_REQUEST)
|
|
1731
|
+
.message(String.format("%s: %s",
|
|
1732
|
+
violation.getPropertyPath(),
|
|
1733
|
+
violation.getMessage()))
|
|
1734
|
+
.build();
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
</details>
|
|
1740
|
+
|
|
1741
|
+
### 49.3 設定ファイル
|
|
1742
|
+
|
|
1743
|
+
<details>
|
|
1744
|
+
<summary>application.yml(GraphQL 追加設定)</summary>
|
|
1745
|
+
|
|
1746
|
+
```yaml
|
|
1747
|
+
spring:
|
|
1748
|
+
graphql:
|
|
1749
|
+
path: /graphql
|
|
1750
|
+
graphiql:
|
|
1751
|
+
enabled: true # 開発時に GraphiQL UI を有効化
|
|
1752
|
+
path: /graphiql
|
|
1753
|
+
websocket:
|
|
1754
|
+
path: /graphql # Subscription 用 WebSocket
|
|
1755
|
+
schema:
|
|
1756
|
+
locations: classpath:graphql/
|
|
1757
|
+
printer:
|
|
1758
|
+
enabled: true # スキーマ出力
|
|
1759
|
+
|
|
1760
|
+
# 既存の設定はそのまま
|
|
1761
|
+
datasource:
|
|
1762
|
+
url: jdbc:postgresql://localhost:5432/production
|
|
1763
|
+
username: postgres
|
|
1764
|
+
password: postgres
|
|
1765
|
+
|
|
1766
|
+
mybatis:
|
|
1767
|
+
mapper-locations: classpath:mapper/*.xml
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
</details>
|
|
1771
|
+
|
|
1772
|
+
### 49.4 GraphQL Playground からの呼び出し
|
|
1773
|
+
|
|
1774
|
+
```graphql
|
|
1775
|
+
# 品目取得
|
|
1776
|
+
query {
|
|
1777
|
+
item(itemCode: "PROD-001") {
|
|
1778
|
+
itemCode
|
|
1779
|
+
itemName
|
|
1780
|
+
category
|
|
1781
|
+
leadTime
|
|
1782
|
+
bom {
|
|
1783
|
+
childItemCode
|
|
1784
|
+
requiredQuantity
|
|
1785
|
+
childItem {
|
|
1786
|
+
itemName
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
# 品目一覧(ページネーション)
|
|
1793
|
+
query {
|
|
1794
|
+
items(category: PRODUCT, page: 0, size: 10) {
|
|
1795
|
+
edges {
|
|
1796
|
+
node {
|
|
1797
|
+
itemCode
|
|
1798
|
+
itemName
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
pageInfo {
|
|
1802
|
+
totalElements
|
|
1803
|
+
hasNextPage
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
# 品目登録
|
|
1809
|
+
mutation {
|
|
1810
|
+
createItem(input: {
|
|
1811
|
+
itemCode: "NEW-001"
|
|
1812
|
+
itemName: "新規製品"
|
|
1813
|
+
category: PRODUCT
|
|
1814
|
+
leadTime: 7
|
|
1815
|
+
}) {
|
|
1816
|
+
itemCode
|
|
1817
|
+
itemName
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
# MRP 進捗購読
|
|
1822
|
+
subscription {
|
|
1823
|
+
mrpProgress(executionId: "exec-123") {
|
|
1824
|
+
phase
|
|
1825
|
+
current
|
|
1826
|
+
total
|
|
1827
|
+
message
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
### 49.5 GraphQL vs REST vs gRPC の使い分け
|
|
1833
|
+
|
|
1834
|
+
| 用途 | 推奨 | 理由 |
|
|
1835
|
+
|------|------|------|
|
|
1836
|
+
| **フロントエンド API** | GraphQL | 柔軟なデータ取得、Over-fetching 防止 |
|
|
1837
|
+
| **モバイルアプリ** | GraphQL | 帯域制限、必要なデータのみ取得 |
|
|
1838
|
+
| **マイクロサービス間** | gRPC | 高パフォーマンス、型安全 |
|
|
1839
|
+
| **サードパーティ連携** | REST | 広く普及、シンプル |
|
|
1840
|
+
| **リアルタイム更新** | GraphQL Subscription / gRPC Streaming | ネイティブサポート |
|
|
1841
|
+
| **バッチ処理** | gRPC | ストリーミング効率 |
|
|
1842
|
+
|
|
1843
|
+
---
|
|
1844
|
+
|
|
1845
|
+
## まとめ
|
|
1846
|
+
|
|
1847
|
+
本研究では、GraphQL による生産管理システム API を実装しました。
|
|
1848
|
+
|
|
1849
|
+
### API サーバー版との比較
|
|
1850
|
+
|
|
1851
|
+
| 観点 | REST API(第32章) | gRPC(研究2) | GraphQL(本研究) |
|
|
1852
|
+
|------|-------------------|-----------------|-------------------|
|
|
1853
|
+
| **プロトコル** | HTTP/1.1 + JSON | HTTP/2 + Protobuf | HTTP + JSON |
|
|
1854
|
+
| **スキーマ** | OpenAPI(任意) | .proto(必須) | .graphqls(必須) |
|
|
1855
|
+
| **データ取得** | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
1856
|
+
| **エンドポイント** | 複数 | 複数 | 単一 |
|
|
1857
|
+
| **リアルタイム** | WebSocket 別実装 | ストリーミング | Subscription |
|
|
1858
|
+
| **ドメイン層** | 共有 | 共有 | 共有 |
|
|
1859
|
+
| **アプリケーション層** | 共有 | 共有 | 共有 |
|
|
1860
|
+
| **Input Adapter** | REST Controller | gRPC Service | GraphQL Resolver |
|
|
1861
|
+
|
|
1862
|
+
### 実装した GraphQL 操作
|
|
1863
|
+
|
|
1864
|
+
| 操作タイプ | 操作名 | 説明 |
|
|
1865
|
+
|-----------|--------|------|
|
|
1866
|
+
| **Query** | item, items | 品目取得 |
|
|
1867
|
+
| | bomTree, whereUsed | BOM 展開・逆展開 |
|
|
1868
|
+
| | purchaseOrder, purchaseOrders | 発注取得 |
|
|
1869
|
+
| **Mutation** | createItem, updateItem, deleteItem | 品目 CRUD |
|
|
1870
|
+
| | createPurchaseOrder, confirmPurchaseOrder | 発注操作 |
|
|
1871
|
+
| | recordReceiving | 入荷登録 |
|
|
1872
|
+
| | executeMrp | MRP 実行 |
|
|
1873
|
+
| **Subscription** | mrpProgress | MRP 進捗通知 |
|
|
1874
|
+
| | orderStatusChanged | 発注ステータス変更 |
|
|
1875
|
+
| | stockChanged | 在庫変動 |
|
|
1876
|
+
|
|
1877
|
+
### 技術スタック
|
|
1878
|
+
|
|
1879
|
+
- **Spring for GraphQL**: Spring Boot 統合
|
|
1880
|
+
- **GraphQL Java**: GraphQL 実行エンジン
|
|
1881
|
+
- **Extended Scalars**: Date, BigDecimal 等のカスタム型
|
|
1882
|
+
- **DataLoader**: N+1 問題対策
|
|
1883
|
+
- **WebSocket**: Subscription サポート
|
|
1884
|
+
|
|
1885
|
+
### GraphQL を選択すべき場面
|
|
1886
|
+
|
|
1887
|
+
1. **フロントエンド主導の開発**: クライアントがデータ形状を決定
|
|
1888
|
+
2. **モバイルアプリ**: 帯域制限下での効率的なデータ取得
|
|
1889
|
+
3. **複雑な関連データ**: 1回のクエリで関連データを取得
|
|
1890
|
+
4. **リアルタイム更新**: Subscription でプッシュ通知
|
|
1891
|
+
5. **API 統合**: 複数のバックエンドを単一エンドポイントで提供
|
|
1892
|
+
|
|
1893
|
+
---
|
|
1894
|
+
|
|
1895
|
+
[目次へ戻る](../index.md)
|