@k2works/claude-code-booster 3.4.1 → 3.6.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 +239 -239
- 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 -88
- 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-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 +162 -161
- 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 +29 -1
- 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 +183 -183
- 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 +532 -532
- 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 -580
- 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 -242
- 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 -544
- 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 -682
- 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 -560
- 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 -2636
- 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 -663
- 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 +28 -0
- 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 -68
- 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 -135
- 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
- package/lib/assets/.claude/agent-memory/xp-programmer/MEMORY.md +0 -6
- package/lib/assets/.claude/agent-memory/xp-programmer/project_cargo_tracker.md +0 -11
- package/lib/assets/.claude/agent-memory/xp-programmer/project_ddd_patterns.md +0 -27
- package/lib/assets/.claude/agent-memory/xp-programmer/project_us07_route_assignment.md +0 -19
|
@@ -1,2106 +1,2106 @@
|
|
|
1
|
-
# 研究 4:GraphQL サービスの実装
|
|
2
|
-
|
|
3
|
-
## はじめに
|
|
4
|
-
|
|
5
|
-
本パートでは、REST API や gRPC とは異なるアプローチとして、**GraphQL** による財務会計システムを実装します。クライアントが必要なデータを正確に指定できる柔軟なクエリと、リアルタイム更新を実現する Subscription を活用します。
|
|
6
|
-
|
|
7
|
-
ヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 第 22 章:GraphQL サーバーの基礎
|
|
12
|
-
|
|
13
|
-
### 22.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
|
-
- AccountResolver
|
|
36
|
-
- JournalResolver
|
|
37
|
-
- BalanceResolver
|
|
38
|
-
- ReportResolver
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
package "Shared" {
|
|
43
|
-
RECTANGLE "GraphQL Schema\n(.graphqls files)" as schema {
|
|
44
|
-
- schema.graphqls
|
|
45
|
-
- account.graphqls
|
|
46
|
-
- journal.graphqls
|
|
47
|
-
- balance.graphqls
|
|
48
|
-
- report.graphqls
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
client --> schema : "スキーマに基づいて\nクエリを構築"
|
|
54
|
-
server --> schema : "スキーマに基づいて\nリゾルバを実装"
|
|
55
|
-
client <--> server : "HTTP/WebSocket\n(JSON)"
|
|
56
|
-
|
|
57
|
-
note bottom of schema
|
|
58
|
-
スキーマ駆動開発
|
|
59
|
-
クライアント主導のデータ取得
|
|
60
|
-
型安全な API
|
|
61
|
-
end note
|
|
62
|
-
|
|
63
|
-
@enduml
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
**REST API / gRPC / GraphQL の比較:**
|
|
67
|
-
|
|
68
|
-
| 特徴 | REST API | gRPC | GraphQL |
|
|
69
|
-
|------|----------|------|---------|
|
|
70
|
-
| プロトコル | HTTP/1.1 | HTTP/2 | HTTP/1.1 or HTTP/2 |
|
|
71
|
-
| データ形式 | JSON | Protocol Buffers | JSON |
|
|
72
|
-
| スキーマ | OpenAPI (任意) | .proto (必須) | .graphqls (必須) |
|
|
73
|
-
| データ取得 | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
74
|
-
| エンドポイント | 複数 | 複数 | 単一 |
|
|
75
|
-
| リアルタイム | WebSocket 別実装 | ストリーミング | Subscription |
|
|
76
|
-
| 主な用途 | 汎用 API | マイクロサービス | フロントエンド向け |
|
|
77
|
-
|
|
78
|
-
### 22.2 3 つの操作タイプ
|
|
79
|
-
|
|
80
|
-
GraphQL は 3 つの操作タイプをサポートします:
|
|
81
|
-
|
|
82
|
-
```plantuml
|
|
83
|
-
@startuml graphql_operations
|
|
84
|
-
skinparam backgroundColor #FEFEFE
|
|
85
|
-
|
|
86
|
-
rectangle "1. Query\n(読み取り)" as query #LightBlue {
|
|
87
|
-
}
|
|
88
|
-
note right of query
|
|
89
|
-
{ accounts { code name category } }
|
|
90
|
-
→ { accounts: [...] }
|
|
91
|
-
|
|
92
|
-
データの取得
|
|
93
|
-
複数リソースを1回で取得可能
|
|
94
|
-
end note
|
|
95
|
-
|
|
96
|
-
rectangle "2. Mutation\n(書き込み)" as mutation #LightGreen {
|
|
97
|
-
}
|
|
98
|
-
note right of mutation
|
|
99
|
-
mutation { createJournal(...) }
|
|
100
|
-
→ { createJournal: {...} }
|
|
101
|
-
|
|
102
|
-
データの作成・更新・削除
|
|
103
|
-
end note
|
|
104
|
-
|
|
105
|
-
rectangle "3. Subscription\n(リアルタイム)" as subscription #LightCoral {
|
|
106
|
-
}
|
|
107
|
-
note right of subscription
|
|
108
|
-
subscription { balanceChanged }
|
|
109
|
-
→ (WebSocket でプッシュ通知)
|
|
110
|
-
|
|
111
|
-
リアルタイム更新の受信
|
|
112
|
-
end note
|
|
113
|
-
|
|
114
|
-
query -[hidden]-> mutation
|
|
115
|
-
mutation -[hidden]-> subscription
|
|
116
|
-
|
|
117
|
-
@enduml
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**用途:**
|
|
121
|
-
|
|
122
|
-
1. **Query**: データ取得(勘定科目一覧、仕訳照会、残高照会)
|
|
123
|
-
2. **Mutation**: データ更新(仕訳登録、仕訳取消、決算仕訳)
|
|
124
|
-
3. **Subscription**: リアルタイム通知(残高変動、仕訳登録通知)
|
|
125
|
-
|
|
126
|
-
### 22.3 GraphQL におけるヘキサゴナルアーキテクチャ
|
|
127
|
-
|
|
128
|
-
GraphQL を導入しても、ヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
129
|
-
|
|
130
|
-
```plantuml
|
|
131
|
-
@startuml hexagonal_graphql
|
|
132
|
-
!define RECTANGLE class
|
|
133
|
-
|
|
134
|
-
package "Hexagonal Architecture (GraphQL版)" {
|
|
135
|
-
|
|
136
|
-
RECTANGLE "Application Core\n(Domain + Use Cases)" as core {
|
|
137
|
-
- Account (勘定科目)
|
|
138
|
-
- Journal (仕訳)
|
|
139
|
-
- DailyBalance (日次残高)
|
|
140
|
-
- MonthlyBalance (月次残高)
|
|
141
|
-
- AccountUseCase
|
|
142
|
-
- JournalUseCase
|
|
143
|
-
- BalanceUseCase
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
RECTANGLE "Input Adapters\n(Driving Side)" as input {
|
|
147
|
-
- REST Controller(既存)
|
|
148
|
-
- gRPC Service(既存)
|
|
149
|
-
- GraphQL Resolver(新規追加)
|
|
150
|
-
- DataFetcher
|
|
151
|
-
- DataLoader
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
RECTANGLE "Output Adapters\n(Driven Side)" as output {
|
|
155
|
-
- MyBatis Repository
|
|
156
|
-
- Database Access
|
|
157
|
-
- Entity Mapping
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
input --> core : "Input Ports\n(Use Cases)"
|
|
162
|
-
core --> output : "Output Ports\n(Repository Interfaces)"
|
|
163
|
-
|
|
164
|
-
note top of core
|
|
165
|
-
既存のビジネスロジック
|
|
166
|
-
REST API / gRPC 版と完全に共有
|
|
167
|
-
GraphQL 固有のコードは含まない
|
|
168
|
-
end note
|
|
169
|
-
|
|
170
|
-
note left of input
|
|
171
|
-
GraphQL リゾルバを
|
|
172
|
-
Input Adapter として追加
|
|
173
|
-
既存の REST/gRPC と共存可能
|
|
174
|
-
end note
|
|
175
|
-
|
|
176
|
-
note right of output
|
|
177
|
-
既存の Repository を
|
|
178
|
-
そのまま使用
|
|
179
|
-
変更不要
|
|
180
|
-
end note
|
|
181
|
-
|
|
182
|
-
@enduml
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### 22.4 ディレクトリ構成
|
|
186
|
-
|
|
187
|
-
既存の構成に `infrastructure/graphql/` を追加するだけです。
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
src/main/java/com/example/accounting/
|
|
191
|
-
├── domain/ # ドメイン層(API版と共通)
|
|
192
|
-
│ ├── model/
|
|
193
|
-
│ │ ├── account/
|
|
194
|
-
│ │ ├── journal/
|
|
195
|
-
│ │ ├── balance/
|
|
196
|
-
│ │ └── report/
|
|
197
|
-
│ └── exception/
|
|
198
|
-
│
|
|
199
|
-
├── application/ # アプリケーション層(API版と共通)
|
|
200
|
-
│ ├── port/
|
|
201
|
-
│ │ ├── in/ # Input Port(ユースケース)
|
|
202
|
-
│ │ └── out/ # Output Port(リポジトリ)
|
|
203
|
-
│ └── service/
|
|
204
|
-
│
|
|
205
|
-
├── infrastructure/
|
|
206
|
-
│ ├── persistence/ # Output Adapter(DB実装)- 既存
|
|
207
|
-
│ │ ├── mapper/
|
|
208
|
-
│ │ └── repository/
|
|
209
|
-
│ ├── rest/ # Input Adapter(REST実装)- 既存
|
|
210
|
-
│ ├── grpc/ # Input Adapter(gRPC実装)- 既存
|
|
211
|
-
│ └── graphql/ # Input Adapter(GraphQL実装)- 新規追加
|
|
212
|
-
│ ├── resolver/ # Query/Mutation リゾルバ
|
|
213
|
-
│ ├── dataloader/ # N+1 問題対策
|
|
214
|
-
│ ├── scalar/ # カスタムスカラー型
|
|
215
|
-
│ └── subscription/ # Subscription ハンドラ
|
|
216
|
-
│
|
|
217
|
-
├── config/
|
|
218
|
-
│
|
|
219
|
-
└── src/main/resources/
|
|
220
|
-
└── graphql/ # GraphQL スキーマ定義
|
|
221
|
-
├── schema.graphqls
|
|
222
|
-
├── account.graphqls
|
|
223
|
-
├── journal.graphqls
|
|
224
|
-
├── balance.graphqls
|
|
225
|
-
└── report.graphqls
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### 22.5 技術スタックの追加
|
|
229
|
-
|
|
230
|
-
既存の `build.gradle.kts` に GraphQL 関連の依存関係を追加します。
|
|
231
|
-
|
|
232
|
-
<details>
|
|
233
|
-
<summary>build.gradle.kts(差分)</summary>
|
|
234
|
-
|
|
235
|
-
```kotlin
|
|
236
|
-
dependencies {
|
|
237
|
-
// 既存の依存関係(Spring Boot, MyBatis, PostgreSQL等)はそのまま
|
|
238
|
-
|
|
239
|
-
// GraphQL 関連を追加
|
|
240
|
-
implementation("org.springframework.boot:spring-boot-starter-graphql")
|
|
241
|
-
implementation("org.springframework.boot:spring-boot-starter-websocket") // Subscription 用
|
|
242
|
-
|
|
243
|
-
// GraphQL 拡張
|
|
244
|
-
implementation("com.graphql-java:graphql-java-extended-scalars:21.0")
|
|
245
|
-
|
|
246
|
-
// Test
|
|
247
|
-
testImplementation("org.springframework.graphql:spring-graphql-test")
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
</details>
|
|
252
|
-
|
|
253
|
-
**追加パッケージの説明:**
|
|
254
|
-
|
|
255
|
-
| パッケージ | 用途 |
|
|
256
|
-
|-----------|------|
|
|
257
|
-
| spring-boot-starter-graphql | Spring Boot GraphQL 統合 |
|
|
258
|
-
| spring-boot-starter-websocket | Subscription (WebSocket) |
|
|
259
|
-
| graphql-java-extended-scalars | DateTime, BigDecimal 等のスカラー型 |
|
|
260
|
-
| spring-graphql-test | GraphQL テストサポート |
|
|
261
|
-
|
|
262
|
-
### 22.6 GraphQL 設定
|
|
263
|
-
|
|
264
|
-
<details>
|
|
265
|
-
<summary>application.yml(差分)</summary>
|
|
266
|
-
|
|
267
|
-
```yaml
|
|
268
|
-
spring:
|
|
269
|
-
graphql:
|
|
270
|
-
graphiql:
|
|
271
|
-
enabled: true
|
|
272
|
-
path: /graphiql
|
|
273
|
-
websocket:
|
|
274
|
-
path: /graphql
|
|
275
|
-
schema:
|
|
276
|
-
printer:
|
|
277
|
-
enabled: true
|
|
278
|
-
locations:
|
|
279
|
-
- classpath:graphql/
|
|
280
|
-
cors:
|
|
281
|
-
allowed-origins: "*"
|
|
282
|
-
allowed-methods: GET, POST
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
</details>
|
|
286
|
-
|
|
287
|
-
<details>
|
|
288
|
-
<summary>GraphQLConfig.java</summary>
|
|
289
|
-
|
|
290
|
-
```java
|
|
291
|
-
package com.example.accounting.config;
|
|
292
|
-
|
|
293
|
-
import graphql.scalars.ExtendedScalars;
|
|
294
|
-
import graphql.schema.idl.RuntimeWiring;
|
|
295
|
-
import org.springframework.context.annotation.Bean;
|
|
296
|
-
import org.springframework.context.annotation.Configuration;
|
|
297
|
-
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* GraphQL 設定
|
|
301
|
-
*/
|
|
302
|
-
@Configuration
|
|
303
|
-
public class GraphQLConfig {
|
|
304
|
-
|
|
305
|
-
@Bean
|
|
306
|
-
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
|
|
307
|
-
return wiringBuilder -> wiringBuilder
|
|
308
|
-
.scalar(ExtendedScalars.Date)
|
|
309
|
-
.scalar(ExtendedScalars.DateTime)
|
|
310
|
-
.scalar(ExtendedScalars.GraphQLBigDecimal)
|
|
311
|
-
.scalar(ExtendedScalars.GraphQLLong);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
</details>
|
|
317
|
-
|
|
318
|
-
---
|
|
319
|
-
|
|
320
|
-
## 第 23 章:GraphQL スキーマの定義
|
|
321
|
-
|
|
322
|
-
### 23.1 ルートスキーマ
|
|
323
|
-
|
|
324
|
-
<details>
|
|
325
|
-
<summary>src/main/resources/graphql/schema.graphqls</summary>
|
|
326
|
-
|
|
327
|
-
```graphql
|
|
328
|
-
# ルートスキーマ
|
|
329
|
-
type Query {
|
|
330
|
-
# 勘定科目
|
|
331
|
-
account(accountCode: ID!): Account
|
|
332
|
-
accounts(bsplCategory: BSPLCategory, page: Int, size: Int): AccountConnection!
|
|
333
|
-
accountHierarchy(accountCode: ID!): [Account!]!
|
|
334
|
-
|
|
335
|
-
# 課税取引
|
|
336
|
-
taxTransaction(taxCode: ID!): TaxTransaction
|
|
337
|
-
taxTransactions: [TaxTransaction!]!
|
|
338
|
-
|
|
339
|
-
# 勘定科目構成
|
|
340
|
-
accountConfig(accountCode: ID!): AccountConfig
|
|
341
|
-
accountConfigs(parentPath: String): [AccountConfig!]!
|
|
342
|
-
|
|
343
|
-
# 仕訳
|
|
344
|
-
journal(journalNumber: ID!): Journal
|
|
345
|
-
journals(fromDate: Date, toDate: Date, accountCode: ID, page: Int, size: Int): JournalConnection!
|
|
346
|
-
|
|
347
|
-
# 残高
|
|
348
|
-
dailyBalance(date: Date!, accountCode: ID!, subAccountCode: ID): DailyBalance
|
|
349
|
-
dailyBalances(date: Date!, page: Int, size: Int): DailyBalanceConnection!
|
|
350
|
-
|
|
351
|
-
monthlyBalance(fiscalYear: Int!, month: Int!, accountCode: ID!, subAccountCode: ID): MonthlyBalance
|
|
352
|
-
monthlyBalances(fiscalYear: Int!, month: Int!, page: Int, size: Int): MonthlyBalanceConnection!
|
|
353
|
-
|
|
354
|
-
# 帳票
|
|
355
|
-
dailyReport(date: Date!): DailyReport!
|
|
356
|
-
trialBalance(fiscalYear: Int!, month: Int!): TrialBalance!
|
|
357
|
-
financialStatements(fiscalYear: Int!): FinancialStatements!
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
type Mutation {
|
|
361
|
-
# 勘定科目
|
|
362
|
-
createAccount(input: CreateAccountInput!): Account!
|
|
363
|
-
updateAccount(input: UpdateAccountInput!): Account!
|
|
364
|
-
deleteAccount(accountCode: ID!): Boolean!
|
|
365
|
-
|
|
366
|
-
# 課税取引
|
|
367
|
-
createTaxTransaction(input: CreateTaxTransactionInput!): TaxTransaction!
|
|
368
|
-
updateTaxTransaction(input: UpdateTaxTransactionInput!): TaxTransaction!
|
|
369
|
-
deleteTaxTransaction(taxCode: ID!): Boolean!
|
|
370
|
-
|
|
371
|
-
# 仕訳
|
|
372
|
-
createJournal(input: CreateJournalInput!): Journal!
|
|
373
|
-
updateJournal(input: UpdateJournalInput!): Journal!
|
|
374
|
-
deleteJournal(journalNumber: ID!): Boolean!
|
|
375
|
-
reverseJournal(journalNumber: ID!, reason: String): Journal!
|
|
376
|
-
|
|
377
|
-
# 残高更新
|
|
378
|
-
recalculateDailyBalance(date: Date!): RecalculateResult!
|
|
379
|
-
recalculateMonthlyBalance(fiscalYear: Int!, month: Int!): RecalculateResult!
|
|
380
|
-
|
|
381
|
-
# 決算処理
|
|
382
|
-
executeClosing(input: ExecuteClosingInput!): ClosingResult!
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
type Subscription {
|
|
386
|
-
# 残高変動
|
|
387
|
-
balanceChanged(accountCode: ID): BalanceChange!
|
|
388
|
-
|
|
389
|
-
# 仕訳登録通知
|
|
390
|
-
journalCreated: JournalNotification!
|
|
391
|
-
|
|
392
|
-
# 決算処理進捗
|
|
393
|
-
closingProgressUpdated(closingId: ID!): ClosingProgress!
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
# ページネーション共通型
|
|
397
|
-
type PageInfo {
|
|
398
|
-
hasNextPage: Boolean!
|
|
399
|
-
hasPreviousPage: Boolean!
|
|
400
|
-
totalElements: Int!
|
|
401
|
-
totalPages: Int!
|
|
402
|
-
currentPage: Int!
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
# カスタムスカラー
|
|
406
|
-
scalar Date
|
|
407
|
-
scalar DateTime
|
|
408
|
-
scalar BigDecimal
|
|
409
|
-
scalar Long
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
</details>
|
|
413
|
-
|
|
414
|
-
### 23.2 勘定科目スキーマ
|
|
415
|
-
|
|
416
|
-
<details>
|
|
417
|
-
<summary>src/main/resources/graphql/account.graphqls</summary>
|
|
418
|
-
|
|
419
|
-
```graphql
|
|
420
|
-
# BSPL区分
|
|
421
|
-
enum BSPLCategory {
|
|
422
|
-
BS # 貸借対照表
|
|
423
|
-
PL # 損益計算書
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
# 貸借区分
|
|
427
|
-
enum DebitCreditCategory {
|
|
428
|
-
DEBIT # 借方
|
|
429
|
-
CREDIT # 貸方
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
# 取引要素区分
|
|
433
|
-
enum TransactionElementCategory {
|
|
434
|
-
ASSET # 資産
|
|
435
|
-
LIABILITY # 負債
|
|
436
|
-
CAPITAL # 資本
|
|
437
|
-
REVENUE # 収益
|
|
438
|
-
EXPENSE # 費用
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
# 集計区分
|
|
442
|
-
enum AggregationCategory {
|
|
443
|
-
HEADING # 見出科目
|
|
444
|
-
SUBTOTAL # 集計科目
|
|
445
|
-
POSTING # 計上科目
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
# 勘定科目
|
|
449
|
-
type Account {
|
|
450
|
-
accountCode: ID!
|
|
451
|
-
accountName: String!
|
|
452
|
-
accountShortName: String
|
|
453
|
-
accountKana: String
|
|
454
|
-
bsplCategory: BSPLCategory!
|
|
455
|
-
debitCreditCategory: DebitCreditCategory!
|
|
456
|
-
transactionElementCategory: TransactionElementCategory!
|
|
457
|
-
aggregationCategory: AggregationCategory!
|
|
458
|
-
managementAccountingCategory: String
|
|
459
|
-
expenseCategory: String
|
|
460
|
-
ledgerOutputCategory: String
|
|
461
|
-
subAccountType: String
|
|
462
|
-
taxCalculationCategory: String
|
|
463
|
-
taxTransactionCode: ID
|
|
464
|
-
dueDateManagementCategory: String
|
|
465
|
-
createdAt: DateTime!
|
|
466
|
-
updatedAt: DateTime!
|
|
467
|
-
updatedBy: String
|
|
468
|
-
|
|
469
|
-
# 関連データ(必要な場合のみ取得)
|
|
470
|
-
taxTransaction: TaxTransaction
|
|
471
|
-
accountConfig: AccountConfig
|
|
472
|
-
childAccounts: [Account!]!
|
|
473
|
-
dailyBalances(date: Date): [DailyBalance!]!
|
|
474
|
-
monthlyBalances(fiscalYear: Int, month: Int): [MonthlyBalance!]!
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
# 勘定科目一覧(ページネーション付き)
|
|
478
|
-
type AccountConnection {
|
|
479
|
-
edges: [AccountEdge!]!
|
|
480
|
-
pageInfo: PageInfo!
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
type AccountEdge {
|
|
484
|
-
node: Account!
|
|
485
|
-
cursor: String!
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
# 課税取引マスタ
|
|
489
|
-
type TaxTransaction {
|
|
490
|
-
taxCode: ID!
|
|
491
|
-
taxName: String!
|
|
492
|
-
taxRate: BigDecimal!
|
|
493
|
-
updatedAt: DateTime!
|
|
494
|
-
updatedBy: String
|
|
495
|
-
|
|
496
|
-
# 関連データ
|
|
497
|
-
accounts: [Account!]!
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
# 勘定科目構成マスタ
|
|
501
|
-
type AccountConfig {
|
|
502
|
-
accountCode: ID!
|
|
503
|
-
accountPath: String!
|
|
504
|
-
updatedAt: DateTime!
|
|
505
|
-
updatedBy: String
|
|
506
|
-
|
|
507
|
-
# 関連データ
|
|
508
|
-
account: Account!
|
|
509
|
-
parentAccount: Account
|
|
510
|
-
childAccounts: [Account!]!
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
# 入力型
|
|
514
|
-
input CreateAccountInput {
|
|
515
|
-
accountCode: ID!
|
|
516
|
-
accountName: String!
|
|
517
|
-
accountShortName: String
|
|
518
|
-
accountKana: String
|
|
519
|
-
bsplCategory: BSPLCategory!
|
|
520
|
-
debitCreditCategory: DebitCreditCategory!
|
|
521
|
-
transactionElementCategory: TransactionElementCategory!
|
|
522
|
-
aggregationCategory: AggregationCategory!
|
|
523
|
-
managementAccountingCategory: String
|
|
524
|
-
expenseCategory: String
|
|
525
|
-
ledgerOutputCategory: String
|
|
526
|
-
subAccountType: String
|
|
527
|
-
taxCalculationCategory: String
|
|
528
|
-
taxTransactionCode: ID
|
|
529
|
-
dueDateManagementCategory: String
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
input UpdateAccountInput {
|
|
533
|
-
accountCode: ID!
|
|
534
|
-
accountName: String
|
|
535
|
-
accountShortName: String
|
|
536
|
-
accountKana: String
|
|
537
|
-
bsplCategory: BSPLCategory
|
|
538
|
-
debitCreditCategory: DebitCreditCategory
|
|
539
|
-
transactionElementCategory: TransactionElementCategory
|
|
540
|
-
aggregationCategory: AggregationCategory
|
|
541
|
-
managementAccountingCategory: String
|
|
542
|
-
expenseCategory: String
|
|
543
|
-
ledgerOutputCategory: String
|
|
544
|
-
subAccountType: String
|
|
545
|
-
taxCalculationCategory: String
|
|
546
|
-
taxTransactionCode: ID
|
|
547
|
-
dueDateManagementCategory: String
|
|
548
|
-
}
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
</details>
|
|
552
|
-
|
|
553
|
-
### 23.3 仕訳スキーマ
|
|
554
|
-
|
|
555
|
-
<details>
|
|
556
|
-
<summary>src/main/resources/graphql/journal.graphqls</summary>
|
|
557
|
-
|
|
558
|
-
```graphql
|
|
559
|
-
# 仕訳伝票区分
|
|
560
|
-
enum JournalVoucherType {
|
|
561
|
-
NORMAL # 通常仕訳
|
|
562
|
-
ADJUSTMENT # 調整仕訳
|
|
563
|
-
CLOSING # 決算仕訳
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
# 仕訳
|
|
567
|
-
type Journal {
|
|
568
|
-
journalNumber: ID!
|
|
569
|
-
postingDate: Date!
|
|
570
|
-
entryDate: Date!
|
|
571
|
-
isClosingEntry: Boolean!
|
|
572
|
-
isSingleEntry: Boolean!
|
|
573
|
-
voucherType: JournalVoucherType!
|
|
574
|
-
isRecurring: Boolean!
|
|
575
|
-
employeeCode: String
|
|
576
|
-
departmentCode: String
|
|
577
|
-
isReversal: Boolean!
|
|
578
|
-
reversalJournalNumber: ID
|
|
579
|
-
createdAt: DateTime!
|
|
580
|
-
updatedAt: DateTime!
|
|
581
|
-
|
|
582
|
-
# 関連データ
|
|
583
|
-
details: [JournalDetail!]!
|
|
584
|
-
department: Department
|
|
585
|
-
totalDebitAmount: BigDecimal!
|
|
586
|
-
totalCreditAmount: BigDecimal!
|
|
587
|
-
isBalanced: Boolean!
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
# 仕訳明細
|
|
591
|
-
type JournalDetail {
|
|
592
|
-
journalNumber: ID!
|
|
593
|
-
lineNumber: Int!
|
|
594
|
-
description: String
|
|
595
|
-
createdAt: DateTime!
|
|
596
|
-
updatedAt: DateTime!
|
|
597
|
-
|
|
598
|
-
# 関連データ
|
|
599
|
-
debitCreditDetails: [JournalDebitCreditDetail!]!
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
# 仕訳貸借明細
|
|
603
|
-
type JournalDebitCreditDetail {
|
|
604
|
-
journalNumber: ID!
|
|
605
|
-
lineNumber: Int!
|
|
606
|
-
debitCreditCategory: DebitCreditCategory!
|
|
607
|
-
currencyCode: String
|
|
608
|
-
exchangeRate: BigDecimal
|
|
609
|
-
departmentCode: String
|
|
610
|
-
projectCode: String
|
|
611
|
-
accountCode: ID!
|
|
612
|
-
subAccountCode: String
|
|
613
|
-
amount: BigDecimal!
|
|
614
|
-
baseAmount: BigDecimal!
|
|
615
|
-
taxCategory: String
|
|
616
|
-
taxRate: BigDecimal
|
|
617
|
-
taxCalculationCategory: String
|
|
618
|
-
dueDate: Date
|
|
619
|
-
cashFlowFlag: Boolean
|
|
620
|
-
segmentCode: String
|
|
621
|
-
counterAccountCode: String
|
|
622
|
-
counterSubAccountCode: String
|
|
623
|
-
tagCode: String
|
|
624
|
-
tagDescription: String
|
|
625
|
-
createdAt: DateTime!
|
|
626
|
-
updatedAt: DateTime!
|
|
627
|
-
|
|
628
|
-
# 関連データ
|
|
629
|
-
account: Account!
|
|
630
|
-
counterAccount: Account
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
# 仕訳一覧
|
|
634
|
-
type JournalConnection {
|
|
635
|
-
edges: [JournalEdge!]!
|
|
636
|
-
pageInfo: PageInfo!
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
type JournalEdge {
|
|
640
|
-
node: Journal!
|
|
641
|
-
cursor: String!
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
# 仕訳登録通知イベント
|
|
645
|
-
type JournalNotification {
|
|
646
|
-
journalNumber: ID!
|
|
647
|
-
postingDate: Date!
|
|
648
|
-
totalDebitAmount: BigDecimal!
|
|
649
|
-
totalCreditAmount: BigDecimal!
|
|
650
|
-
createdBy: String
|
|
651
|
-
createdAt: DateTime!
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
# 入力型
|
|
655
|
-
input CreateJournalInput {
|
|
656
|
-
postingDate: Date!
|
|
657
|
-
isClosingEntry: Boolean
|
|
658
|
-
isSingleEntry: Boolean
|
|
659
|
-
voucherType: JournalVoucherType
|
|
660
|
-
isRecurring: Boolean
|
|
661
|
-
employeeCode: String
|
|
662
|
-
departmentCode: String
|
|
663
|
-
details: [JournalDetailInput!]!
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
input JournalDetailInput {
|
|
667
|
-
lineNumber: Int!
|
|
668
|
-
description: String
|
|
669
|
-
debitCreditDetails: [JournalDebitCreditDetailInput!]!
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
input JournalDebitCreditDetailInput {
|
|
673
|
-
debitCreditCategory: DebitCreditCategory!
|
|
674
|
-
currencyCode: String
|
|
675
|
-
exchangeRate: BigDecimal
|
|
676
|
-
departmentCode: String
|
|
677
|
-
projectCode: String
|
|
678
|
-
accountCode: ID!
|
|
679
|
-
subAccountCode: String
|
|
680
|
-
amount: BigDecimal!
|
|
681
|
-
baseAmount: BigDecimal
|
|
682
|
-
taxCategory: String
|
|
683
|
-
taxRate: BigDecimal
|
|
684
|
-
dueDate: Date
|
|
685
|
-
segmentCode: String
|
|
686
|
-
counterAccountCode: String
|
|
687
|
-
tagCode: String
|
|
688
|
-
tagDescription: String
|
|
689
|
-
}
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
</details>
|
|
693
|
-
|
|
694
|
-
### 23.4 残高・帳票スキーマ
|
|
695
|
-
|
|
696
|
-
<details>
|
|
697
|
-
<summary>src/main/resources/graphql/balance.graphqls</summary>
|
|
698
|
-
|
|
699
|
-
```graphql
|
|
700
|
-
# 日次勘定科目残高
|
|
701
|
-
type DailyBalance {
|
|
702
|
-
postingDate: Date!
|
|
703
|
-
accountCode: ID!
|
|
704
|
-
subAccountCode: String
|
|
705
|
-
departmentCode: String
|
|
706
|
-
projectCode: String
|
|
707
|
-
isClosingEntry: Boolean!
|
|
708
|
-
debitAmount: BigDecimal!
|
|
709
|
-
creditAmount: BigDecimal!
|
|
710
|
-
balance: BigDecimal!
|
|
711
|
-
|
|
712
|
-
# 関連データ
|
|
713
|
-
account: Account!
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
# 日次残高一覧
|
|
717
|
-
type DailyBalanceConnection {
|
|
718
|
-
edges: [DailyBalanceEdge!]!
|
|
719
|
-
pageInfo: PageInfo!
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
type DailyBalanceEdge {
|
|
723
|
-
node: DailyBalance!
|
|
724
|
-
cursor: String!
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
# 月次勘定科目残高
|
|
728
|
-
type MonthlyBalance {
|
|
729
|
-
fiscalYear: Int!
|
|
730
|
-
month: Int!
|
|
731
|
-
accountCode: ID!
|
|
732
|
-
subAccountCode: String
|
|
733
|
-
departmentCode: String
|
|
734
|
-
projectCode: String
|
|
735
|
-
isClosingEntry: Boolean!
|
|
736
|
-
beginningBalance: BigDecimal!
|
|
737
|
-
debitAmount: BigDecimal!
|
|
738
|
-
creditAmount: BigDecimal!
|
|
739
|
-
endingBalance: BigDecimal!
|
|
740
|
-
|
|
741
|
-
# 関連データ
|
|
742
|
-
account: Account!
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
# 残高変動イベント
|
|
746
|
-
type BalanceChange {
|
|
747
|
-
accountCode: ID!
|
|
748
|
-
accountName: String!
|
|
749
|
-
changeType: BalanceChangeType!
|
|
750
|
-
previousBalance: BigDecimal!
|
|
751
|
-
currentBalance: BigDecimal!
|
|
752
|
-
changeAmount: BigDecimal!
|
|
753
|
-
timestamp: DateTime!
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
enum BalanceChangeType {
|
|
757
|
-
DEBIT # 借方記入
|
|
758
|
-
CREDIT # 貸方記入
|
|
759
|
-
ADJUSTMENT # 調整
|
|
760
|
-
CLOSING # 決算
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
# 再計算結果
|
|
764
|
-
type RecalculateResult {
|
|
765
|
-
processedCount: Int!
|
|
766
|
-
updatedAccounts: [String!]!
|
|
767
|
-
timestamp: DateTime!
|
|
768
|
-
}
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
</details>
|
|
772
|
-
|
|
773
|
-
<details>
|
|
774
|
-
<summary>src/main/resources/graphql/report.graphqls</summary>
|
|
775
|
-
|
|
776
|
-
```graphql
|
|
777
|
-
# 日計表
|
|
778
|
-
type DailyReport {
|
|
779
|
-
reportDate: Date!
|
|
780
|
-
entries: [DailyReportEntry!]!
|
|
781
|
-
totalDebitAmount: BigDecimal!
|
|
782
|
-
totalCreditAmount: BigDecimal!
|
|
783
|
-
generatedAt: DateTime!
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
type DailyReportEntry {
|
|
787
|
-
accountCode: ID!
|
|
788
|
-
accountName: String!
|
|
789
|
-
debitAmount: BigDecimal!
|
|
790
|
-
creditAmount: BigDecimal!
|
|
791
|
-
balance: BigDecimal!
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
# 合計残高試算表
|
|
795
|
-
type TrialBalance {
|
|
796
|
-
fiscalYear: Int!
|
|
797
|
-
month: Int!
|
|
798
|
-
entries: [TrialBalanceEntry!]!
|
|
799
|
-
totalDebitAmount: BigDecimal!
|
|
800
|
-
totalCreditAmount: BigDecimal!
|
|
801
|
-
totalDebitBalance: BigDecimal!
|
|
802
|
-
totalCreditBalance: BigDecimal!
|
|
803
|
-
generatedAt: DateTime!
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
type TrialBalanceEntry {
|
|
807
|
-
accountCode: ID!
|
|
808
|
-
accountName: String!
|
|
809
|
-
bsplCategory: BSPLCategory!
|
|
810
|
-
debitAmount: BigDecimal!
|
|
811
|
-
creditAmount: BigDecimal!
|
|
812
|
-
debitBalance: BigDecimal!
|
|
813
|
-
creditBalance: BigDecimal!
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
# 財務諸表
|
|
817
|
-
type FinancialStatements {
|
|
818
|
-
fiscalYear: Int!
|
|
819
|
-
balanceSheet: BalanceSheet!
|
|
820
|
-
incomeStatement: IncomeStatement!
|
|
821
|
-
generatedAt: DateTime!
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
# 貸借対照表
|
|
825
|
-
type BalanceSheet {
|
|
826
|
-
fiscalYear: Int!
|
|
827
|
-
assetEntries: [FinancialStatementEntry!]!
|
|
828
|
-
liabilityEntries: [FinancialStatementEntry!]!
|
|
829
|
-
capitalEntries: [FinancialStatementEntry!]!
|
|
830
|
-
totalAssets: BigDecimal!
|
|
831
|
-
totalLiabilities: BigDecimal!
|
|
832
|
-
totalCapital: BigDecimal!
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
# 損益計算書
|
|
836
|
-
type IncomeStatement {
|
|
837
|
-
fiscalYear: Int!
|
|
838
|
-
revenueEntries: [FinancialStatementEntry!]!
|
|
839
|
-
expenseEntries: [FinancialStatementEntry!]!
|
|
840
|
-
totalRevenue: BigDecimal!
|
|
841
|
-
totalExpense: BigDecimal!
|
|
842
|
-
netIncome: BigDecimal!
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
type FinancialStatementEntry {
|
|
846
|
-
accountCode: ID!
|
|
847
|
-
accountName: String!
|
|
848
|
-
level: Int!
|
|
849
|
-
amount: BigDecimal!
|
|
850
|
-
isSubtotal: Boolean!
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
# 決算処理結果
|
|
854
|
-
type ClosingResult {
|
|
855
|
-
closingId: ID!
|
|
856
|
-
fiscalYear: Int!
|
|
857
|
-
processedJournalCount: Int!
|
|
858
|
-
closingJournals: [Journal!]!
|
|
859
|
-
completedAt: DateTime!
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
# 決算処理進捗
|
|
863
|
-
type ClosingProgress {
|
|
864
|
-
closingId: ID!
|
|
865
|
-
status: ClosingStatus!
|
|
866
|
-
currentStep: String!
|
|
867
|
-
processedCount: Int!
|
|
868
|
-
totalCount: Int!
|
|
869
|
-
progressPercent: Float!
|
|
870
|
-
updatedAt: DateTime!
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
enum ClosingStatus {
|
|
874
|
-
PROCESSING # 処理中
|
|
875
|
-
COMPLETED # 完了
|
|
876
|
-
FAILED # 失敗
|
|
877
|
-
}
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
</details>
|
|
881
|
-
|
|
882
|
-
---
|
|
883
|
-
|
|
884
|
-
## 第 24 章:マスタ API の実装
|
|
885
|
-
|
|
886
|
-
### 24.1 Query リゾルバ
|
|
887
|
-
|
|
888
|
-
<details>
|
|
889
|
-
<summary>QueryResolver.java</summary>
|
|
890
|
-
|
|
891
|
-
```java
|
|
892
|
-
package com.example.accounting.infrastructure.graphql.resolver;
|
|
893
|
-
|
|
894
|
-
import com.example.accounting.application.port.in.AccountUseCase;
|
|
895
|
-
import com.example.accounting.application.port.in.JournalUseCase;
|
|
896
|
-
import com.example.accounting.application.port.in.BalanceUseCase;
|
|
897
|
-
import com.example.accounting.domain.model.account.Account;
|
|
898
|
-
import com.example.accounting.domain.model.account.TaxTransaction;
|
|
899
|
-
import com.example.accounting.domain.model.account.AccountConfig;
|
|
900
|
-
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
901
|
-
import org.springframework.graphql.data.method.annotation.Argument;
|
|
902
|
-
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
903
|
-
import org.springframework.stereotype.Controller;
|
|
904
|
-
|
|
905
|
-
import java.util.List;
|
|
906
|
-
|
|
907
|
-
/**
|
|
908
|
-
* GraphQL Query リゾルバ
|
|
909
|
-
*/
|
|
910
|
-
@Controller
|
|
911
|
-
public class QueryResolver {
|
|
912
|
-
|
|
913
|
-
private final AccountUseCase accountUseCase;
|
|
914
|
-
private final JournalUseCase journalUseCase;
|
|
915
|
-
private final BalanceUseCase balanceUseCase;
|
|
916
|
-
|
|
917
|
-
public QueryResolver(
|
|
918
|
-
AccountUseCase accountUseCase,
|
|
919
|
-
JournalUseCase journalUseCase,
|
|
920
|
-
BalanceUseCase balanceUseCase) {
|
|
921
|
-
this.accountUseCase = accountUseCase;
|
|
922
|
-
this.journalUseCase = journalUseCase;
|
|
923
|
-
this.balanceUseCase = balanceUseCase;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// === 勘定科目 ===
|
|
927
|
-
|
|
928
|
-
@QueryMapping
|
|
929
|
-
public Account account(@Argument String accountCode) {
|
|
930
|
-
return accountUseCase.findByCode(accountCode)
|
|
931
|
-
.orElse(null);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
@QueryMapping
|
|
935
|
-
public AccountConnection accounts(
|
|
936
|
-
@Argument String bsplCategory,
|
|
937
|
-
@Argument Integer page,
|
|
938
|
-
@Argument Integer size) {
|
|
939
|
-
|
|
940
|
-
int pageNum = page != null ? page : 0;
|
|
941
|
-
int pageSize = size != null ? size : 20;
|
|
942
|
-
|
|
943
|
-
List<Account> accounts = accountUseCase.findAll(bsplCategory, pageNum, pageSize);
|
|
944
|
-
long totalCount = accountUseCase.count(bsplCategory);
|
|
945
|
-
|
|
946
|
-
return AccountConnection.of(accounts, pageNum, pageSize, totalCount);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
@QueryMapping
|
|
950
|
-
public List<Account> accountHierarchy(@Argument String accountCode) {
|
|
951
|
-
return accountUseCase.findHierarchy(accountCode);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// === 課税取引 ===
|
|
955
|
-
|
|
956
|
-
@QueryMapping
|
|
957
|
-
public TaxTransaction taxTransaction(@Argument String taxCode) {
|
|
958
|
-
return accountUseCase.findTaxTransaction(taxCode)
|
|
959
|
-
.orElse(null);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
@QueryMapping
|
|
963
|
-
public List<TaxTransaction> taxTransactions() {
|
|
964
|
-
return accountUseCase.findAllTaxTransactions();
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// === 勘定科目構成 ===
|
|
968
|
-
|
|
969
|
-
@QueryMapping
|
|
970
|
-
public AccountConfig accountConfig(@Argument String accountCode) {
|
|
971
|
-
return accountUseCase.findAccountConfig(accountCode)
|
|
972
|
-
.orElse(null);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
@QueryMapping
|
|
976
|
-
public List<AccountConfig> accountConfigs(@Argument String parentPath) {
|
|
977
|
-
if (parentPath != null) {
|
|
978
|
-
return accountUseCase.findAccountConfigsByPath(parentPath);
|
|
979
|
-
}
|
|
980
|
-
return accountUseCase.findAllAccountConfigs();
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
</details>
|
|
986
|
-
|
|
987
|
-
### 24.2 N+1 問題とは
|
|
988
|
-
|
|
989
|
-
GraphQL では、ネストしたデータを取得する際に **N+1 問題** が発生しやすくなります。
|
|
990
|
-
|
|
991
|
-
```plantuml
|
|
992
|
-
@startuml n_plus_1_problem
|
|
993
|
-
|
|
994
|
-
title N+1 問題の発生パターン
|
|
995
|
-
|
|
996
|
-
participant "GraphQL Client" as client
|
|
997
|
-
participant "AccountResolver" as resolver
|
|
998
|
-
participant "TaxTransactionRepository" as repo
|
|
999
|
-
database "PostgreSQL" as db
|
|
1000
|
-
|
|
1001
|
-
client -> resolver : accounts(first: 10)
|
|
1002
|
-
resolver -> db : SELECT * FROM 勘定科目マスタ LIMIT 10
|
|
1003
|
-
db --> resolver : 10件の勘定科目
|
|
1004
|
-
|
|
1005
|
-
loop 各勘定科目に対して(N回)
|
|
1006
|
-
resolver -> repo : findByCode(taxCode)
|
|
1007
|
-
repo -> db : SELECT * FROM 課税取引マスタ WHERE 課税取引コード = ?
|
|
1008
|
-
db --> repo : 課税取引データ
|
|
1009
|
-
repo --> resolver : 課税取引
|
|
1010
|
-
end
|
|
1011
|
-
|
|
1012
|
-
resolver --> client : 勘定科目一覧(課税取引付き)
|
|
1013
|
-
|
|
1014
|
-
note right of db
|
|
1015
|
-
合計 11回のクエリ
|
|
1016
|
-
(1 + N = 1 + 10)
|
|
1017
|
-
|
|
1018
|
-
勘定科目が100件なら101回!
|
|
1019
|
-
end note
|
|
1020
|
-
|
|
1021
|
-
@enduml
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
### 24.3 DataLoader による解決
|
|
1025
|
-
|
|
1026
|
-
**DataLoader** は、複数の個別リクエストをバッチ処理にまとめることで N+1 問題を解決します。
|
|
1027
|
-
|
|
1028
|
-
```plantuml
|
|
1029
|
-
@startuml dataloader_solution
|
|
1030
|
-
|
|
1031
|
-
title DataLoader による N+1 問題の解決
|
|
1032
|
-
|
|
1033
|
-
participant "GraphQL Client" as client
|
|
1034
|
-
participant "AccountResolver" as resolver
|
|
1035
|
-
participant "TaxTransactionDataLoader" as loader
|
|
1036
|
-
participant "TaxTransactionRepository" as repo
|
|
1037
|
-
database "PostgreSQL" as db
|
|
1038
|
-
|
|
1039
|
-
client -> resolver : accounts(first: 10)
|
|
1040
|
-
resolver -> db : SELECT * FROM 勘定科目マスタ LIMIT 10
|
|
1041
|
-
db --> resolver : 10件の勘定科目
|
|
1042
|
-
|
|
1043
|
-
loop 各勘定科目に対して
|
|
1044
|
-
resolver -> loader : load(taxCode)
|
|
1045
|
-
note right of loader : キューに追加
|
|
1046
|
-
end
|
|
1047
|
-
|
|
1048
|
-
loader -> repo : loadMany([code1, code2, ...code10])
|
|
1049
|
-
repo -> db : SELECT * FROM 課税取引マスタ WHERE 課税取引コード IN (?, ?, ..., ?)
|
|
1050
|
-
db --> repo : 課税取引データ
|
|
1051
|
-
repo --> loader : Map<課税取引コード, 課税取引>
|
|
1052
|
-
loader --> resolver : 各勘定科目の課税取引
|
|
1053
|
-
|
|
1054
|
-
resolver --> client : 勘定科目一覧(課税取引付き)
|
|
1055
|
-
|
|
1056
|
-
note right of db
|
|
1057
|
-
合計 2回のクエリ
|
|
1058
|
-
(1 + 1 = 2)
|
|
1059
|
-
|
|
1060
|
-
勘定科目が100件でも2回!
|
|
1061
|
-
end note
|
|
1062
|
-
|
|
1063
|
-
@enduml
|
|
1064
|
-
```
|
|
1065
|
-
|
|
1066
|
-
<details>
|
|
1067
|
-
<summary>TaxTransactionDataLoader.java</summary>
|
|
1068
|
-
|
|
1069
|
-
```java
|
|
1070
|
-
package com.example.accounting.infrastructure.graphql.dataloader;
|
|
1071
|
-
|
|
1072
|
-
import com.example.accounting.application.port.out.AccountRepository;
|
|
1073
|
-
import com.example.accounting.domain.model.account.TaxTransaction;
|
|
1074
|
-
import org.dataloader.BatchLoaderEnvironment;
|
|
1075
|
-
import org.dataloader.MappedBatchLoader;
|
|
1076
|
-
import org.springframework.stereotype.Component;
|
|
1077
|
-
|
|
1078
|
-
import java.util.Map;
|
|
1079
|
-
import java.util.Set;
|
|
1080
|
-
import java.util.concurrent.CompletableFuture;
|
|
1081
|
-
import java.util.concurrent.CompletionStage;
|
|
1082
|
-
import java.util.function.Function;
|
|
1083
|
-
import java.util.stream.Collectors;
|
|
1084
|
-
|
|
1085
|
-
/**
|
|
1086
|
-
* 課税取引データの DataLoader
|
|
1087
|
-
*/
|
|
1088
|
-
@Component
|
|
1089
|
-
public class TaxTransactionDataLoader implements MappedBatchLoader<String, TaxTransaction> {
|
|
1090
|
-
|
|
1091
|
-
private final AccountRepository accountRepository;
|
|
1092
|
-
|
|
1093
|
-
public TaxTransactionDataLoader(AccountRepository accountRepository) {
|
|
1094
|
-
this.accountRepository = accountRepository;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
@Override
|
|
1098
|
-
public CompletionStage<Map<String, TaxTransaction>> load(
|
|
1099
|
-
Set<String> taxCodes,
|
|
1100
|
-
BatchLoaderEnvironment environment) {
|
|
1101
|
-
|
|
1102
|
-
return CompletableFuture.supplyAsync(() -> {
|
|
1103
|
-
return accountRepository.findTaxTransactionsByCodes(taxCodes).stream()
|
|
1104
|
-
.collect(Collectors.toMap(
|
|
1105
|
-
TaxTransaction::getTaxCode,
|
|
1106
|
-
Function.identity()
|
|
1107
|
-
));
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
```
|
|
1112
|
-
|
|
1113
|
-
</details>
|
|
1114
|
-
|
|
1115
|
-
### 24.4 勘定科目リゾルバの実装
|
|
1116
|
-
|
|
1117
|
-
<details>
|
|
1118
|
-
<summary>AccountResolver.java</summary>
|
|
1119
|
-
|
|
1120
|
-
```java
|
|
1121
|
-
package com.example.accounting.infrastructure.graphql.resolver;
|
|
1122
|
-
|
|
1123
|
-
import com.example.accounting.application.port.in.AccountUseCase;
|
|
1124
|
-
import com.example.accounting.config.DataLoaderConfig;
|
|
1125
|
-
import com.example.accounting.domain.model.account.Account;
|
|
1126
|
-
import com.example.accounting.domain.model.account.TaxTransaction;
|
|
1127
|
-
import com.example.accounting.domain.model.account.AccountConfig;
|
|
1128
|
-
import com.example.accounting.domain.model.balance.DailyBalance;
|
|
1129
|
-
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
1130
|
-
import graphql.schema.DataFetchingEnvironment;
|
|
1131
|
-
import org.dataloader.DataLoader;
|
|
1132
|
-
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1133
|
-
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
|
1134
|
-
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
1135
|
-
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
|
1136
|
-
import org.springframework.stereotype.Controller;
|
|
1137
|
-
|
|
1138
|
-
import java.util.List;
|
|
1139
|
-
import java.util.concurrent.CompletableFuture;
|
|
1140
|
-
|
|
1141
|
-
/**
|
|
1142
|
-
* 勘定科目 GraphQL リゾルバ
|
|
1143
|
-
*/
|
|
1144
|
-
@Controller
|
|
1145
|
-
public class AccountResolver {
|
|
1146
|
-
|
|
1147
|
-
private final AccountUseCase accountUseCase;
|
|
1148
|
-
|
|
1149
|
-
public AccountResolver(AccountUseCase accountUseCase) {
|
|
1150
|
-
this.accountUseCase = accountUseCase;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
// === Query ===
|
|
1154
|
-
|
|
1155
|
-
@QueryMapping
|
|
1156
|
-
public Account account(@Argument String accountCode) {
|
|
1157
|
-
return accountUseCase.findByCode(accountCode).orElse(null);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
@QueryMapping
|
|
1161
|
-
public AccountConnection accounts(
|
|
1162
|
-
@Argument String bsplCategory,
|
|
1163
|
-
@Argument Integer page,
|
|
1164
|
-
@Argument Integer size) {
|
|
1165
|
-
|
|
1166
|
-
int pageNum = page != null ? page : 0;
|
|
1167
|
-
int pageSize = size != null ? size : 20;
|
|
1168
|
-
|
|
1169
|
-
List<Account> accounts = accountUseCase.findAll(bsplCategory, pageNum, pageSize);
|
|
1170
|
-
long totalCount = accountUseCase.count(bsplCategory);
|
|
1171
|
-
|
|
1172
|
-
return AccountConnection.of(accounts, pageNum, pageSize, totalCount);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// === Mutation ===
|
|
1176
|
-
|
|
1177
|
-
@MutationMapping
|
|
1178
|
-
public Account createAccount(@Argument CreateAccountInput input) {
|
|
1179
|
-
Account account = Account.builder()
|
|
1180
|
-
.accountCode(input.accountCode())
|
|
1181
|
-
.accountName(input.accountName())
|
|
1182
|
-
.accountShortName(input.accountShortName())
|
|
1183
|
-
.bsplCategory(input.bsplCategory())
|
|
1184
|
-
.debitCreditCategory(input.debitCreditCategory())
|
|
1185
|
-
.transactionElementCategory(input.transactionElementCategory())
|
|
1186
|
-
.aggregationCategory(input.aggregationCategory())
|
|
1187
|
-
.taxTransactionCode(input.taxTransactionCode())
|
|
1188
|
-
.build();
|
|
1189
|
-
|
|
1190
|
-
return accountUseCase.create(account);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
@MutationMapping
|
|
1194
|
-
public boolean deleteAccount(@Argument String accountCode) {
|
|
1195
|
-
return accountUseCase.delete(accountCode);
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// === フィールドリゾルバ(DataLoader 使用)===
|
|
1199
|
-
|
|
1200
|
-
@SchemaMapping(typeName = "Account", field = "taxTransaction")
|
|
1201
|
-
public CompletableFuture<TaxTransaction> taxTransaction(
|
|
1202
|
-
Account account,
|
|
1203
|
-
DataFetchingEnvironment env) {
|
|
1204
|
-
|
|
1205
|
-
if (account.getTaxTransactionCode() == null) {
|
|
1206
|
-
return CompletableFuture.completedFuture(null);
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
DataLoader<String, TaxTransaction> loader =
|
|
1210
|
-
env.getDataLoader(DataLoaderConfig.TAX_TRANSACTION_LOADER);
|
|
1211
|
-
|
|
1212
|
-
return loader.load(account.getTaxTransactionCode());
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
@SchemaMapping(typeName = "Account", field = "accountConfig")
|
|
1216
|
-
public CompletableFuture<AccountConfig> accountConfig(
|
|
1217
|
-
Account account,
|
|
1218
|
-
DataFetchingEnvironment env) {
|
|
1219
|
-
|
|
1220
|
-
DataLoader<String, AccountConfig> loader =
|
|
1221
|
-
env.getDataLoader(DataLoaderConfig.ACCOUNT_CONFIG_LOADER);
|
|
1222
|
-
|
|
1223
|
-
return loader.load(account.getAccountCode());
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
@SchemaMapping(typeName = "Account", field = "childAccounts")
|
|
1227
|
-
public List<Account> childAccounts(Account account) {
|
|
1228
|
-
return accountUseCase.findChildAccounts(account.getAccountCode());
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
```
|
|
1232
|
-
|
|
1233
|
-
</details>
|
|
1234
|
-
|
|
1235
|
-
---
|
|
1236
|
-
|
|
1237
|
-
## 第 25 章:トランザクション API の実装
|
|
1238
|
-
|
|
1239
|
-
### 25.1 仕訳リゾルバの実装
|
|
1240
|
-
|
|
1241
|
-
<details>
|
|
1242
|
-
<summary>JournalResolver.java</summary>
|
|
1243
|
-
|
|
1244
|
-
```java
|
|
1245
|
-
package com.example.accounting.infrastructure.graphql.resolver;
|
|
1246
|
-
|
|
1247
|
-
import com.example.accounting.application.port.in.JournalUseCase;
|
|
1248
|
-
import com.example.accounting.config.DataLoaderConfig;
|
|
1249
|
-
import com.example.accounting.domain.model.journal.Journal;
|
|
1250
|
-
import com.example.accounting.domain.model.journal.JournalDetail;
|
|
1251
|
-
import com.example.accounting.domain.model.journal.JournalDebitCreditDetail;
|
|
1252
|
-
import com.example.accounting.domain.model.account.Account;
|
|
1253
|
-
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
1254
|
-
import graphql.schema.DataFetchingEnvironment;
|
|
1255
|
-
import org.dataloader.DataLoader;
|
|
1256
|
-
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1257
|
-
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
|
1258
|
-
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
1259
|
-
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
|
1260
|
-
import org.springframework.stereotype.Controller;
|
|
1261
|
-
|
|
1262
|
-
import java.math.BigDecimal;
|
|
1263
|
-
import java.time.LocalDate;
|
|
1264
|
-
import java.util.List;
|
|
1265
|
-
import java.util.concurrent.CompletableFuture;
|
|
1266
|
-
|
|
1267
|
-
/**
|
|
1268
|
-
* 仕訳 GraphQL リゾルバ
|
|
1269
|
-
*/
|
|
1270
|
-
@Controller
|
|
1271
|
-
public class JournalResolver {
|
|
1272
|
-
|
|
1273
|
-
private final JournalUseCase journalUseCase;
|
|
1274
|
-
|
|
1275
|
-
public JournalResolver(JournalUseCase journalUseCase) {
|
|
1276
|
-
this.journalUseCase = journalUseCase;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// === Query ===
|
|
1280
|
-
|
|
1281
|
-
@QueryMapping
|
|
1282
|
-
public Journal journal(@Argument String journalNumber) {
|
|
1283
|
-
return journalUseCase.findByNumber(journalNumber).orElse(null);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
@QueryMapping
|
|
1287
|
-
public JournalConnection journals(
|
|
1288
|
-
@Argument LocalDate fromDate,
|
|
1289
|
-
@Argument LocalDate toDate,
|
|
1290
|
-
@Argument String accountCode,
|
|
1291
|
-
@Argument Integer page,
|
|
1292
|
-
@Argument Integer size) {
|
|
1293
|
-
|
|
1294
|
-
int pageNum = page != null ? page : 0;
|
|
1295
|
-
int pageSize = size != null ? size : 20;
|
|
1296
|
-
|
|
1297
|
-
List<Journal> journals = journalUseCase.findAll(
|
|
1298
|
-
fromDate, toDate, accountCode, pageNum, pageSize);
|
|
1299
|
-
long totalCount = journalUseCase.count(fromDate, toDate, accountCode);
|
|
1300
|
-
|
|
1301
|
-
return JournalConnection.of(journals, pageNum, pageSize, totalCount);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// === Mutation ===
|
|
1305
|
-
|
|
1306
|
-
@MutationMapping
|
|
1307
|
-
public Journal createJournal(@Argument CreateJournalInput input) {
|
|
1308
|
-
Journal journal = Journal.builder()
|
|
1309
|
-
.postingDate(input.postingDate())
|
|
1310
|
-
.entryDate(LocalDate.now())
|
|
1311
|
-
.isClosingEntry(input.isClosingEntry() != null ? input.isClosingEntry() : false)
|
|
1312
|
-
.voucherType(input.voucherType())
|
|
1313
|
-
.employeeCode(input.employeeCode())
|
|
1314
|
-
.departmentCode(input.departmentCode())
|
|
1315
|
-
.details(convertToDetails(input.details()))
|
|
1316
|
-
.build();
|
|
1317
|
-
|
|
1318
|
-
// 貸借バランスチェック
|
|
1319
|
-
validateBalance(journal);
|
|
1320
|
-
|
|
1321
|
-
return journalUseCase.create(journal);
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
@MutationMapping
|
|
1325
|
-
public boolean deleteJournal(@Argument String journalNumber) {
|
|
1326
|
-
return journalUseCase.delete(journalNumber);
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
@MutationMapping
|
|
1330
|
-
public Journal reverseJournal(
|
|
1331
|
-
@Argument String journalNumber,
|
|
1332
|
-
@Argument String reason) {
|
|
1333
|
-
return journalUseCase.reverse(journalNumber, reason);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// === フィールドリゾルバ ===
|
|
1337
|
-
|
|
1338
|
-
@SchemaMapping(typeName = "Journal", field = "totalDebitAmount")
|
|
1339
|
-
public BigDecimal totalDebitAmount(Journal journal) {
|
|
1340
|
-
return journal.getDetails().stream()
|
|
1341
|
-
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1342
|
-
.filter(dc -> dc.getDebitCreditCategory().isDebit())
|
|
1343
|
-
.map(JournalDebitCreditDetail::getAmount)
|
|
1344
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
@SchemaMapping(typeName = "Journal", field = "totalCreditAmount")
|
|
1348
|
-
public BigDecimal totalCreditAmount(Journal journal) {
|
|
1349
|
-
return journal.getDetails().stream()
|
|
1350
|
-
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1351
|
-
.filter(dc -> dc.getDebitCreditCategory().isCredit())
|
|
1352
|
-
.map(JournalDebitCreditDetail::getAmount)
|
|
1353
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
@SchemaMapping(typeName = "Journal", field = "isBalanced")
|
|
1357
|
-
public boolean isBalanced(Journal journal) {
|
|
1358
|
-
BigDecimal debit = totalDebitAmount(journal);
|
|
1359
|
-
BigDecimal credit = totalCreditAmount(journal);
|
|
1360
|
-
return debit.compareTo(credit) == 0;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
@SchemaMapping(typeName = "JournalDebitCreditDetail", field = "account")
|
|
1364
|
-
public CompletableFuture<Account> account(
|
|
1365
|
-
JournalDebitCreditDetail detail,
|
|
1366
|
-
DataFetchingEnvironment env) {
|
|
1367
|
-
|
|
1368
|
-
DataLoader<String, Account> loader =
|
|
1369
|
-
env.getDataLoader(DataLoaderConfig.ACCOUNT_LOADER);
|
|
1370
|
-
|
|
1371
|
-
return loader.load(detail.getAccountCode());
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// === Private Methods ===
|
|
1375
|
-
|
|
1376
|
-
private void validateBalance(Journal journal) {
|
|
1377
|
-
BigDecimal debit = journal.getDetails().stream()
|
|
1378
|
-
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1379
|
-
.filter(dc -> dc.getDebitCreditCategory().isDebit())
|
|
1380
|
-
.map(JournalDebitCreditDetail::getAmount)
|
|
1381
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1382
|
-
|
|
1383
|
-
BigDecimal credit = journal.getDetails().stream()
|
|
1384
|
-
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1385
|
-
.filter(dc -> dc.getDebitCreditCategory().isCredit())
|
|
1386
|
-
.map(JournalDebitCreditDetail::getAmount)
|
|
1387
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1388
|
-
|
|
1389
|
-
if (debit.compareTo(credit) != 0) {
|
|
1390
|
-
throw new IllegalArgumentException(
|
|
1391
|
-
"貸借が一致しません。借方合計: " + debit + ", 貸方合計: " + credit);
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
</details>
|
|
1398
|
-
|
|
1399
|
-
### 25.2 GraphQL Subscription の実装
|
|
1400
|
-
|
|
1401
|
-
GraphQL **Subscription** は、サーバーからクライアントへのリアルタイム通知を実現する仕組みです。
|
|
1402
|
-
|
|
1403
|
-
```plantuml
|
|
1404
|
-
@startuml subscription_flow
|
|
1405
|
-
|
|
1406
|
-
title GraphQL Subscription フロー
|
|
1407
|
-
|
|
1408
|
-
participant "Client" as client
|
|
1409
|
-
participant "WebSocket" as ws
|
|
1410
|
-
participant "GraphQL Server" as server
|
|
1411
|
-
participant "EventPublisher" as publisher
|
|
1412
|
-
participant "JournalService" as service
|
|
1413
|
-
|
|
1414
|
-
== Subscription 開始 ==
|
|
1415
|
-
client -> ws : subscription { balanceChanged }
|
|
1416
|
-
ws -> server : WebSocket 接続確立
|
|
1417
|
-
server -> publisher : 購読登録
|
|
1418
|
-
|
|
1419
|
-
== 仕訳登録時 ==
|
|
1420
|
-
service -> publisher : 仕訳登録イベント発行
|
|
1421
|
-
publisher -> server : イベント受信
|
|
1422
|
-
server -> ws : BalanceChange データ送信
|
|
1423
|
-
ws -> client : リアルタイム通知
|
|
1424
|
-
|
|
1425
|
-
== Subscription 終了 ==
|
|
1426
|
-
client -> ws : 接続終了
|
|
1427
|
-
ws -> server : WebSocket 切断
|
|
1428
|
-
server -> publisher : 購読解除
|
|
1429
|
-
|
|
1430
|
-
@enduml
|
|
1431
|
-
```
|
|
1432
|
-
|
|
1433
|
-
<details>
|
|
1434
|
-
<summary>SubscriptionResolver.java</summary>
|
|
1435
|
-
|
|
1436
|
-
```java
|
|
1437
|
-
package com.example.accounting.infrastructure.graphql.subscription;
|
|
1438
|
-
|
|
1439
|
-
import com.example.accounting.domain.event.BalanceChangeEvent;
|
|
1440
|
-
import com.example.accounting.domain.event.JournalCreatedEvent;
|
|
1441
|
-
import com.example.accounting.domain.event.ClosingProgressEvent;
|
|
1442
|
-
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
1443
|
-
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1444
|
-
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
|
1445
|
-
import org.springframework.stereotype.Controller;
|
|
1446
|
-
import reactor.core.publisher.Flux;
|
|
1447
|
-
|
|
1448
|
-
/**
|
|
1449
|
-
* GraphQL Subscription リゾルバ
|
|
1450
|
-
*/
|
|
1451
|
-
@Controller
|
|
1452
|
-
public class SubscriptionResolver {
|
|
1453
|
-
|
|
1454
|
-
private final BalanceEventPublisher balanceEventPublisher;
|
|
1455
|
-
private final JournalEventPublisher journalEventPublisher;
|
|
1456
|
-
private final ClosingEventPublisher closingEventPublisher;
|
|
1457
|
-
|
|
1458
|
-
public SubscriptionResolver(
|
|
1459
|
-
BalanceEventPublisher balanceEventPublisher,
|
|
1460
|
-
JournalEventPublisher journalEventPublisher,
|
|
1461
|
-
ClosingEventPublisher closingEventPublisher) {
|
|
1462
|
-
this.balanceEventPublisher = balanceEventPublisher;
|
|
1463
|
-
this.journalEventPublisher = journalEventPublisher;
|
|
1464
|
-
this.closingEventPublisher = closingEventPublisher;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
@SubscriptionMapping
|
|
1468
|
-
public Flux<BalanceChange> balanceChanged(@Argument String accountCode) {
|
|
1469
|
-
Flux<BalanceChangeEvent> eventFlux = balanceEventPublisher.getEventFlux();
|
|
1470
|
-
|
|
1471
|
-
if (accountCode != null) {
|
|
1472
|
-
eventFlux = eventFlux.filter(event ->
|
|
1473
|
-
accountCode.equals(event.getAccountCode()));
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
return eventFlux.map(this::toBalanceChange);
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
@SubscriptionMapping
|
|
1480
|
-
public Flux<JournalNotification> journalCreated() {
|
|
1481
|
-
return journalEventPublisher.getEventFlux()
|
|
1482
|
-
.map(this::toJournalNotification);
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
@SubscriptionMapping
|
|
1486
|
-
public Flux<ClosingProgress> closingProgressUpdated(@Argument String closingId) {
|
|
1487
|
-
return closingEventPublisher.getEventFlux()
|
|
1488
|
-
.filter(event -> closingId.equals(event.getClosingId()))
|
|
1489
|
-
.map(this::toClosingProgress);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
private BalanceChange toBalanceChange(BalanceChangeEvent event) {
|
|
1493
|
-
return new BalanceChange(
|
|
1494
|
-
event.getAccountCode(),
|
|
1495
|
-
event.getAccountName(),
|
|
1496
|
-
event.getChangeType(),
|
|
1497
|
-
event.getPreviousBalance(),
|
|
1498
|
-
event.getCurrentBalance(),
|
|
1499
|
-
event.getChangeAmount(),
|
|
1500
|
-
event.getTimestamp()
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
private JournalNotification toJournalNotification(JournalCreatedEvent event) {
|
|
1505
|
-
return new JournalNotification(
|
|
1506
|
-
event.getJournalNumber(),
|
|
1507
|
-
event.getPostingDate(),
|
|
1508
|
-
event.getTotalDebitAmount(),
|
|
1509
|
-
event.getTotalCreditAmount(),
|
|
1510
|
-
event.getCreatedBy(),
|
|
1511
|
-
event.getCreatedAt()
|
|
1512
|
-
);
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
private ClosingProgress toClosingProgress(ClosingProgressEvent event) {
|
|
1516
|
-
return new ClosingProgress(
|
|
1517
|
-
event.getClosingId(),
|
|
1518
|
-
event.getStatus(),
|
|
1519
|
-
event.getCurrentStep(),
|
|
1520
|
-
event.getProcessedCount(),
|
|
1521
|
-
event.getTotalCount(),
|
|
1522
|
-
event.getProgressPercent(),
|
|
1523
|
-
event.getUpdatedAt()
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
```
|
|
1528
|
-
|
|
1529
|
-
</details>
|
|
1530
|
-
|
|
1531
|
-
<details>
|
|
1532
|
-
<summary>BalanceEventPublisher.java</summary>
|
|
1533
|
-
|
|
1534
|
-
```java
|
|
1535
|
-
package com.example.accounting.infrastructure.graphql.subscription;
|
|
1536
|
-
|
|
1537
|
-
import com.example.accounting.domain.event.BalanceChangeEvent;
|
|
1538
|
-
import org.springframework.stereotype.Component;
|
|
1539
|
-
import reactor.core.publisher.Flux;
|
|
1540
|
-
import reactor.core.publisher.Sinks;
|
|
1541
|
-
|
|
1542
|
-
/**
|
|
1543
|
-
* 残高変動イベントパブリッシャー
|
|
1544
|
-
*/
|
|
1545
|
-
@Component
|
|
1546
|
-
public class BalanceEventPublisher {
|
|
1547
|
-
|
|
1548
|
-
private final Sinks.Many<BalanceChangeEvent> sink;
|
|
1549
|
-
|
|
1550
|
-
public BalanceEventPublisher() {
|
|
1551
|
-
this.sink = Sinks.many().multicast().onBackpressureBuffer();
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
public void publish(BalanceChangeEvent event) {
|
|
1555
|
-
sink.tryEmitNext(event);
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
public Flux<BalanceChangeEvent> getEventFlux() {
|
|
1559
|
-
return sink.asFlux();
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
```
|
|
1563
|
-
|
|
1564
|
-
</details>
|
|
1565
|
-
|
|
1566
|
-
---
|
|
1567
|
-
|
|
1568
|
-
## 第 26 章:エラーハンドリングとバリデーション
|
|
1569
|
-
|
|
1570
|
-
### 26.1 GraphQL エラー設計
|
|
1571
|
-
|
|
1572
|
-
GraphQL では、エラーは `errors` フィールドとして返されます。
|
|
1573
|
-
|
|
1574
|
-
```plantuml
|
|
1575
|
-
@startuml graphql_error_handling
|
|
1576
|
-
|
|
1577
|
-
title GraphQL エラーハンドリング
|
|
1578
|
-
|
|
1579
|
-
package "GraphQL Layer" {
|
|
1580
|
-
[GraphQL Error Handler]
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
package "Application Layer" {
|
|
1584
|
-
[Use Case Exception]
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
package "Domain Layer" {
|
|
1588
|
-
[Domain Exception]
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
[Domain Exception] --> [Use Case Exception] : 伝播
|
|
1592
|
-
[Use Case Exception] --> [GraphQL Error Handler] : 伝播
|
|
1593
|
-
|
|
1594
|
-
note right of [Domain Exception]
|
|
1595
|
-
ビジネスルール違反
|
|
1596
|
-
- 貸借不一致
|
|
1597
|
-
- 科目未登録
|
|
1598
|
-
- 決算済み期間
|
|
1599
|
-
end note
|
|
1600
|
-
|
|
1601
|
-
note right of [Use Case Exception]
|
|
1602
|
-
ユースケース固有
|
|
1603
|
-
- リソース未発見
|
|
1604
|
-
- バリデーションエラー
|
|
1605
|
-
- 権限不足
|
|
1606
|
-
end note
|
|
1607
|
-
|
|
1608
|
-
note right of [GraphQL Error Handler]
|
|
1609
|
-
GraphQL エラー形式に変換
|
|
1610
|
-
- エラーコード
|
|
1611
|
-
- エラーメッセージ
|
|
1612
|
-
- エラー詳細
|
|
1613
|
-
end note
|
|
1614
|
-
|
|
1615
|
-
@enduml
|
|
1616
|
-
```
|
|
1617
|
-
|
|
1618
|
-
### 26.2 カスタム例外クラス
|
|
1619
|
-
|
|
1620
|
-
<details>
|
|
1621
|
-
<summary>AccountingException.java(基底クラス)</summary>
|
|
1622
|
-
|
|
1623
|
-
```java
|
|
1624
|
-
package com.example.accounting.domain.exception;
|
|
1625
|
-
|
|
1626
|
-
/**
|
|
1627
|
-
* 財務会計ドメイン例外の基底クラス
|
|
1628
|
-
*/
|
|
1629
|
-
public abstract class AccountingException extends RuntimeException {
|
|
1630
|
-
|
|
1631
|
-
private final String errorCode;
|
|
1632
|
-
|
|
1633
|
-
protected AccountingException(String errorCode, String message) {
|
|
1634
|
-
super(message);
|
|
1635
|
-
this.errorCode = errorCode;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
protected AccountingException(String errorCode, String message, Throwable cause) {
|
|
1639
|
-
super(message, cause);
|
|
1640
|
-
this.errorCode = errorCode;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
public String getErrorCode() {
|
|
1644
|
-
return errorCode;
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
```
|
|
1648
|
-
|
|
1649
|
-
</details>
|
|
1650
|
-
|
|
1651
|
-
<details>
|
|
1652
|
-
<summary>BalanceNotMatchException.java</summary>
|
|
1653
|
-
|
|
1654
|
-
```java
|
|
1655
|
-
package com.example.accounting.domain.exception;
|
|
1656
|
-
|
|
1657
|
-
import java.math.BigDecimal;
|
|
1658
|
-
|
|
1659
|
-
/**
|
|
1660
|
-
* 貸借不一致例外
|
|
1661
|
-
*/
|
|
1662
|
-
public class BalanceNotMatchException extends AccountingException {
|
|
1663
|
-
|
|
1664
|
-
private final BigDecimal debitTotal;
|
|
1665
|
-
private final BigDecimal creditTotal;
|
|
1666
|
-
|
|
1667
|
-
public BalanceNotMatchException(BigDecimal debitTotal, BigDecimal creditTotal) {
|
|
1668
|
-
super("BALANCE_NOT_MATCH",
|
|
1669
|
-
String.format("貸借が一致しません。借方合計: %s, 貸方合計: %s", debitTotal, creditTotal));
|
|
1670
|
-
this.debitTotal = debitTotal;
|
|
1671
|
-
this.creditTotal = creditTotal;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
public BigDecimal getDebitTotal() {
|
|
1675
|
-
return debitTotal;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
public BigDecimal getCreditTotal() {
|
|
1679
|
-
return creditTotal;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
```
|
|
1683
|
-
|
|
1684
|
-
</details>
|
|
1685
|
-
|
|
1686
|
-
### 26.3 GraphQL 例外ハンドラー
|
|
1687
|
-
|
|
1688
|
-
<details>
|
|
1689
|
-
<summary>GraphQLExceptionHandler.java</summary>
|
|
1690
|
-
|
|
1691
|
-
```java
|
|
1692
|
-
package com.example.accounting.infrastructure.graphql.exception;
|
|
1693
|
-
|
|
1694
|
-
import com.example.accounting.domain.exception.*;
|
|
1695
|
-
import graphql.GraphQLError;
|
|
1696
|
-
import graphql.GraphqlErrorBuilder;
|
|
1697
|
-
import graphql.schema.DataFetchingEnvironment;
|
|
1698
|
-
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
|
|
1699
|
-
import org.springframework.graphql.execution.ErrorType;
|
|
1700
|
-
import org.springframework.stereotype.Component;
|
|
1701
|
-
|
|
1702
|
-
import java.util.HashMap;
|
|
1703
|
-
import java.util.Map;
|
|
1704
|
-
|
|
1705
|
-
/**
|
|
1706
|
-
* GraphQL 例外ハンドラー
|
|
1707
|
-
*/
|
|
1708
|
-
@Component
|
|
1709
|
-
public class GraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {
|
|
1710
|
-
|
|
1711
|
-
@Override
|
|
1712
|
-
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
|
|
1713
|
-
if (ex instanceof AccountingException accountingEx) {
|
|
1714
|
-
return handleAccountingException(accountingEx, env);
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
if (ex instanceof IllegalArgumentException) {
|
|
1718
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1719
|
-
.errorType(ErrorType.BAD_REQUEST)
|
|
1720
|
-
.message(ex.getMessage())
|
|
1721
|
-
.build();
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
// その他の例外
|
|
1725
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1726
|
-
.errorType(ErrorType.INTERNAL_ERROR)
|
|
1727
|
-
.message("内部エラーが発生しました")
|
|
1728
|
-
.build();
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
private GraphQLError handleAccountingException(
|
|
1732
|
-
AccountingException ex, DataFetchingEnvironment env) {
|
|
1733
|
-
|
|
1734
|
-
Map<String, Object> extensions = new HashMap<>();
|
|
1735
|
-
extensions.put("errorCode", ex.getErrorCode());
|
|
1736
|
-
|
|
1737
|
-
ErrorType errorType = mapToErrorType(ex);
|
|
1738
|
-
|
|
1739
|
-
// 例外固有の情報を追加
|
|
1740
|
-
if (ex instanceof BalanceNotMatchException balanceEx) {
|
|
1741
|
-
extensions.put("debitTotal", balanceEx.getDebitTotal());
|
|
1742
|
-
extensions.put("creditTotal", balanceEx.getCreditTotal());
|
|
1743
|
-
} else if (ex instanceof AccountNotFoundException accountEx) {
|
|
1744
|
-
extensions.put("accountCode", accountEx.getAccountCode());
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
return GraphqlErrorBuilder.newError(env)
|
|
1748
|
-
.errorType(errorType)
|
|
1749
|
-
.message(ex.getMessage())
|
|
1750
|
-
.extensions(extensions)
|
|
1751
|
-
.build();
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
private ErrorType mapToErrorType(AccountingException ex) {
|
|
1755
|
-
if (ex instanceof AccountNotFoundException ||
|
|
1756
|
-
ex instanceof JournalNotFoundException) {
|
|
1757
|
-
return ErrorType.NOT_FOUND;
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
if (ex instanceof BalanceNotMatchException ||
|
|
1761
|
-
ex instanceof ClosedPeriodException) {
|
|
1762
|
-
return ErrorType.BAD_REQUEST;
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
return ErrorType.INTERNAL_ERROR;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
```
|
|
1769
|
-
|
|
1770
|
-
</details>
|
|
1771
|
-
|
|
1772
|
-
### 26.4 Spring for GraphQL テスト
|
|
1773
|
-
|
|
1774
|
-
<details>
|
|
1775
|
-
<summary>AccountResolverTest.java</summary>
|
|
1776
|
-
|
|
1777
|
-
```java
|
|
1778
|
-
package com.example.accounting.infrastructure.graphql;
|
|
1779
|
-
|
|
1780
|
-
import org.junit.jupiter.api.Test;
|
|
1781
|
-
import org.springframework.beans.factory.annotation.Autowired;
|
|
1782
|
-
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
|
|
1783
|
-
import org.springframework.boot.test.context.SpringBootTest;
|
|
1784
|
-
import org.springframework.graphql.test.tester.HttpGraphQlTester;
|
|
1785
|
-
import org.springframework.test.context.DynamicPropertyRegistry;
|
|
1786
|
-
import org.springframework.test.context.DynamicPropertySource;
|
|
1787
|
-
import org.testcontainers.containers.PostgreSQLContainer;
|
|
1788
|
-
import org.testcontainers.junit.jupiter.Container;
|
|
1789
|
-
import org.testcontainers.junit.jupiter.Testcontainers;
|
|
1790
|
-
|
|
1791
|
-
import static org.assertj.core.api.Assertions.assertThat;
|
|
1792
|
-
|
|
1793
|
-
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1794
|
-
@AutoConfigureHttpGraphQlTester
|
|
1795
|
-
@Testcontainers
|
|
1796
|
-
class AccountResolverTest {
|
|
1797
|
-
|
|
1798
|
-
@Container
|
|
1799
|
-
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
|
|
1800
|
-
|
|
1801
|
-
@DynamicPropertySource
|
|
1802
|
-
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
1803
|
-
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
1804
|
-
registry.add("spring.datasource.username", postgres::getUsername);
|
|
1805
|
-
registry.add("spring.datasource.password", postgres::getPassword);
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
@Autowired
|
|
1809
|
-
private HttpGraphQlTester graphQlTester;
|
|
1810
|
-
|
|
1811
|
-
@Test
|
|
1812
|
-
void testQueryAccount() {
|
|
1813
|
-
// language=GraphQL
|
|
1814
|
-
String query = """
|
|
1815
|
-
query {
|
|
1816
|
-
account(accountCode: "11110") {
|
|
1817
|
-
accountCode
|
|
1818
|
-
accountName
|
|
1819
|
-
bsplCategory
|
|
1820
|
-
debitCreditCategory
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
""";
|
|
1824
|
-
|
|
1825
|
-
graphQlTester.document(query)
|
|
1826
|
-
.execute()
|
|
1827
|
-
.path("account.accountCode")
|
|
1828
|
-
.entity(String.class)
|
|
1829
|
-
.isEqualTo("11110");
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
@Test
|
|
1833
|
-
void testMutationCreateAccount() {
|
|
1834
|
-
// language=GraphQL
|
|
1835
|
-
String mutation = """
|
|
1836
|
-
mutation {
|
|
1837
|
-
createAccount(input: {
|
|
1838
|
-
accountCode: "99999"
|
|
1839
|
-
accountName: "テスト科目"
|
|
1840
|
-
bsplCategory: BS
|
|
1841
|
-
debitCreditCategory: DEBIT
|
|
1842
|
-
transactionElementCategory: ASSET
|
|
1843
|
-
aggregationCategory: POSTING
|
|
1844
|
-
}) {
|
|
1845
|
-
accountCode
|
|
1846
|
-
accountName
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
""";
|
|
1850
|
-
|
|
1851
|
-
graphQlTester.document(mutation)
|
|
1852
|
-
.execute()
|
|
1853
|
-
.path("createAccount.accountCode")
|
|
1854
|
-
.entity(String.class)
|
|
1855
|
-
.isEqualTo("99999");
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
```
|
|
1859
|
-
|
|
1860
|
-
</details>
|
|
1861
|
-
|
|
1862
|
-
### 26.5 GraphiQL による動作確認
|
|
1863
|
-
|
|
1864
|
-
サーバー起動後、`http://localhost:8080/graphiql` にアクセスして対話的にクエリを実行できます。
|
|
1865
|
-
|
|
1866
|
-
```graphql
|
|
1867
|
-
# 勘定科目の取得
|
|
1868
|
-
query {
|
|
1869
|
-
account(accountCode: "11110") {
|
|
1870
|
-
accountCode
|
|
1871
|
-
accountName
|
|
1872
|
-
bsplCategory
|
|
1873
|
-
debitCreditCategory
|
|
1874
|
-
taxTransaction {
|
|
1875
|
-
taxCode
|
|
1876
|
-
taxName
|
|
1877
|
-
taxRate
|
|
1878
|
-
}
|
|
1879
|
-
dailyBalances {
|
|
1880
|
-
postingDate
|
|
1881
|
-
debitAmount
|
|
1882
|
-
creditAmount
|
|
1883
|
-
balance
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
# 仕訳登録
|
|
1889
|
-
mutation {
|
|
1890
|
-
createJournal(input: {
|
|
1891
|
-
postingDate: "2025-12-29"
|
|
1892
|
-
voucherType: NORMAL
|
|
1893
|
-
details: [
|
|
1894
|
-
{
|
|
1895
|
-
lineNumber: 1
|
|
1896
|
-
description: "現金売上"
|
|
1897
|
-
debitCreditDetails: [
|
|
1898
|
-
{
|
|
1899
|
-
debitCreditCategory: DEBIT
|
|
1900
|
-
accountCode: "11110"
|
|
1901
|
-
amount: "100000"
|
|
1902
|
-
}
|
|
1903
|
-
{
|
|
1904
|
-
debitCreditCategory: CREDIT
|
|
1905
|
-
accountCode: "41100"
|
|
1906
|
-
amount: "100000"
|
|
1907
|
-
}
|
|
1908
|
-
]
|
|
1909
|
-
}
|
|
1910
|
-
]
|
|
1911
|
-
}) {
|
|
1912
|
-
journalNumber
|
|
1913
|
-
postingDate
|
|
1914
|
-
totalDebitAmount
|
|
1915
|
-
totalCreditAmount
|
|
1916
|
-
isBalanced
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
# 試算表
|
|
1921
|
-
query {
|
|
1922
|
-
trialBalance(fiscalYear: 2025, month: 12) {
|
|
1923
|
-
fiscalYear
|
|
1924
|
-
month
|
|
1925
|
-
entries {
|
|
1926
|
-
accountCode
|
|
1927
|
-
accountName
|
|
1928
|
-
bsplCategory
|
|
1929
|
-
debitAmount
|
|
1930
|
-
creditAmount
|
|
1931
|
-
debitBalance
|
|
1932
|
-
creditBalance
|
|
1933
|
-
}
|
|
1934
|
-
totalDebitAmount
|
|
1935
|
-
totalCreditAmount
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
```
|
|
1939
|
-
|
|
1940
|
-
---
|
|
1941
|
-
|
|
1942
|
-
## 第 27 章:まとめと発展
|
|
1943
|
-
|
|
1944
|
-
### 27.1 GraphQL 実装の振り返り
|
|
1945
|
-
|
|
1946
|
-
```plantuml
|
|
1947
|
-
@startuml graphql_summary
|
|
1948
|
-
|
|
1949
|
-
title 財務会計システム GraphQL 実装全体像
|
|
1950
|
-
|
|
1951
|
-
package "GraphQL Schema" {
|
|
1952
|
-
[schema.graphqls]
|
|
1953
|
-
[account.graphqls]
|
|
1954
|
-
[journal.graphqls]
|
|
1955
|
-
[balance.graphqls]
|
|
1956
|
-
[report.graphqls]
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
package "Resolvers" {
|
|
1960
|
-
[QueryResolver]
|
|
1961
|
-
[AccountResolver]
|
|
1962
|
-
[JournalResolver]
|
|
1963
|
-
[BalanceResolver]
|
|
1964
|
-
[ReportResolver]
|
|
1965
|
-
[SubscriptionResolver]
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
package "DataLoaders" {
|
|
1969
|
-
[TaxTransactionDataLoader]
|
|
1970
|
-
[DailyBalanceDataLoader]
|
|
1971
|
-
[MonthlyBalanceDataLoader]
|
|
1972
|
-
[AccountConfigDataLoader]
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
package "Application Layer" {
|
|
1976
|
-
[AccountUseCase]
|
|
1977
|
-
[JournalUseCase]
|
|
1978
|
-
[BalanceUseCase]
|
|
1979
|
-
[ReportUseCase]
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
package "Infrastructure" {
|
|
1983
|
-
[MyBatis Repository]
|
|
1984
|
-
[Event Publisher]
|
|
1985
|
-
[PostgreSQL]
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
[schema.graphqls] --> [QueryResolver]
|
|
1989
|
-
[QueryResolver] --> [AccountUseCase]
|
|
1990
|
-
[AccountResolver] --> [TaxTransactionDataLoader]
|
|
1991
|
-
[AccountResolver] --> [DailyBalanceDataLoader]
|
|
1992
|
-
[JournalResolver] --> [JournalUseCase]
|
|
1993
|
-
[SubscriptionResolver] --> [Event Publisher]
|
|
1994
|
-
|
|
1995
|
-
[AccountUseCase] --> [MyBatis Repository]
|
|
1996
|
-
[MyBatis Repository] --> [PostgreSQL]
|
|
1997
|
-
|
|
1998
|
-
@enduml
|
|
1999
|
-
```
|
|
2000
|
-
|
|
2001
|
-
### 27.2 REST / gRPC / GraphQL の使い分け
|
|
2002
|
-
|
|
2003
|
-
| 観点 | REST API | gRPC | GraphQL |
|
|
2004
|
-
|------|----------|------|---------|
|
|
2005
|
-
| **クライアント** | Web ブラウザ、外部連携 | マイクロサービス間 | SPA、モバイルアプリ |
|
|
2006
|
-
| **データ量** | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
2007
|
-
| **リアルタイム** | WebSocket 別実装 | ストリーミング | Subscription |
|
|
2008
|
-
| **型安全性** | △(OpenAPI) | ◎(.proto) | ◎(.graphqls) |
|
|
2009
|
-
| **学習コスト** | 低 | 中 | 中 |
|
|
2010
|
-
| **ブラウザ対応** | ◎ | △(gRPC-Web) | ◎ |
|
|
2011
|
-
|
|
2012
|
-
```plantuml
|
|
2013
|
-
@startuml api_usage
|
|
2014
|
-
|
|
2015
|
-
title API 使い分けパターン
|
|
2016
|
-
|
|
2017
|
-
package "フロントエンド" {
|
|
2018
|
-
[経理担当者 Web UI] as web
|
|
2019
|
-
[経営ダッシュボード] as dashboard
|
|
2020
|
-
[Excel アドイン] as excel
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
package "バックエンド" {
|
|
2024
|
-
[財務会計システム] as accounting
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
package "連携システム" {
|
|
2028
|
-
[販売管理システム] as sales
|
|
2029
|
-
[購買管理システム] as purchase
|
|
2030
|
-
[人事給与システム] as hr
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
web --> accounting : GraphQL\n(柔軟なクエリ)
|
|
2034
|
-
dashboard --> accounting : GraphQL\n(リアルタイム更新)
|
|
2035
|
-
excel --> accounting : REST API\n(シンプルな連携)
|
|
2036
|
-
|
|
2037
|
-
sales --> accounting : gRPC\n(高性能・自動仕訳)
|
|
2038
|
-
purchase --> accounting : gRPC\n(高性能・自動仕訳)
|
|
2039
|
-
hr --> accounting : gRPC\n(給与仕訳連携)
|
|
2040
|
-
|
|
2041
|
-
@enduml
|
|
2042
|
-
```
|
|
2043
|
-
|
|
2044
|
-
### 27.3 ヘキサゴナルアーキテクチャの利点
|
|
2045
|
-
|
|
2046
|
-
複数の API 形式を同時提供できるのは、ヘキサゴナルアーキテクチャの恩恵です。
|
|
2047
|
-
|
|
2048
|
-
```plantuml
|
|
2049
|
-
@startuml hexagonal_benefit
|
|
2050
|
-
|
|
2051
|
-
title 複数 API の同時提供
|
|
2052
|
-
|
|
2053
|
-
package "Input Adapters" {
|
|
2054
|
-
[REST Controller]
|
|
2055
|
-
[gRPC Service]
|
|
2056
|
-
[GraphQL Resolver]
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
package "Application Core" {
|
|
2060
|
-
[AccountUseCase]
|
|
2061
|
-
[JournalUseCase]
|
|
2062
|
-
[BalanceUseCase]
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
package "Output Adapters" {
|
|
2066
|
-
[MyBatis Repository]
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
[REST Controller] --> [AccountUseCase]
|
|
2070
|
-
[gRPC Service] --> [AccountUseCase]
|
|
2071
|
-
[GraphQL Resolver] --> [AccountUseCase]
|
|
2072
|
-
|
|
2073
|
-
[REST Controller] --> [JournalUseCase]
|
|
2074
|
-
[gRPC Service] --> [JournalUseCase]
|
|
2075
|
-
[GraphQL Resolver] --> [JournalUseCase]
|
|
2076
|
-
|
|
2077
|
-
note top of [Application Core]
|
|
2078
|
-
ビジネスロジックは
|
|
2079
|
-
API 形式に依存しない
|
|
2080
|
-
|
|
2081
|
-
1つのコードベースで
|
|
2082
|
-
複数の API を提供
|
|
2083
|
-
end note
|
|
2084
|
-
|
|
2085
|
-
@enduml
|
|
2086
|
-
```
|
|
2087
|
-
|
|
2088
|
-
### 27.4 今後の発展
|
|
2089
|
-
|
|
2090
|
-
本パートで構築した GraphQL サービスをベースに、以下の拡張が可能です:
|
|
2091
|
-
|
|
2092
|
-
1. **認証・認可**: Spring Security + JWT による GraphQL 認証
|
|
2093
|
-
2. **キャッシュ**: Redis によるクエリ結果キャッシュ
|
|
2094
|
-
3. **監視**: GraphQL メトリクスの収集と監視
|
|
2095
|
-
4. **フェデレーション**: Apollo Federation による複数サービス統合
|
|
2096
|
-
5. **コード生成**: GraphQL Code Generator によるクライアントコード自動生成
|
|
2097
|
-
|
|
2098
|
-
### 27.5 学習した内容
|
|
2099
|
-
|
|
2100
|
-
1. **GraphQL の基本概念**: Query、Mutation、Subscription の 3 つの操作タイプ
|
|
2101
|
-
2. **スキーマ駆動開発**: `.graphqls` ファイルによる型安全な API 定義
|
|
2102
|
-
3. **N+1 問題と DataLoader**: バッチ処理によるパフォーマンス最適化
|
|
2103
|
-
4. **リゾルバ実装**: `@QueryMapping`、`@MutationMapping`、`@SchemaMapping`
|
|
2104
|
-
5. **Subscription**: WebSocket によるリアルタイム通知
|
|
2105
|
-
6. **エラーハンドリング**: カスタム例外と GraphQL エラー形式への変換
|
|
2106
|
-
7. **テスト**: `HttpGraphQlTester` による統合テスト
|
|
1
|
+
# 研究 4:GraphQL サービスの実装
|
|
2
|
+
|
|
3
|
+
## はじめに
|
|
4
|
+
|
|
5
|
+
本パートでは、REST API や gRPC とは異なるアプローチとして、**GraphQL** による財務会計システムを実装します。クライアントが必要なデータを正確に指定できる柔軟なクエリと、リアルタイム更新を実現する Subscription を活用します。
|
|
6
|
+
|
|
7
|
+
ヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 第 22 章:GraphQL サーバーの基礎
|
|
12
|
+
|
|
13
|
+
### 22.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
|
+
- AccountResolver
|
|
36
|
+
- JournalResolver
|
|
37
|
+
- BalanceResolver
|
|
38
|
+
- ReportResolver
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
package "Shared" {
|
|
43
|
+
RECTANGLE "GraphQL Schema\n(.graphqls files)" as schema {
|
|
44
|
+
- schema.graphqls
|
|
45
|
+
- account.graphqls
|
|
46
|
+
- journal.graphqls
|
|
47
|
+
- balance.graphqls
|
|
48
|
+
- report.graphqls
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
client --> schema : "スキーマに基づいて\nクエリを構築"
|
|
54
|
+
server --> schema : "スキーマに基づいて\nリゾルバを実装"
|
|
55
|
+
client <--> server : "HTTP/WebSocket\n(JSON)"
|
|
56
|
+
|
|
57
|
+
note bottom of schema
|
|
58
|
+
スキーマ駆動開発
|
|
59
|
+
クライアント主導のデータ取得
|
|
60
|
+
型安全な API
|
|
61
|
+
end note
|
|
62
|
+
|
|
63
|
+
@enduml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**REST API / gRPC / GraphQL の比較:**
|
|
67
|
+
|
|
68
|
+
| 特徴 | REST API | gRPC | GraphQL |
|
|
69
|
+
|------|----------|------|---------|
|
|
70
|
+
| プロトコル | HTTP/1.1 | HTTP/2 | HTTP/1.1 or HTTP/2 |
|
|
71
|
+
| データ形式 | JSON | Protocol Buffers | JSON |
|
|
72
|
+
| スキーマ | OpenAPI (任意) | .proto (必須) | .graphqls (必須) |
|
|
73
|
+
| データ取得 | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
74
|
+
| エンドポイント | 複数 | 複数 | 単一 |
|
|
75
|
+
| リアルタイム | WebSocket 別実装 | ストリーミング | Subscription |
|
|
76
|
+
| 主な用途 | 汎用 API | マイクロサービス | フロントエンド向け |
|
|
77
|
+
|
|
78
|
+
### 22.2 3 つの操作タイプ
|
|
79
|
+
|
|
80
|
+
GraphQL は 3 つの操作タイプをサポートします:
|
|
81
|
+
|
|
82
|
+
```plantuml
|
|
83
|
+
@startuml graphql_operations
|
|
84
|
+
skinparam backgroundColor #FEFEFE
|
|
85
|
+
|
|
86
|
+
rectangle "1. Query\n(読み取り)" as query #LightBlue {
|
|
87
|
+
}
|
|
88
|
+
note right of query
|
|
89
|
+
{ accounts { code name category } }
|
|
90
|
+
→ { accounts: [...] }
|
|
91
|
+
|
|
92
|
+
データの取得
|
|
93
|
+
複数リソースを1回で取得可能
|
|
94
|
+
end note
|
|
95
|
+
|
|
96
|
+
rectangle "2. Mutation\n(書き込み)" as mutation #LightGreen {
|
|
97
|
+
}
|
|
98
|
+
note right of mutation
|
|
99
|
+
mutation { createJournal(...) }
|
|
100
|
+
→ { createJournal: {...} }
|
|
101
|
+
|
|
102
|
+
データの作成・更新・削除
|
|
103
|
+
end note
|
|
104
|
+
|
|
105
|
+
rectangle "3. Subscription\n(リアルタイム)" as subscription #LightCoral {
|
|
106
|
+
}
|
|
107
|
+
note right of subscription
|
|
108
|
+
subscription { balanceChanged }
|
|
109
|
+
→ (WebSocket でプッシュ通知)
|
|
110
|
+
|
|
111
|
+
リアルタイム更新の受信
|
|
112
|
+
end note
|
|
113
|
+
|
|
114
|
+
query -[hidden]-> mutation
|
|
115
|
+
mutation -[hidden]-> subscription
|
|
116
|
+
|
|
117
|
+
@enduml
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**用途:**
|
|
121
|
+
|
|
122
|
+
1. **Query**: データ取得(勘定科目一覧、仕訳照会、残高照会)
|
|
123
|
+
2. **Mutation**: データ更新(仕訳登録、仕訳取消、決算仕訳)
|
|
124
|
+
3. **Subscription**: リアルタイム通知(残高変動、仕訳登録通知)
|
|
125
|
+
|
|
126
|
+
### 22.3 GraphQL におけるヘキサゴナルアーキテクチャ
|
|
127
|
+
|
|
128
|
+
GraphQL を導入しても、ヘキサゴナルアーキテクチャ(ドメイン層・アプリケーション層)はそのまま共有し、**Input Adapter として GraphQL リゾルバ層のみを追加**します。
|
|
129
|
+
|
|
130
|
+
```plantuml
|
|
131
|
+
@startuml hexagonal_graphql
|
|
132
|
+
!define RECTANGLE class
|
|
133
|
+
|
|
134
|
+
package "Hexagonal Architecture (GraphQL版)" {
|
|
135
|
+
|
|
136
|
+
RECTANGLE "Application Core\n(Domain + Use Cases)" as core {
|
|
137
|
+
- Account (勘定科目)
|
|
138
|
+
- Journal (仕訳)
|
|
139
|
+
- DailyBalance (日次残高)
|
|
140
|
+
- MonthlyBalance (月次残高)
|
|
141
|
+
- AccountUseCase
|
|
142
|
+
- JournalUseCase
|
|
143
|
+
- BalanceUseCase
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
RECTANGLE "Input Adapters\n(Driving Side)" as input {
|
|
147
|
+
- REST Controller(既存)
|
|
148
|
+
- gRPC Service(既存)
|
|
149
|
+
- GraphQL Resolver(新規追加)
|
|
150
|
+
- DataFetcher
|
|
151
|
+
- DataLoader
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
RECTANGLE "Output Adapters\n(Driven Side)" as output {
|
|
155
|
+
- MyBatis Repository
|
|
156
|
+
- Database Access
|
|
157
|
+
- Entity Mapping
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
input --> core : "Input Ports\n(Use Cases)"
|
|
162
|
+
core --> output : "Output Ports\n(Repository Interfaces)"
|
|
163
|
+
|
|
164
|
+
note top of core
|
|
165
|
+
既存のビジネスロジック
|
|
166
|
+
REST API / gRPC 版と完全に共有
|
|
167
|
+
GraphQL 固有のコードは含まない
|
|
168
|
+
end note
|
|
169
|
+
|
|
170
|
+
note left of input
|
|
171
|
+
GraphQL リゾルバを
|
|
172
|
+
Input Adapter として追加
|
|
173
|
+
既存の REST/gRPC と共存可能
|
|
174
|
+
end note
|
|
175
|
+
|
|
176
|
+
note right of output
|
|
177
|
+
既存の Repository を
|
|
178
|
+
そのまま使用
|
|
179
|
+
変更不要
|
|
180
|
+
end note
|
|
181
|
+
|
|
182
|
+
@enduml
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 22.4 ディレクトリ構成
|
|
186
|
+
|
|
187
|
+
既存の構成に `infrastructure/graphql/` を追加するだけです。
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
src/main/java/com/example/accounting/
|
|
191
|
+
├── domain/ # ドメイン層(API版と共通)
|
|
192
|
+
│ ├── model/
|
|
193
|
+
│ │ ├── account/
|
|
194
|
+
│ │ ├── journal/
|
|
195
|
+
│ │ ├── balance/
|
|
196
|
+
│ │ └── report/
|
|
197
|
+
│ └── exception/
|
|
198
|
+
│
|
|
199
|
+
├── application/ # アプリケーション層(API版と共通)
|
|
200
|
+
│ ├── port/
|
|
201
|
+
│ │ ├── in/ # Input Port(ユースケース)
|
|
202
|
+
│ │ └── out/ # Output Port(リポジトリ)
|
|
203
|
+
│ └── service/
|
|
204
|
+
│
|
|
205
|
+
├── infrastructure/
|
|
206
|
+
│ ├── persistence/ # Output Adapter(DB実装)- 既存
|
|
207
|
+
│ │ ├── mapper/
|
|
208
|
+
│ │ └── repository/
|
|
209
|
+
│ ├── rest/ # Input Adapter(REST実装)- 既存
|
|
210
|
+
│ ├── grpc/ # Input Adapter(gRPC実装)- 既存
|
|
211
|
+
│ └── graphql/ # Input Adapter(GraphQL実装)- 新規追加
|
|
212
|
+
│ ├── resolver/ # Query/Mutation リゾルバ
|
|
213
|
+
│ ├── dataloader/ # N+1 問題対策
|
|
214
|
+
│ ├── scalar/ # カスタムスカラー型
|
|
215
|
+
│ └── subscription/ # Subscription ハンドラ
|
|
216
|
+
│
|
|
217
|
+
├── config/
|
|
218
|
+
│
|
|
219
|
+
└── src/main/resources/
|
|
220
|
+
└── graphql/ # GraphQL スキーマ定義
|
|
221
|
+
├── schema.graphqls
|
|
222
|
+
├── account.graphqls
|
|
223
|
+
├── journal.graphqls
|
|
224
|
+
├── balance.graphqls
|
|
225
|
+
└── report.graphqls
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 22.5 技術スタックの追加
|
|
229
|
+
|
|
230
|
+
既存の `build.gradle.kts` に GraphQL 関連の依存関係を追加します。
|
|
231
|
+
|
|
232
|
+
<details>
|
|
233
|
+
<summary>build.gradle.kts(差分)</summary>
|
|
234
|
+
|
|
235
|
+
```kotlin
|
|
236
|
+
dependencies {
|
|
237
|
+
// 既存の依存関係(Spring Boot, MyBatis, PostgreSQL等)はそのまま
|
|
238
|
+
|
|
239
|
+
// GraphQL 関連を追加
|
|
240
|
+
implementation("org.springframework.boot:spring-boot-starter-graphql")
|
|
241
|
+
implementation("org.springframework.boot:spring-boot-starter-websocket") // Subscription 用
|
|
242
|
+
|
|
243
|
+
// GraphQL 拡張
|
|
244
|
+
implementation("com.graphql-java:graphql-java-extended-scalars:21.0")
|
|
245
|
+
|
|
246
|
+
// Test
|
|
247
|
+
testImplementation("org.springframework.graphql:spring-graphql-test")
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
</details>
|
|
252
|
+
|
|
253
|
+
**追加パッケージの説明:**
|
|
254
|
+
|
|
255
|
+
| パッケージ | 用途 |
|
|
256
|
+
|-----------|------|
|
|
257
|
+
| spring-boot-starter-graphql | Spring Boot GraphQL 統合 |
|
|
258
|
+
| spring-boot-starter-websocket | Subscription (WebSocket) |
|
|
259
|
+
| graphql-java-extended-scalars | DateTime, BigDecimal 等のスカラー型 |
|
|
260
|
+
| spring-graphql-test | GraphQL テストサポート |
|
|
261
|
+
|
|
262
|
+
### 22.6 GraphQL 設定
|
|
263
|
+
|
|
264
|
+
<details>
|
|
265
|
+
<summary>application.yml(差分)</summary>
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
spring:
|
|
269
|
+
graphql:
|
|
270
|
+
graphiql:
|
|
271
|
+
enabled: true
|
|
272
|
+
path: /graphiql
|
|
273
|
+
websocket:
|
|
274
|
+
path: /graphql
|
|
275
|
+
schema:
|
|
276
|
+
printer:
|
|
277
|
+
enabled: true
|
|
278
|
+
locations:
|
|
279
|
+
- classpath:graphql/
|
|
280
|
+
cors:
|
|
281
|
+
allowed-origins: "*"
|
|
282
|
+
allowed-methods: GET, POST
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
</details>
|
|
286
|
+
|
|
287
|
+
<details>
|
|
288
|
+
<summary>GraphQLConfig.java</summary>
|
|
289
|
+
|
|
290
|
+
```java
|
|
291
|
+
package com.example.accounting.config;
|
|
292
|
+
|
|
293
|
+
import graphql.scalars.ExtendedScalars;
|
|
294
|
+
import graphql.schema.idl.RuntimeWiring;
|
|
295
|
+
import org.springframework.context.annotation.Bean;
|
|
296
|
+
import org.springframework.context.annotation.Configuration;
|
|
297
|
+
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* GraphQL 設定
|
|
301
|
+
*/
|
|
302
|
+
@Configuration
|
|
303
|
+
public class GraphQLConfig {
|
|
304
|
+
|
|
305
|
+
@Bean
|
|
306
|
+
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
|
|
307
|
+
return wiringBuilder -> wiringBuilder
|
|
308
|
+
.scalar(ExtendedScalars.Date)
|
|
309
|
+
.scalar(ExtendedScalars.DateTime)
|
|
310
|
+
.scalar(ExtendedScalars.GraphQLBigDecimal)
|
|
311
|
+
.scalar(ExtendedScalars.GraphQLLong);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
</details>
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## 第 23 章:GraphQL スキーマの定義
|
|
321
|
+
|
|
322
|
+
### 23.1 ルートスキーマ
|
|
323
|
+
|
|
324
|
+
<details>
|
|
325
|
+
<summary>src/main/resources/graphql/schema.graphqls</summary>
|
|
326
|
+
|
|
327
|
+
```graphql
|
|
328
|
+
# ルートスキーマ
|
|
329
|
+
type Query {
|
|
330
|
+
# 勘定科目
|
|
331
|
+
account(accountCode: ID!): Account
|
|
332
|
+
accounts(bsplCategory: BSPLCategory, page: Int, size: Int): AccountConnection!
|
|
333
|
+
accountHierarchy(accountCode: ID!): [Account!]!
|
|
334
|
+
|
|
335
|
+
# 課税取引
|
|
336
|
+
taxTransaction(taxCode: ID!): TaxTransaction
|
|
337
|
+
taxTransactions: [TaxTransaction!]!
|
|
338
|
+
|
|
339
|
+
# 勘定科目構成
|
|
340
|
+
accountConfig(accountCode: ID!): AccountConfig
|
|
341
|
+
accountConfigs(parentPath: String): [AccountConfig!]!
|
|
342
|
+
|
|
343
|
+
# 仕訳
|
|
344
|
+
journal(journalNumber: ID!): Journal
|
|
345
|
+
journals(fromDate: Date, toDate: Date, accountCode: ID, page: Int, size: Int): JournalConnection!
|
|
346
|
+
|
|
347
|
+
# 残高
|
|
348
|
+
dailyBalance(date: Date!, accountCode: ID!, subAccountCode: ID): DailyBalance
|
|
349
|
+
dailyBalances(date: Date!, page: Int, size: Int): DailyBalanceConnection!
|
|
350
|
+
|
|
351
|
+
monthlyBalance(fiscalYear: Int!, month: Int!, accountCode: ID!, subAccountCode: ID): MonthlyBalance
|
|
352
|
+
monthlyBalances(fiscalYear: Int!, month: Int!, page: Int, size: Int): MonthlyBalanceConnection!
|
|
353
|
+
|
|
354
|
+
# 帳票
|
|
355
|
+
dailyReport(date: Date!): DailyReport!
|
|
356
|
+
trialBalance(fiscalYear: Int!, month: Int!): TrialBalance!
|
|
357
|
+
financialStatements(fiscalYear: Int!): FinancialStatements!
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
type Mutation {
|
|
361
|
+
# 勘定科目
|
|
362
|
+
createAccount(input: CreateAccountInput!): Account!
|
|
363
|
+
updateAccount(input: UpdateAccountInput!): Account!
|
|
364
|
+
deleteAccount(accountCode: ID!): Boolean!
|
|
365
|
+
|
|
366
|
+
# 課税取引
|
|
367
|
+
createTaxTransaction(input: CreateTaxTransactionInput!): TaxTransaction!
|
|
368
|
+
updateTaxTransaction(input: UpdateTaxTransactionInput!): TaxTransaction!
|
|
369
|
+
deleteTaxTransaction(taxCode: ID!): Boolean!
|
|
370
|
+
|
|
371
|
+
# 仕訳
|
|
372
|
+
createJournal(input: CreateJournalInput!): Journal!
|
|
373
|
+
updateJournal(input: UpdateJournalInput!): Journal!
|
|
374
|
+
deleteJournal(journalNumber: ID!): Boolean!
|
|
375
|
+
reverseJournal(journalNumber: ID!, reason: String): Journal!
|
|
376
|
+
|
|
377
|
+
# 残高更新
|
|
378
|
+
recalculateDailyBalance(date: Date!): RecalculateResult!
|
|
379
|
+
recalculateMonthlyBalance(fiscalYear: Int!, month: Int!): RecalculateResult!
|
|
380
|
+
|
|
381
|
+
# 決算処理
|
|
382
|
+
executeClosing(input: ExecuteClosingInput!): ClosingResult!
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
type Subscription {
|
|
386
|
+
# 残高変動
|
|
387
|
+
balanceChanged(accountCode: ID): BalanceChange!
|
|
388
|
+
|
|
389
|
+
# 仕訳登録通知
|
|
390
|
+
journalCreated: JournalNotification!
|
|
391
|
+
|
|
392
|
+
# 決算処理進捗
|
|
393
|
+
closingProgressUpdated(closingId: ID!): ClosingProgress!
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# ページネーション共通型
|
|
397
|
+
type PageInfo {
|
|
398
|
+
hasNextPage: Boolean!
|
|
399
|
+
hasPreviousPage: Boolean!
|
|
400
|
+
totalElements: Int!
|
|
401
|
+
totalPages: Int!
|
|
402
|
+
currentPage: Int!
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# カスタムスカラー
|
|
406
|
+
scalar Date
|
|
407
|
+
scalar DateTime
|
|
408
|
+
scalar BigDecimal
|
|
409
|
+
scalar Long
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
</details>
|
|
413
|
+
|
|
414
|
+
### 23.2 勘定科目スキーマ
|
|
415
|
+
|
|
416
|
+
<details>
|
|
417
|
+
<summary>src/main/resources/graphql/account.graphqls</summary>
|
|
418
|
+
|
|
419
|
+
```graphql
|
|
420
|
+
# BSPL区分
|
|
421
|
+
enum BSPLCategory {
|
|
422
|
+
BS # 貸借対照表
|
|
423
|
+
PL # 損益計算書
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# 貸借区分
|
|
427
|
+
enum DebitCreditCategory {
|
|
428
|
+
DEBIT # 借方
|
|
429
|
+
CREDIT # 貸方
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# 取引要素区分
|
|
433
|
+
enum TransactionElementCategory {
|
|
434
|
+
ASSET # 資産
|
|
435
|
+
LIABILITY # 負債
|
|
436
|
+
CAPITAL # 資本
|
|
437
|
+
REVENUE # 収益
|
|
438
|
+
EXPENSE # 費用
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# 集計区分
|
|
442
|
+
enum AggregationCategory {
|
|
443
|
+
HEADING # 見出科目
|
|
444
|
+
SUBTOTAL # 集計科目
|
|
445
|
+
POSTING # 計上科目
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# 勘定科目
|
|
449
|
+
type Account {
|
|
450
|
+
accountCode: ID!
|
|
451
|
+
accountName: String!
|
|
452
|
+
accountShortName: String
|
|
453
|
+
accountKana: String
|
|
454
|
+
bsplCategory: BSPLCategory!
|
|
455
|
+
debitCreditCategory: DebitCreditCategory!
|
|
456
|
+
transactionElementCategory: TransactionElementCategory!
|
|
457
|
+
aggregationCategory: AggregationCategory!
|
|
458
|
+
managementAccountingCategory: String
|
|
459
|
+
expenseCategory: String
|
|
460
|
+
ledgerOutputCategory: String
|
|
461
|
+
subAccountType: String
|
|
462
|
+
taxCalculationCategory: String
|
|
463
|
+
taxTransactionCode: ID
|
|
464
|
+
dueDateManagementCategory: String
|
|
465
|
+
createdAt: DateTime!
|
|
466
|
+
updatedAt: DateTime!
|
|
467
|
+
updatedBy: String
|
|
468
|
+
|
|
469
|
+
# 関連データ(必要な場合のみ取得)
|
|
470
|
+
taxTransaction: TaxTransaction
|
|
471
|
+
accountConfig: AccountConfig
|
|
472
|
+
childAccounts: [Account!]!
|
|
473
|
+
dailyBalances(date: Date): [DailyBalance!]!
|
|
474
|
+
monthlyBalances(fiscalYear: Int, month: Int): [MonthlyBalance!]!
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# 勘定科目一覧(ページネーション付き)
|
|
478
|
+
type AccountConnection {
|
|
479
|
+
edges: [AccountEdge!]!
|
|
480
|
+
pageInfo: PageInfo!
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
type AccountEdge {
|
|
484
|
+
node: Account!
|
|
485
|
+
cursor: String!
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# 課税取引マスタ
|
|
489
|
+
type TaxTransaction {
|
|
490
|
+
taxCode: ID!
|
|
491
|
+
taxName: String!
|
|
492
|
+
taxRate: BigDecimal!
|
|
493
|
+
updatedAt: DateTime!
|
|
494
|
+
updatedBy: String
|
|
495
|
+
|
|
496
|
+
# 関連データ
|
|
497
|
+
accounts: [Account!]!
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# 勘定科目構成マスタ
|
|
501
|
+
type AccountConfig {
|
|
502
|
+
accountCode: ID!
|
|
503
|
+
accountPath: String!
|
|
504
|
+
updatedAt: DateTime!
|
|
505
|
+
updatedBy: String
|
|
506
|
+
|
|
507
|
+
# 関連データ
|
|
508
|
+
account: Account!
|
|
509
|
+
parentAccount: Account
|
|
510
|
+
childAccounts: [Account!]!
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
# 入力型
|
|
514
|
+
input CreateAccountInput {
|
|
515
|
+
accountCode: ID!
|
|
516
|
+
accountName: String!
|
|
517
|
+
accountShortName: String
|
|
518
|
+
accountKana: String
|
|
519
|
+
bsplCategory: BSPLCategory!
|
|
520
|
+
debitCreditCategory: DebitCreditCategory!
|
|
521
|
+
transactionElementCategory: TransactionElementCategory!
|
|
522
|
+
aggregationCategory: AggregationCategory!
|
|
523
|
+
managementAccountingCategory: String
|
|
524
|
+
expenseCategory: String
|
|
525
|
+
ledgerOutputCategory: String
|
|
526
|
+
subAccountType: String
|
|
527
|
+
taxCalculationCategory: String
|
|
528
|
+
taxTransactionCode: ID
|
|
529
|
+
dueDateManagementCategory: String
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
input UpdateAccountInput {
|
|
533
|
+
accountCode: ID!
|
|
534
|
+
accountName: String
|
|
535
|
+
accountShortName: String
|
|
536
|
+
accountKana: String
|
|
537
|
+
bsplCategory: BSPLCategory
|
|
538
|
+
debitCreditCategory: DebitCreditCategory
|
|
539
|
+
transactionElementCategory: TransactionElementCategory
|
|
540
|
+
aggregationCategory: AggregationCategory
|
|
541
|
+
managementAccountingCategory: String
|
|
542
|
+
expenseCategory: String
|
|
543
|
+
ledgerOutputCategory: String
|
|
544
|
+
subAccountType: String
|
|
545
|
+
taxCalculationCategory: String
|
|
546
|
+
taxTransactionCode: ID
|
|
547
|
+
dueDateManagementCategory: String
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
</details>
|
|
552
|
+
|
|
553
|
+
### 23.3 仕訳スキーマ
|
|
554
|
+
|
|
555
|
+
<details>
|
|
556
|
+
<summary>src/main/resources/graphql/journal.graphqls</summary>
|
|
557
|
+
|
|
558
|
+
```graphql
|
|
559
|
+
# 仕訳伝票区分
|
|
560
|
+
enum JournalVoucherType {
|
|
561
|
+
NORMAL # 通常仕訳
|
|
562
|
+
ADJUSTMENT # 調整仕訳
|
|
563
|
+
CLOSING # 決算仕訳
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
# 仕訳
|
|
567
|
+
type Journal {
|
|
568
|
+
journalNumber: ID!
|
|
569
|
+
postingDate: Date!
|
|
570
|
+
entryDate: Date!
|
|
571
|
+
isClosingEntry: Boolean!
|
|
572
|
+
isSingleEntry: Boolean!
|
|
573
|
+
voucherType: JournalVoucherType!
|
|
574
|
+
isRecurring: Boolean!
|
|
575
|
+
employeeCode: String
|
|
576
|
+
departmentCode: String
|
|
577
|
+
isReversal: Boolean!
|
|
578
|
+
reversalJournalNumber: ID
|
|
579
|
+
createdAt: DateTime!
|
|
580
|
+
updatedAt: DateTime!
|
|
581
|
+
|
|
582
|
+
# 関連データ
|
|
583
|
+
details: [JournalDetail!]!
|
|
584
|
+
department: Department
|
|
585
|
+
totalDebitAmount: BigDecimal!
|
|
586
|
+
totalCreditAmount: BigDecimal!
|
|
587
|
+
isBalanced: Boolean!
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
# 仕訳明細
|
|
591
|
+
type JournalDetail {
|
|
592
|
+
journalNumber: ID!
|
|
593
|
+
lineNumber: Int!
|
|
594
|
+
description: String
|
|
595
|
+
createdAt: DateTime!
|
|
596
|
+
updatedAt: DateTime!
|
|
597
|
+
|
|
598
|
+
# 関連データ
|
|
599
|
+
debitCreditDetails: [JournalDebitCreditDetail!]!
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
# 仕訳貸借明細
|
|
603
|
+
type JournalDebitCreditDetail {
|
|
604
|
+
journalNumber: ID!
|
|
605
|
+
lineNumber: Int!
|
|
606
|
+
debitCreditCategory: DebitCreditCategory!
|
|
607
|
+
currencyCode: String
|
|
608
|
+
exchangeRate: BigDecimal
|
|
609
|
+
departmentCode: String
|
|
610
|
+
projectCode: String
|
|
611
|
+
accountCode: ID!
|
|
612
|
+
subAccountCode: String
|
|
613
|
+
amount: BigDecimal!
|
|
614
|
+
baseAmount: BigDecimal!
|
|
615
|
+
taxCategory: String
|
|
616
|
+
taxRate: BigDecimal
|
|
617
|
+
taxCalculationCategory: String
|
|
618
|
+
dueDate: Date
|
|
619
|
+
cashFlowFlag: Boolean
|
|
620
|
+
segmentCode: String
|
|
621
|
+
counterAccountCode: String
|
|
622
|
+
counterSubAccountCode: String
|
|
623
|
+
tagCode: String
|
|
624
|
+
tagDescription: String
|
|
625
|
+
createdAt: DateTime!
|
|
626
|
+
updatedAt: DateTime!
|
|
627
|
+
|
|
628
|
+
# 関連データ
|
|
629
|
+
account: Account!
|
|
630
|
+
counterAccount: Account
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
# 仕訳一覧
|
|
634
|
+
type JournalConnection {
|
|
635
|
+
edges: [JournalEdge!]!
|
|
636
|
+
pageInfo: PageInfo!
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
type JournalEdge {
|
|
640
|
+
node: Journal!
|
|
641
|
+
cursor: String!
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
# 仕訳登録通知イベント
|
|
645
|
+
type JournalNotification {
|
|
646
|
+
journalNumber: ID!
|
|
647
|
+
postingDate: Date!
|
|
648
|
+
totalDebitAmount: BigDecimal!
|
|
649
|
+
totalCreditAmount: BigDecimal!
|
|
650
|
+
createdBy: String
|
|
651
|
+
createdAt: DateTime!
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
# 入力型
|
|
655
|
+
input CreateJournalInput {
|
|
656
|
+
postingDate: Date!
|
|
657
|
+
isClosingEntry: Boolean
|
|
658
|
+
isSingleEntry: Boolean
|
|
659
|
+
voucherType: JournalVoucherType
|
|
660
|
+
isRecurring: Boolean
|
|
661
|
+
employeeCode: String
|
|
662
|
+
departmentCode: String
|
|
663
|
+
details: [JournalDetailInput!]!
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
input JournalDetailInput {
|
|
667
|
+
lineNumber: Int!
|
|
668
|
+
description: String
|
|
669
|
+
debitCreditDetails: [JournalDebitCreditDetailInput!]!
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
input JournalDebitCreditDetailInput {
|
|
673
|
+
debitCreditCategory: DebitCreditCategory!
|
|
674
|
+
currencyCode: String
|
|
675
|
+
exchangeRate: BigDecimal
|
|
676
|
+
departmentCode: String
|
|
677
|
+
projectCode: String
|
|
678
|
+
accountCode: ID!
|
|
679
|
+
subAccountCode: String
|
|
680
|
+
amount: BigDecimal!
|
|
681
|
+
baseAmount: BigDecimal
|
|
682
|
+
taxCategory: String
|
|
683
|
+
taxRate: BigDecimal
|
|
684
|
+
dueDate: Date
|
|
685
|
+
segmentCode: String
|
|
686
|
+
counterAccountCode: String
|
|
687
|
+
tagCode: String
|
|
688
|
+
tagDescription: String
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
</details>
|
|
693
|
+
|
|
694
|
+
### 23.4 残高・帳票スキーマ
|
|
695
|
+
|
|
696
|
+
<details>
|
|
697
|
+
<summary>src/main/resources/graphql/balance.graphqls</summary>
|
|
698
|
+
|
|
699
|
+
```graphql
|
|
700
|
+
# 日次勘定科目残高
|
|
701
|
+
type DailyBalance {
|
|
702
|
+
postingDate: Date!
|
|
703
|
+
accountCode: ID!
|
|
704
|
+
subAccountCode: String
|
|
705
|
+
departmentCode: String
|
|
706
|
+
projectCode: String
|
|
707
|
+
isClosingEntry: Boolean!
|
|
708
|
+
debitAmount: BigDecimal!
|
|
709
|
+
creditAmount: BigDecimal!
|
|
710
|
+
balance: BigDecimal!
|
|
711
|
+
|
|
712
|
+
# 関連データ
|
|
713
|
+
account: Account!
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
# 日次残高一覧
|
|
717
|
+
type DailyBalanceConnection {
|
|
718
|
+
edges: [DailyBalanceEdge!]!
|
|
719
|
+
pageInfo: PageInfo!
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
type DailyBalanceEdge {
|
|
723
|
+
node: DailyBalance!
|
|
724
|
+
cursor: String!
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# 月次勘定科目残高
|
|
728
|
+
type MonthlyBalance {
|
|
729
|
+
fiscalYear: Int!
|
|
730
|
+
month: Int!
|
|
731
|
+
accountCode: ID!
|
|
732
|
+
subAccountCode: String
|
|
733
|
+
departmentCode: String
|
|
734
|
+
projectCode: String
|
|
735
|
+
isClosingEntry: Boolean!
|
|
736
|
+
beginningBalance: BigDecimal!
|
|
737
|
+
debitAmount: BigDecimal!
|
|
738
|
+
creditAmount: BigDecimal!
|
|
739
|
+
endingBalance: BigDecimal!
|
|
740
|
+
|
|
741
|
+
# 関連データ
|
|
742
|
+
account: Account!
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
# 残高変動イベント
|
|
746
|
+
type BalanceChange {
|
|
747
|
+
accountCode: ID!
|
|
748
|
+
accountName: String!
|
|
749
|
+
changeType: BalanceChangeType!
|
|
750
|
+
previousBalance: BigDecimal!
|
|
751
|
+
currentBalance: BigDecimal!
|
|
752
|
+
changeAmount: BigDecimal!
|
|
753
|
+
timestamp: DateTime!
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
enum BalanceChangeType {
|
|
757
|
+
DEBIT # 借方記入
|
|
758
|
+
CREDIT # 貸方記入
|
|
759
|
+
ADJUSTMENT # 調整
|
|
760
|
+
CLOSING # 決算
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
# 再計算結果
|
|
764
|
+
type RecalculateResult {
|
|
765
|
+
processedCount: Int!
|
|
766
|
+
updatedAccounts: [String!]!
|
|
767
|
+
timestamp: DateTime!
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
</details>
|
|
772
|
+
|
|
773
|
+
<details>
|
|
774
|
+
<summary>src/main/resources/graphql/report.graphqls</summary>
|
|
775
|
+
|
|
776
|
+
```graphql
|
|
777
|
+
# 日計表
|
|
778
|
+
type DailyReport {
|
|
779
|
+
reportDate: Date!
|
|
780
|
+
entries: [DailyReportEntry!]!
|
|
781
|
+
totalDebitAmount: BigDecimal!
|
|
782
|
+
totalCreditAmount: BigDecimal!
|
|
783
|
+
generatedAt: DateTime!
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
type DailyReportEntry {
|
|
787
|
+
accountCode: ID!
|
|
788
|
+
accountName: String!
|
|
789
|
+
debitAmount: BigDecimal!
|
|
790
|
+
creditAmount: BigDecimal!
|
|
791
|
+
balance: BigDecimal!
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
# 合計残高試算表
|
|
795
|
+
type TrialBalance {
|
|
796
|
+
fiscalYear: Int!
|
|
797
|
+
month: Int!
|
|
798
|
+
entries: [TrialBalanceEntry!]!
|
|
799
|
+
totalDebitAmount: BigDecimal!
|
|
800
|
+
totalCreditAmount: BigDecimal!
|
|
801
|
+
totalDebitBalance: BigDecimal!
|
|
802
|
+
totalCreditBalance: BigDecimal!
|
|
803
|
+
generatedAt: DateTime!
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
type TrialBalanceEntry {
|
|
807
|
+
accountCode: ID!
|
|
808
|
+
accountName: String!
|
|
809
|
+
bsplCategory: BSPLCategory!
|
|
810
|
+
debitAmount: BigDecimal!
|
|
811
|
+
creditAmount: BigDecimal!
|
|
812
|
+
debitBalance: BigDecimal!
|
|
813
|
+
creditBalance: BigDecimal!
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
# 財務諸表
|
|
817
|
+
type FinancialStatements {
|
|
818
|
+
fiscalYear: Int!
|
|
819
|
+
balanceSheet: BalanceSheet!
|
|
820
|
+
incomeStatement: IncomeStatement!
|
|
821
|
+
generatedAt: DateTime!
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
# 貸借対照表
|
|
825
|
+
type BalanceSheet {
|
|
826
|
+
fiscalYear: Int!
|
|
827
|
+
assetEntries: [FinancialStatementEntry!]!
|
|
828
|
+
liabilityEntries: [FinancialStatementEntry!]!
|
|
829
|
+
capitalEntries: [FinancialStatementEntry!]!
|
|
830
|
+
totalAssets: BigDecimal!
|
|
831
|
+
totalLiabilities: BigDecimal!
|
|
832
|
+
totalCapital: BigDecimal!
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
# 損益計算書
|
|
836
|
+
type IncomeStatement {
|
|
837
|
+
fiscalYear: Int!
|
|
838
|
+
revenueEntries: [FinancialStatementEntry!]!
|
|
839
|
+
expenseEntries: [FinancialStatementEntry!]!
|
|
840
|
+
totalRevenue: BigDecimal!
|
|
841
|
+
totalExpense: BigDecimal!
|
|
842
|
+
netIncome: BigDecimal!
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
type FinancialStatementEntry {
|
|
846
|
+
accountCode: ID!
|
|
847
|
+
accountName: String!
|
|
848
|
+
level: Int!
|
|
849
|
+
amount: BigDecimal!
|
|
850
|
+
isSubtotal: Boolean!
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
# 決算処理結果
|
|
854
|
+
type ClosingResult {
|
|
855
|
+
closingId: ID!
|
|
856
|
+
fiscalYear: Int!
|
|
857
|
+
processedJournalCount: Int!
|
|
858
|
+
closingJournals: [Journal!]!
|
|
859
|
+
completedAt: DateTime!
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
# 決算処理進捗
|
|
863
|
+
type ClosingProgress {
|
|
864
|
+
closingId: ID!
|
|
865
|
+
status: ClosingStatus!
|
|
866
|
+
currentStep: String!
|
|
867
|
+
processedCount: Int!
|
|
868
|
+
totalCount: Int!
|
|
869
|
+
progressPercent: Float!
|
|
870
|
+
updatedAt: DateTime!
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
enum ClosingStatus {
|
|
874
|
+
PROCESSING # 処理中
|
|
875
|
+
COMPLETED # 完了
|
|
876
|
+
FAILED # 失敗
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
</details>
|
|
881
|
+
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
## 第 24 章:マスタ API の実装
|
|
885
|
+
|
|
886
|
+
### 24.1 Query リゾルバ
|
|
887
|
+
|
|
888
|
+
<details>
|
|
889
|
+
<summary>QueryResolver.java</summary>
|
|
890
|
+
|
|
891
|
+
```java
|
|
892
|
+
package com.example.accounting.infrastructure.graphql.resolver;
|
|
893
|
+
|
|
894
|
+
import com.example.accounting.application.port.in.AccountUseCase;
|
|
895
|
+
import com.example.accounting.application.port.in.JournalUseCase;
|
|
896
|
+
import com.example.accounting.application.port.in.BalanceUseCase;
|
|
897
|
+
import com.example.accounting.domain.model.account.Account;
|
|
898
|
+
import com.example.accounting.domain.model.account.TaxTransaction;
|
|
899
|
+
import com.example.accounting.domain.model.account.AccountConfig;
|
|
900
|
+
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
901
|
+
import org.springframework.graphql.data.method.annotation.Argument;
|
|
902
|
+
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
903
|
+
import org.springframework.stereotype.Controller;
|
|
904
|
+
|
|
905
|
+
import java.util.List;
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* GraphQL Query リゾルバ
|
|
909
|
+
*/
|
|
910
|
+
@Controller
|
|
911
|
+
public class QueryResolver {
|
|
912
|
+
|
|
913
|
+
private final AccountUseCase accountUseCase;
|
|
914
|
+
private final JournalUseCase journalUseCase;
|
|
915
|
+
private final BalanceUseCase balanceUseCase;
|
|
916
|
+
|
|
917
|
+
public QueryResolver(
|
|
918
|
+
AccountUseCase accountUseCase,
|
|
919
|
+
JournalUseCase journalUseCase,
|
|
920
|
+
BalanceUseCase balanceUseCase) {
|
|
921
|
+
this.accountUseCase = accountUseCase;
|
|
922
|
+
this.journalUseCase = journalUseCase;
|
|
923
|
+
this.balanceUseCase = balanceUseCase;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// === 勘定科目 ===
|
|
927
|
+
|
|
928
|
+
@QueryMapping
|
|
929
|
+
public Account account(@Argument String accountCode) {
|
|
930
|
+
return accountUseCase.findByCode(accountCode)
|
|
931
|
+
.orElse(null);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
@QueryMapping
|
|
935
|
+
public AccountConnection accounts(
|
|
936
|
+
@Argument String bsplCategory,
|
|
937
|
+
@Argument Integer page,
|
|
938
|
+
@Argument Integer size) {
|
|
939
|
+
|
|
940
|
+
int pageNum = page != null ? page : 0;
|
|
941
|
+
int pageSize = size != null ? size : 20;
|
|
942
|
+
|
|
943
|
+
List<Account> accounts = accountUseCase.findAll(bsplCategory, pageNum, pageSize);
|
|
944
|
+
long totalCount = accountUseCase.count(bsplCategory);
|
|
945
|
+
|
|
946
|
+
return AccountConnection.of(accounts, pageNum, pageSize, totalCount);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
@QueryMapping
|
|
950
|
+
public List<Account> accountHierarchy(@Argument String accountCode) {
|
|
951
|
+
return accountUseCase.findHierarchy(accountCode);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// === 課税取引 ===
|
|
955
|
+
|
|
956
|
+
@QueryMapping
|
|
957
|
+
public TaxTransaction taxTransaction(@Argument String taxCode) {
|
|
958
|
+
return accountUseCase.findTaxTransaction(taxCode)
|
|
959
|
+
.orElse(null);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
@QueryMapping
|
|
963
|
+
public List<TaxTransaction> taxTransactions() {
|
|
964
|
+
return accountUseCase.findAllTaxTransactions();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// === 勘定科目構成 ===
|
|
968
|
+
|
|
969
|
+
@QueryMapping
|
|
970
|
+
public AccountConfig accountConfig(@Argument String accountCode) {
|
|
971
|
+
return accountUseCase.findAccountConfig(accountCode)
|
|
972
|
+
.orElse(null);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
@QueryMapping
|
|
976
|
+
public List<AccountConfig> accountConfigs(@Argument String parentPath) {
|
|
977
|
+
if (parentPath != null) {
|
|
978
|
+
return accountUseCase.findAccountConfigsByPath(parentPath);
|
|
979
|
+
}
|
|
980
|
+
return accountUseCase.findAllAccountConfigs();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
</details>
|
|
986
|
+
|
|
987
|
+
### 24.2 N+1 問題とは
|
|
988
|
+
|
|
989
|
+
GraphQL では、ネストしたデータを取得する際に **N+1 問題** が発生しやすくなります。
|
|
990
|
+
|
|
991
|
+
```plantuml
|
|
992
|
+
@startuml n_plus_1_problem
|
|
993
|
+
|
|
994
|
+
title N+1 問題の発生パターン
|
|
995
|
+
|
|
996
|
+
participant "GraphQL Client" as client
|
|
997
|
+
participant "AccountResolver" as resolver
|
|
998
|
+
participant "TaxTransactionRepository" as repo
|
|
999
|
+
database "PostgreSQL" as db
|
|
1000
|
+
|
|
1001
|
+
client -> resolver : accounts(first: 10)
|
|
1002
|
+
resolver -> db : SELECT * FROM 勘定科目マスタ LIMIT 10
|
|
1003
|
+
db --> resolver : 10件の勘定科目
|
|
1004
|
+
|
|
1005
|
+
loop 各勘定科目に対して(N回)
|
|
1006
|
+
resolver -> repo : findByCode(taxCode)
|
|
1007
|
+
repo -> db : SELECT * FROM 課税取引マスタ WHERE 課税取引コード = ?
|
|
1008
|
+
db --> repo : 課税取引データ
|
|
1009
|
+
repo --> resolver : 課税取引
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
resolver --> client : 勘定科目一覧(課税取引付き)
|
|
1013
|
+
|
|
1014
|
+
note right of db
|
|
1015
|
+
合計 11回のクエリ
|
|
1016
|
+
(1 + N = 1 + 10)
|
|
1017
|
+
|
|
1018
|
+
勘定科目が100件なら101回!
|
|
1019
|
+
end note
|
|
1020
|
+
|
|
1021
|
+
@enduml
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### 24.3 DataLoader による解決
|
|
1025
|
+
|
|
1026
|
+
**DataLoader** は、複数の個別リクエストをバッチ処理にまとめることで N+1 問題を解決します。
|
|
1027
|
+
|
|
1028
|
+
```plantuml
|
|
1029
|
+
@startuml dataloader_solution
|
|
1030
|
+
|
|
1031
|
+
title DataLoader による N+1 問題の解決
|
|
1032
|
+
|
|
1033
|
+
participant "GraphQL Client" as client
|
|
1034
|
+
participant "AccountResolver" as resolver
|
|
1035
|
+
participant "TaxTransactionDataLoader" as loader
|
|
1036
|
+
participant "TaxTransactionRepository" as repo
|
|
1037
|
+
database "PostgreSQL" as db
|
|
1038
|
+
|
|
1039
|
+
client -> resolver : accounts(first: 10)
|
|
1040
|
+
resolver -> db : SELECT * FROM 勘定科目マスタ LIMIT 10
|
|
1041
|
+
db --> resolver : 10件の勘定科目
|
|
1042
|
+
|
|
1043
|
+
loop 各勘定科目に対して
|
|
1044
|
+
resolver -> loader : load(taxCode)
|
|
1045
|
+
note right of loader : キューに追加
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
loader -> repo : loadMany([code1, code2, ...code10])
|
|
1049
|
+
repo -> db : SELECT * FROM 課税取引マスタ WHERE 課税取引コード IN (?, ?, ..., ?)
|
|
1050
|
+
db --> repo : 課税取引データ
|
|
1051
|
+
repo --> loader : Map<課税取引コード, 課税取引>
|
|
1052
|
+
loader --> resolver : 各勘定科目の課税取引
|
|
1053
|
+
|
|
1054
|
+
resolver --> client : 勘定科目一覧(課税取引付き)
|
|
1055
|
+
|
|
1056
|
+
note right of db
|
|
1057
|
+
合計 2回のクエリ
|
|
1058
|
+
(1 + 1 = 2)
|
|
1059
|
+
|
|
1060
|
+
勘定科目が100件でも2回!
|
|
1061
|
+
end note
|
|
1062
|
+
|
|
1063
|
+
@enduml
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
<details>
|
|
1067
|
+
<summary>TaxTransactionDataLoader.java</summary>
|
|
1068
|
+
|
|
1069
|
+
```java
|
|
1070
|
+
package com.example.accounting.infrastructure.graphql.dataloader;
|
|
1071
|
+
|
|
1072
|
+
import com.example.accounting.application.port.out.AccountRepository;
|
|
1073
|
+
import com.example.accounting.domain.model.account.TaxTransaction;
|
|
1074
|
+
import org.dataloader.BatchLoaderEnvironment;
|
|
1075
|
+
import org.dataloader.MappedBatchLoader;
|
|
1076
|
+
import org.springframework.stereotype.Component;
|
|
1077
|
+
|
|
1078
|
+
import java.util.Map;
|
|
1079
|
+
import java.util.Set;
|
|
1080
|
+
import java.util.concurrent.CompletableFuture;
|
|
1081
|
+
import java.util.concurrent.CompletionStage;
|
|
1082
|
+
import java.util.function.Function;
|
|
1083
|
+
import java.util.stream.Collectors;
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* 課税取引データの DataLoader
|
|
1087
|
+
*/
|
|
1088
|
+
@Component
|
|
1089
|
+
public class TaxTransactionDataLoader implements MappedBatchLoader<String, TaxTransaction> {
|
|
1090
|
+
|
|
1091
|
+
private final AccountRepository accountRepository;
|
|
1092
|
+
|
|
1093
|
+
public TaxTransactionDataLoader(AccountRepository accountRepository) {
|
|
1094
|
+
this.accountRepository = accountRepository;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
@Override
|
|
1098
|
+
public CompletionStage<Map<String, TaxTransaction>> load(
|
|
1099
|
+
Set<String> taxCodes,
|
|
1100
|
+
BatchLoaderEnvironment environment) {
|
|
1101
|
+
|
|
1102
|
+
return CompletableFuture.supplyAsync(() -> {
|
|
1103
|
+
return accountRepository.findTaxTransactionsByCodes(taxCodes).stream()
|
|
1104
|
+
.collect(Collectors.toMap(
|
|
1105
|
+
TaxTransaction::getTaxCode,
|
|
1106
|
+
Function.identity()
|
|
1107
|
+
));
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
</details>
|
|
1114
|
+
|
|
1115
|
+
### 24.4 勘定科目リゾルバの実装
|
|
1116
|
+
|
|
1117
|
+
<details>
|
|
1118
|
+
<summary>AccountResolver.java</summary>
|
|
1119
|
+
|
|
1120
|
+
```java
|
|
1121
|
+
package com.example.accounting.infrastructure.graphql.resolver;
|
|
1122
|
+
|
|
1123
|
+
import com.example.accounting.application.port.in.AccountUseCase;
|
|
1124
|
+
import com.example.accounting.config.DataLoaderConfig;
|
|
1125
|
+
import com.example.accounting.domain.model.account.Account;
|
|
1126
|
+
import com.example.accounting.domain.model.account.TaxTransaction;
|
|
1127
|
+
import com.example.accounting.domain.model.account.AccountConfig;
|
|
1128
|
+
import com.example.accounting.domain.model.balance.DailyBalance;
|
|
1129
|
+
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
1130
|
+
import graphql.schema.DataFetchingEnvironment;
|
|
1131
|
+
import org.dataloader.DataLoader;
|
|
1132
|
+
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1133
|
+
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
|
1134
|
+
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
1135
|
+
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
|
1136
|
+
import org.springframework.stereotype.Controller;
|
|
1137
|
+
|
|
1138
|
+
import java.util.List;
|
|
1139
|
+
import java.util.concurrent.CompletableFuture;
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* 勘定科目 GraphQL リゾルバ
|
|
1143
|
+
*/
|
|
1144
|
+
@Controller
|
|
1145
|
+
public class AccountResolver {
|
|
1146
|
+
|
|
1147
|
+
private final AccountUseCase accountUseCase;
|
|
1148
|
+
|
|
1149
|
+
public AccountResolver(AccountUseCase accountUseCase) {
|
|
1150
|
+
this.accountUseCase = accountUseCase;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// === Query ===
|
|
1154
|
+
|
|
1155
|
+
@QueryMapping
|
|
1156
|
+
public Account account(@Argument String accountCode) {
|
|
1157
|
+
return accountUseCase.findByCode(accountCode).orElse(null);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
@QueryMapping
|
|
1161
|
+
public AccountConnection accounts(
|
|
1162
|
+
@Argument String bsplCategory,
|
|
1163
|
+
@Argument Integer page,
|
|
1164
|
+
@Argument Integer size) {
|
|
1165
|
+
|
|
1166
|
+
int pageNum = page != null ? page : 0;
|
|
1167
|
+
int pageSize = size != null ? size : 20;
|
|
1168
|
+
|
|
1169
|
+
List<Account> accounts = accountUseCase.findAll(bsplCategory, pageNum, pageSize);
|
|
1170
|
+
long totalCount = accountUseCase.count(bsplCategory);
|
|
1171
|
+
|
|
1172
|
+
return AccountConnection.of(accounts, pageNum, pageSize, totalCount);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// === Mutation ===
|
|
1176
|
+
|
|
1177
|
+
@MutationMapping
|
|
1178
|
+
public Account createAccount(@Argument CreateAccountInput input) {
|
|
1179
|
+
Account account = Account.builder()
|
|
1180
|
+
.accountCode(input.accountCode())
|
|
1181
|
+
.accountName(input.accountName())
|
|
1182
|
+
.accountShortName(input.accountShortName())
|
|
1183
|
+
.bsplCategory(input.bsplCategory())
|
|
1184
|
+
.debitCreditCategory(input.debitCreditCategory())
|
|
1185
|
+
.transactionElementCategory(input.transactionElementCategory())
|
|
1186
|
+
.aggregationCategory(input.aggregationCategory())
|
|
1187
|
+
.taxTransactionCode(input.taxTransactionCode())
|
|
1188
|
+
.build();
|
|
1189
|
+
|
|
1190
|
+
return accountUseCase.create(account);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
@MutationMapping
|
|
1194
|
+
public boolean deleteAccount(@Argument String accountCode) {
|
|
1195
|
+
return accountUseCase.delete(accountCode);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// === フィールドリゾルバ(DataLoader 使用)===
|
|
1199
|
+
|
|
1200
|
+
@SchemaMapping(typeName = "Account", field = "taxTransaction")
|
|
1201
|
+
public CompletableFuture<TaxTransaction> taxTransaction(
|
|
1202
|
+
Account account,
|
|
1203
|
+
DataFetchingEnvironment env) {
|
|
1204
|
+
|
|
1205
|
+
if (account.getTaxTransactionCode() == null) {
|
|
1206
|
+
return CompletableFuture.completedFuture(null);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
DataLoader<String, TaxTransaction> loader =
|
|
1210
|
+
env.getDataLoader(DataLoaderConfig.TAX_TRANSACTION_LOADER);
|
|
1211
|
+
|
|
1212
|
+
return loader.load(account.getTaxTransactionCode());
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
@SchemaMapping(typeName = "Account", field = "accountConfig")
|
|
1216
|
+
public CompletableFuture<AccountConfig> accountConfig(
|
|
1217
|
+
Account account,
|
|
1218
|
+
DataFetchingEnvironment env) {
|
|
1219
|
+
|
|
1220
|
+
DataLoader<String, AccountConfig> loader =
|
|
1221
|
+
env.getDataLoader(DataLoaderConfig.ACCOUNT_CONFIG_LOADER);
|
|
1222
|
+
|
|
1223
|
+
return loader.load(account.getAccountCode());
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
@SchemaMapping(typeName = "Account", field = "childAccounts")
|
|
1227
|
+
public List<Account> childAccounts(Account account) {
|
|
1228
|
+
return accountUseCase.findChildAccounts(account.getAccountCode());
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
</details>
|
|
1234
|
+
|
|
1235
|
+
---
|
|
1236
|
+
|
|
1237
|
+
## 第 25 章:トランザクション API の実装
|
|
1238
|
+
|
|
1239
|
+
### 25.1 仕訳リゾルバの実装
|
|
1240
|
+
|
|
1241
|
+
<details>
|
|
1242
|
+
<summary>JournalResolver.java</summary>
|
|
1243
|
+
|
|
1244
|
+
```java
|
|
1245
|
+
package com.example.accounting.infrastructure.graphql.resolver;
|
|
1246
|
+
|
|
1247
|
+
import com.example.accounting.application.port.in.JournalUseCase;
|
|
1248
|
+
import com.example.accounting.config.DataLoaderConfig;
|
|
1249
|
+
import com.example.accounting.domain.model.journal.Journal;
|
|
1250
|
+
import com.example.accounting.domain.model.journal.JournalDetail;
|
|
1251
|
+
import com.example.accounting.domain.model.journal.JournalDebitCreditDetail;
|
|
1252
|
+
import com.example.accounting.domain.model.account.Account;
|
|
1253
|
+
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
1254
|
+
import graphql.schema.DataFetchingEnvironment;
|
|
1255
|
+
import org.dataloader.DataLoader;
|
|
1256
|
+
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1257
|
+
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
|
1258
|
+
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
|
1259
|
+
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
|
1260
|
+
import org.springframework.stereotype.Controller;
|
|
1261
|
+
|
|
1262
|
+
import java.math.BigDecimal;
|
|
1263
|
+
import java.time.LocalDate;
|
|
1264
|
+
import java.util.List;
|
|
1265
|
+
import java.util.concurrent.CompletableFuture;
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* 仕訳 GraphQL リゾルバ
|
|
1269
|
+
*/
|
|
1270
|
+
@Controller
|
|
1271
|
+
public class JournalResolver {
|
|
1272
|
+
|
|
1273
|
+
private final JournalUseCase journalUseCase;
|
|
1274
|
+
|
|
1275
|
+
public JournalResolver(JournalUseCase journalUseCase) {
|
|
1276
|
+
this.journalUseCase = journalUseCase;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// === Query ===
|
|
1280
|
+
|
|
1281
|
+
@QueryMapping
|
|
1282
|
+
public Journal journal(@Argument String journalNumber) {
|
|
1283
|
+
return journalUseCase.findByNumber(journalNumber).orElse(null);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
@QueryMapping
|
|
1287
|
+
public JournalConnection journals(
|
|
1288
|
+
@Argument LocalDate fromDate,
|
|
1289
|
+
@Argument LocalDate toDate,
|
|
1290
|
+
@Argument String accountCode,
|
|
1291
|
+
@Argument Integer page,
|
|
1292
|
+
@Argument Integer size) {
|
|
1293
|
+
|
|
1294
|
+
int pageNum = page != null ? page : 0;
|
|
1295
|
+
int pageSize = size != null ? size : 20;
|
|
1296
|
+
|
|
1297
|
+
List<Journal> journals = journalUseCase.findAll(
|
|
1298
|
+
fromDate, toDate, accountCode, pageNum, pageSize);
|
|
1299
|
+
long totalCount = journalUseCase.count(fromDate, toDate, accountCode);
|
|
1300
|
+
|
|
1301
|
+
return JournalConnection.of(journals, pageNum, pageSize, totalCount);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// === Mutation ===
|
|
1305
|
+
|
|
1306
|
+
@MutationMapping
|
|
1307
|
+
public Journal createJournal(@Argument CreateJournalInput input) {
|
|
1308
|
+
Journal journal = Journal.builder()
|
|
1309
|
+
.postingDate(input.postingDate())
|
|
1310
|
+
.entryDate(LocalDate.now())
|
|
1311
|
+
.isClosingEntry(input.isClosingEntry() != null ? input.isClosingEntry() : false)
|
|
1312
|
+
.voucherType(input.voucherType())
|
|
1313
|
+
.employeeCode(input.employeeCode())
|
|
1314
|
+
.departmentCode(input.departmentCode())
|
|
1315
|
+
.details(convertToDetails(input.details()))
|
|
1316
|
+
.build();
|
|
1317
|
+
|
|
1318
|
+
// 貸借バランスチェック
|
|
1319
|
+
validateBalance(journal);
|
|
1320
|
+
|
|
1321
|
+
return journalUseCase.create(journal);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
@MutationMapping
|
|
1325
|
+
public boolean deleteJournal(@Argument String journalNumber) {
|
|
1326
|
+
return journalUseCase.delete(journalNumber);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
@MutationMapping
|
|
1330
|
+
public Journal reverseJournal(
|
|
1331
|
+
@Argument String journalNumber,
|
|
1332
|
+
@Argument String reason) {
|
|
1333
|
+
return journalUseCase.reverse(journalNumber, reason);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// === フィールドリゾルバ ===
|
|
1337
|
+
|
|
1338
|
+
@SchemaMapping(typeName = "Journal", field = "totalDebitAmount")
|
|
1339
|
+
public BigDecimal totalDebitAmount(Journal journal) {
|
|
1340
|
+
return journal.getDetails().stream()
|
|
1341
|
+
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1342
|
+
.filter(dc -> dc.getDebitCreditCategory().isDebit())
|
|
1343
|
+
.map(JournalDebitCreditDetail::getAmount)
|
|
1344
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
@SchemaMapping(typeName = "Journal", field = "totalCreditAmount")
|
|
1348
|
+
public BigDecimal totalCreditAmount(Journal journal) {
|
|
1349
|
+
return journal.getDetails().stream()
|
|
1350
|
+
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1351
|
+
.filter(dc -> dc.getDebitCreditCategory().isCredit())
|
|
1352
|
+
.map(JournalDebitCreditDetail::getAmount)
|
|
1353
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
@SchemaMapping(typeName = "Journal", field = "isBalanced")
|
|
1357
|
+
public boolean isBalanced(Journal journal) {
|
|
1358
|
+
BigDecimal debit = totalDebitAmount(journal);
|
|
1359
|
+
BigDecimal credit = totalCreditAmount(journal);
|
|
1360
|
+
return debit.compareTo(credit) == 0;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
@SchemaMapping(typeName = "JournalDebitCreditDetail", field = "account")
|
|
1364
|
+
public CompletableFuture<Account> account(
|
|
1365
|
+
JournalDebitCreditDetail detail,
|
|
1366
|
+
DataFetchingEnvironment env) {
|
|
1367
|
+
|
|
1368
|
+
DataLoader<String, Account> loader =
|
|
1369
|
+
env.getDataLoader(DataLoaderConfig.ACCOUNT_LOADER);
|
|
1370
|
+
|
|
1371
|
+
return loader.load(detail.getAccountCode());
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// === Private Methods ===
|
|
1375
|
+
|
|
1376
|
+
private void validateBalance(Journal journal) {
|
|
1377
|
+
BigDecimal debit = journal.getDetails().stream()
|
|
1378
|
+
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1379
|
+
.filter(dc -> dc.getDebitCreditCategory().isDebit())
|
|
1380
|
+
.map(JournalDebitCreditDetail::getAmount)
|
|
1381
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1382
|
+
|
|
1383
|
+
BigDecimal credit = journal.getDetails().stream()
|
|
1384
|
+
.flatMap(d -> d.getDebitCreditDetails().stream())
|
|
1385
|
+
.filter(dc -> dc.getDebitCreditCategory().isCredit())
|
|
1386
|
+
.map(JournalDebitCreditDetail::getAmount)
|
|
1387
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
1388
|
+
|
|
1389
|
+
if (debit.compareTo(credit) != 0) {
|
|
1390
|
+
throw new IllegalArgumentException(
|
|
1391
|
+
"貸借が一致しません。借方合計: " + debit + ", 貸方合計: " + credit);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
</details>
|
|
1398
|
+
|
|
1399
|
+
### 25.2 GraphQL Subscription の実装
|
|
1400
|
+
|
|
1401
|
+
GraphQL **Subscription** は、サーバーからクライアントへのリアルタイム通知を実現する仕組みです。
|
|
1402
|
+
|
|
1403
|
+
```plantuml
|
|
1404
|
+
@startuml subscription_flow
|
|
1405
|
+
|
|
1406
|
+
title GraphQL Subscription フロー
|
|
1407
|
+
|
|
1408
|
+
participant "Client" as client
|
|
1409
|
+
participant "WebSocket" as ws
|
|
1410
|
+
participant "GraphQL Server" as server
|
|
1411
|
+
participant "EventPublisher" as publisher
|
|
1412
|
+
participant "JournalService" as service
|
|
1413
|
+
|
|
1414
|
+
== Subscription 開始 ==
|
|
1415
|
+
client -> ws : subscription { balanceChanged }
|
|
1416
|
+
ws -> server : WebSocket 接続確立
|
|
1417
|
+
server -> publisher : 購読登録
|
|
1418
|
+
|
|
1419
|
+
== 仕訳登録時 ==
|
|
1420
|
+
service -> publisher : 仕訳登録イベント発行
|
|
1421
|
+
publisher -> server : イベント受信
|
|
1422
|
+
server -> ws : BalanceChange データ送信
|
|
1423
|
+
ws -> client : リアルタイム通知
|
|
1424
|
+
|
|
1425
|
+
== Subscription 終了 ==
|
|
1426
|
+
client -> ws : 接続終了
|
|
1427
|
+
ws -> server : WebSocket 切断
|
|
1428
|
+
server -> publisher : 購読解除
|
|
1429
|
+
|
|
1430
|
+
@enduml
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
<details>
|
|
1434
|
+
<summary>SubscriptionResolver.java</summary>
|
|
1435
|
+
|
|
1436
|
+
```java
|
|
1437
|
+
package com.example.accounting.infrastructure.graphql.subscription;
|
|
1438
|
+
|
|
1439
|
+
import com.example.accounting.domain.event.BalanceChangeEvent;
|
|
1440
|
+
import com.example.accounting.domain.event.JournalCreatedEvent;
|
|
1441
|
+
import com.example.accounting.domain.event.ClosingProgressEvent;
|
|
1442
|
+
import com.example.accounting.infrastructure.graphql.dto.*;
|
|
1443
|
+
import org.springframework.graphql.data.method.annotation.Argument;
|
|
1444
|
+
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
|
1445
|
+
import org.springframework.stereotype.Controller;
|
|
1446
|
+
import reactor.core.publisher.Flux;
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* GraphQL Subscription リゾルバ
|
|
1450
|
+
*/
|
|
1451
|
+
@Controller
|
|
1452
|
+
public class SubscriptionResolver {
|
|
1453
|
+
|
|
1454
|
+
private final BalanceEventPublisher balanceEventPublisher;
|
|
1455
|
+
private final JournalEventPublisher journalEventPublisher;
|
|
1456
|
+
private final ClosingEventPublisher closingEventPublisher;
|
|
1457
|
+
|
|
1458
|
+
public SubscriptionResolver(
|
|
1459
|
+
BalanceEventPublisher balanceEventPublisher,
|
|
1460
|
+
JournalEventPublisher journalEventPublisher,
|
|
1461
|
+
ClosingEventPublisher closingEventPublisher) {
|
|
1462
|
+
this.balanceEventPublisher = balanceEventPublisher;
|
|
1463
|
+
this.journalEventPublisher = journalEventPublisher;
|
|
1464
|
+
this.closingEventPublisher = closingEventPublisher;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
@SubscriptionMapping
|
|
1468
|
+
public Flux<BalanceChange> balanceChanged(@Argument String accountCode) {
|
|
1469
|
+
Flux<BalanceChangeEvent> eventFlux = balanceEventPublisher.getEventFlux();
|
|
1470
|
+
|
|
1471
|
+
if (accountCode != null) {
|
|
1472
|
+
eventFlux = eventFlux.filter(event ->
|
|
1473
|
+
accountCode.equals(event.getAccountCode()));
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return eventFlux.map(this::toBalanceChange);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
@SubscriptionMapping
|
|
1480
|
+
public Flux<JournalNotification> journalCreated() {
|
|
1481
|
+
return journalEventPublisher.getEventFlux()
|
|
1482
|
+
.map(this::toJournalNotification);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
@SubscriptionMapping
|
|
1486
|
+
public Flux<ClosingProgress> closingProgressUpdated(@Argument String closingId) {
|
|
1487
|
+
return closingEventPublisher.getEventFlux()
|
|
1488
|
+
.filter(event -> closingId.equals(event.getClosingId()))
|
|
1489
|
+
.map(this::toClosingProgress);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
private BalanceChange toBalanceChange(BalanceChangeEvent event) {
|
|
1493
|
+
return new BalanceChange(
|
|
1494
|
+
event.getAccountCode(),
|
|
1495
|
+
event.getAccountName(),
|
|
1496
|
+
event.getChangeType(),
|
|
1497
|
+
event.getPreviousBalance(),
|
|
1498
|
+
event.getCurrentBalance(),
|
|
1499
|
+
event.getChangeAmount(),
|
|
1500
|
+
event.getTimestamp()
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
private JournalNotification toJournalNotification(JournalCreatedEvent event) {
|
|
1505
|
+
return new JournalNotification(
|
|
1506
|
+
event.getJournalNumber(),
|
|
1507
|
+
event.getPostingDate(),
|
|
1508
|
+
event.getTotalDebitAmount(),
|
|
1509
|
+
event.getTotalCreditAmount(),
|
|
1510
|
+
event.getCreatedBy(),
|
|
1511
|
+
event.getCreatedAt()
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
private ClosingProgress toClosingProgress(ClosingProgressEvent event) {
|
|
1516
|
+
return new ClosingProgress(
|
|
1517
|
+
event.getClosingId(),
|
|
1518
|
+
event.getStatus(),
|
|
1519
|
+
event.getCurrentStep(),
|
|
1520
|
+
event.getProcessedCount(),
|
|
1521
|
+
event.getTotalCount(),
|
|
1522
|
+
event.getProgressPercent(),
|
|
1523
|
+
event.getUpdatedAt()
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
</details>
|
|
1530
|
+
|
|
1531
|
+
<details>
|
|
1532
|
+
<summary>BalanceEventPublisher.java</summary>
|
|
1533
|
+
|
|
1534
|
+
```java
|
|
1535
|
+
package com.example.accounting.infrastructure.graphql.subscription;
|
|
1536
|
+
|
|
1537
|
+
import com.example.accounting.domain.event.BalanceChangeEvent;
|
|
1538
|
+
import org.springframework.stereotype.Component;
|
|
1539
|
+
import reactor.core.publisher.Flux;
|
|
1540
|
+
import reactor.core.publisher.Sinks;
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* 残高変動イベントパブリッシャー
|
|
1544
|
+
*/
|
|
1545
|
+
@Component
|
|
1546
|
+
public class BalanceEventPublisher {
|
|
1547
|
+
|
|
1548
|
+
private final Sinks.Many<BalanceChangeEvent> sink;
|
|
1549
|
+
|
|
1550
|
+
public BalanceEventPublisher() {
|
|
1551
|
+
this.sink = Sinks.many().multicast().onBackpressureBuffer();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
public void publish(BalanceChangeEvent event) {
|
|
1555
|
+
sink.tryEmitNext(event);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
public Flux<BalanceChangeEvent> getEventFlux() {
|
|
1559
|
+
return sink.asFlux();
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
</details>
|
|
1565
|
+
|
|
1566
|
+
---
|
|
1567
|
+
|
|
1568
|
+
## 第 26 章:エラーハンドリングとバリデーション
|
|
1569
|
+
|
|
1570
|
+
### 26.1 GraphQL エラー設計
|
|
1571
|
+
|
|
1572
|
+
GraphQL では、エラーは `errors` フィールドとして返されます。
|
|
1573
|
+
|
|
1574
|
+
```plantuml
|
|
1575
|
+
@startuml graphql_error_handling
|
|
1576
|
+
|
|
1577
|
+
title GraphQL エラーハンドリング
|
|
1578
|
+
|
|
1579
|
+
package "GraphQL Layer" {
|
|
1580
|
+
[GraphQL Error Handler]
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
package "Application Layer" {
|
|
1584
|
+
[Use Case Exception]
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
package "Domain Layer" {
|
|
1588
|
+
[Domain Exception]
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
[Domain Exception] --> [Use Case Exception] : 伝播
|
|
1592
|
+
[Use Case Exception] --> [GraphQL Error Handler] : 伝播
|
|
1593
|
+
|
|
1594
|
+
note right of [Domain Exception]
|
|
1595
|
+
ビジネスルール違反
|
|
1596
|
+
- 貸借不一致
|
|
1597
|
+
- 科目未登録
|
|
1598
|
+
- 決算済み期間
|
|
1599
|
+
end note
|
|
1600
|
+
|
|
1601
|
+
note right of [Use Case Exception]
|
|
1602
|
+
ユースケース固有
|
|
1603
|
+
- リソース未発見
|
|
1604
|
+
- バリデーションエラー
|
|
1605
|
+
- 権限不足
|
|
1606
|
+
end note
|
|
1607
|
+
|
|
1608
|
+
note right of [GraphQL Error Handler]
|
|
1609
|
+
GraphQL エラー形式に変換
|
|
1610
|
+
- エラーコード
|
|
1611
|
+
- エラーメッセージ
|
|
1612
|
+
- エラー詳細
|
|
1613
|
+
end note
|
|
1614
|
+
|
|
1615
|
+
@enduml
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
### 26.2 カスタム例外クラス
|
|
1619
|
+
|
|
1620
|
+
<details>
|
|
1621
|
+
<summary>AccountingException.java(基底クラス)</summary>
|
|
1622
|
+
|
|
1623
|
+
```java
|
|
1624
|
+
package com.example.accounting.domain.exception;
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* 財務会計ドメイン例外の基底クラス
|
|
1628
|
+
*/
|
|
1629
|
+
public abstract class AccountingException extends RuntimeException {
|
|
1630
|
+
|
|
1631
|
+
private final String errorCode;
|
|
1632
|
+
|
|
1633
|
+
protected AccountingException(String errorCode, String message) {
|
|
1634
|
+
super(message);
|
|
1635
|
+
this.errorCode = errorCode;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
protected AccountingException(String errorCode, String message, Throwable cause) {
|
|
1639
|
+
super(message, cause);
|
|
1640
|
+
this.errorCode = errorCode;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
public String getErrorCode() {
|
|
1644
|
+
return errorCode;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
```
|
|
1648
|
+
|
|
1649
|
+
</details>
|
|
1650
|
+
|
|
1651
|
+
<details>
|
|
1652
|
+
<summary>BalanceNotMatchException.java</summary>
|
|
1653
|
+
|
|
1654
|
+
```java
|
|
1655
|
+
package com.example.accounting.domain.exception;
|
|
1656
|
+
|
|
1657
|
+
import java.math.BigDecimal;
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* 貸借不一致例外
|
|
1661
|
+
*/
|
|
1662
|
+
public class BalanceNotMatchException extends AccountingException {
|
|
1663
|
+
|
|
1664
|
+
private final BigDecimal debitTotal;
|
|
1665
|
+
private final BigDecimal creditTotal;
|
|
1666
|
+
|
|
1667
|
+
public BalanceNotMatchException(BigDecimal debitTotal, BigDecimal creditTotal) {
|
|
1668
|
+
super("BALANCE_NOT_MATCH",
|
|
1669
|
+
String.format("貸借が一致しません。借方合計: %s, 貸方合計: %s", debitTotal, creditTotal));
|
|
1670
|
+
this.debitTotal = debitTotal;
|
|
1671
|
+
this.creditTotal = creditTotal;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
public BigDecimal getDebitTotal() {
|
|
1675
|
+
return debitTotal;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
public BigDecimal getCreditTotal() {
|
|
1679
|
+
return creditTotal;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
</details>
|
|
1685
|
+
|
|
1686
|
+
### 26.3 GraphQL 例外ハンドラー
|
|
1687
|
+
|
|
1688
|
+
<details>
|
|
1689
|
+
<summary>GraphQLExceptionHandler.java</summary>
|
|
1690
|
+
|
|
1691
|
+
```java
|
|
1692
|
+
package com.example.accounting.infrastructure.graphql.exception;
|
|
1693
|
+
|
|
1694
|
+
import com.example.accounting.domain.exception.*;
|
|
1695
|
+
import graphql.GraphQLError;
|
|
1696
|
+
import graphql.GraphqlErrorBuilder;
|
|
1697
|
+
import graphql.schema.DataFetchingEnvironment;
|
|
1698
|
+
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
|
|
1699
|
+
import org.springframework.graphql.execution.ErrorType;
|
|
1700
|
+
import org.springframework.stereotype.Component;
|
|
1701
|
+
|
|
1702
|
+
import java.util.HashMap;
|
|
1703
|
+
import java.util.Map;
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* GraphQL 例外ハンドラー
|
|
1707
|
+
*/
|
|
1708
|
+
@Component
|
|
1709
|
+
public class GraphQLExceptionHandler extends DataFetcherExceptionResolverAdapter {
|
|
1710
|
+
|
|
1711
|
+
@Override
|
|
1712
|
+
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
|
|
1713
|
+
if (ex instanceof AccountingException accountingEx) {
|
|
1714
|
+
return handleAccountingException(accountingEx, env);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (ex instanceof IllegalArgumentException) {
|
|
1718
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1719
|
+
.errorType(ErrorType.BAD_REQUEST)
|
|
1720
|
+
.message(ex.getMessage())
|
|
1721
|
+
.build();
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// その他の例外
|
|
1725
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1726
|
+
.errorType(ErrorType.INTERNAL_ERROR)
|
|
1727
|
+
.message("内部エラーが発生しました")
|
|
1728
|
+
.build();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
private GraphQLError handleAccountingException(
|
|
1732
|
+
AccountingException ex, DataFetchingEnvironment env) {
|
|
1733
|
+
|
|
1734
|
+
Map<String, Object> extensions = new HashMap<>();
|
|
1735
|
+
extensions.put("errorCode", ex.getErrorCode());
|
|
1736
|
+
|
|
1737
|
+
ErrorType errorType = mapToErrorType(ex);
|
|
1738
|
+
|
|
1739
|
+
// 例外固有の情報を追加
|
|
1740
|
+
if (ex instanceof BalanceNotMatchException balanceEx) {
|
|
1741
|
+
extensions.put("debitTotal", balanceEx.getDebitTotal());
|
|
1742
|
+
extensions.put("creditTotal", balanceEx.getCreditTotal());
|
|
1743
|
+
} else if (ex instanceof AccountNotFoundException accountEx) {
|
|
1744
|
+
extensions.put("accountCode", accountEx.getAccountCode());
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return GraphqlErrorBuilder.newError(env)
|
|
1748
|
+
.errorType(errorType)
|
|
1749
|
+
.message(ex.getMessage())
|
|
1750
|
+
.extensions(extensions)
|
|
1751
|
+
.build();
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
private ErrorType mapToErrorType(AccountingException ex) {
|
|
1755
|
+
if (ex instanceof AccountNotFoundException ||
|
|
1756
|
+
ex instanceof JournalNotFoundException) {
|
|
1757
|
+
return ErrorType.NOT_FOUND;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (ex instanceof BalanceNotMatchException ||
|
|
1761
|
+
ex instanceof ClosedPeriodException) {
|
|
1762
|
+
return ErrorType.BAD_REQUEST;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
return ErrorType.INTERNAL_ERROR;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
</details>
|
|
1771
|
+
|
|
1772
|
+
### 26.4 Spring for GraphQL テスト
|
|
1773
|
+
|
|
1774
|
+
<details>
|
|
1775
|
+
<summary>AccountResolverTest.java</summary>
|
|
1776
|
+
|
|
1777
|
+
```java
|
|
1778
|
+
package com.example.accounting.infrastructure.graphql;
|
|
1779
|
+
|
|
1780
|
+
import org.junit.jupiter.api.Test;
|
|
1781
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
1782
|
+
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
|
|
1783
|
+
import org.springframework.boot.test.context.SpringBootTest;
|
|
1784
|
+
import org.springframework.graphql.test.tester.HttpGraphQlTester;
|
|
1785
|
+
import org.springframework.test.context.DynamicPropertyRegistry;
|
|
1786
|
+
import org.springframework.test.context.DynamicPropertySource;
|
|
1787
|
+
import org.testcontainers.containers.PostgreSQLContainer;
|
|
1788
|
+
import org.testcontainers.junit.jupiter.Container;
|
|
1789
|
+
import org.testcontainers.junit.jupiter.Testcontainers;
|
|
1790
|
+
|
|
1791
|
+
import static org.assertj.core.api.Assertions.assertThat;
|
|
1792
|
+
|
|
1793
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1794
|
+
@AutoConfigureHttpGraphQlTester
|
|
1795
|
+
@Testcontainers
|
|
1796
|
+
class AccountResolverTest {
|
|
1797
|
+
|
|
1798
|
+
@Container
|
|
1799
|
+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
|
|
1800
|
+
|
|
1801
|
+
@DynamicPropertySource
|
|
1802
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
1803
|
+
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
1804
|
+
registry.add("spring.datasource.username", postgres::getUsername);
|
|
1805
|
+
registry.add("spring.datasource.password", postgres::getPassword);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
@Autowired
|
|
1809
|
+
private HttpGraphQlTester graphQlTester;
|
|
1810
|
+
|
|
1811
|
+
@Test
|
|
1812
|
+
void testQueryAccount() {
|
|
1813
|
+
// language=GraphQL
|
|
1814
|
+
String query = """
|
|
1815
|
+
query {
|
|
1816
|
+
account(accountCode: "11110") {
|
|
1817
|
+
accountCode
|
|
1818
|
+
accountName
|
|
1819
|
+
bsplCategory
|
|
1820
|
+
debitCreditCategory
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
""";
|
|
1824
|
+
|
|
1825
|
+
graphQlTester.document(query)
|
|
1826
|
+
.execute()
|
|
1827
|
+
.path("account.accountCode")
|
|
1828
|
+
.entity(String.class)
|
|
1829
|
+
.isEqualTo("11110");
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
@Test
|
|
1833
|
+
void testMutationCreateAccount() {
|
|
1834
|
+
// language=GraphQL
|
|
1835
|
+
String mutation = """
|
|
1836
|
+
mutation {
|
|
1837
|
+
createAccount(input: {
|
|
1838
|
+
accountCode: "99999"
|
|
1839
|
+
accountName: "テスト科目"
|
|
1840
|
+
bsplCategory: BS
|
|
1841
|
+
debitCreditCategory: DEBIT
|
|
1842
|
+
transactionElementCategory: ASSET
|
|
1843
|
+
aggregationCategory: POSTING
|
|
1844
|
+
}) {
|
|
1845
|
+
accountCode
|
|
1846
|
+
accountName
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
""";
|
|
1850
|
+
|
|
1851
|
+
graphQlTester.document(mutation)
|
|
1852
|
+
.execute()
|
|
1853
|
+
.path("createAccount.accountCode")
|
|
1854
|
+
.entity(String.class)
|
|
1855
|
+
.isEqualTo("99999");
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
</details>
|
|
1861
|
+
|
|
1862
|
+
### 26.5 GraphiQL による動作確認
|
|
1863
|
+
|
|
1864
|
+
サーバー起動後、`http://localhost:8080/graphiql` にアクセスして対話的にクエリを実行できます。
|
|
1865
|
+
|
|
1866
|
+
```graphql
|
|
1867
|
+
# 勘定科目の取得
|
|
1868
|
+
query {
|
|
1869
|
+
account(accountCode: "11110") {
|
|
1870
|
+
accountCode
|
|
1871
|
+
accountName
|
|
1872
|
+
bsplCategory
|
|
1873
|
+
debitCreditCategory
|
|
1874
|
+
taxTransaction {
|
|
1875
|
+
taxCode
|
|
1876
|
+
taxName
|
|
1877
|
+
taxRate
|
|
1878
|
+
}
|
|
1879
|
+
dailyBalances {
|
|
1880
|
+
postingDate
|
|
1881
|
+
debitAmount
|
|
1882
|
+
creditAmount
|
|
1883
|
+
balance
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
# 仕訳登録
|
|
1889
|
+
mutation {
|
|
1890
|
+
createJournal(input: {
|
|
1891
|
+
postingDate: "2025-12-29"
|
|
1892
|
+
voucherType: NORMAL
|
|
1893
|
+
details: [
|
|
1894
|
+
{
|
|
1895
|
+
lineNumber: 1
|
|
1896
|
+
description: "現金売上"
|
|
1897
|
+
debitCreditDetails: [
|
|
1898
|
+
{
|
|
1899
|
+
debitCreditCategory: DEBIT
|
|
1900
|
+
accountCode: "11110"
|
|
1901
|
+
amount: "100000"
|
|
1902
|
+
}
|
|
1903
|
+
{
|
|
1904
|
+
debitCreditCategory: CREDIT
|
|
1905
|
+
accountCode: "41100"
|
|
1906
|
+
amount: "100000"
|
|
1907
|
+
}
|
|
1908
|
+
]
|
|
1909
|
+
}
|
|
1910
|
+
]
|
|
1911
|
+
}) {
|
|
1912
|
+
journalNumber
|
|
1913
|
+
postingDate
|
|
1914
|
+
totalDebitAmount
|
|
1915
|
+
totalCreditAmount
|
|
1916
|
+
isBalanced
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
# 試算表
|
|
1921
|
+
query {
|
|
1922
|
+
trialBalance(fiscalYear: 2025, month: 12) {
|
|
1923
|
+
fiscalYear
|
|
1924
|
+
month
|
|
1925
|
+
entries {
|
|
1926
|
+
accountCode
|
|
1927
|
+
accountName
|
|
1928
|
+
bsplCategory
|
|
1929
|
+
debitAmount
|
|
1930
|
+
creditAmount
|
|
1931
|
+
debitBalance
|
|
1932
|
+
creditBalance
|
|
1933
|
+
}
|
|
1934
|
+
totalDebitAmount
|
|
1935
|
+
totalCreditAmount
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
---
|
|
1941
|
+
|
|
1942
|
+
## 第 27 章:まとめと発展
|
|
1943
|
+
|
|
1944
|
+
### 27.1 GraphQL 実装の振り返り
|
|
1945
|
+
|
|
1946
|
+
```plantuml
|
|
1947
|
+
@startuml graphql_summary
|
|
1948
|
+
|
|
1949
|
+
title 財務会計システム GraphQL 実装全体像
|
|
1950
|
+
|
|
1951
|
+
package "GraphQL Schema" {
|
|
1952
|
+
[schema.graphqls]
|
|
1953
|
+
[account.graphqls]
|
|
1954
|
+
[journal.graphqls]
|
|
1955
|
+
[balance.graphqls]
|
|
1956
|
+
[report.graphqls]
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
package "Resolvers" {
|
|
1960
|
+
[QueryResolver]
|
|
1961
|
+
[AccountResolver]
|
|
1962
|
+
[JournalResolver]
|
|
1963
|
+
[BalanceResolver]
|
|
1964
|
+
[ReportResolver]
|
|
1965
|
+
[SubscriptionResolver]
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
package "DataLoaders" {
|
|
1969
|
+
[TaxTransactionDataLoader]
|
|
1970
|
+
[DailyBalanceDataLoader]
|
|
1971
|
+
[MonthlyBalanceDataLoader]
|
|
1972
|
+
[AccountConfigDataLoader]
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
package "Application Layer" {
|
|
1976
|
+
[AccountUseCase]
|
|
1977
|
+
[JournalUseCase]
|
|
1978
|
+
[BalanceUseCase]
|
|
1979
|
+
[ReportUseCase]
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
package "Infrastructure" {
|
|
1983
|
+
[MyBatis Repository]
|
|
1984
|
+
[Event Publisher]
|
|
1985
|
+
[PostgreSQL]
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
[schema.graphqls] --> [QueryResolver]
|
|
1989
|
+
[QueryResolver] --> [AccountUseCase]
|
|
1990
|
+
[AccountResolver] --> [TaxTransactionDataLoader]
|
|
1991
|
+
[AccountResolver] --> [DailyBalanceDataLoader]
|
|
1992
|
+
[JournalResolver] --> [JournalUseCase]
|
|
1993
|
+
[SubscriptionResolver] --> [Event Publisher]
|
|
1994
|
+
|
|
1995
|
+
[AccountUseCase] --> [MyBatis Repository]
|
|
1996
|
+
[MyBatis Repository] --> [PostgreSQL]
|
|
1997
|
+
|
|
1998
|
+
@enduml
|
|
1999
|
+
```
|
|
2000
|
+
|
|
2001
|
+
### 27.2 REST / gRPC / GraphQL の使い分け
|
|
2002
|
+
|
|
2003
|
+
| 観点 | REST API | gRPC | GraphQL |
|
|
2004
|
+
|------|----------|------|---------|
|
|
2005
|
+
| **クライアント** | Web ブラウザ、外部連携 | マイクロサービス間 | SPA、モバイルアプリ |
|
|
2006
|
+
| **データ量** | 固定レスポンス | 固定レスポンス | クライアント指定 |
|
|
2007
|
+
| **リアルタイム** | WebSocket 別実装 | ストリーミング | Subscription |
|
|
2008
|
+
| **型安全性** | △(OpenAPI) | ◎(.proto) | ◎(.graphqls) |
|
|
2009
|
+
| **学習コスト** | 低 | 中 | 中 |
|
|
2010
|
+
| **ブラウザ対応** | ◎ | △(gRPC-Web) | ◎ |
|
|
2011
|
+
|
|
2012
|
+
```plantuml
|
|
2013
|
+
@startuml api_usage
|
|
2014
|
+
|
|
2015
|
+
title API 使い分けパターン
|
|
2016
|
+
|
|
2017
|
+
package "フロントエンド" {
|
|
2018
|
+
[経理担当者 Web UI] as web
|
|
2019
|
+
[経営ダッシュボード] as dashboard
|
|
2020
|
+
[Excel アドイン] as excel
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
package "バックエンド" {
|
|
2024
|
+
[財務会計システム] as accounting
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
package "連携システム" {
|
|
2028
|
+
[販売管理システム] as sales
|
|
2029
|
+
[購買管理システム] as purchase
|
|
2030
|
+
[人事給与システム] as hr
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
web --> accounting : GraphQL\n(柔軟なクエリ)
|
|
2034
|
+
dashboard --> accounting : GraphQL\n(リアルタイム更新)
|
|
2035
|
+
excel --> accounting : REST API\n(シンプルな連携)
|
|
2036
|
+
|
|
2037
|
+
sales --> accounting : gRPC\n(高性能・自動仕訳)
|
|
2038
|
+
purchase --> accounting : gRPC\n(高性能・自動仕訳)
|
|
2039
|
+
hr --> accounting : gRPC\n(給与仕訳連携)
|
|
2040
|
+
|
|
2041
|
+
@enduml
|
|
2042
|
+
```
|
|
2043
|
+
|
|
2044
|
+
### 27.3 ヘキサゴナルアーキテクチャの利点
|
|
2045
|
+
|
|
2046
|
+
複数の API 形式を同時提供できるのは、ヘキサゴナルアーキテクチャの恩恵です。
|
|
2047
|
+
|
|
2048
|
+
```plantuml
|
|
2049
|
+
@startuml hexagonal_benefit
|
|
2050
|
+
|
|
2051
|
+
title 複数 API の同時提供
|
|
2052
|
+
|
|
2053
|
+
package "Input Adapters" {
|
|
2054
|
+
[REST Controller]
|
|
2055
|
+
[gRPC Service]
|
|
2056
|
+
[GraphQL Resolver]
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
package "Application Core" {
|
|
2060
|
+
[AccountUseCase]
|
|
2061
|
+
[JournalUseCase]
|
|
2062
|
+
[BalanceUseCase]
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
package "Output Adapters" {
|
|
2066
|
+
[MyBatis Repository]
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
[REST Controller] --> [AccountUseCase]
|
|
2070
|
+
[gRPC Service] --> [AccountUseCase]
|
|
2071
|
+
[GraphQL Resolver] --> [AccountUseCase]
|
|
2072
|
+
|
|
2073
|
+
[REST Controller] --> [JournalUseCase]
|
|
2074
|
+
[gRPC Service] --> [JournalUseCase]
|
|
2075
|
+
[GraphQL Resolver] --> [JournalUseCase]
|
|
2076
|
+
|
|
2077
|
+
note top of [Application Core]
|
|
2078
|
+
ビジネスロジックは
|
|
2079
|
+
API 形式に依存しない
|
|
2080
|
+
|
|
2081
|
+
1つのコードベースで
|
|
2082
|
+
複数の API を提供
|
|
2083
|
+
end note
|
|
2084
|
+
|
|
2085
|
+
@enduml
|
|
2086
|
+
```
|
|
2087
|
+
|
|
2088
|
+
### 27.4 今後の発展
|
|
2089
|
+
|
|
2090
|
+
本パートで構築した GraphQL サービスをベースに、以下の拡張が可能です:
|
|
2091
|
+
|
|
2092
|
+
1. **認証・認可**: Spring Security + JWT による GraphQL 認証
|
|
2093
|
+
2. **キャッシュ**: Redis によるクエリ結果キャッシュ
|
|
2094
|
+
3. **監視**: GraphQL メトリクスの収集と監視
|
|
2095
|
+
4. **フェデレーション**: Apollo Federation による複数サービス統合
|
|
2096
|
+
5. **コード生成**: GraphQL Code Generator によるクライアントコード自動生成
|
|
2097
|
+
|
|
2098
|
+
### 27.5 学習した内容
|
|
2099
|
+
|
|
2100
|
+
1. **GraphQL の基本概念**: Query、Mutation、Subscription の 3 つの操作タイプ
|
|
2101
|
+
2. **スキーマ駆動開発**: `.graphqls` ファイルによる型安全な API 定義
|
|
2102
|
+
3. **N+1 問題と DataLoader**: バッチ処理によるパフォーマンス最適化
|
|
2103
|
+
4. **リゾルバ実装**: `@QueryMapping`、`@MutationMapping`、`@SchemaMapping`
|
|
2104
|
+
5. **Subscription**: WebSocket によるリアルタイム通知
|
|
2105
|
+
6. **エラーハンドリング**: カスタム例外と GraphQL エラー形式への変換
|
|
2106
|
+
7. **テスト**: `HttpGraphQlTester` による統合テスト
|