aiwf 0.3.6 → 0.3.8
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/.aiwf/00_PROJECT_MANIFEST.md +226 -2
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/S03_M02_sprint_meta.md +112 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T01_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T02_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T03_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T04_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T05_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T06_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T07_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T08_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T09_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T100_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T101_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T102_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T103_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T104_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T105_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T106_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T107_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T108_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T109_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T10_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T110_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T111_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T112_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T11_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T12_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T13_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T14_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T15_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T16_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T17_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T18_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T19_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T20_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T21_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T22_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T23_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T24_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T25_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T26_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T27_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T28_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T29_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T30_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T31_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T32_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T33_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T34_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T35_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T36_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T37_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T38_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T39_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T40_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T41_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T42_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T43_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T44_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T45_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T46_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T47_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T48_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T49_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T50_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T51_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T52_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T53_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T54_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T55_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T56_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T57_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T58_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T59_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T60_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T61_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T62_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T63_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T64_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T65_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T66_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T67_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T68_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T69_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T70_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T71_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T72_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T73_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T74_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T75_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T76_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T77_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T78_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T79_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T80_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T81_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T82_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T83_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T84_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T85_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T86_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T87_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T88_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T89_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T90_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T91_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T92_S03_task_with_special_characters.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T93_S03_test.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T94_S03_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T95_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T96_S03_next_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T97_S03_new_test_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T98_S03_first_task.md +39 -0
- package/.aiwf/03_SPRINTS/S03_M02_ecosystem_integration/T99_S03_test_task.md +39 -0
- package/.aiwf/test-results-s02.json +5 -5
- package/.claude/commands/aiwf/aiwf_compress_aggressive_persona.md +3 -15
- package/.claude/commands/aiwf/aiwf_compress_minimal_persona.md +2 -15
- package/.claude/commands/aiwf/aiwf_compress_stats.md +2 -51
- package/.claude/commands/aiwf/aiwf_compress_with_persona.md +5 -18
- package/.claude/commands/aiwf/aiwf_create_feature_ledger.md +1 -1
- package/.claude/commands/aiwf/aiwf_default_mode.md +1 -1
- package/.claude/commands/aiwf/aiwf_evaluate.md +3 -16
- package/.claude/commands/aiwf/aiwf_get_feature_details.md +1 -1
- package/.claude/commands/aiwf/aiwf_link_feature_to_milestone.md +1 -1
- package/.claude/commands/aiwf/aiwf_list_features.md +1 -1
- package/.claude/commands/aiwf/aiwf_switch_language.md +48 -120
- package/.claude/commands/aiwf/aiwf_update_feature_status.md +1 -1
- package/claude-code/aiwf/en/.claude/commands/aiwf/aiwf_create_testcase.md +561 -0
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_compress_aggressive_persona.md +2 -2
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_compress_minimal_persona.md +2 -2
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_compress_stats.md +10 -47
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_compress_with_persona.md +2 -2
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_create_feature_ledger.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_create_testcase.md +561 -0
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_default_mode.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_evaluate.md +2 -2
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_get_feature_details.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_link_feature_to_milestone.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_list_features.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_persona_architect.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_persona_backend.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_persona_data_analyst.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_persona_frontend.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_persona_security.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_persona_status.md +1 -1
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_update_feature_status.md +1 -1
- package/package.json +1 -1
- package/src/cli/index.js +174 -0
- package/src/commands/compress.js +60 -0
- package/src/commands/create-project.js +23 -17
- package/src/commands/evaluate.js +459 -0
- package/src/commands/feature.js +365 -0
- package/src/commands/persona.js +37 -16
- package/src/commands/token.js +312 -0
- package/src/lib/file-downloader.js +189 -33
- package/src/lib/installer.js +38 -11
- package/src/lib/resource-loader.js +250 -0
- package/src/lib/resources/commands/ai-persona.js +602 -0
- package/src/lib/resources/commands/compress-context.js +389 -0
- package/src/lib/resources/commands/evaluate.js +246 -0
- package/src/lib/resources/commands/feature-ledger.js +565 -0
- package/src/lib/resources/commands/feature_commit_report.js +370 -0
- package/src/lib/resources/commands/persona-context-apply.js +562 -0
- package/src/lib/resources/commands/scan_git_history.js +234 -0
- package/src/lib/resources/commands/sync_feature_commits.js +154 -0
- package/src/lib/resources/commands/token-tracking.js +443 -0
- package/src/lib/resources/config/commit-patterns.js +103 -0
- package/src/lib/resources/config/language.json +6 -0
- package/src/lib/resources/personas/PERSONA_INDEX.md +57 -0
- package/src/lib/resources/personas/analyst.json +44 -0
- package/src/lib/resources/personas/architect/best_practices.md +136 -0
- package/src/lib/resources/personas/architect/knowledge_base.md +377 -0
- package/src/lib/resources/personas/architect.json +44 -0
- package/src/lib/resources/personas/backend/best_practices.md +766 -0
- package/src/lib/resources/personas/backend/knowledge_base.md +1070 -0
- package/src/lib/resources/personas/data_analyst/best_practices.md +594 -0
- package/src/lib/resources/personas/data_analyst/knowledge_base.md +1057 -0
- package/src/lib/resources/personas/developer.json +44 -0
- package/src/lib/resources/personas/developer.md +37 -0
- package/src/lib/resources/personas/evaluation_criteria.json +137 -0
- package/src/lib/resources/personas/frontend/best_practices.md +445 -0
- package/src/lib/resources/personas/frontend/knowledge_base.md +729 -0
- package/src/lib/resources/personas/persona-index.json +31 -0
- package/src/lib/resources/personas/reviewer.json +44 -0
- package/src/lib/resources/personas/security/best_practices.md +307 -0
- package/src/lib/resources/personas/security/knowledge_base.md +498 -0
- package/src/lib/resources/personas/tester.json +44 -0
- package/src/lib/resources/templates/README.md +78 -0
- package/src/lib/resources/templates/api-server/config.json +64 -0
- package/src/lib/resources/templates/api-server/template/.aiwf/config.json +56 -0
- package/src/lib/resources/templates/api-server/template/.aiwf/feature-ledger.json +42 -0
- package/src/lib/resources/templates/api-server/template/.aiwf/personas/backend-engineer.json +40 -0
- package/src/lib/resources/templates/api-server/template/.aiwf/scripts/cli.js +111 -0
- package/src/lib/resources/templates/api-server/template/.env.example +29 -0
- package/src/lib/resources/templates/api-server/template/.eslintrc.json +23 -0
- package/src/lib/resources/templates/api-server/template/README.md +171 -0
- package/src/lib/resources/templates/api-server/template/jest.config.js +26 -0
- package/src/lib/resources/templates/api-server/template/nodemon.json +9 -0
- package/src/lib/resources/templates/api-server/template/package.json +61 -0
- package/src/lib/resources/templates/api-server/template/src/app.ts +60 -0
- package/src/lib/resources/templates/api-server/template/src/config/swagger.ts +38 -0
- package/src/lib/resources/templates/api-server/template/src/controllers/aiwfController.ts +68 -0
- package/src/lib/resources/templates/api-server/template/src/controllers/authController.ts +128 -0
- package/src/lib/resources/templates/api-server/template/src/controllers/statusController.ts +28 -0
- package/src/lib/resources/templates/api-server/template/src/index.ts +28 -0
- package/src/lib/resources/templates/api-server/template/src/middleware/aiwfMiddleware.ts +50 -0
- package/src/lib/resources/templates/api-server/template/src/middleware/authMiddleware.ts +41 -0
- package/src/lib/resources/templates/api-server/template/src/middleware/errorHandler.ts +42 -0
- package/src/lib/resources/templates/api-server/template/src/middleware/notFoundHandler.ts +14 -0
- package/src/lib/resources/templates/api-server/template/src/middleware/requestLogger.ts +20 -0
- package/src/lib/resources/templates/api-server/template/src/routes/aiwf.ts +42 -0
- package/src/lib/resources/templates/api-server/template/src/routes/index.ts +13 -0
- package/src/lib/resources/templates/api-server/template/src/routes/v1/index.ts +67 -0
- package/src/lib/resources/templates/api-server/template/src/utils/logger.ts +37 -0
- package/src/lib/resources/templates/api-server/template/tests/app.test.ts +40 -0
- package/src/lib/resources/templates/api-server/template/tsconfig.json +43 -0
- package/src/lib/resources/templates/npm-library/config.json +61 -0
- package/src/lib/resources/templates/npm-library/template/LICENSE +21 -0
- package/src/lib/resources/templates/npm-library/template/README.md +201 -0
- package/src/lib/resources/templates/npm-library/template/package.json +78 -0
- package/src/lib/resources/templates/web-app/config.json +53 -0
- package/src/lib/resources/templates/web-app/template/.aiwf/config.json +54 -0
- package/src/lib/resources/templates/web-app/template/.aiwf/feature-ledger.json +37 -0
- package/src/lib/resources/templates/web-app/template/.aiwf/personas/fullstack-developer.json +40 -0
- package/src/lib/resources/templates/web-app/template/.aiwf/scripts/cli.js +110 -0
- package/src/lib/resources/templates/web-app/template/.eslintrc.cjs +20 -0
- package/src/lib/resources/templates/web-app/template/README.md +151 -0
- package/src/lib/resources/templates/web-app/template/index.html +14 -0
- package/src/lib/resources/templates/web-app/template/package.json +44 -0
- package/src/lib/resources/templates/web-app/template/postcss.config.js +6 -0
- package/src/lib/resources/templates/web-app/template/public/vite.svg +1 -0
- package/src/lib/resources/templates/web-app/template/src/App.tsx +21 -0
- package/src/lib/resources/templates/web-app/template/src/components/Layout.tsx +57 -0
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/ContextStatus.tsx +107 -0
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/FeatureLedger.tsx +93 -0
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/TokenUsage.tsx +71 -0
- package/src/lib/resources/templates/web-app/template/src/index.css +60 -0
- package/src/lib/resources/templates/web-app/template/src/main.tsx +10 -0
- package/src/lib/resources/templates/web-app/template/src/pages/AiwfDashboard.tsx +60 -0
- package/src/lib/resources/templates/web-app/template/src/pages/HomePage.tsx +89 -0
- package/src/lib/resources/templates/web-app/template/src/pages/NotFound.tsx +25 -0
- package/src/lib/resources/templates/web-app/template/src/stores/aiwfStore.ts +126 -0
- package/src/lib/resources/templates/web-app/template/src/types/global.d.ts +9 -0
- package/src/lib/resources/templates/web-app/template/src/vite-env.d.ts +1 -0
- package/src/lib/resources/templates/web-app/template/tailwind.config.js +30 -0
- package/src/lib/resources/templates/web-app/template/tsconfig.json +37 -0
- package/src/lib/resources/templates/web-app/template/tsconfig.node.json +10 -0
- package/src/lib/resources/templates/web-app/template/vite.config.ts +23 -0
- package/src/lib/resources/utils/background-monitor.js +223 -0
- package/src/lib/resources/utils/compression-metrics.js +1031 -0
- package/src/lib/resources/utils/compression-strategies.js +557 -0
- package/src/lib/resources/utils/content-normalizer.js +455 -0
- package/src/lib/resources/utils/context-compressor.js +583 -0
- package/src/lib/resources/utils/context-rule-parser.js +180 -0
- package/src/lib/resources/utils/context-token-monitor.js +463 -0
- package/src/lib/resources/utils/context-update-manager.js +502 -0
- package/src/lib/resources/utils/feature-updater.js +271 -0
- package/src/lib/resources/utils/git-utils.js +220 -0
- package/src/lib/resources/utils/importance-classifier.js +645 -0
- package/src/lib/resources/utils/information-filter.js +710 -0
- package/src/lib/resources/utils/persona-aware-compressor.js +598 -0
- package/src/lib/resources/utils/prompt-injector.js +242 -0
- package/src/lib/resources/utils/simplified-evaluator.js +119 -0
- package/src/lib/resources/utils/text-summarizer.js +492 -0
- package/src/lib/resources/utils/token-counter.js +177 -0
- package/src/lib/resources/utils/token-monitor.js +484 -0
- package/src/lib/resources/utils/token-reporter.js +453 -0
- package/src/lib/resources/utils/token-storage.js +433 -0
- package/src/lib/resources/utils/token-tracker.js +283 -0
- package/claude-code/aiwf/ko/.claude/commands/aiwf/aiwf_switch_language.md +0 -160
- /package/claude-code/aiwf/ko/.claude/commands/aiwf/{aiwf_update_docs_kr.md → aiwf_update_docs.md} +0 -0
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
# Backend 페르소나 Knowledge Base
|
|
2
|
+
|
|
3
|
+
## 서버 아키텍처 패턴
|
|
4
|
+
|
|
5
|
+
### Clean Architecture
|
|
6
|
+
```
|
|
7
|
+
┌─────────────────────────────────────┐
|
|
8
|
+
│ Controllers/Routes │
|
|
9
|
+
├─────────────────────────────────────┤
|
|
10
|
+
│ Use Cases │
|
|
11
|
+
├─────────────────────────────────────┤
|
|
12
|
+
│ Domain/Entities │
|
|
13
|
+
├─────────────────────────────────────┤
|
|
14
|
+
│ Data Access/Repositories │
|
|
15
|
+
└─────────────────────────────────────┘
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
// Domain Entity
|
|
20
|
+
class User {
|
|
21
|
+
constructor(id, email, passwordHash) {
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.email = email;
|
|
24
|
+
this.passwordHash = passwordHash;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static create(email, password) {
|
|
28
|
+
if (!this.isValidEmail(email)) {
|
|
29
|
+
throw new DomainError('Invalid email format');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!this.isStrongPassword(password)) {
|
|
33
|
+
throw new DomainError('Password does not meet requirements');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new User(
|
|
37
|
+
generateId(),
|
|
38
|
+
email.toLowerCase(),
|
|
39
|
+
hashPassword(password)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static isValidEmail(email) {
|
|
44
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static isStrongPassword(password) {
|
|
48
|
+
return password.length >= 8 &&
|
|
49
|
+
/[A-Z]/.test(password) &&
|
|
50
|
+
/[a-z]/.test(password) &&
|
|
51
|
+
/[0-9]/.test(password);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Use Case
|
|
56
|
+
class RegisterUserUseCase {
|
|
57
|
+
constructor(userRepository, emailService) {
|
|
58
|
+
this.userRepository = userRepository;
|
|
59
|
+
this.emailService = emailService;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async execute(request) {
|
|
63
|
+
// 비즈니스 규칙 검증
|
|
64
|
+
const existingUser = await this.userRepository.findByEmail(request.email);
|
|
65
|
+
if (existingUser) {
|
|
66
|
+
throw new ConflictError('Email already registered');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 도메인 객체 생성
|
|
70
|
+
const user = User.create(request.email, request.password);
|
|
71
|
+
|
|
72
|
+
// 영속성 계층에 저장
|
|
73
|
+
await this.userRepository.save(user);
|
|
74
|
+
|
|
75
|
+
// 부가 작업
|
|
76
|
+
await this.emailService.sendWelcomeEmail(user.email);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id: user.id,
|
|
80
|
+
email: user.email
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Repository Interface
|
|
86
|
+
class UserRepository {
|
|
87
|
+
async save(user) {
|
|
88
|
+
throw new Error('Method not implemented');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async findByEmail(email) {
|
|
92
|
+
throw new Error('Method not implemented');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async findById(id) {
|
|
96
|
+
throw new Error('Method not implemented');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Repository Implementation
|
|
101
|
+
class PostgresUserRepository extends UserRepository {
|
|
102
|
+
constructor(db) {
|
|
103
|
+
super();
|
|
104
|
+
this.db = db;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async save(user) {
|
|
108
|
+
const query = `
|
|
109
|
+
INSERT INTO users (id, email, password_hash, created_at)
|
|
110
|
+
VALUES ($1, $2, $3, NOW())
|
|
111
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
112
|
+
email = EXCLUDED.email,
|
|
113
|
+
password_hash = EXCLUDED.password_hash,
|
|
114
|
+
updated_at = NOW()
|
|
115
|
+
RETURNING *
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const result = await this.db.query(query, [
|
|
119
|
+
user.id,
|
|
120
|
+
user.email,
|
|
121
|
+
user.passwordHash
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
return this.toDomainEntity(result.rows[0]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async findByEmail(email) {
|
|
128
|
+
const query = 'SELECT * FROM users WHERE email = $1';
|
|
129
|
+
const result = await this.db.query(query, [email.toLowerCase()]);
|
|
130
|
+
|
|
131
|
+
if (result.rows.length === 0) return null;
|
|
132
|
+
return this.toDomainEntity(result.rows[0]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
toDomainEntity(row) {
|
|
136
|
+
return new User(row.id, row.email, row.password_hash);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Event-Driven Architecture
|
|
142
|
+
```javascript
|
|
143
|
+
// Event Bus Implementation
|
|
144
|
+
class EventBus {
|
|
145
|
+
constructor() {
|
|
146
|
+
this.handlers = new Map();
|
|
147
|
+
this.middlewares = [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
subscribe(eventType, handler) {
|
|
151
|
+
if (!this.handlers.has(eventType)) {
|
|
152
|
+
this.handlers.set(eventType, []);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.handlers.get(eventType).push(handler);
|
|
156
|
+
|
|
157
|
+
// Return unsubscribe function
|
|
158
|
+
return () => {
|
|
159
|
+
const handlers = this.handlers.get(eventType);
|
|
160
|
+
const index = handlers.indexOf(handler);
|
|
161
|
+
if (index > -1) {
|
|
162
|
+
handlers.splice(index, 1);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async publish(eventType, payload) {
|
|
168
|
+
const event = {
|
|
169
|
+
type: eventType,
|
|
170
|
+
payload,
|
|
171
|
+
timestamp: new Date().toISOString(),
|
|
172
|
+
id: generateId()
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Apply middlewares
|
|
176
|
+
for (const middleware of this.middlewares) {
|
|
177
|
+
await middleware(event);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Execute handlers
|
|
181
|
+
const handlers = this.handlers.get(eventType) || [];
|
|
182
|
+
const promises = handlers.map(handler =>
|
|
183
|
+
this.executeHandler(handler, event)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await Promise.allSettled(promises);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async executeHandler(handler, event) {
|
|
190
|
+
try {
|
|
191
|
+
await handler(event);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(`Handler error for ${event.type}:`, error);
|
|
194
|
+
// Could publish error event here
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
use(middleware) {
|
|
199
|
+
this.middlewares.push(middleware);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Domain Events
|
|
204
|
+
class OrderCreatedEvent {
|
|
205
|
+
constructor(order) {
|
|
206
|
+
this.orderId = order.id;
|
|
207
|
+
this.userId = order.userId;
|
|
208
|
+
this.items = order.items;
|
|
209
|
+
this.total = order.total;
|
|
210
|
+
this.timestamp = new Date().toISOString();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Event Handlers
|
|
215
|
+
class EmailNotificationHandler {
|
|
216
|
+
constructor(emailService, userRepository) {
|
|
217
|
+
this.emailService = emailService;
|
|
218
|
+
this.userRepository = userRepository;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async handleOrderCreated(event) {
|
|
222
|
+
const user = await this.userRepository.findById(event.payload.userId);
|
|
223
|
+
|
|
224
|
+
await this.emailService.send({
|
|
225
|
+
to: user.email,
|
|
226
|
+
subject: 'Order Confirmation',
|
|
227
|
+
template: 'order-confirmation',
|
|
228
|
+
data: {
|
|
229
|
+
orderNumber: event.payload.orderId,
|
|
230
|
+
items: event.payload.items,
|
|
231
|
+
total: event.payload.total
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Event Sourcing
|
|
238
|
+
class EventStore {
|
|
239
|
+
constructor(db) {
|
|
240
|
+
this.db = db;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async append(streamId, events) {
|
|
244
|
+
const query = `
|
|
245
|
+
INSERT INTO events (stream_id, event_type, payload, version, timestamp)
|
|
246
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
247
|
+
`;
|
|
248
|
+
|
|
249
|
+
const client = await this.db.connect();
|
|
250
|
+
try {
|
|
251
|
+
await client.query('BEGIN');
|
|
252
|
+
|
|
253
|
+
for (const event of events) {
|
|
254
|
+
await client.query(query, [
|
|
255
|
+
streamId,
|
|
256
|
+
event.type,
|
|
257
|
+
JSON.stringify(event.payload),
|
|
258
|
+
event.version,
|
|
259
|
+
event.timestamp
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await client.query('COMMIT');
|
|
264
|
+
} catch (error) {
|
|
265
|
+
await client.query('ROLLBACK');
|
|
266
|
+
throw error;
|
|
267
|
+
} finally {
|
|
268
|
+
client.release();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getEvents(streamId, fromVersion = 0) {
|
|
273
|
+
const query = `
|
|
274
|
+
SELECT * FROM events
|
|
275
|
+
WHERE stream_id = $1 AND version > $2
|
|
276
|
+
ORDER BY version ASC
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
const result = await this.db.query(query, [streamId, fromVersion]);
|
|
280
|
+
|
|
281
|
+
return result.rows.map(row => ({
|
|
282
|
+
type: row.event_type,
|
|
283
|
+
payload: row.payload,
|
|
284
|
+
version: row.version,
|
|
285
|
+
timestamp: row.timestamp
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## 데이터베이스 패턴
|
|
292
|
+
|
|
293
|
+
### Repository Pattern with Unit of Work
|
|
294
|
+
```javascript
|
|
295
|
+
// Unit of Work
|
|
296
|
+
class UnitOfWork {
|
|
297
|
+
constructor(db) {
|
|
298
|
+
this.db = db;
|
|
299
|
+
this.client = null;
|
|
300
|
+
this.repositories = new Map();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async begin() {
|
|
304
|
+
this.client = await this.db.connect();
|
|
305
|
+
await this.client.query('BEGIN');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async commit() {
|
|
309
|
+
if (!this.client) {
|
|
310
|
+
throw new Error('No transaction in progress');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await this.client.query('COMMIT');
|
|
314
|
+
this.client.release();
|
|
315
|
+
this.client = null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async rollback() {
|
|
319
|
+
if (!this.client) {
|
|
320
|
+
throw new Error('No transaction in progress');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await this.client.query('ROLLBACK');
|
|
324
|
+
this.client.release();
|
|
325
|
+
this.client = null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getRepository(name, RepositoryClass) {
|
|
329
|
+
if (!this.repositories.has(name)) {
|
|
330
|
+
this.repositories.set(
|
|
331
|
+
name,
|
|
332
|
+
new RepositoryClass(this.client || this.db)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return this.repositories.get(name);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Usage
|
|
341
|
+
class OrderService {
|
|
342
|
+
constructor(unitOfWork) {
|
|
343
|
+
this.unitOfWork = unitOfWork;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async createOrder(orderData) {
|
|
347
|
+
await this.unitOfWork.begin();
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const userRepo = this.unitOfWork.getRepository('user', UserRepository);
|
|
351
|
+
const orderRepo = this.unitOfWork.getRepository('order', OrderRepository);
|
|
352
|
+
const inventoryRepo = this.unitOfWork.getRepository('inventory', InventoryRepository);
|
|
353
|
+
|
|
354
|
+
// Validate user
|
|
355
|
+
const user = await userRepo.findById(orderData.userId);
|
|
356
|
+
if (!user) throw new Error('User not found');
|
|
357
|
+
|
|
358
|
+
// Check inventory
|
|
359
|
+
for (const item of orderData.items) {
|
|
360
|
+
const available = await inventoryRepo.checkAvailability(
|
|
361
|
+
item.productId,
|
|
362
|
+
item.quantity
|
|
363
|
+
);
|
|
364
|
+
if (!available) {
|
|
365
|
+
throw new Error(`Insufficient inventory for ${item.productId}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Create order
|
|
370
|
+
const order = await orderRepo.create(orderData);
|
|
371
|
+
|
|
372
|
+
// Update inventory
|
|
373
|
+
for (const item of orderData.items) {
|
|
374
|
+
await inventoryRepo.decrementStock(item.productId, item.quantity);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await this.unitOfWork.commit();
|
|
378
|
+
return order;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
await this.unitOfWork.rollback();
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Database Migration Management
|
|
388
|
+
```javascript
|
|
389
|
+
// Migration System
|
|
390
|
+
class MigrationRunner {
|
|
391
|
+
constructor(db, migrationsPath) {
|
|
392
|
+
this.db = db;
|
|
393
|
+
this.migrationsPath = migrationsPath;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async run() {
|
|
397
|
+
await this.ensureMigrationTable();
|
|
398
|
+
|
|
399
|
+
const appliedMigrations = await this.getAppliedMigrations();
|
|
400
|
+
const migrations = await this.loadMigrations();
|
|
401
|
+
|
|
402
|
+
for (const migration of migrations) {
|
|
403
|
+
if (!appliedMigrations.includes(migration.id)) {
|
|
404
|
+
await this.applyMigration(migration);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async ensureMigrationTable() {
|
|
410
|
+
await this.db.query(`
|
|
411
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
412
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
413
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
414
|
+
)
|
|
415
|
+
`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async getAppliedMigrations() {
|
|
419
|
+
const result = await this.db.query('SELECT id FROM migrations');
|
|
420
|
+
return result.rows.map(row => row.id);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async loadMigrations() {
|
|
424
|
+
const files = fs.readdirSync(this.migrationsPath);
|
|
425
|
+
const migrations = [];
|
|
426
|
+
|
|
427
|
+
for (const file of files) {
|
|
428
|
+
if (file.endsWith('.js')) {
|
|
429
|
+
const migration = require(path.join(this.migrationsPath, file));
|
|
430
|
+
migrations.push({
|
|
431
|
+
id: file,
|
|
432
|
+
up: migration.up,
|
|
433
|
+
down: migration.down
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return migrations.sort((a, b) => a.id.localeCompare(b.id));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async applyMigration(migration) {
|
|
442
|
+
const client = await this.db.connect();
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
await client.query('BEGIN');
|
|
446
|
+
|
|
447
|
+
console.log(`Applying migration: ${migration.id}`);
|
|
448
|
+
await migration.up(client);
|
|
449
|
+
|
|
450
|
+
await client.query(
|
|
451
|
+
'INSERT INTO migrations (id) VALUES ($1)',
|
|
452
|
+
[migration.id]
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
await client.query('COMMIT');
|
|
456
|
+
console.log(`Migration ${migration.id} applied successfully`);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
await client.query('ROLLBACK');
|
|
459
|
+
console.error(`Failed to apply migration ${migration.id}:`, error);
|
|
460
|
+
throw error;
|
|
461
|
+
} finally {
|
|
462
|
+
client.release();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Migration Example
|
|
468
|
+
// migrations/001_create_users_table.js
|
|
469
|
+
module.exports = {
|
|
470
|
+
async up(db) {
|
|
471
|
+
await db.query(`
|
|
472
|
+
CREATE TABLE users (
|
|
473
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
474
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
475
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
476
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
477
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
478
|
+
)
|
|
479
|
+
`);
|
|
480
|
+
|
|
481
|
+
await db.query(`
|
|
482
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
483
|
+
CREATE INDEX idx_users_created_at ON users(created_at);
|
|
484
|
+
`);
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
async down(db) {
|
|
488
|
+
await db.query('DROP TABLE IF EXISTS users');
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## API 패턴
|
|
494
|
+
|
|
495
|
+
### GraphQL DataLoader Pattern
|
|
496
|
+
```javascript
|
|
497
|
+
// DataLoader for N+1 query prevention
|
|
498
|
+
class UserLoader {
|
|
499
|
+
constructor(userRepository) {
|
|
500
|
+
this.userRepository = userRepository;
|
|
501
|
+
this.loader = new DataLoader(
|
|
502
|
+
keys => this.batchLoadUsers(keys),
|
|
503
|
+
{ cache: true }
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async batchLoadUsers(userIds) {
|
|
508
|
+
const users = await this.userRepository.findByIds(userIds);
|
|
509
|
+
|
|
510
|
+
// Map users to maintain order
|
|
511
|
+
const userMap = new Map();
|
|
512
|
+
users.forEach(user => userMap.set(user.id, user));
|
|
513
|
+
|
|
514
|
+
return userIds.map(id => userMap.get(id) || null);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async load(userId) {
|
|
518
|
+
return this.loader.load(userId);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async loadMany(userIds) {
|
|
522
|
+
return this.loader.loadMany(userIds);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
clearCache(userId) {
|
|
526
|
+
this.loader.clear(userId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// GraphQL Resolvers with DataLoader
|
|
531
|
+
const resolvers = {
|
|
532
|
+
Query: {
|
|
533
|
+
user: async (parent, { id }, { dataloaders }) => {
|
|
534
|
+
return dataloaders.user.load(id);
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
users: async (parent, args, { userRepository }) => {
|
|
538
|
+
return userRepository.findAll(args);
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
User: {
|
|
543
|
+
orders: async (user, args, { dataloaders }) => {
|
|
544
|
+
return dataloaders.ordersByUser.load(user.id);
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
profile: async (user, args, { dataloaders }) => {
|
|
548
|
+
return dataloaders.profile.load(user.id);
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
Order: {
|
|
553
|
+
user: async (order, args, { dataloaders }) => {
|
|
554
|
+
return dataloaders.user.load(order.userId);
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
items: async (order, args, { dataloaders }) => {
|
|
558
|
+
return dataloaders.orderItems.load(order.id);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Context Factory
|
|
564
|
+
function createGraphQLContext(req) {
|
|
565
|
+
const repositories = {
|
|
566
|
+
user: new UserRepository(db),
|
|
567
|
+
order: new OrderRepository(db),
|
|
568
|
+
profile: new ProfileRepository(db)
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const dataloaders = {
|
|
572
|
+
user: new UserLoader(repositories.user),
|
|
573
|
+
ordersByUser: new OrdersByUserLoader(repositories.order),
|
|
574
|
+
profile: new ProfileLoader(repositories.profile),
|
|
575
|
+
orderItems: new OrderItemsLoader(repositories.order)
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
req,
|
|
580
|
+
repositories,
|
|
581
|
+
dataloaders,
|
|
582
|
+
user: req.user // From auth middleware
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### REST API Versioning
|
|
588
|
+
```javascript
|
|
589
|
+
// API Version Management
|
|
590
|
+
class APIVersionManager {
|
|
591
|
+
constructor() {
|
|
592
|
+
this.versions = new Map();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
register(version, routes) {
|
|
596
|
+
this.versions.set(version, routes);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
middleware() {
|
|
600
|
+
return (req, res, next) => {
|
|
601
|
+
// Extract version from header or URL
|
|
602
|
+
const version = this.extractVersion(req);
|
|
603
|
+
|
|
604
|
+
if (!this.versions.has(version)) {
|
|
605
|
+
return res.status(400).json({
|
|
606
|
+
error: 'Unsupported API version'
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
req.apiVersion = version;
|
|
611
|
+
next();
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
extractVersion(req) {
|
|
616
|
+
// Priority: Header > URL > Default
|
|
617
|
+
if (req.headers['api-version']) {
|
|
618
|
+
return req.headers['api-version'];
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const urlMatch = req.path.match(/^\/api\/v(\d+)/);
|
|
622
|
+
if (urlMatch) {
|
|
623
|
+
return `v${urlMatch[1]}`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return 'v1'; // Default version
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
getRouter(version) {
|
|
630
|
+
return this.versions.get(version);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Version-specific implementations
|
|
635
|
+
// v1/users.js
|
|
636
|
+
const v1UserRoutes = {
|
|
637
|
+
getUser: async (req, res) => {
|
|
638
|
+
const user = await userService.findById(req.params.id);
|
|
639
|
+
res.json({
|
|
640
|
+
id: user.id,
|
|
641
|
+
email: user.email,
|
|
642
|
+
name: `${user.firstName} ${user.lastName}`
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// v2/users.js
|
|
648
|
+
const v2UserRoutes = {
|
|
649
|
+
getUser: async (req, res) => {
|
|
650
|
+
const user = await userService.findById(req.params.id);
|
|
651
|
+
res.json({
|
|
652
|
+
id: user.id,
|
|
653
|
+
email: user.email,
|
|
654
|
+
profile: {
|
|
655
|
+
firstName: user.firstName,
|
|
656
|
+
lastName: user.lastName,
|
|
657
|
+
displayName: user.displayName
|
|
658
|
+
},
|
|
659
|
+
preferences: user.preferences
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## 성능 최적화
|
|
666
|
+
|
|
667
|
+
### Connection Pooling
|
|
668
|
+
```javascript
|
|
669
|
+
// Database Connection Pool
|
|
670
|
+
class DatabasePool {
|
|
671
|
+
constructor(config) {
|
|
672
|
+
this.config = config;
|
|
673
|
+
this.pool = new Pool({
|
|
674
|
+
host: config.host,
|
|
675
|
+
port: config.port,
|
|
676
|
+
database: config.database,
|
|
677
|
+
user: config.user,
|
|
678
|
+
password: config.password,
|
|
679
|
+
max: config.maxConnections || 20,
|
|
680
|
+
idleTimeoutMillis: config.idleTimeout || 30000,
|
|
681
|
+
connectionTimeoutMillis: config.connectionTimeout || 2000
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Connection lifecycle events
|
|
685
|
+
this.pool.on('connect', (client) => {
|
|
686
|
+
console.log('New client connected');
|
|
687
|
+
client.query('SET search_path TO public');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
this.pool.on('error', (err, client) => {
|
|
691
|
+
console.error('Unexpected error on idle client', err);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async query(text, params) {
|
|
696
|
+
const start = Date.now();
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const result = await this.pool.query(text, params);
|
|
700
|
+
const duration = Date.now() - start;
|
|
701
|
+
|
|
702
|
+
// Log slow queries
|
|
703
|
+
if (duration > 1000) {
|
|
704
|
+
console.warn('Slow query detected:', {
|
|
705
|
+
query: text,
|
|
706
|
+
duration,
|
|
707
|
+
rows: result.rowCount
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return result;
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error('Query error:', error);
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async transaction(callback) {
|
|
719
|
+
const client = await this.pool.connect();
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
await client.query('BEGIN');
|
|
723
|
+
const result = await callback(client);
|
|
724
|
+
await client.query('COMMIT');
|
|
725
|
+
return result;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
await client.query('ROLLBACK');
|
|
728
|
+
throw error;
|
|
729
|
+
} finally {
|
|
730
|
+
client.release();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async end() {
|
|
735
|
+
await this.pool.end();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Caching Strategies
|
|
741
|
+
```javascript
|
|
742
|
+
// Multi-layer Cache
|
|
743
|
+
class CacheManager {
|
|
744
|
+
constructor(memoryCache, redisCache) {
|
|
745
|
+
this.L1 = memoryCache; // In-memory cache
|
|
746
|
+
this.L2 = redisCache; // Redis cache
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async get(key, options = {}) {
|
|
750
|
+
// Check L1 cache
|
|
751
|
+
const l1Value = this.L1.get(key);
|
|
752
|
+
if (l1Value !== undefined) {
|
|
753
|
+
return l1Value;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Check L2 cache
|
|
757
|
+
const l2Value = await this.L2.get(key);
|
|
758
|
+
if (l2Value !== null) {
|
|
759
|
+
// Populate L1 cache
|
|
760
|
+
this.L1.set(key, l2Value, options.ttl);
|
|
761
|
+
return l2Value;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async set(key, value, options = {}) {
|
|
768
|
+
// Set in both caches
|
|
769
|
+
this.L1.set(key, value, options.ttl);
|
|
770
|
+
await this.L2.set(key, value, options.ttl || 3600);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async invalidate(pattern) {
|
|
774
|
+
// Clear from L1
|
|
775
|
+
this.L1.clear(pattern);
|
|
776
|
+
|
|
777
|
+
// Clear from L2
|
|
778
|
+
await this.L2.invalidate(pattern);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Query Result Cache
|
|
783
|
+
class QueryCache {
|
|
784
|
+
constructor(cache, db) {
|
|
785
|
+
this.cache = cache;
|
|
786
|
+
this.db = db;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async query(sql, params, options = {}) {
|
|
790
|
+
const cacheKey = this.generateCacheKey(sql, params);
|
|
791
|
+
|
|
792
|
+
// Try cache first
|
|
793
|
+
const cached = await this.cache.get(cacheKey);
|
|
794
|
+
if (cached && !options.skipCache) {
|
|
795
|
+
return cached;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Execute query
|
|
799
|
+
const result = await this.db.query(sql, params);
|
|
800
|
+
|
|
801
|
+
// Cache result
|
|
802
|
+
if (options.cache !== false) {
|
|
803
|
+
await this.cache.set(
|
|
804
|
+
cacheKey,
|
|
805
|
+
result.rows,
|
|
806
|
+
options.ttl || 300
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return result.rows;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
generateCacheKey(sql, params) {
|
|
814
|
+
const hash = crypto
|
|
815
|
+
.createHash('sha256')
|
|
816
|
+
.update(sql + JSON.stringify(params))
|
|
817
|
+
.digest('hex');
|
|
818
|
+
|
|
819
|
+
return `query:${hash}`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async invalidatePattern(table) {
|
|
823
|
+
// Invalidate all queries related to a table
|
|
824
|
+
await this.cache.invalidate(`query:*${table}*`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
## 보안 구현
|
|
830
|
+
|
|
831
|
+
### OAuth 2.0 Server Implementation
|
|
832
|
+
```javascript
|
|
833
|
+
// OAuth 2.0 Authorization Server
|
|
834
|
+
class OAuth2Server {
|
|
835
|
+
constructor(clientStore, tokenStore, userStore) {
|
|
836
|
+
this.clientStore = clientStore;
|
|
837
|
+
this.tokenStore = tokenStore;
|
|
838
|
+
this.userStore = userStore;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Authorization Code Grant
|
|
842
|
+
async authorize(request) {
|
|
843
|
+
const {
|
|
844
|
+
client_id,
|
|
845
|
+
redirect_uri,
|
|
846
|
+
response_type,
|
|
847
|
+
scope,
|
|
848
|
+
state
|
|
849
|
+
} = request.query;
|
|
850
|
+
|
|
851
|
+
// Validate client
|
|
852
|
+
const client = await this.clientStore.findById(client_id);
|
|
853
|
+
if (!client) {
|
|
854
|
+
throw new OAuth2Error('invalid_client');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Validate redirect URI
|
|
858
|
+
if (!client.redirectUris.includes(redirect_uri)) {
|
|
859
|
+
throw new OAuth2Error('invalid_redirect_uri');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Generate authorization code
|
|
863
|
+
const code = crypto.randomBytes(32).toString('hex');
|
|
864
|
+
|
|
865
|
+
await this.tokenStore.saveAuthCode({
|
|
866
|
+
code,
|
|
867
|
+
clientId: client_id,
|
|
868
|
+
userId: request.user.id,
|
|
869
|
+
redirectUri: redirect_uri,
|
|
870
|
+
scope: scope.split(' '),
|
|
871
|
+
expiresAt: Date.now() + 600000 // 10 minutes
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Redirect with code
|
|
875
|
+
const redirectUrl = new URL(redirect_uri);
|
|
876
|
+
redirectUrl.searchParams.set('code', code);
|
|
877
|
+
if (state) {
|
|
878
|
+
redirectUrl.searchParams.set('state', state);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return redirectUrl.toString();
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Token Exchange
|
|
885
|
+
async token(request) {
|
|
886
|
+
const { grant_type } = request.body;
|
|
887
|
+
|
|
888
|
+
switch (grant_type) {
|
|
889
|
+
case 'authorization_code':
|
|
890
|
+
return this.authorizationCodeGrant(request.body);
|
|
891
|
+
case 'refresh_token':
|
|
892
|
+
return this.refreshTokenGrant(request.body);
|
|
893
|
+
case 'client_credentials':
|
|
894
|
+
return this.clientCredentialsGrant(request.body);
|
|
895
|
+
default:
|
|
896
|
+
throw new OAuth2Error('unsupported_grant_type');
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async authorizationCodeGrant(params) {
|
|
901
|
+
const { code, client_id, client_secret, redirect_uri } = params;
|
|
902
|
+
|
|
903
|
+
// Validate client credentials
|
|
904
|
+
const client = await this.clientStore.findById(client_id);
|
|
905
|
+
if (!client || client.secret !== client_secret) {
|
|
906
|
+
throw new OAuth2Error('invalid_client');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Validate authorization code
|
|
910
|
+
const authCode = await this.tokenStore.getAuthCode(code);
|
|
911
|
+
if (!authCode || authCode.clientId !== client_id) {
|
|
912
|
+
throw new OAuth2Error('invalid_grant');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (Date.now() > authCode.expiresAt) {
|
|
916
|
+
throw new OAuth2Error('invalid_grant', 'Code expired');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (authCode.redirectUri !== redirect_uri) {
|
|
920
|
+
throw new OAuth2Error('invalid_grant');
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Generate tokens
|
|
924
|
+
const accessToken = this.generateAccessToken(
|
|
925
|
+
authCode.userId,
|
|
926
|
+
client_id,
|
|
927
|
+
authCode.scope
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
const refreshToken = crypto.randomBytes(32).toString('hex');
|
|
931
|
+
|
|
932
|
+
// Save tokens
|
|
933
|
+
await this.tokenStore.saveTokens({
|
|
934
|
+
accessToken: accessToken.token,
|
|
935
|
+
refreshToken,
|
|
936
|
+
userId: authCode.userId,
|
|
937
|
+
clientId: client_id,
|
|
938
|
+
scope: authCode.scope,
|
|
939
|
+
accessTokenExpiresAt: accessToken.expiresAt,
|
|
940
|
+
refreshTokenExpiresAt: Date.now() + 2592000000 // 30 days
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// Revoke authorization code
|
|
944
|
+
await this.tokenStore.revokeAuthCode(code);
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
access_token: accessToken.token,
|
|
948
|
+
token_type: 'Bearer',
|
|
949
|
+
expires_in: 3600,
|
|
950
|
+
refresh_token: refreshToken,
|
|
951
|
+
scope: authCode.scope.join(' ')
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
generateAccessToken(userId, clientId, scope) {
|
|
956
|
+
const payload = {
|
|
957
|
+
sub: userId,
|
|
958
|
+
client: clientId,
|
|
959
|
+
scope,
|
|
960
|
+
iat: Math.floor(Date.now() / 1000),
|
|
961
|
+
exp: Math.floor(Date.now() / 1000) + 3600
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const token = jwt.sign(payload, process.env.JWT_SECRET);
|
|
965
|
+
|
|
966
|
+
return {
|
|
967
|
+
token,
|
|
968
|
+
expiresAt: Date.now() + 3600000
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### API Key Management
|
|
975
|
+
```javascript
|
|
976
|
+
// API Key Service
|
|
977
|
+
class APIKeyService {
|
|
978
|
+
constructor(db, crypto) {
|
|
979
|
+
this.db = db;
|
|
980
|
+
this.crypto = crypto;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async generateAPIKey(userId, name, permissions = []) {
|
|
984
|
+
// Generate secure random key
|
|
985
|
+
const keyValue = this.generateSecureKey();
|
|
986
|
+
const keyHash = await this.hashAPIKey(keyValue);
|
|
987
|
+
|
|
988
|
+
// Store key metadata
|
|
989
|
+
const apiKey = await this.db.query(`
|
|
990
|
+
INSERT INTO api_keys
|
|
991
|
+
(user_id, name, key_hash, permissions, last_used_at)
|
|
992
|
+
VALUES ($1, $2, $3, $4, NULL)
|
|
993
|
+
RETURNING id, created_at
|
|
994
|
+
`, [userId, name, keyHash, JSON.stringify(permissions)]);
|
|
995
|
+
|
|
996
|
+
// Return key value only once
|
|
997
|
+
return {
|
|
998
|
+
id: apiKey.rows[0].id,
|
|
999
|
+
key: `sk_live_${keyValue}`,
|
|
1000
|
+
name,
|
|
1001
|
+
permissions,
|
|
1002
|
+
createdAt: apiKey.rows[0].created_at
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
generateSecureKey() {
|
|
1007
|
+
return crypto.randomBytes(32).toString('base64')
|
|
1008
|
+
.replace(/\+/g, '-')
|
|
1009
|
+
.replace(/\//g, '_')
|
|
1010
|
+
.replace(/=/g, '');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async hashAPIKey(key) {
|
|
1014
|
+
return crypto
|
|
1015
|
+
.createHash('sha256')
|
|
1016
|
+
.update(key)
|
|
1017
|
+
.digest('hex');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async validateAPIKey(keyValue) {
|
|
1021
|
+
// Extract actual key
|
|
1022
|
+
const key = keyValue.replace(/^sk_(live|test)_/, '');
|
|
1023
|
+
const keyHash = await this.hashAPIKey(key);
|
|
1024
|
+
|
|
1025
|
+
// Find key in database
|
|
1026
|
+
const result = await this.db.query(`
|
|
1027
|
+
UPDATE api_keys
|
|
1028
|
+
SET last_used_at = NOW(), usage_count = usage_count + 1
|
|
1029
|
+
WHERE key_hash = $1 AND revoked_at IS NULL
|
|
1030
|
+
RETURNING *
|
|
1031
|
+
`, [keyHash]);
|
|
1032
|
+
|
|
1033
|
+
if (result.rows.length === 0) {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const apiKey = result.rows[0];
|
|
1038
|
+
|
|
1039
|
+
// Check rate limits
|
|
1040
|
+
if (apiKey.rate_limit) {
|
|
1041
|
+
const rateLimitExceeded = await this.checkRateLimit(
|
|
1042
|
+
apiKey.id,
|
|
1043
|
+
apiKey.rate_limit
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
if (rateLimitExceeded) {
|
|
1047
|
+
throw new Error('Rate limit exceeded');
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return {
|
|
1052
|
+
id: apiKey.id,
|
|
1053
|
+
userId: apiKey.user_id,
|
|
1054
|
+
permissions: apiKey.permissions,
|
|
1055
|
+
rateLimit: apiKey.rate_limit
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async revokeAPIKey(keyId, userId) {
|
|
1060
|
+
const result = await this.db.query(`
|
|
1061
|
+
UPDATE api_keys
|
|
1062
|
+
SET revoked_at = NOW()
|
|
1063
|
+
WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
|
|
1064
|
+
RETURNING id
|
|
1065
|
+
`, [keyId, userId]);
|
|
1066
|
+
|
|
1067
|
+
return result.rows.length > 0;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
```
|