moai-adk 0.4.5__py3-none-any.whl → 0.20.1__py3-none-any.whl
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.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +1 -1
- moai_adk/__main__.py +74 -1
- moai_adk/cli/commands/__init__.py +1 -1
- moai_adk/cli/commands/analyze.py +119 -0
- moai_adk/cli/commands/backup.py +25 -1
- moai_adk/cli/commands/doctor.py +31 -5
- moai_adk/cli/commands/improve_user_experience.py +307 -0
- moai_adk/cli/commands/init.py +111 -10
- moai_adk/cli/commands/status.py +33 -3
- moai_adk/cli/commands/update.py +921 -130
- moai_adk/cli/commands/validate_links.py +120 -0
- moai_adk/cli/prompts/init_prompts.py +22 -87
- moai_adk/core/analysis/__init__.py +9 -0
- moai_adk/core/analysis/session_analyzer.py +388 -0
- moai_adk/core/analysis/tag_chain_analyzer.py +344 -0
- moai_adk/core/analysis/tag_chain_repair.py +879 -0
- moai_adk/core/config/__init__.py +19 -0
- moai_adk/core/config/migration.py +235 -0
- moai_adk/core/git/__init__.py +1 -1
- moai_adk/core/git/branch.py +1 -1
- moai_adk/core/git/commit.py +1 -1
- moai_adk/core/git/manager.py +1 -1
- moai_adk/core/issue_creator.py +313 -0
- moai_adk/core/mcp/setup.py +56 -0
- moai_adk/core/mcp/setup_old.py +296 -0
- moai_adk/core/project/backup_utils.py +1 -1
- moai_adk/core/project/checker.py +2 -2
- moai_adk/core/project/detector.py +211 -12
- moai_adk/core/project/initializer.py +85 -15
- moai_adk/core/project/phase_executor.py +76 -13
- moai_adk/core/project/validator.py +13 -13
- moai_adk/core/quality/__init__.py +1 -1
- moai_adk/core/quality/trust_checker.py +1 -1
- moai_adk/core/quality/validators/__init__.py +1 -1
- moai_adk/core/quality/validators/base_validator.py +1 -1
- moai_adk/core/tags/__init__.py +86 -0
- moai_adk/core/tags/auto_corrector.py +693 -0
- moai_adk/core/tags/ci_validator.py +463 -0
- moai_adk/core/tags/cli.py +283 -0
- moai_adk/core/tags/generator.py +109 -0
- moai_adk/core/tags/inserter.py +99 -0
- moai_adk/core/tags/mapper.py +126 -0
- moai_adk/core/tags/parser.py +76 -0
- moai_adk/core/tags/policy_validator.py +580 -0
- moai_adk/core/tags/pre_commit_validator.py +421 -0
- moai_adk/core/tags/reporter.py +956 -0
- moai_adk/core/tags/rollback_manager.py +525 -0
- moai_adk/core/tags/tags.py +149 -0
- moai_adk/core/tags/validator.py +897 -0
- moai_adk/core/template/__init__.py +1 -1
- moai_adk/core/template/backup.py +1 -1
- moai_adk/core/template/merger.py +50 -1
- moai_adk/core/template/processor.py +119 -13
- moai_adk/core/template_engine.py +268 -0
- moai_adk/templates/.claude/agents/alfred/backend-expert.md +348 -0
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +209 -944
- moai_adk/templates/.claude/agents/alfred/database-expert.md +352 -0
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +34 -5
- moai_adk/templates/.claude/agents/alfred/devops-expert.md +464 -0
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +38 -8
- moai_adk/templates/.claude/agents/alfred/format-expert.md +469 -0
- moai_adk/templates/.claude/agents/alfred/frontend-expert.md +357 -0
- moai_adk/templates/.claude/agents/alfred/git-manager.md +128 -9
- moai_adk/templates/.claude/agents/alfred/implementation-planner.md +104 -6
- moai_adk/templates/.claude/agents/alfred/project-manager.md +88 -16
- moai_adk/templates/.claude/agents/alfred/quality-gate.md +36 -9
- moai_adk/templates/.claude/agents/alfred/security-expert.md +270 -0
- moai_adk/templates/.claude/agents/alfred/skill-factory.md +865 -0
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +214 -43
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +111 -9
- moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +309 -160
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +36 -7
- moai_adk/templates/.claude/agents/alfred/ui-ux-expert.md +605 -0
- moai_adk/templates/.claude/commands/alfred/0-project.md +393 -966
- moai_adk/templates/.claude/commands/alfred/1-plan.md +651 -367
- moai_adk/templates/.claude/commands/alfred/2-run.md +388 -241
- moai_adk/templates/.claude/commands/alfred/3-sync.md +1921 -410
- moai_adk/templates/.claude/commands/alfred/9-feedback.md +153 -0
- moai_adk/templates/.claude/commands/alfred/release-new.md +3604 -0
- moai_adk/templates/.claude/hooks/alfred/core/project.py +484 -20
- moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
- moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +108 -0
- moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +14 -6
- moai_adk/templates/.claude/hooks/alfred/post_tool__enable_streaming_ui.py +50 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__tag_auto_corrector.py +407 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +99 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__realtime_tag_monitor.py +335 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__tag_policy_validator.py +325 -0
- moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__auto_cleanup.py +580 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +298 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +170 -0
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/checkpoint.py +3 -3
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/context.py +5 -5
- moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +749 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +230 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/__init__.py +21 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/daily_analysis.py +351 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +154 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +174 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +87 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/user.py +61 -0
- moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +111 -0
- moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
- moai_adk/templates/.claude/hooks/alfred/utils/hook_config.py +94 -0
- moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +161 -0
- moai_adk/templates/.claude/output-styles/alfred/alfred-moai-adk-beginner.md +267 -0
- moai_adk/templates/.claude/output-styles/alfred/keating-personal-tutor.md +440 -0
- moai_adk/templates/.claude/output-styles/alfred/r2d2-agentic-coding.md +583 -0
- moai_adk/templates/.claude/settings.json +96 -14
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/SKILL.md +70 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/examples.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/reference.md +242 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/SKILL.md +237 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/examples.md +871 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/reference.md +653 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/README.md +162 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/SKILL.md +227 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/examples.md +354 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/reference.md +158 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/SKILL.md +179 -79
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/examples.md +117 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/scripts/pre-review-check.sh +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/SKILL.md +132 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/reference.md +444 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/SKILL.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/SKILL.md +51 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/examples.md +355 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/reference.md +239 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/SKILL.md +323 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/examples.md +286 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/reference.md +126 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/SKILL.md +229 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/reference.md +150 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/SKILL.md +87 -73
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/README.md +42 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/SKILL.md +429 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/examples.md +520 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/SKILL.md +89 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/examples.md +122 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/reference.md +369 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/SKILL.md +508 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/examples.md +481 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/reference.md +100 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/SKILL.md +77 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/examples.md +265 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/reference.md +539 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/SKILL.md +320 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/reference.md +84 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/README.md +137 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/SKILL.md +219 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples/validate-spec.sh +161 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples.md +541 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/reference.md +622 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/SKILL.md +19 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/reference.md +211 -0
- moai_adk/templates/.claude/skills/moai-alfred-workflow/SKILL.md +288 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/SKILL.md +269 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/templates/agent-template.md +32 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/SKILL.md +298 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/templates/CLAUDE-template.md +26 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/SKILL.md +307 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/templates/command-template.md +21 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/SKILL.md +252 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/pre-bash-check.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/preserve-permissions.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/validate-bash-command.py +24 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/SKILL.md +199 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/templates/settings-mcp-template.json +39 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/SKILL.md +316 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/templates/session-summary-template.md +18 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/SKILL.md +263 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/templates/settings-complete-template.json +30 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/CHECKLIST.md +482 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/EXAMPLES.md +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/INTERACTIVE-DISCOVERY.md +524 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/METADATA.md +477 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PARALLEL-ANALYSIS-REPORT.md +429 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PYTHON-VERSION-MATRIX.md +391 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-FACTORY-WORKFLOW.md +431 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-UPDATE-ADVISOR.md +577 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL.md +273 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STEP-BY-STEP-GUIDE.md +466 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STRUCTURE.md +583 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/WEB-RESEARCH.md +526 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/reference.md +608 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/generate-structure.sh +328 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/validate-skill.sh +312 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/SKILL_TEMPLATE.md +245 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/examples-template.md +285 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/reference-template.md +278 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/scripts-template.sh +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/SKILL.md +291 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/templates/SKILL-template.md +15 -0
- moai_adk/templates/.claude/skills/moai-change-logger/SKILL.md +563 -0
- moai_adk/templates/.claude/skills/moai-design-systems/SKILL.md +802 -0
- moai_adk/templates/.claude/skills/moai-design-systems/examples.md +1238 -0
- moai_adk/templates/.claude/skills/moai-design-systems/reference.md +673 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +234 -43
- moai_adk/templates/.claude/skills/moai-domain-backend/examples.md +1633 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/reference.md +660 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/SKILL.md +97 -69
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/SKILL.md +97 -72
- moai_adk/templates/.claude/skills/moai-domain-data-science/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-domain-database/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-database/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-domain-devops/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +102 -73
- moai_adk/templates/.claude/skills/moai-domain-frontend/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-domain-ml/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/SKILL.md +97 -67
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-security/SKILL.md +97 -79
- moai_adk/templates/.claude/skills/moai-domain-security/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-security/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/SKILL.md +97 -71
- moai_adk/templates/.claude/skills/moai-domain-web-api/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/SKILL.md +265 -64
- moai_adk/templates/.claude/skills/moai-essentials-debug/examples.md +1064 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/reference.md +1047 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/SKILL.md +87 -78
- moai_adk/templates/.claude/skills/moai-essentials-perf/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/SKILL.md +87 -70
- moai_adk/templates/.claude/skills/moai-essentials-refactor/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/SKILL.md +87 -86
- moai_adk/templates/.claude/skills/moai-essentials-review/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/SKILL.md +80 -62
- moai_adk/templates/.claude/skills/moai-foundation-ears/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/SKILL.md +207 -50
- moai_adk/templates/.claude/skills/moai-foundation-git/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/reference.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/SKILL.md +90 -71
- moai_adk/templates/.claude/skills/moai-foundation-langs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/SKILL.md +78 -58
- moai_adk/templates/.claude/skills/moai-foundation-specs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/SKILL.md +78 -51
- moai_adk/templates/.claude/skills/moai-foundation-tags/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/.!11330!examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/SKILL.md +253 -32
- moai_adk/templates/.claude/skills/moai-foundation-trust/examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/reference.md +1099 -0
- moai_adk/templates/.claude/skills/moai-jit-docs-enhanced/SKILL.md +460 -0
- moai_adk/templates/.claude/skills/moai-lang-c/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-c/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-c/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/SKILL.md +98 -76
- moai_adk/templates/.claude/skills/moai-lang-cpp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/SKILL.md +2358 -70
- moai_adk/templates/.claude/skills/moai-lang-csharp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/SKILL.md +2962 -68
- moai_adk/templates/.claude/skills/moai-lang-dart/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +1898 -70
- moai_adk/templates/.claude/skills/moai-lang-go/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-go/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +1465 -68
- moai_adk/templates/.claude/skills/moai-lang-java/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-java/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/SKILL.md +2364 -66
- moai_adk/templates/.claude/skills/moai-lang-javascript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/reference.md +32 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/SKILL.md +1630 -69
- moai_adk/templates/.claude/skills/moai-lang-kotlin/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +89 -61
- moai_adk/templates/.claude/skills/moai-lang-php/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-php/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +735 -66
- moai_adk/templates/.claude/skills/moai-lang-python/examples.md +624 -0
- moai_adk/templates/.claude/skills/moai-lang-python/reference.md +316 -0
- moai_adk/templates/.claude/skills/moai-lang-r/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-lang-r/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-r/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/SKILL.md +98 -73
- moai_adk/templates/.claude/skills/moai-lang-ruby/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +1834 -70
- moai_adk/templates/.claude/skills/moai-lang-rust/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +99 -74
- moai_adk/templates/.claude/skills/moai-lang-scala/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-lang-shell/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-sql/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/SKILL.md +1959 -69
- moai_adk/templates/.claude/skills/moai-lang-swift/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-template/SKILL.md +348 -0
- moai_adk/templates/.claude/skills/moai-lang-template/VARIABLES.md +98 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +1230 -66
- moai_adk/templates/.claude/skills/moai-lang-typescript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/reference.md +34 -0
- moai_adk/templates/.claude/skills/moai-learning-optimizer/SKILL.md +575 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/README.md +50 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/SKILL.md +304 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/examples.md +417 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/reference.md +704 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/README.md +87 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/examples.md +1109 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/reference.md +514 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/validate.py +106 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/README.md +11 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/SKILL.md +622 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/examples.md +20 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/reference.md +12 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/README.md +152 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/SKILL.md +285 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/examples.md +333 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/reference.md +386 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/README.md +49 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/SKILL.md +319 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/examples.md +58 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/reference.md +123 -0
- moai_adk/templates/.claude/skills/moai-session-info/SKILL.md +314 -0
- moai_adk/templates/.claude/skills/moai-streaming-ui/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-tag-policy-validator/SKILL.md +570 -0
- moai_adk/templates/.git-hooks/pre-commit +66 -0
- moai_adk/templates/.git-hooks/pre-push +255 -0
- moai_adk/templates/.github/workflows/c-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/cpp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/csharp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/dart-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/go-tag-validation.yml +130 -0
- moai_adk/templates/.github/workflows/java-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/javascript-tag-validation.yml +135 -0
- moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/moai-gitflow.yml +166 -3
- moai_adk/templates/.github/workflows/moai-release-create.yml +100 -0
- moai_adk/templates/.github/workflows/moai-release-pipeline.yml +188 -0
- moai_adk/templates/.github/workflows/php-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/python-tag-validation.yml +118 -0
- moai_adk/templates/.github/workflows/release.yml +118 -0
- moai_adk/templates/.github/workflows/ruby-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/rust-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/shell-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/spec-issue-sync.yml +338 -0
- moai_adk/templates/.github/workflows/swift-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/tag-report.yml +269 -0
- moai_adk/templates/.github/workflows/tag-validation.yml +186 -0
- moai_adk/templates/.github/workflows/typescript-tag-validation.yml +154 -0
- moai_adk/templates/.mcp.json +31 -0
- moai_adk/templates/.moai/config.json +80 -7
- moai_adk/templates/CLAUDE.md +562 -546
- moai_adk/utils/banner.py +5 -5
- moai_adk/utils/common.py +294 -0
- moai_adk/utils/link_validator.py +235 -0
- moai_adk/utils/logger.py +8 -8
- moai_adk/utils/user_experience.py +451 -0
- moai_adk-0.20.1.dist-info/METADATA +233 -0
- moai_adk-0.20.1.dist-info/RECORD +404 -0
- moai_adk/templates/.claude/hooks/alfred/README.md +0 -230
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +0 -156
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +0 -85
- moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +0 -25
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +0 -92
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +0 -70
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +0 -41
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +0 -636
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +0 -692
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +0 -470
- moai_adk/templates/.claude/skills/moai-alfred-debugger-pro/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-ears-authoring/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-git-workflow/SKILL.md +0 -95
- moai_adk/templates/.claude/skills/moai-alfred-performance-optimizer/SKILL.md +0 -105
- moai_adk/templates/.claude/skills/moai-alfred-refactoring-coach/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-spec-metadata-validation/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-tag-scanning/SKILL.md +0 -90
- moai_adk/templates/.claude/skills/moai-alfred-trust-validation/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/SKILL.md +0 -87
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/examples.md +0 -62
- moai_adk/templates/.claude/skills/moai-claude-code/SKILL.md +0 -94
- moai_adk/templates/.claude/skills/moai-claude-code/examples.md +0 -513
- moai_adk/templates/.claude/skills/moai-claude-code/reference.md +0 -433
- moai_adk/templates/.claude/skills/moai-claude-code/templates/agent-full.md +0 -332
- moai_adk/templates/.claude/skills/moai-claude-code/templates/command-full.md +0 -384
- moai_adk/templates/.claude/skills/moai-claude-code/templates/plugin-full.json +0 -363
- moai_adk/templates/.claude/skills/moai-claude-code/templates/settings-full.json +0 -595
- moai_adk/templates/.claude/skills/moai-claude-code/templates/skill-full.md +0 -496
- moai_adk/templates/.claude/skills/moai-lang-clojure/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-lang-haskell/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-julia/SKILL.md +0 -98
- moai_adk/templates/.claude/skills/moai-lang-lua/SKILL.md +0 -98
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +0 -69
- moai_adk/templates/.moai/memory/development-guide.md +0 -344
- moai_adk/templates/.moai/memory/gitflow-protection-policy.md +0 -220
- moai_adk/templates/.moai/memory/spec-metadata.md +0 -356
- moai_adk/templates/.moai/project/product.md +0 -161
- moai_adk/templates/.moai/project/structure.md +0 -156
- moai_adk/templates/.moai/project/tech.md +0 -227
- moai_adk/templates/__init__.py +0 -2
- moai_adk-0.4.5.dist-info/METADATA +0 -369
- moai_adk-0.4.5.dist-info/RECORD +0 -152
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/WHEEL +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,96 +1,2394 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
2
|
name: moai-lang-javascript
|
|
4
|
-
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
created: 2025-11-06
|
|
5
|
+
updated: 2025-11-06
|
|
6
|
+
status: active
|
|
7
|
+
description: "JavaScript best practices with Node.js, modern frameworks, and ecosystem patterns for 2025"
|
|
8
|
+
keywords: [javascript, nodejs, frontend, backend, legacy, maintenance, modernization, browser]
|
|
5
9
|
allowed-tools:
|
|
6
10
|
- Read
|
|
11
|
+
- Write
|
|
12
|
+
- Edit
|
|
7
13
|
- Bash
|
|
14
|
+
- WebFetch
|
|
15
|
+
- WebSearch
|
|
8
16
|
---
|
|
9
17
|
|
|
10
|
-
# JavaScript
|
|
18
|
+
# JavaScript Development Mastery
|
|
19
|
+
|
|
20
|
+
**Modern JavaScript Development with 2025 Best Practices**
|
|
21
|
+
|
|
22
|
+
> Comprehensive JavaScript development guidance covering Node.js backend development, legacy browser support, modern framework integration, and progressive enhancement patterns using the latest tools and methodologies.
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
25
|
+
|
|
26
|
+
- **Node.js Backend Services**: Express, Koa, and modern server-side JavaScript
|
|
27
|
+
- **Legacy Browser Support**: Progressive enhancement and polyfill strategies
|
|
28
|
+
- **API Development**: REST, GraphQL, and real-time communication
|
|
29
|
+
- **CLI Tools & Automation**: Node.js-based developer utilities
|
|
30
|
+
- **Modern Build Systems**: Webpack, Vite, and bundling optimization
|
|
31
|
+
- **Testing & Quality**: Unit testing, integration testing, and code quality
|
|
32
|
+
- **Migration Strategies**: Legacy code modernization and TypeScript migration
|
|
33
|
+
|
|
34
|
+
## When to Use
|
|
11
35
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
### Perfect Scenarios
|
|
37
|
+
- **Legacy JavaScript application maintenance and modernization**
|
|
38
|
+
- **Node.js backend services and APIs**
|
|
39
|
+
- **CLI tools and developer utilities**
|
|
40
|
+
- **Progressive web applications requiring broad browser support**
|
|
41
|
+
- **Projects needing gradual TypeScript migration**
|
|
42
|
+
- **Serverless functions and edge computing**
|
|
43
|
+
- **Real-time applications with WebSockets**
|
|
19
44
|
|
|
20
|
-
|
|
45
|
+
### Common Triggers
|
|
46
|
+
- "Create Node.js API"
|
|
47
|
+
- "Modernize JavaScript legacy code"
|
|
48
|
+
- "Set up Express server"
|
|
49
|
+
- "JavaScript best practices"
|
|
50
|
+
- "Migrate to TypeScript"
|
|
51
|
+
- "Browser compatibility issues"
|
|
21
52
|
|
|
22
|
-
|
|
53
|
+
## Tool Version Matrix (2025-11-06)
|
|
23
54
|
|
|
24
|
-
|
|
55
|
+
### Core JavaScript
|
|
56
|
+
- **Node.js**: 22.x (LTS) / 20.x (Active LTS)
|
|
57
|
+
- **npm**: 10.x - Node package manager
|
|
58
|
+
- **Yarn**: 4.x - Alternative package manager
|
|
59
|
+
- **pnpm**: 9.x - Fast, disk space efficient package manager
|
|
25
60
|
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
61
|
+
### Backend Frameworks
|
|
62
|
+
- **Express**: 4.21.x / 5.1.x (new default) - Web framework
|
|
63
|
+
- **Koa**: 2.15.x - Next generation web framework
|
|
64
|
+
- **Fastify**: 5.x - Fast and low overhead web framework
|
|
65
|
+
- **NestJS**: 10.x - Progressive Node.js framework
|
|
66
|
+
- **Hapi**: 21.x - Rich framework for building applications
|
|
30
67
|
|
|
31
|
-
|
|
68
|
+
### Frontend Integration
|
|
69
|
+
- **React**: 19.x - UI library (for integration patterns)
|
|
70
|
+
- **Vue**: 3.5.x - Progressive framework
|
|
71
|
+
- **Webpack**: 5.x - Module bundler
|
|
72
|
+
- **Vite**: 6.x - Fast build tool
|
|
73
|
+
- **Rollup**: 4.x - Module bundler for libraries
|
|
32
74
|
|
|
33
|
-
|
|
34
|
-
- **
|
|
35
|
-
-
|
|
36
|
-
-
|
|
75
|
+
### Database & Storage
|
|
76
|
+
- **Prisma**: 5.22.x - Next-generation ORM
|
|
77
|
+
- **Sequelize**: 6.37.x - SQL ORM
|
|
78
|
+
- **Mongoose**: 8.8.x - MongoDB ODM
|
|
79
|
+
- **Redis**: 4.7.x - Redis client
|
|
80
|
+
- **LowDB**: 7.x - Small JSON database
|
|
37
81
|
|
|
38
|
-
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
82
|
+
### Testing Tools
|
|
83
|
+
- **Jest**: 30.x - JavaScript testing framework
|
|
84
|
+
- **Mocha**: 10.x - Feature-rich test framework
|
|
85
|
+
- **Chai**: 5.x - BDD/TDD assertion library
|
|
86
|
+
- **Supertest**: 7.x - HTTP assertion testing
|
|
87
|
+
- **Playwright**: 1.48.x - End-to-end testing
|
|
42
88
|
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
46
|
-
-
|
|
89
|
+
### Development Tools
|
|
90
|
+
- **ESLint**: 9.x - Pluggable linter
|
|
91
|
+
- **Prettier**: 3.3.x - Code formatter
|
|
92
|
+
- **Babel**: 7.26.x - JavaScript compiler
|
|
93
|
+
- **Nodemon**: 3.1.x - Monitor for changes and restart
|
|
94
|
+
- **PM2**: 5.4.x - Production process manager
|
|
47
95
|
|
|
48
|
-
|
|
49
|
-
- ES6+ features (arrow functions, destructuring, spread/rest)
|
|
50
|
-
- Async/await over callbacks
|
|
51
|
-
- Module imports (ESM) over CommonJS
|
|
96
|
+
## Ecosystem Overview
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
- File ≤300 LOC, function ≤50 LOC
|
|
55
|
-
- Prefer `const` over `let`, avoid `var`
|
|
56
|
-
- Guard clauses for early returns
|
|
57
|
-
- Meaningful names, avoid abbreviations
|
|
98
|
+
### Project Setup (2025 Best Practice)
|
|
58
99
|
|
|
59
|
-
## Examples
|
|
60
100
|
```bash
|
|
61
|
-
|
|
101
|
+
# Modern Node.js project with package.json
|
|
102
|
+
npm init -y
|
|
103
|
+
npm install express helmet cors compression morgan dotenv
|
|
104
|
+
npm install -D nodemon jest supertest eslint prettier concurrently
|
|
105
|
+
|
|
106
|
+
# Project structure
|
|
107
|
+
mkdir -p {src/{routes,middleware,services,utils,models},test/{unit,integration},config,scripts}
|
|
108
|
+
|
|
109
|
+
# TypeScript support for gradual migration
|
|
110
|
+
npm install -D typescript @types/node @types/express ts-node
|
|
111
|
+
|
|
112
|
+
# Modern build setup
|
|
113
|
+
npm install -D vite @vitejs/plugin-node
|
|
62
114
|
```
|
|
63
115
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
116
|
+
### Modern Project Structure
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
my-javascript-project/
|
|
120
|
+
├── package.json # Package configuration
|
|
121
|
+
├── package-lock.json # Lock file
|
|
122
|
+
├── .eslintrc.js # ESLint configuration
|
|
123
|
+
├── .prettierrc # Prettier configuration
|
|
124
|
+
├── .gitignore
|
|
125
|
+
├── README.md
|
|
126
|
+
├── jest.config.js # Jest testing configuration
|
|
127
|
+
├── nodemon.json # Nodemon configuration
|
|
128
|
+
├── src/
|
|
129
|
+
│ ├── app.js # Application entry point
|
|
130
|
+
│ ├── routes/ # API routes
|
|
131
|
+
│ │ ├── index.js
|
|
132
|
+
│ │ ├── users.js
|
|
133
|
+
│ │ └── auth.js
|
|
134
|
+
│ ├── middleware/ # Express middleware
|
|
135
|
+
│ │ ├── auth.js
|
|
136
|
+
│ │ ├── validation.js
|
|
137
|
+
│ │ └── errorHandler.js
|
|
138
|
+
│ ├── services/ # Business logic
|
|
139
|
+
│ │ ├── userService.js
|
|
140
|
+
│ │ ├── emailService.js
|
|
141
|
+
│ │ └── cacheService.js
|
|
142
|
+
│ ├── models/ # Data models
|
|
143
|
+
│ │ ├── User.js
|
|
144
|
+
│ │ └── index.js
|
|
145
|
+
│ ├── utils/ # Utility functions
|
|
146
|
+
│ │ ├── logger.js
|
|
147
|
+
│ │ ├── validator.js
|
|
148
|
+
│ │ └── helpers.js
|
|
149
|
+
│ └── config/ # Configuration
|
|
150
|
+
│ ├── database.js
|
|
151
|
+
│ └── index.js
|
|
152
|
+
├── test/
|
|
153
|
+
│ ├── unit/ # Unit tests
|
|
154
|
+
│ ├── integration/ # Integration tests
|
|
155
|
+
│ └── fixtures/ # Test data
|
|
156
|
+
├── scripts/ # Build and utility scripts
|
|
157
|
+
├── docs/ # Documentation
|
|
158
|
+
└── dist/ # Build output
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Modern JavaScript Patterns
|
|
162
|
+
|
|
163
|
+
### Async Programming with Modern Patterns
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
// src/services/userService.js
|
|
167
|
+
const { promisify } = require('util');
|
|
168
|
+
const crypto = require('crypto');
|
|
169
|
+
const EventEmitter = require('events');
|
|
170
|
+
|
|
171
|
+
class UserService extends EventEmitter {
|
|
172
|
+
constructor(database) {
|
|
173
|
+
super();
|
|
174
|
+
this.db = database;
|
|
175
|
+
this.cache = new Map();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Async/await pattern for database operations
|
|
179
|
+
async createUser(userData) {
|
|
180
|
+
try {
|
|
181
|
+
// Validate input
|
|
182
|
+
this.validateUserData(userData);
|
|
183
|
+
|
|
184
|
+
// Hash password
|
|
185
|
+
const hashedPassword = await this.hashPassword(userData.password);
|
|
186
|
+
|
|
187
|
+
// Create user object
|
|
188
|
+
const user = {
|
|
189
|
+
id: crypto.randomUUID(),
|
|
190
|
+
email: userData.email,
|
|
191
|
+
name: userData.name,
|
|
192
|
+
password: hashedPassword,
|
|
193
|
+
createdAt: new Date(),
|
|
194
|
+
updatedAt: new Date()
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Save to database
|
|
198
|
+
const savedUser = await this.db.collection('users').insertOne(user);
|
|
199
|
+
|
|
200
|
+
// Emit event
|
|
201
|
+
this.emit('userCreated', savedUser);
|
|
202
|
+
|
|
203
|
+
// Remove password before returning
|
|
204
|
+
const { password, ...userResponse } = savedUser;
|
|
205
|
+
|
|
206
|
+
return userResponse;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.emit('error', error);
|
|
209
|
+
throw new Error(`Failed to create user: ${error.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async getUserById(userId) {
|
|
214
|
+
// Check cache first
|
|
215
|
+
if (this.cache.has(userId)) {
|
|
216
|
+
return this.cache.get(userId);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const user = await this.db.collection('users').findOne({ id: userId });
|
|
221
|
+
|
|
222
|
+
if (!user) {
|
|
223
|
+
throw new Error('User not found');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Cache result for 5 minutes
|
|
227
|
+
this.cache.set(userId, user);
|
|
228
|
+
setTimeout(() => this.cache.delete(userId), 5 * 60 * 1000);
|
|
229
|
+
|
|
230
|
+
const { password, ...userResponse } = user;
|
|
231
|
+
return userResponse;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.emit('error', error);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Batch processing with Promise.allSettled
|
|
239
|
+
async processUsersBatch(userIds) {
|
|
240
|
+
const userPromises = userIds.map(id =>
|
|
241
|
+
this.getUserById(id).catch(error => ({ id, error: error.message }))
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const results = await Promise.allSettled(userPromises);
|
|
245
|
+
|
|
246
|
+
return results.map((result, index) => ({
|
|
247
|
+
userId: userIds[index],
|
|
248
|
+
status: result.status,
|
|
249
|
+
value: result.status === 'fulfilled' ? result.value : null,
|
|
250
|
+
reason: result.status === 'rejected' ? result.reason : null
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Async iterator pattern for streaming large datasets
|
|
255
|
+
async *getAllUsersStream() {
|
|
256
|
+
let cursor;
|
|
257
|
+
try {
|
|
258
|
+
cursor = await this.db.collection('users').find();
|
|
259
|
+
|
|
260
|
+
while (await cursor.hasNext()) {
|
|
261
|
+
const user = await cursor.next();
|
|
262
|
+
const { password, ...userResponse } = user;
|
|
263
|
+
yield userResponse;
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
if (cursor) {
|
|
267
|
+
await cursor.close();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Promise-based helper methods
|
|
273
|
+
async hashPassword(password) {
|
|
274
|
+
const scrypt = promisify(crypto.scrypt);
|
|
275
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
276
|
+
const derivedKey = await scrypt(password, salt, 64);
|
|
277
|
+
return `${salt}:${derivedKey.toString('hex')}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
validateUserData(userData) {
|
|
281
|
+
const errors = [];
|
|
282
|
+
|
|
283
|
+
if (!userData.email || !this.isValidEmail(userData.email)) {
|
|
284
|
+
errors.push('Valid email is required');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!userData.name || userData.name.length < 2) {
|
|
288
|
+
errors.push('Name must be at least 2 characters');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!userData.password || userData.password.length < 8) {
|
|
292
|
+
errors.push('Password must be at least 8 characters');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (errors.length > 0) {
|
|
296
|
+
throw new Error(errors.join(', '));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
isValidEmail(email) {
|
|
301
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
302
|
+
return emailRegex.test(email);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = UserService;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Express.js Application with Modern Middleware
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
// src/app.js
|
|
313
|
+
const express = require('express');
|
|
314
|
+
const helmet = require('helmet');
|
|
315
|
+
const cors = require('cors');
|
|
316
|
+
const compression = require('compression');
|
|
317
|
+
const morgan = require('morgan');
|
|
318
|
+
const rateLimit = require('express-rate-limit');
|
|
319
|
+
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
320
|
+
|
|
321
|
+
const userRoutes = require('./routes/users');
|
|
322
|
+
const authRoutes = require('./routes/auth');
|
|
323
|
+
const errorHandler = require('./middleware/errorHandler');
|
|
324
|
+
const logger = require('./utils/logger');
|
|
325
|
+
const config = require('./config');
|
|
326
|
+
|
|
327
|
+
const app = express();
|
|
328
|
+
|
|
329
|
+
// Security middleware
|
|
330
|
+
app.use(helmet({
|
|
331
|
+
contentSecurityPolicy: {
|
|
332
|
+
directives: {
|
|
333
|
+
defaultSrc: ["'self'"],
|
|
334
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
335
|
+
scriptSrc: ["'self'"],
|
|
336
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
// CORS configuration
|
|
342
|
+
app.use(cors({
|
|
343
|
+
origin: config.cors.origins,
|
|
344
|
+
credentials: true,
|
|
345
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
346
|
+
allowedHeaders: ['Content-Type', 'Authorization']
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
// Rate limiting
|
|
350
|
+
const limiter = rateLimit({
|
|
351
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
352
|
+
max: config.rateLimit.max, // limit each IP to 100 requests per windowMs
|
|
353
|
+
message: {
|
|
354
|
+
error: 'Too many requests from this IP, please try again later.'
|
|
355
|
+
},
|
|
356
|
+
standardHeaders: true,
|
|
357
|
+
legacyHeaders: false,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
app.use('/api/', limiter);
|
|
361
|
+
|
|
362
|
+
// Body parsing middleware
|
|
363
|
+
app.use(express.json({
|
|
364
|
+
limit: '10mb',
|
|
365
|
+
verify: (req, res, buf) => {
|
|
366
|
+
req.rawBody = buf;
|
|
367
|
+
}
|
|
368
|
+
}));
|
|
369
|
+
|
|
370
|
+
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
371
|
+
|
|
372
|
+
// Compression middleware
|
|
373
|
+
app.use(compression());
|
|
374
|
+
|
|
375
|
+
// Logging middleware
|
|
376
|
+
app.use(morgan('combined', {
|
|
377
|
+
stream: {
|
|
378
|
+
write: (message) => logger.info(message.trim())
|
|
379
|
+
}
|
|
380
|
+
}));
|
|
381
|
+
|
|
382
|
+
// Health check endpoint
|
|
383
|
+
app.get('/health', (req, res) => {
|
|
384
|
+
res.status(200).json({
|
|
385
|
+
status: 'healthy',
|
|
386
|
+
timestamp: new Date().toISOString(),
|
|
387
|
+
uptime: process.uptime(),
|
|
388
|
+
version: process.env.npm_package_version
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// API routes
|
|
393
|
+
app.use('/api/users', userRoutes);
|
|
394
|
+
app.use('/api/auth', authRoutes);
|
|
395
|
+
|
|
396
|
+
// API documentation proxy (if using Swagger)
|
|
397
|
+
if (config.env === 'development') {
|
|
398
|
+
app.use('/api-docs', createProxyMiddleware({
|
|
399
|
+
target: 'http://localhost:3001',
|
|
400
|
+
changeOrigin: true,
|
|
401
|
+
pathRewrite: {
|
|
402
|
+
'^/api-docs': ''
|
|
403
|
+
}
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 404 handler
|
|
408
|
+
app.use('*', (req, res) => {
|
|
409
|
+
res.status(404).json({
|
|
410
|
+
error: 'Route not found',
|
|
411
|
+
path: req.originalUrl,
|
|
412
|
+
method: req.method
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Error handling middleware (must be last)
|
|
417
|
+
app.use(errorHandler);
|
|
418
|
+
|
|
419
|
+
module.exports = app;
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Modern Route Handlers
|
|
423
|
+
|
|
424
|
+
```javascript
|
|
425
|
+
// src/routes/users.js
|
|
426
|
+
const express = require('express');
|
|
427
|
+
const { body, param, query, validationResult } = require('express-validator');
|
|
428
|
+
const UserService = require('../services/userService');
|
|
429
|
+
const auth = require('../middleware/auth');
|
|
430
|
+
const logger = require('../utils/logger');
|
|
431
|
+
|
|
432
|
+
const router = express.Router();
|
|
433
|
+
const userService = new UserService(database); // Assuming database is available
|
|
434
|
+
|
|
435
|
+
// Validation middleware
|
|
436
|
+
const handleValidationErrors = (req, res, next) => {
|
|
437
|
+
const errors = validationResult(req);
|
|
438
|
+
if (!errors.isEmpty()) {
|
|
439
|
+
return res.status(400).json({
|
|
440
|
+
error: 'Validation failed',
|
|
441
|
+
details: errors.array()
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
next();
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// POST /api/users - Create user
|
|
448
|
+
router.post('/',
|
|
449
|
+
[
|
|
450
|
+
body('email').isEmail().normalizeEmail(),
|
|
451
|
+
body('name').isLength({ min: 2, max: 100 }).trim().escape(),
|
|
452
|
+
body('password').isLength({ min: 8 }).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
|
|
453
|
+
],
|
|
454
|
+
handleValidationErrors,
|
|
455
|
+
async (req, res, next) => {
|
|
456
|
+
try {
|
|
457
|
+
const user = await userService.createUser(req.body);
|
|
458
|
+
|
|
459
|
+
res.status(201).json({
|
|
460
|
+
success: true,
|
|
461
|
+
data: user,
|
|
462
|
+
message: 'User created successfully'
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
logger.info(`User created: ${user.id}`);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
next(error);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
// GET /api/users - Get users with pagination and filtering
|
|
473
|
+
router.get('/',
|
|
474
|
+
[
|
|
475
|
+
query('page').optional().isInt({ min: 1 }).toInt(),
|
|
476
|
+
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
|
|
477
|
+
query('search').optional().isLength({ min: 1, max: 100 }).trim().escape(),
|
|
478
|
+
],
|
|
479
|
+
handleValidationErrors,
|
|
480
|
+
async (req, res, next) => {
|
|
481
|
+
try {
|
|
482
|
+
const { page = 1, limit = 10, search } = req.query;
|
|
483
|
+
const skip = (page - 1) * limit;
|
|
484
|
+
|
|
485
|
+
// Build filter
|
|
486
|
+
const filter = {};
|
|
487
|
+
if (search) {
|
|
488
|
+
filter.$or = [
|
|
489
|
+
{ name: { $regex: search, $options: 'i' } },
|
|
490
|
+
{ email: { $regex: search, $options: 'i' } }
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Get users
|
|
495
|
+
const users = await userService.getUsers(filter, { skip, limit });
|
|
496
|
+
const total = await userService.countUsers(filter);
|
|
497
|
+
|
|
498
|
+
res.json({
|
|
499
|
+
success: true,
|
|
500
|
+
data: {
|
|
501
|
+
users,
|
|
502
|
+
pagination: {
|
|
503
|
+
page,
|
|
504
|
+
limit,
|
|
505
|
+
total,
|
|
506
|
+
pages: Math.ceil(total / limit)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
} catch (error) {
|
|
511
|
+
next(error);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// GET /api/users/:id - Get user by ID
|
|
517
|
+
router.get('/:id',
|
|
518
|
+
[
|
|
519
|
+
param('id').isUUID().withMessage('Invalid user ID format')
|
|
520
|
+
],
|
|
521
|
+
handleValidationErrors,
|
|
522
|
+
async (req, res, next) => {
|
|
523
|
+
try {
|
|
524
|
+
const user = await userService.getUserById(req.params.id);
|
|
525
|
+
|
|
526
|
+
if (!user) {
|
|
527
|
+
return res.status(404).json({
|
|
528
|
+
error: 'User not found',
|
|
529
|
+
userId: req.params.id
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
res.json({
|
|
534
|
+
success: true,
|
|
535
|
+
data: user
|
|
536
|
+
});
|
|
537
|
+
} catch (error) {
|
|
538
|
+
next(error);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// PUT /api/users/:id - Update user
|
|
544
|
+
router.put('/:id',
|
|
545
|
+
auth.authenticate,
|
|
546
|
+
[
|
|
547
|
+
param('id').isUUID().withMessage('Invalid user ID format'),
|
|
548
|
+
body('name').optional().isLength({ min: 2, max: 100 }).trim().escape(),
|
|
549
|
+
body('email').optional().isEmail().normalizeEmail(),
|
|
550
|
+
],
|
|
551
|
+
handleValidationErrors,
|
|
552
|
+
async (req, res, next) => {
|
|
553
|
+
try {
|
|
554
|
+
// Check if user is updating their own profile or is admin
|
|
555
|
+
if (req.user.id !== req.params.id && req.user.role !== 'admin') {
|
|
556
|
+
return res.status(403).json({
|
|
557
|
+
error: 'Forbidden: You can only update your own profile'
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const updateData = {};
|
|
562
|
+
if (req.body.name) updateData.name = req.body.name;
|
|
563
|
+
if (req.body.email) updateData.email = req.body.email;
|
|
564
|
+
|
|
565
|
+
const user = await userService.updateUser(req.params.id, updateData);
|
|
566
|
+
|
|
567
|
+
res.json({
|
|
568
|
+
success: true,
|
|
569
|
+
data: user,
|
|
570
|
+
message: 'User updated successfully'
|
|
571
|
+
});
|
|
572
|
+
} catch (error) {
|
|
573
|
+
next(error);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// DELETE /api/users/:id - Delete user
|
|
579
|
+
router.delete('/:id',
|
|
580
|
+
auth.authenticate,
|
|
581
|
+
auth.requireRole('admin'),
|
|
582
|
+
[
|
|
583
|
+
param('id').isUUID().withMessage('Invalid user ID format')
|
|
584
|
+
],
|
|
585
|
+
handleValidationErrors,
|
|
586
|
+
async (req, res, next) => {
|
|
587
|
+
try {
|
|
588
|
+
await userService.deleteUser(req.params.id);
|
|
589
|
+
|
|
590
|
+
res.json({
|
|
591
|
+
success: true,
|
|
592
|
+
message: 'User deleted successfully'
|
|
593
|
+
});
|
|
594
|
+
} catch (error) {
|
|
595
|
+
next(error);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
module.exports = router;
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Performance Optimization
|
|
604
|
+
|
|
605
|
+
### Caching and Memory Management
|
|
606
|
+
|
|
607
|
+
```javascript
|
|
608
|
+
// src/services/cacheService.js
|
|
609
|
+
const NodeCache = require('node-cache');
|
|
610
|
+
const redis = require('redis');
|
|
611
|
+
const crypto = require('crypto');
|
|
612
|
+
|
|
613
|
+
class CacheService {
|
|
614
|
+
constructor(options = {}) {
|
|
615
|
+
// In-memory cache for frequently accessed data
|
|
616
|
+
this.memoryCache = new NodeCache({
|
|
617
|
+
stdTTL: options.memoryTTL || 300, // 5 minutes
|
|
618
|
+
checkperiod: options.checkPeriod || 60, // 1 minute
|
|
619
|
+
useClones: false // Improve performance for objects
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Redis cache for distributed caching
|
|
623
|
+
if (options.redis) {
|
|
624
|
+
this.redisClient = redis.createClient(options.redis);
|
|
625
|
+
this.redisClient.on('error', (err) => {
|
|
626
|
+
console.error('Redis client error:', err);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Multi-level caching
|
|
632
|
+
async get(key) {
|
|
633
|
+
// Check memory cache first
|
|
634
|
+
let value = this.memoryCache.get(key);
|
|
635
|
+
if (value !== undefined) {
|
|
636
|
+
return value;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Check Redis cache
|
|
640
|
+
if (this.redisClient) {
|
|
641
|
+
try {
|
|
642
|
+
value = await this.redisClient.get(key);
|
|
643
|
+
if (value) {
|
|
644
|
+
const parsed = JSON.parse(value);
|
|
645
|
+
// Store in memory cache for faster access
|
|
646
|
+
this.memoryCache.set(key, parsed);
|
|
647
|
+
return parsed;
|
|
648
|
+
}
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.error('Redis get error:', error);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async set(key, value, options = {}) {
|
|
658
|
+
const serialized = JSON.stringify(value);
|
|
659
|
+
|
|
660
|
+
// Set in memory cache
|
|
661
|
+
this.memoryCache.set(key, value, options.ttl);
|
|
662
|
+
|
|
663
|
+
// Set in Redis cache
|
|
664
|
+
if (this.redisClient) {
|
|
665
|
+
try {
|
|
666
|
+
await this.redisClient.setEx(key, options.ttl || 300, serialized);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error('Redis set error:', error);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Cache warming
|
|
674
|
+
async warmup(dataLoader, keys) {
|
|
675
|
+
const promises = keys.map(async (key) => {
|
|
676
|
+
const value = await dataLoader(key);
|
|
677
|
+
await this.set(key, value);
|
|
678
|
+
return { key, value };
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
return Promise.all(promises);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Cache invalidation with tags
|
|
685
|
+
async invalidateByPattern(pattern) {
|
|
686
|
+
// Invalidate memory cache
|
|
687
|
+
const keys = this.memoryCache.keys();
|
|
688
|
+
const regex = new RegExp(pattern);
|
|
689
|
+
|
|
690
|
+
keys.forEach(key => {
|
|
691
|
+
if (regex.test(key)) {
|
|
692
|
+
this.memoryCache.del(key);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Invalidate Redis cache
|
|
697
|
+
if (this.redisClient) {
|
|
698
|
+
try {
|
|
699
|
+
const redisKeys = await this.redisClient.keys(pattern);
|
|
700
|
+
if (redisKeys.length > 0) {
|
|
701
|
+
await this.redisClient.del(redisKeys);
|
|
702
|
+
}
|
|
703
|
+
} catch (error) {
|
|
704
|
+
console.error('Redis pattern deletion error:', error);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
generateCacheKey(prefix, params) {
|
|
710
|
+
const sortedParams = Object.keys(params)
|
|
711
|
+
.sort()
|
|
712
|
+
.reduce((result, key) => {
|
|
713
|
+
result[key] = params[key];
|
|
714
|
+
return result;
|
|
715
|
+
}, {});
|
|
716
|
+
|
|
717
|
+
const paramString = JSON.stringify(sortedParams);
|
|
718
|
+
const hash = crypto.createHash('md5').update(paramString).digest('hex');
|
|
719
|
+
|
|
720
|
+
return `${prefix}:${hash}`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Memory leak prevention
|
|
724
|
+
cleanup() {
|
|
725
|
+
this.memoryCache.flushAll();
|
|
726
|
+
|
|
727
|
+
if (this.redisClient) {
|
|
728
|
+
this.redisClient.quit();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
module.exports = CacheService;
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### Memory-Efficient Data Processing
|
|
737
|
+
|
|
738
|
+
```javascript
|
|
739
|
+
// src/utils/streamProcessor.js
|
|
740
|
+
const { Transform, pipeline } = require('stream');
|
|
741
|
+
const fs = require('fs');
|
|
742
|
+
const readline = require('readline');
|
|
743
|
+
const { promisify } = require('util');
|
|
744
|
+
|
|
745
|
+
const pipelineAsync = promisify(pipeline);
|
|
746
|
+
|
|
747
|
+
class StreamProcessor {
|
|
748
|
+
// Process large files without loading into memory
|
|
749
|
+
static async processLargeFile(filePath, processor) {
|
|
750
|
+
const fileStream = fs.createReadStream(filePath);
|
|
751
|
+
const rl = readline.createInterface({
|
|
752
|
+
input: fileStream,
|
|
753
|
+
crlfDelay: Infinity
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
let lineNumber = 0;
|
|
757
|
+
const results = [];
|
|
758
|
+
|
|
759
|
+
for await (const line of rl) {
|
|
760
|
+
try {
|
|
761
|
+
const result = await processor(line, lineNumber);
|
|
762
|
+
if (result !== null) {
|
|
763
|
+
results.push(result);
|
|
764
|
+
}
|
|
765
|
+
} catch (error) {
|
|
766
|
+
console.error(`Error processing line ${lineNumber}:`, error);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
lineNumber++;
|
|
770
|
+
|
|
771
|
+
// Yield control periodically to prevent blocking
|
|
772
|
+
if (lineNumber % 1000 === 0) {
|
|
773
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return results;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Transform stream for data processing
|
|
781
|
+
static createTransformStream(processor) {
|
|
782
|
+
return new Transform({
|
|
783
|
+
objectMode: true,
|
|
784
|
+
transform(chunk, encoding, callback) {
|
|
785
|
+
try {
|
|
786
|
+
const result = processor(chunk);
|
|
787
|
+
callback(null, result);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
callback(error);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Batch processing with memory management
|
|
796
|
+
static async* batchProcessor(data, batchSize = 100) {
|
|
797
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
798
|
+
const batch = data.slice(i, i + batchSize);
|
|
799
|
+
yield batch;
|
|
800
|
+
|
|
801
|
+
// Force garbage collection every 10 batches
|
|
802
|
+
if (i % (batchSize * 10) === 0) {
|
|
803
|
+
if (global.gc) {
|
|
804
|
+
global.gc();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Memory-efficient CSV processing
|
|
811
|
+
static async processCSV(filePath, options = {}) {
|
|
812
|
+
const { transform, filter, batchSize = 1000 } = options;
|
|
813
|
+
const results = [];
|
|
814
|
+
let currentBatch = [];
|
|
815
|
+
|
|
816
|
+
return new Promise((resolve, reject) => {
|
|
817
|
+
const fileStream = fs.createReadStream(filePath);
|
|
818
|
+
const rl = readline.createInterface({
|
|
819
|
+
input: fileStream,
|
|
820
|
+
crlfDelay: Infinity
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
let lineNumber = 0;
|
|
824
|
+
let headers = [];
|
|
825
|
+
|
|
826
|
+
rl.on('line', async (line) => {
|
|
827
|
+
try {
|
|
828
|
+
const values = line.split(',');
|
|
829
|
+
|
|
830
|
+
if (lineNumber === 0) {
|
|
831
|
+
headers = values;
|
|
832
|
+
} else {
|
|
833
|
+
const record = {};
|
|
834
|
+
headers.forEach((header, index) => {
|
|
835
|
+
record[header.trim()] = values[index]?.trim() || '';
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Apply filter if provided
|
|
839
|
+
if (filter && !filter(record)) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Apply transform if provided
|
|
844
|
+
const processedRecord = transform ? transform(record) : record;
|
|
845
|
+
|
|
846
|
+
currentBatch.push(processedRecord);
|
|
847
|
+
|
|
848
|
+
// Process batch when full
|
|
849
|
+
if (currentBatch.length >= batchSize) {
|
|
850
|
+
results.push(...currentBatch);
|
|
851
|
+
currentBatch = [];
|
|
852
|
+
|
|
853
|
+
// Yield control
|
|
854
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
} catch (error) {
|
|
858
|
+
console.error(`Error processing line ${lineNumber}:`, error);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
lineNumber++;
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
rl.on('close', () => {
|
|
865
|
+
// Process remaining records
|
|
866
|
+
results.push(...currentBatch);
|
|
867
|
+
resolve(results);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
rl.on('error', reject);
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
module.exports = StreamProcessor;
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
## Testing Strategies
|
|
879
|
+
|
|
880
|
+
### Comprehensive Unit Testing
|
|
881
|
+
|
|
882
|
+
```javascript
|
|
883
|
+
// test/unit/userService.test.js
|
|
884
|
+
const UserService = require('../../src/services/userService');
|
|
885
|
+
const { EventEmitter } = require('events');
|
|
886
|
+
|
|
887
|
+
// Mock database
|
|
888
|
+
const mockDatabase = {
|
|
889
|
+
collection: jest.fn(() => ({
|
|
890
|
+
insertOne: jest.fn(),
|
|
891
|
+
findOne: jest.fn(),
|
|
892
|
+
find: jest.fn(),
|
|
893
|
+
updateOne: jest.fn(),
|
|
894
|
+
deleteOne: jest.fn(),
|
|
895
|
+
}))
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
describe('UserService', () => {
|
|
899
|
+
let userService;
|
|
900
|
+
|
|
901
|
+
beforeEach(() => {
|
|
902
|
+
userService = new UserService(mockDatabase);
|
|
903
|
+
jest.clearAllMocks();
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
describe('createUser', () => {
|
|
907
|
+
it('should create a user with valid data', async () => {
|
|
908
|
+
const userData = {
|
|
909
|
+
email: 'test@example.com',
|
|
910
|
+
name: 'Test User',
|
|
911
|
+
password: 'Password123'
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
const mockUser = {
|
|
915
|
+
id: 'user-123',
|
|
916
|
+
email: userData.email,
|
|
917
|
+
name: userData.name,
|
|
918
|
+
password: 'hashed-password',
|
|
919
|
+
createdAt: new Date(),
|
|
920
|
+
updatedAt: new Date()
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
mockDatabase.collection().insertOne.mockResolvedValue(mockUser);
|
|
924
|
+
|
|
925
|
+
const result = await userService.createUser(userData);
|
|
926
|
+
|
|
927
|
+
expect(result).toEqual({
|
|
928
|
+
id: mockUser.id,
|
|
929
|
+
email: mockUser.email,
|
|
930
|
+
name: mockUser.name,
|
|
931
|
+
createdAt: mockUser.createdAt,
|
|
932
|
+
updatedAt: mockUser.updatedAt
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
expect(mockDatabase.collection).toHaveBeenCalledWith('users');
|
|
936
|
+
expect(mockDatabase.collection().insertOne).toHaveBeenCalledWith(
|
|
937
|
+
expect.objectContaining({
|
|
938
|
+
email: userData.email,
|
|
939
|
+
name: userData.name,
|
|
940
|
+
password: expect.stringMatching(/^[a-f0-9]+:.+$/)
|
|
941
|
+
})
|
|
942
|
+
);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('should reject invalid email', async () => {
|
|
946
|
+
const userData = {
|
|
947
|
+
email: 'invalid-email',
|
|
948
|
+
name: 'Test User',
|
|
949
|
+
password: 'Password123'
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
await expect(userService.createUser(userData))
|
|
953
|
+
.rejects.toThrow('Valid email is required');
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('should reject short password', async () => {
|
|
957
|
+
const userData = {
|
|
958
|
+
email: 'test@example.com',
|
|
959
|
+
name: 'Test User',
|
|
960
|
+
password: 'short'
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
await expect(userService.createUser(userData))
|
|
964
|
+
.rejects.toThrow('Password must be at least 8 characters');
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('should emit userCreated event', async () => {
|
|
968
|
+
const userData = {
|
|
969
|
+
email: 'test@example.com',
|
|
970
|
+
name: 'Test User',
|
|
971
|
+
password: 'Password123'
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
const mockUser = {
|
|
975
|
+
id: 'user-123',
|
|
976
|
+
email: userData.email,
|
|
977
|
+
name: userData.name,
|
|
978
|
+
password: 'hashed-password',
|
|
979
|
+
createdAt: new Date(),
|
|
980
|
+
updatedAt: new Date()
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
mockDatabase.collection().insertOne.mockResolvedValue(mockUser);
|
|
984
|
+
|
|
985
|
+
const eventSpy = jest.fn();
|
|
986
|
+
userService.on('userCreated', eventSpy);
|
|
987
|
+
|
|
988
|
+
await userService.createUser(userData);
|
|
989
|
+
|
|
990
|
+
expect(eventSpy).toHaveBeenCalledWith(mockUser);
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
describe('getUserById', () => {
|
|
995
|
+
it('should return user when found', async () => {
|
|
996
|
+
const userId = 'user-123';
|
|
997
|
+
const mockUser = {
|
|
998
|
+
id: userId,
|
|
999
|
+
email: 'test@example.com',
|
|
1000
|
+
name: 'Test User',
|
|
1001
|
+
password: 'hashed-password'
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
mockDatabase.collection().findOne.mockResolvedValue(mockUser);
|
|
1005
|
+
|
|
1006
|
+
const result = await userService.getUserById(userId);
|
|
1007
|
+
|
|
1008
|
+
expect(result).toEqual({
|
|
1009
|
+
id: mockUser.id,
|
|
1010
|
+
email: mockUser.email,
|
|
1011
|
+
name: mockUser.name
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('should return null when user not found', async () => {
|
|
1016
|
+
const userId = 'nonexistent-user';
|
|
1017
|
+
|
|
1018
|
+
mockDatabase.collection().findOne.mockResolvedValue(null);
|
|
1019
|
+
|
|
1020
|
+
const result = await userService.getUserById(userId);
|
|
1021
|
+
|
|
1022
|
+
expect(result).toBeNull();
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it('should use cache for subsequent calls', async () => {
|
|
1026
|
+
const userId = 'user-123';
|
|
1027
|
+
const mockUser = {
|
|
1028
|
+
id: userId,
|
|
1029
|
+
email: 'test@example.com',
|
|
1030
|
+
name: 'Test User',
|
|
1031
|
+
password: 'hashed-password'
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
mockDatabase.collection().findOne.mockResolvedValue(mockUser);
|
|
1035
|
+
|
|
1036
|
+
// First call
|
|
1037
|
+
await userService.getUserById(userId);
|
|
1038
|
+
// Second call (should use cache)
|
|
1039
|
+
await userService.getUserById(userId);
|
|
1040
|
+
|
|
1041
|
+
// Database should be called only once
|
|
1042
|
+
expect(mockDatabase.collection().findOne).toHaveBeenCalledTimes(1);
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
describe('processUsersBatch', () => {
|
|
1047
|
+
it('should process users successfully', async () => {
|
|
1048
|
+
const userIds = ['user-1', 'user-2', 'user-3'];
|
|
1049
|
+
|
|
1050
|
+
mockDatabase.collection().findOne.mockImplementation((query) => {
|
|
1051
|
+
const user = {
|
|
1052
|
+
id: query.id,
|
|
1053
|
+
email: `${query.id}@example.com`,
|
|
1054
|
+
name: `User ${query.id}`,
|
|
1055
|
+
password: 'hashed-password'
|
|
1056
|
+
};
|
|
1057
|
+
return Promise.resolve(user);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
const results = await userService.processUsersBatch(userIds);
|
|
1061
|
+
|
|
1062
|
+
expect(results).toHaveLength(3);
|
|
1063
|
+
expect(results[0]).toEqual({
|
|
1064
|
+
userId: 'user-1',
|
|
1065
|
+
status: 'fulfilled',
|
|
1066
|
+
value: expect.objectContaining({ id: 'user-1' }),
|
|
1067
|
+
reason: null
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('should handle errors gracefully', async () => {
|
|
1072
|
+
const userIds = ['user-1', 'invalid-user'];
|
|
1073
|
+
|
|
1074
|
+
mockDatabase.collection().findOne.mockImplementation((query) => {
|
|
1075
|
+
if (query.id === 'invalid-user') {
|
|
1076
|
+
return Promise.reject(new Error('Database error'));
|
|
1077
|
+
}
|
|
1078
|
+
return Promise.resolve({
|
|
1079
|
+
id: query.id,
|
|
1080
|
+
email: `${query.id}@example.com`,
|
|
1081
|
+
name: `User ${query.id}`,
|
|
1082
|
+
password: 'hashed-password'
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const results = await userService.processUsersBatch(userIds);
|
|
1087
|
+
|
|
1088
|
+
expect(results).toHaveLength(2);
|
|
1089
|
+
expect(results[0].status).toBe('fulfilled');
|
|
1090
|
+
expect(results[1].status).toBe('rejected');
|
|
1091
|
+
expect(results[1].reason).toContain('Database error');
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
describe('getAllUsersStream', () => {
|
|
1096
|
+
it('should stream users one by one', async () => {
|
|
1097
|
+
const mockUsers = [
|
|
1098
|
+
{ id: 'user-1', email: 'user1@example.com', name: 'User 1', password: 'hash1' },
|
|
1099
|
+
{ id: 'user-2', email: 'user2@example.com', name: 'User 2', password: 'hash2' }
|
|
1100
|
+
];
|
|
1101
|
+
|
|
1102
|
+
const mockCursor = {
|
|
1103
|
+
hasNext: jest.fn()
|
|
1104
|
+
.mockResolvedValueOnce(true)
|
|
1105
|
+
.mockResolvedValueOnce(true)
|
|
1106
|
+
.mockResolvedValueOnce(false),
|
|
1107
|
+
next: jest.fn()
|
|
1108
|
+
.mockResolvedValueOnce(mockUsers[0])
|
|
1109
|
+
.mockResolvedValueOnce(mockUsers[1]),
|
|
1110
|
+
close: jest.fn().mockResolvedValue()
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
mockDatabase.collection().find.mockResolvedValue(mockCursor);
|
|
1114
|
+
|
|
1115
|
+
const users = [];
|
|
1116
|
+
for await (const user of userService.getAllUsersStream()) {
|
|
1117
|
+
users.push(user);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
expect(users).toHaveLength(2);
|
|
1121
|
+
expect(users[0]).toEqual({
|
|
1122
|
+
id: 'user-1',
|
|
1123
|
+
email: 'user1@example.com',
|
|
1124
|
+
name: 'User 1'
|
|
1125
|
+
});
|
|
1126
|
+
expect(mockCursor.close).toHaveBeenCalled();
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
### Integration Testing with Test Databases
|
|
1133
|
+
|
|
1134
|
+
```javascript
|
|
1135
|
+
// test/integration/api.test.js
|
|
1136
|
+
const request = require('supertest');
|
|
1137
|
+
const app = require('../../src/app');
|
|
1138
|
+
const { MongoMemoryServer } = require('mongodb-memory-server');
|
|
1139
|
+
const { MongoClient } = require('mongodb');
|
|
1140
|
+
|
|
1141
|
+
describe('User API Integration Tests', () => {
|
|
1142
|
+
let mongoServer;
|
|
1143
|
+
let client;
|
|
1144
|
+
let database;
|
|
1145
|
+
|
|
1146
|
+
beforeAll(async () => {
|
|
1147
|
+
// Start in-memory MongoDB server
|
|
1148
|
+
mongoServer = await MongoMemoryServer.create();
|
|
1149
|
+
const mongoUri = mongoServer.getUri();
|
|
1150
|
+
|
|
1151
|
+
// Connect to test database
|
|
1152
|
+
client = new MongoClient(mongoUri);
|
|
1153
|
+
await client.connect();
|
|
1154
|
+
database = client.db('testdb');
|
|
1155
|
+
|
|
1156
|
+
// Mock database in app
|
|
1157
|
+
require('../../src/config/database').setDatabase(database);
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
afterAll(async () => {
|
|
1161
|
+
await client.close();
|
|
1162
|
+
await mongoServer.stop();
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
beforeEach(async () => {
|
|
1166
|
+
// Clean up database before each test
|
|
1167
|
+
await database.collection('users').deleteMany({});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
describe('POST /api/users', () => {
|
|
1171
|
+
it('should create a new user', async () => {
|
|
1172
|
+
const userData = {
|
|
1173
|
+
email: 'integration@example.com',
|
|
1174
|
+
name: 'Integration User',
|
|
1175
|
+
password: 'Password123'
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const response = await request(app)
|
|
1179
|
+
.post('/api/users')
|
|
1180
|
+
.send(userData)
|
|
1181
|
+
.expect(201);
|
|
1182
|
+
|
|
1183
|
+
expect(response.body.success).toBe(true);
|
|
1184
|
+
expect(response.body.data.email).toBe(userData.email);
|
|
1185
|
+
expect(response.body.data.name).toBe(userData.name);
|
|
1186
|
+
expect(response.body.data.password).toBeUndefined(); // Password should not be returned
|
|
1187
|
+
|
|
1188
|
+
// Verify user was actually created in database
|
|
1189
|
+
const userInDb = await database.collection('users').findOne({ email: userData.email });
|
|
1190
|
+
expect(userInDb).toBeTruthy();
|
|
1191
|
+
expect(userInDb.name).toBe(userData.name);
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
it('should return validation error for invalid data', async () => {
|
|
1195
|
+
const invalidUserData = {
|
|
1196
|
+
email: 'invalid-email',
|
|
1197
|
+
name: 'A', // Too short
|
|
1198
|
+
password: '123' // Too short
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const response = await request(app)
|
|
1202
|
+
.post('/api/users')
|
|
1203
|
+
.send(invalidUserData)
|
|
1204
|
+
.expect(400);
|
|
68
1205
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1206
|
+
expect(response.body.error).toBe('Validation failed');
|
|
1207
|
+
expect(response.body.details).toHaveLength(3);
|
|
1208
|
+
});
|
|
72
1209
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1210
|
+
it('should return error for duplicate email', async () => {
|
|
1211
|
+
const userData = {
|
|
1212
|
+
email: 'duplicate@example.com',
|
|
1213
|
+
name: 'User One',
|
|
1214
|
+
password: 'Password123'
|
|
1215
|
+
};
|
|
76
1216
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1217
|
+
// Create first user
|
|
1218
|
+
await request(app)
|
|
1219
|
+
.post('/api/users')
|
|
1220
|
+
.send(userData)
|
|
1221
|
+
.expect(201);
|
|
80
1222
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1223
|
+
// Try to create user with same email
|
|
1224
|
+
const response = await request(app)
|
|
1225
|
+
.post('/api/users')
|
|
1226
|
+
.send({
|
|
1227
|
+
...userData,
|
|
1228
|
+
name: 'User Two'
|
|
1229
|
+
})
|
|
1230
|
+
.expect(500);
|
|
84
1231
|
|
|
85
|
-
|
|
86
|
-
|
|
1232
|
+
expect(response.body.error).toContain('duplicate');
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
87
1235
|
|
|
88
|
-
|
|
1236
|
+
describe('GET /api/users', () => {
|
|
1237
|
+
beforeEach(async () => {
|
|
1238
|
+
// Insert test data
|
|
1239
|
+
await database.collection('users').insertMany([
|
|
1240
|
+
{
|
|
1241
|
+
id: 'user-1',
|
|
1242
|
+
email: 'user1@example.com',
|
|
1243
|
+
name: 'User One',
|
|
1244
|
+
password: 'hashed-pass',
|
|
1245
|
+
createdAt: new Date()
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
id: 'user-2',
|
|
1249
|
+
email: 'user2@example.com',
|
|
1250
|
+
name: 'User Two',
|
|
1251
|
+
password: 'hashed-pass',
|
|
1252
|
+
createdAt: new Date()
|
|
1253
|
+
}
|
|
1254
|
+
]);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it('should return paginated users', async () => {
|
|
1258
|
+
const response = await request(app)
|
|
1259
|
+
.get('/api/users?page=1&limit=10')
|
|
1260
|
+
.expect(200);
|
|
1261
|
+
|
|
1262
|
+
expect(response.body.success).toBe(true);
|
|
1263
|
+
expect(response.body.data.users).toHaveLength(2);
|
|
1264
|
+
expect(response.body.data.pagination).toEqual({
|
|
1265
|
+
page: 1,
|
|
1266
|
+
limit: 10,
|
|
1267
|
+
total: 2,
|
|
1268
|
+
pages: 1
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
it('should search users by name', async () => {
|
|
1273
|
+
const response = await request(app)
|
|
1274
|
+
.get('/api/users?search=One')
|
|
1275
|
+
.expect(200);
|
|
1276
|
+
|
|
1277
|
+
expect(response.body.data.users).toHaveLength(1);
|
|
1278
|
+
expect(response.body.data.users[0].name).toBe('User One');
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
describe('GET /api/users/:id', () => {
|
|
1283
|
+
let userId;
|
|
1284
|
+
|
|
1285
|
+
beforeEach(async () => {
|
|
1286
|
+
// Create a test user
|
|
1287
|
+
const result = await database.collection('users').insertOne({
|
|
1288
|
+
id: 'test-user-123',
|
|
1289
|
+
email: 'getuser@example.com',
|
|
1290
|
+
name: 'Get User',
|
|
1291
|
+
password: 'hashed-pass',
|
|
1292
|
+
createdAt: new Date()
|
|
1293
|
+
});
|
|
1294
|
+
userId = 'test-user-123';
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it('should return user when valid ID is provided', async () => {
|
|
1298
|
+
const response = await request(app)
|
|
1299
|
+
.get(`/api/users/${userId}`)
|
|
1300
|
+
.expect(200);
|
|
1301
|
+
|
|
1302
|
+
expect(response.body.success).toBe(true);
|
|
1303
|
+
expect(response.body.data.email).toBe('getuser@example.com');
|
|
1304
|
+
expect(response.body.data.password).toBeUndefined();
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
it('should return 404 for non-existent user', async () => {
|
|
1308
|
+
const response = await request(app)
|
|
1309
|
+
.get('/api/users/non-existent-id')
|
|
1310
|
+
.expect(404);
|
|
1311
|
+
|
|
1312
|
+
expect(response.body.error).toBe('User not found');
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
it('should return 400 for invalid ID format', async () => {
|
|
1316
|
+
const response = await request(app)
|
|
1317
|
+
.get('/api/users/invalid-id')
|
|
1318
|
+
.expect(400);
|
|
1319
|
+
|
|
1320
|
+
expect(response.body.error).toBe('Validation failed');
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
});
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
## Security Best Practices
|
|
1327
|
+
|
|
1328
|
+
### Input Validation and Sanitization
|
|
1329
|
+
|
|
1330
|
+
```javascript
|
|
1331
|
+
// src/middleware/validation.js
|
|
1332
|
+
const { body, param, query, validationResult } = require('express-validator');
|
|
1333
|
+
const createDOMPurify = require('dompurify');
|
|
1334
|
+
const { JSDOM } = require('jsdom');
|
|
1335
|
+
|
|
1336
|
+
// Create DOMPurify instance
|
|
1337
|
+
const window = new JSDOM('').window;
|
|
1338
|
+
const dompurify = createDOMPurify(window);
|
|
1339
|
+
|
|
1340
|
+
class SecurityValidator {
|
|
1341
|
+
// XSS prevention
|
|
1342
|
+
static sanitizeHtml(input) {
|
|
1343
|
+
if (typeof input !== 'string') {
|
|
1344
|
+
return input;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
return dompurify.sanitize(input, {
|
|
1348
|
+
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
|
|
1349
|
+
ALLOWED_ATTR: ['href', 'title'],
|
|
1350
|
+
ALLOW_DATA_ATTR: false
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// SQL injection prevention
|
|
1355
|
+
static sanitizeSql(input) {
|
|
1356
|
+
if (typeof input !== 'string') {
|
|
1357
|
+
return input;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Remove dangerous SQL characters and patterns
|
|
1361
|
+
return input
|
|
1362
|
+
.replace(/['"\;]/g, '')
|
|
1363
|
+
.replace(/--/g, '')
|
|
1364
|
+
.replace(/\/\*/g, '')
|
|
1365
|
+
.replace(/\*\//g, '')
|
|
1366
|
+
.replace(/drop\s+table/i, '')
|
|
1367
|
+
.replace(/delete\s+from/i, '')
|
|
1368
|
+
.replace(/insert\s+into/i, '')
|
|
1369
|
+
.replace(/update\s+\w+\s+set/i, '');
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Email validation with advanced checks
|
|
1373
|
+
static isValidEmail(email) {
|
|
1374
|
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
1375
|
+
|
|
1376
|
+
if (!emailRegex.test(email)) {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Additional checks
|
|
1381
|
+
const [localPart, domain] = email.split('@');
|
|
1382
|
+
|
|
1383
|
+
// Local part validation
|
|
1384
|
+
if (localPart.length > 64) return false;
|
|
1385
|
+
if (localPart.startsWith('.') || localPart.endsWith('.')) return false;
|
|
1386
|
+
if (localPart.includes('..')) return false;
|
|
1387
|
+
|
|
1388
|
+
// Domain validation
|
|
1389
|
+
if (domain.length > 253) return false;
|
|
1390
|
+
if (!domain.includes('.')) return false;
|
|
1391
|
+
|
|
1392
|
+
// Check for disposable email domains
|
|
1393
|
+
const disposableDomains = ['10minutemail.com', 'tempmail.org', 'guerrillamail.com'];
|
|
1394
|
+
if (disposableDomains.some(d => domain.toLowerCase().includes(d))) {
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Password strength validation
|
|
1402
|
+
static validatePasswordStrength(password) {
|
|
1403
|
+
const issues = [];
|
|
1404
|
+
|
|
1405
|
+
if (password.length < 8) {
|
|
1406
|
+
issues.push('Password must be at least 8 characters long');
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (password.length > 128) {
|
|
1410
|
+
issues.push('Password must be less than 128 characters');
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (!/[a-z]/.test(password)) {
|
|
1414
|
+
issues.push('Password must contain at least one lowercase letter');
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (!/[A-Z]/.test(password)) {
|
|
1418
|
+
issues.push('Password must contain at least one uppercase letter');
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (!/\d/.test(password)) {
|
|
1422
|
+
issues.push('Password must contain at least one number');
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
|
|
1426
|
+
issues.push('Password must contain at least one special character');
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Check for common patterns
|
|
1430
|
+
const commonPatterns = [
|
|
1431
|
+
/123456/,
|
|
1432
|
+
/password/i,
|
|
1433
|
+
/qwerty/i,
|
|
1434
|
+
/admin/i,
|
|
1435
|
+
/letmein/i
|
|
1436
|
+
];
|
|
1437
|
+
|
|
1438
|
+
if (commonPatterns.some(pattern => pattern.test(password))) {
|
|
1439
|
+
issues.push('Password contains common patterns that make it weak');
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Check for sequential characters
|
|
1443
|
+
const isSequential = (str) => {
|
|
1444
|
+
for (let i = 0; i < str.length - 2; i++) {
|
|
1445
|
+
const char1 = str.charCodeAt(i);
|
|
1446
|
+
const char2 = str.charCodeAt(i + 1);
|
|
1447
|
+
const char3 = str.charCodeAt(i + 2);
|
|
1448
|
+
|
|
1449
|
+
if (char2 === char1 + 1 && char3 === char2 + 1) {
|
|
1450
|
+
return true;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return false;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
if (isSequential(password.toLowerCase())) {
|
|
1457
|
+
issues.push('Password contains sequential characters');
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return {
|
|
1461
|
+
isValid: issues.length === 0,
|
|
1462
|
+
issues,
|
|
1463
|
+
score: Math.max(0, 100 - (issues.length * 20))
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Rate limiting by IP and user
|
|
1468
|
+
static createRateLimiter(options = {}) {
|
|
1469
|
+
const rateLimit = require('express-rate-limit');
|
|
1470
|
+
|
|
1471
|
+
return rateLimit({
|
|
1472
|
+
windowMs: options.windowMs || 15 * 60 * 1000, // 15 minutes
|
|
1473
|
+
max: options.max || 100, // limit each IP
|
|
1474
|
+
message: {
|
|
1475
|
+
error: 'Too many requests, please try again later',
|
|
1476
|
+
retryAfter: options.windowMs / 1000
|
|
1477
|
+
},
|
|
1478
|
+
standardHeaders: true,
|
|
1479
|
+
legacyHeaders: false,
|
|
1480
|
+
keyGenerator: options.keyGenerator || ((req) => {
|
|
1481
|
+
return req.ip + (req.user ? `:${req.user.id}` : '');
|
|
1482
|
+
}),
|
|
1483
|
+
skip: options.skip || ((req) => {
|
|
1484
|
+
// Skip rate limiting for certain routes or trusted IPs
|
|
1485
|
+
return req.path.startsWith('/health') || req.path.startsWith('/metrics');
|
|
1486
|
+
})
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Content Security Policy
|
|
1491
|
+
static getCSPHeaders() {
|
|
1492
|
+
return {
|
|
1493
|
+
'Content-Security-Policy': [
|
|
1494
|
+
"default-src 'self'",
|
|
1495
|
+
"script-src 'self' 'unsafe-inline' https://trusted-cdn.com",
|
|
1496
|
+
"style-src 'self' 'unsafe-inline'",
|
|
1497
|
+
"img-src 'self' data: https:",
|
|
1498
|
+
"font-src 'self'",
|
|
1499
|
+
"connect-src 'self' https://api.example.com",
|
|
1500
|
+
"frame-ancestors 'none'",
|
|
1501
|
+
"base-uri 'self'",
|
|
1502
|
+
"form-action 'self'"
|
|
1503
|
+
].join('; '),
|
|
1504
|
+
'X-Content-Type-Options': 'nosniff',
|
|
1505
|
+
'X-Frame-Options': 'DENY',
|
|
1506
|
+
'X-XSS-Protection': '1; mode=block',
|
|
1507
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
1508
|
+
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
module.exports = SecurityValidator;
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
### Authentication and Authorization
|
|
1517
|
+
|
|
1518
|
+
```javascript
|
|
1519
|
+
// src/middleware/auth.js
|
|
1520
|
+
const jwt = require('jsonwebtoken');
|
|
1521
|
+
const bcrypt = require('bcrypt');
|
|
1522
|
+
const crypto = require('crypto');
|
|
1523
|
+
const UserService = require('../services/userService');
|
|
1524
|
+
|
|
1525
|
+
class AuthMiddleware {
|
|
1526
|
+
constructor(jwtSecret = process.env.JWT_SECRET) {
|
|
1527
|
+
this.jwtSecret = jwtSecret;
|
|
1528
|
+
this.userService = new UserService(database); // Assuming database is available
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// JWT token generation
|
|
1532
|
+
generateToken(payload, options = {}) {
|
|
1533
|
+
const defaultOptions = {
|
|
1534
|
+
expiresIn: '24h',
|
|
1535
|
+
issuer: 'your-app',
|
|
1536
|
+
audience: 'your-app-users'
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
return jwt.sign(payload, this.jwtSecret, { ...defaultOptions, ...options });
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// JWT token verification
|
|
1543
|
+
verifyToken(token) {
|
|
1544
|
+
try {
|
|
1545
|
+
return jwt.verify(token, this.jwtSecret);
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
if (error.name === 'TokenExpiredError') {
|
|
1548
|
+
throw new Error('Token expired');
|
|
1549
|
+
} else if (error.name === 'JsonWebTokenError') {
|
|
1550
|
+
throw new Error('Invalid token');
|
|
1551
|
+
}
|
|
1552
|
+
throw new Error('Token verification failed');
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Authentication middleware
|
|
1557
|
+
authenticate() {
|
|
1558
|
+
return async (req, res, next) => {
|
|
1559
|
+
try {
|
|
1560
|
+
const authHeader = req.headers.authorization;
|
|
1561
|
+
|
|
1562
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
1563
|
+
return res.status(401).json({
|
|
1564
|
+
error: 'Authorization header required'
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const token = authHeader.substring(7);
|
|
1569
|
+
const decoded = this.verifyToken(token);
|
|
1570
|
+
|
|
1571
|
+
// Get user from database to ensure they still exist
|
|
1572
|
+
const user = await this.userService.getUserById(decoded.sub);
|
|
1573
|
+
if (!user) {
|
|
1574
|
+
return res.status(401).json({
|
|
1575
|
+
error: 'User not found'
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Add user to request object
|
|
1580
|
+
req.user = user;
|
|
1581
|
+
req.token = token;
|
|
1582
|
+
|
|
1583
|
+
next();
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
return res.status(401).json({
|
|
1586
|
+
error: error.message
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Role-based authorization
|
|
1593
|
+
requireRole(roles) {
|
|
1594
|
+
return (req, res, next) => {
|
|
1595
|
+
if (!req.user) {
|
|
1596
|
+
return res.status(401).json({
|
|
1597
|
+
error: 'Authentication required'
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const userRoles = Array.isArray(req.user.roles) ? req.user.roles : [req.user.role];
|
|
1602
|
+
const requiredRoles = Array.isArray(roles) ? roles : [roles];
|
|
1603
|
+
|
|
1604
|
+
const hasRequiredRole = requiredRoles.some(role => userRoles.includes(role));
|
|
1605
|
+
|
|
1606
|
+
if (!hasRequiredRole) {
|
|
1607
|
+
return res.status(403).json({
|
|
1608
|
+
error: 'Insufficient permissions',
|
|
1609
|
+
required: requiredRoles,
|
|
1610
|
+
current: userRoles
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
next();
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Permission-based authorization
|
|
1619
|
+
requirePermission(permission) {
|
|
1620
|
+
return (req, res, next) => {
|
|
1621
|
+
if (!req.user) {
|
|
1622
|
+
return res.status(401).json({
|
|
1623
|
+
error: 'Authentication required'
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const userPermissions = req.user.permissions || [];
|
|
1628
|
+
|
|
1629
|
+
if (!userPermissions.includes(permission)) {
|
|
1630
|
+
return res.status(403).json({
|
|
1631
|
+
error: 'Insufficient permissions',
|
|
1632
|
+
required: permission,
|
|
1633
|
+
current: userPermissions
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
next();
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Resource ownership check
|
|
1642
|
+
requireOwnership(resourceIdParam = 'id') {
|
|
1643
|
+
return async (req, res, next) => {
|
|
1644
|
+
try {
|
|
1645
|
+
if (!req.user) {
|
|
1646
|
+
return res.status(401).json({
|
|
1647
|
+
error: 'Authentication required'
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const resourceId = req.params[resourceIdParam];
|
|
1652
|
+
const resource = await this.getResourceById(resourceId, req.path);
|
|
1653
|
+
|
|
1654
|
+
if (!resource) {
|
|
1655
|
+
return res.status(404).json({
|
|
1656
|
+
error: 'Resource not found'
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Check if user owns the resource or is admin
|
|
1661
|
+
const isOwner = resource.userId === req.user.id;
|
|
1662
|
+
const isAdmin = req.user.role === 'admin';
|
|
1663
|
+
|
|
1664
|
+
if (!isOwner && !isAdmin) {
|
|
1665
|
+
return res.status(403).json({
|
|
1666
|
+
error: 'Access denied: You can only access your own resources'
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
req.resource = resource;
|
|
1671
|
+
next();
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
return res.status(500).json({
|
|
1674
|
+
error: 'Authorization check failed'
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Password hashing and verification
|
|
1681
|
+
async hashPassword(password) {
|
|
1682
|
+
const saltRounds = 12;
|
|
1683
|
+
return bcrypt.hash(password, saltRounds);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
async verifyPassword(password, hashedPassword) {
|
|
1687
|
+
return bcrypt.compare(password, hashedPassword);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// Password reset token generation
|
|
1691
|
+
generateResetToken() {
|
|
1692
|
+
return crypto.randomBytes(32).toString('hex');
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Session management
|
|
1696
|
+
async createSession(user, req) {
|
|
1697
|
+
const sessionToken = crypto.randomBytes(32).toString('hex');
|
|
1698
|
+
|
|
1699
|
+
// Store session in database or Redis
|
|
1700
|
+
await this.userService.createSession({
|
|
1701
|
+
token: sessionToken,
|
|
1702
|
+
userId: user.id,
|
|
1703
|
+
userAgent: req.get('User-Agent'),
|
|
1704
|
+
ipAddress: req.ip,
|
|
1705
|
+
createdAt: new Date(),
|
|
1706
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
return sessionToken;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
async invalidateSession(token) {
|
|
1713
|
+
await this.userService.deleteSession(token);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// Helper method to get resource by ID
|
|
1717
|
+
async getResourceById(id, path) {
|
|
1718
|
+
// This is a simplified example
|
|
1719
|
+
// In practice, you'd have different strategies for different resource types
|
|
1720
|
+
|
|
1721
|
+
if (path.includes('/users')) {
|
|
1722
|
+
return await this.userService.getUserById(id);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (path.includes('/posts')) {
|
|
1726
|
+
return await this.userService.getPostById(id);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
module.exports = AuthMiddleware;
|
|
1734
|
+
```
|
|
1735
|
+
|
|
1736
|
+
## Integration Patterns
|
|
1737
|
+
|
|
1738
|
+
### REST API with GraphQL Integration
|
|
1739
|
+
|
|
1740
|
+
```javascript
|
|
1741
|
+
// src/graphql/resolver.js
|
|
1742
|
+
const { gql } = require('apollo-server-express');
|
|
1743
|
+
const UserService = require('../services/userService');
|
|
1744
|
+
|
|
1745
|
+
const userService = new UserService(database);
|
|
1746
|
+
|
|
1747
|
+
// GraphQL type definitions
|
|
1748
|
+
const typeDefs = gql`
|
|
1749
|
+
type User {
|
|
1750
|
+
id: ID!
|
|
1751
|
+
email: String!
|
|
1752
|
+
name: String!
|
|
1753
|
+
createdAt: String!
|
|
1754
|
+
updatedAt: String!
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
type Query {
|
|
1758
|
+
users(limit: Int, offset: Int, search: String): [User!]!
|
|
1759
|
+
user(id: ID!): User
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
type Mutation {
|
|
1763
|
+
createUser(input: CreateUserInput!): User!
|
|
1764
|
+
updateUser(id: ID!, input: UpdateUserInput!): User!
|
|
1765
|
+
deleteUser(id: ID!): Boolean!
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
input CreateUserInput {
|
|
1769
|
+
email: String!
|
|
1770
|
+
name: String!
|
|
1771
|
+
password: String!
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
input UpdateUserInput {
|
|
1775
|
+
name: String
|
|
1776
|
+
email: String
|
|
1777
|
+
}
|
|
1778
|
+
`;
|
|
1779
|
+
|
|
1780
|
+
// GraphQL resolvers
|
|
1781
|
+
const resolvers = {
|
|
1782
|
+
Query: {
|
|
1783
|
+
users: async (_, { limit = 10, offset = 0, search }) => {
|
|
1784
|
+
try {
|
|
1785
|
+
const filter = {};
|
|
1786
|
+
if (search) {
|
|
1787
|
+
filter.$or = [
|
|
1788
|
+
{ name: { $regex: search, $options: 'i' } },
|
|
1789
|
+
{ email: { $regex: search, $options: 'i' } }
|
|
1790
|
+
];
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const users = await userService.getUsers(filter, { limit, offset });
|
|
1794
|
+
return users;
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
throw new Error(`Failed to fetch users: ${error.message}`);
|
|
1797
|
+
}
|
|
1798
|
+
},
|
|
1799
|
+
|
|
1800
|
+
user: async (_, { id }) => {
|
|
1801
|
+
try {
|
|
1802
|
+
const user = await userService.getUserById(id);
|
|
1803
|
+
if (!user) {
|
|
1804
|
+
throw new Error('User not found');
|
|
1805
|
+
}
|
|
1806
|
+
return user;
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
throw new Error(`Failed to fetch user: ${error.message}`);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
},
|
|
1812
|
+
|
|
1813
|
+
Mutation: {
|
|
1814
|
+
createUser: async (_, { input }) => {
|
|
1815
|
+
try {
|
|
1816
|
+
const user = await userService.createUser(input);
|
|
1817
|
+
return user;
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
throw new Error(`Failed to create user: ${error.message}`);
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
|
|
1823
|
+
updateUser: async (_, { id, input }) => {
|
|
1824
|
+
try {
|
|
1825
|
+
const user = await userService.updateUser(id, input);
|
|
1826
|
+
if (!user) {
|
|
1827
|
+
throw new Error('User not found');
|
|
1828
|
+
}
|
|
1829
|
+
return user;
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
throw new Error(`Failed to update user: ${error.message}`);
|
|
1832
|
+
}
|
|
1833
|
+
},
|
|
1834
|
+
|
|
1835
|
+
deleteUser: async (_, { id }) => {
|
|
1836
|
+
try {
|
|
1837
|
+
await userService.deleteUser(id);
|
|
1838
|
+
return true;
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
throw new Error(`Failed to delete user: ${error.message}`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
module.exports = { typeDefs, resolvers };
|
|
1847
|
+
```
|
|
1848
|
+
|
|
1849
|
+
### WebSocket Integration
|
|
1850
|
+
|
|
1851
|
+
```javascript
|
|
1852
|
+
// src/websocket/socketHandler.js
|
|
1853
|
+
const { Server } = require('ws');
|
|
1854
|
+
const jwt = require('jsonwebtoken');
|
|
1855
|
+
const UserService = require('../services/userService');
|
|
1856
|
+
|
|
1857
|
+
class SocketHandler {
|
|
1858
|
+
constructor(server, options = {}) {
|
|
1859
|
+
this.wss = new Server({
|
|
1860
|
+
server,
|
|
1861
|
+
path: options.path || '/ws',
|
|
1862
|
+
verifyClient: this.verifyClient.bind(this)
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
this.clients = new Map(); // userId -> WebSocket connection
|
|
1866
|
+
this.rooms = new Map(); // room -> Set of userIds
|
|
1867
|
+
|
|
1868
|
+
this.setupEventHandlers();
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Client verification
|
|
1872
|
+
async verifyClient(info) {
|
|
1873
|
+
try {
|
|
1874
|
+
const token = this.extractToken(info.req);
|
|
1875
|
+
if (!token) {
|
|
1876
|
+
return false;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
1880
|
+
const user = await new UserService(database).getUserById(decoded.sub);
|
|
1881
|
+
|
|
1882
|
+
if (!user) {
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
info.req.user = user;
|
|
1887
|
+
return true;
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Extract JWT token from request
|
|
1894
|
+
extractToken(req) {
|
|
1895
|
+
const authHeader = req.headers.authorization;
|
|
1896
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
1897
|
+
return authHeader.substring(7);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Also check query parameters for fallback
|
|
1901
|
+
const urlParams = new URL(req.url, `http://${req.headers.host}`).searchParams;
|
|
1902
|
+
return urlParams.get('token');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// Setup WebSocket event handlers
|
|
1906
|
+
setupEventHandlers() {
|
|
1907
|
+
this.wss.on('connection', (ws, req) => {
|
|
1908
|
+
this.handleConnection(ws, req);
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
this.wss.on('error', (error) => {
|
|
1912
|
+
console.error('WebSocket error:', error);
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Handle new connection
|
|
1917
|
+
handleConnection(ws, req) {
|
|
1918
|
+
const user = req.user;
|
|
1919
|
+
|
|
1920
|
+
// Store client connection
|
|
1921
|
+
this.clients.set(user.id, {
|
|
1922
|
+
ws,
|
|
1923
|
+
user,
|
|
1924
|
+
joinedAt: new Date(),
|
|
1925
|
+
lastActivity: new Date()
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
// Send welcome message
|
|
1929
|
+
this.sendToUser(user.id, {
|
|
1930
|
+
type: 'connection',
|
|
1931
|
+
message: 'Connected successfully',
|
|
1932
|
+
timestamp: new Date().toISOString()
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
// Setup message handler
|
|
1936
|
+
ws.on('message', (message) => {
|
|
1937
|
+
this.handleMessage(user.id, message);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
// Setup close handler
|
|
1941
|
+
ws.on('close', () => {
|
|
1942
|
+
this.handleDisconnection(user.id);
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
// Setup error handler
|
|
1946
|
+
ws.on('error', (error) => {
|
|
1947
|
+
console.error(`WebSocket error for user ${user.id}:`, error);
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
// Update last activity
|
|
1951
|
+
ws.on('pong', () => {
|
|
1952
|
+
const client = this.clients.get(user.id);
|
|
1953
|
+
if (client) {
|
|
1954
|
+
client.lastActivity = new Date();
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
console.log(`User ${user.id} connected via WebSocket`);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// Handle incoming messages
|
|
1962
|
+
async handleMessage(userId, message) {
|
|
1963
|
+
try {
|
|
1964
|
+
const client = this.clients.get(userId);
|
|
1965
|
+
if (!client) return;
|
|
1966
|
+
|
|
1967
|
+
client.lastActivity = new Date();
|
|
1968
|
+
|
|
1969
|
+
const data = JSON.parse(message);
|
|
1970
|
+
|
|
1971
|
+
switch (data.type) {
|
|
1972
|
+
case 'join_room':
|
|
1973
|
+
await this.handleJoinRoom(userId, data.room);
|
|
1974
|
+
break;
|
|
1975
|
+
|
|
1976
|
+
case 'leave_room':
|
|
1977
|
+
await this.handleLeaveRoom(userId, data.room);
|
|
1978
|
+
break;
|
|
1979
|
+
|
|
1980
|
+
case 'send_message':
|
|
1981
|
+
await this.handleSendMessage(userId, data);
|
|
1982
|
+
break;
|
|
1983
|
+
|
|
1984
|
+
case 'typing':
|
|
1985
|
+
await this.handleTyping(userId, data);
|
|
1986
|
+
break;
|
|
1987
|
+
|
|
1988
|
+
default:
|
|
1989
|
+
this.sendToUser(userId, {
|
|
1990
|
+
type: 'error',
|
|
1991
|
+
message: 'Unknown message type',
|
|
1992
|
+
timestamp: new Date().toISOString()
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
console.error(`Error handling message from user ${userId}:`, error);
|
|
1997
|
+
this.sendToUser(userId, {
|
|
1998
|
+
type: 'error',
|
|
1999
|
+
message: 'Failed to process message',
|
|
2000
|
+
timestamp: new Date().toISOString()
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// Handle room joining
|
|
2006
|
+
async handleJoinRoom(userId, roomName) {
|
|
2007
|
+
const room = this.rooms.get(roomName) || new Set();
|
|
2008
|
+
room.add(userId);
|
|
2009
|
+
this.rooms.set(roomName, room);
|
|
2010
|
+
|
|
2011
|
+
// Notify room members
|
|
2012
|
+
this.broadcastToRoom(roomName, {
|
|
2013
|
+
type: 'user_joined',
|
|
2014
|
+
userId,
|
|
2015
|
+
room: roomName,
|
|
2016
|
+
timestamp: new Date().toISOString()
|
|
2017
|
+
}, userId);
|
|
2018
|
+
|
|
2019
|
+
// Send confirmation to user
|
|
2020
|
+
this.sendToUser(userId, {
|
|
2021
|
+
type: 'joined_room',
|
|
2022
|
+
room: roomName,
|
|
2023
|
+
timestamp: new Date().toISOString()
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// Handle room leaving
|
|
2028
|
+
async handleLeaveRoom(userId, roomName) {
|
|
2029
|
+
const room = this.rooms.get(roomName);
|
|
2030
|
+
if (room) {
|
|
2031
|
+
room.delete(userId);
|
|
2032
|
+
|
|
2033
|
+
if (room.size === 0) {
|
|
2034
|
+
this.rooms.delete(roomName);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Notify room members
|
|
2038
|
+
this.broadcastToRoom(roomName, {
|
|
2039
|
+
type: 'user_left',
|
|
2040
|
+
userId,
|
|
2041
|
+
room: roomName,
|
|
2042
|
+
timestamp: new Date().toISOString()
|
|
2043
|
+
}, userId);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// Send confirmation to user
|
|
2047
|
+
this.sendToUser(userId, {
|
|
2048
|
+
type: 'left_room',
|
|
2049
|
+
room: roomName,
|
|
2050
|
+
timestamp: new Date().toISOString()
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// Handle message sending
|
|
2055
|
+
async handleSendMessage(userId, data) {
|
|
2056
|
+
const { room, message, type = 'text' } = data;
|
|
2057
|
+
|
|
2058
|
+
// Save message to database (implement this)
|
|
2059
|
+
const savedMessage = await this.saveMessage({
|
|
2060
|
+
userId,
|
|
2061
|
+
room,
|
|
2062
|
+
message,
|
|
2063
|
+
type,
|
|
2064
|
+
timestamp: new Date()
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
// Broadcast to room members
|
|
2068
|
+
this.broadcastToRoom(room, {
|
|
2069
|
+
type: 'new_message',
|
|
2070
|
+
message: savedMessage,
|
|
2071
|
+
timestamp: new Date().toISOString()
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Handle typing indicators
|
|
2076
|
+
async handleTyping(userId, data) {
|
|
2077
|
+
const { room, isTyping } = data;
|
|
2078
|
+
|
|
2079
|
+
this.broadcastToRoom(room, {
|
|
2080
|
+
type: 'typing_indicator',
|
|
2081
|
+
userId,
|
|
2082
|
+
isTyping,
|
|
2083
|
+
timestamp: new Date().toISOString()
|
|
2084
|
+
}, userId);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// Send message to specific user
|
|
2088
|
+
sendToUser(userId, message) {
|
|
2089
|
+
const client = this.clients.get(userId);
|
|
2090
|
+
if (client && client.ws.readyState === client.ws.OPEN) {
|
|
2091
|
+
client.ws.send(JSON.stringify(message));
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Broadcast message to room
|
|
2096
|
+
broadcastToRoom(roomName, message, excludeUserId = null) {
|
|
2097
|
+
const room = this.rooms.get(roomName);
|
|
2098
|
+
if (!room) return;
|
|
2099
|
+
|
|
2100
|
+
room.forEach(userId => {
|
|
2101
|
+
if (userId !== excludeUserId) {
|
|
2102
|
+
this.sendToUser(userId, message);
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Handle client disconnection
|
|
2108
|
+
handleDisconnection(userId) {
|
|
2109
|
+
// Remove from all rooms
|
|
2110
|
+
for (const [roomName, room] of this.rooms.entries()) {
|
|
2111
|
+
room.delete(userId);
|
|
2112
|
+
|
|
2113
|
+
if (room.size === 0) {
|
|
2114
|
+
this.rooms.delete(roomName);
|
|
2115
|
+
} else {
|
|
2116
|
+
// Notify remaining room members
|
|
2117
|
+
this.broadcastToRoom(roomName, {
|
|
2118
|
+
type: 'user_disconnected',
|
|
2119
|
+
userId,
|
|
2120
|
+
timestamp: new Date().toISOString()
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// Remove client
|
|
2126
|
+
this.clients.delete(userId);
|
|
2127
|
+
console.log(`User ${userId} disconnected`);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// Get connection statistics
|
|
2131
|
+
getStats() {
|
|
2132
|
+
return {
|
|
2133
|
+
connectedClients: this.clients.size,
|
|
2134
|
+
activeRooms: this.rooms.size,
|
|
2135
|
+
rooms: Array.from(this.rooms.entries()).map(([name, users]) => ({
|
|
2136
|
+
name,
|
|
2137
|
+
userCount: users.size
|
|
2138
|
+
}))
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// Cleanup inactive connections
|
|
2143
|
+
cleanup() {
|
|
2144
|
+
const now = new Date();
|
|
2145
|
+
const timeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
2146
|
+
|
|
2147
|
+
for (const [userId, client] of this.clients.entries()) {
|
|
2148
|
+
if (now - client.lastActivity > timeoutMs) {
|
|
2149
|
+
client.ws.terminate();
|
|
2150
|
+
this.handleDisconnection(userId);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// Save message to database (implement this)
|
|
2156
|
+
async saveMessage(messageData) {
|
|
2157
|
+
// This would save to your database
|
|
2158
|
+
return {
|
|
2159
|
+
id: crypto.randomUUID(),
|
|
2160
|
+
...messageData
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
module.exports = SocketHandler;
|
|
2166
|
+
```
|
|
2167
|
+
|
|
2168
|
+
## Modern Development Workflow
|
|
2169
|
+
|
|
2170
|
+
### Configuration Management
|
|
2171
|
+
|
|
2172
|
+
```javascript
|
|
2173
|
+
// src/config/index.js
|
|
2174
|
+
const path = require('path');
|
|
2175
|
+
require('dotenv').config();
|
|
2176
|
+
|
|
2177
|
+
class Config {
|
|
2178
|
+
constructor() {
|
|
2179
|
+
this.env = process.env.NODE_ENV || 'development';
|
|
2180
|
+
this.isDevelopment = this.env === 'development';
|
|
2181
|
+
this.isProduction = this.env === 'production';
|
|
2182
|
+
this.isTest = this.env === 'test';
|
|
2183
|
+
|
|
2184
|
+
this.loadConfiguration();
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
loadConfiguration() {
|
|
2188
|
+
this.server = {
|
|
2189
|
+
host: process.env.HOST || '0.0.0.0',
|
|
2190
|
+
port: parseInt(process.env.PORT) || 3000,
|
|
2191
|
+
cors: {
|
|
2192
|
+
origins: this.parseArray(process.env.CORS_ORIGINS) || ['http://localhost:3000']
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
this.database = {
|
|
2197
|
+
url: process.env.DATABASE_URL || 'mongodb://localhost:27017/myapp',
|
|
2198
|
+
options: {
|
|
2199
|
+
useNewUrlParser: true,
|
|
2200
|
+
useUnifiedTopology: true,
|
|
2201
|
+
maxPoolSize: parseInt(process.env.DB_MAX_POOL_SIZE) || 10,
|
|
2202
|
+
serverSelectionTimeoutMS: parseInt(process.env.DB_TIMEOUT) || 5000,
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
this.redis = {
|
|
2207
|
+
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
|
2208
|
+
options: {
|
|
2209
|
+
retryDelayOnFailover: 100,
|
|
2210
|
+
enableReadyCheck: false,
|
|
2211
|
+
maxRetriesPerRequest: null
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
|
|
2215
|
+
this.auth = {
|
|
2216
|
+
jwtSecret: process.env.JWT_SECRET || this.generateSecret(),
|
|
2217
|
+
jwtExpiration: process.env.JWT_EXPIRATION || '24h',
|
|
2218
|
+
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 12
|
|
2219
|
+
};
|
|
2220
|
+
|
|
2221
|
+
this.rateLimit = {
|
|
2222
|
+
max: parseInt(process.env.RATE_LIMIT_MAX) || 100,
|
|
2223
|
+
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW) || 15 * 60 * 1000
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
this.logging = {
|
|
2227
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
2228
|
+
format: process.env.LOG_FORMAT || 'combined',
|
|
2229
|
+
file: process.env.LOG_FILE
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
this.upload = {
|
|
2233
|
+
maxFileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024, // 5MB
|
|
2234
|
+
allowedTypes: this.parseArray(process.env.ALLOWED_FILE_TYPES) || ['image/jpeg', 'image/png'],
|
|
2235
|
+
destination: process.env.UPLOAD_DESTINATION || './uploads'
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
this.email = {
|
|
2239
|
+
provider: process.env.EMAIL_PROVIDER || 'sendgrid',
|
|
2240
|
+
from: process.env.EMAIL_FROM,
|
|
2241
|
+
apiKey: process.env.EMAIL_API_KEY
|
|
2242
|
+
};
|
|
2243
|
+
|
|
2244
|
+
this.monitoring = {
|
|
2245
|
+
enableMetrics: process.env.ENABLE_METRICS === 'true',
|
|
2246
|
+
metricsPort: parseInt(process.env.METRICS_PORT) || 9090,
|
|
2247
|
+
healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL) || 30000
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
parseArray(value) {
|
|
2252
|
+
if (!value) return null;
|
|
2253
|
+
if (Array.isArray(value)) return value;
|
|
2254
|
+
return value.split(',').map(item => item.trim());
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
generateSecret() {
|
|
2258
|
+
if (this.isProduction) {
|
|
2259
|
+
throw new Error('JWT_SECRET must be set in production');
|
|
2260
|
+
}
|
|
2261
|
+
return require('crypto').randomBytes(64).toString('hex');
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
validate() {
|
|
2265
|
+
const errors = [];
|
|
2266
|
+
|
|
2267
|
+
if (this.isProduction && !process.env.JWT_SECRET) {
|
|
2268
|
+
errors.push('JWT_SECRET is required in production');
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
if (!this.database.url) {
|
|
2272
|
+
errors.push('DATABASE_URL is required');
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
if (errors.length > 0) {
|
|
2276
|
+
throw new Error(`Configuration validation failed: ${errors.join(', ')}`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
getDatabaseConfig() {
|
|
2281
|
+
return {
|
|
2282
|
+
url: this.database.url,
|
|
2283
|
+
options: this.database.options
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
getRedisConfig() {
|
|
2288
|
+
return {
|
|
2289
|
+
url: this.redis.url,
|
|
2290
|
+
options: this.redis.options
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// Create singleton instance
|
|
2296
|
+
const config = new Config();
|
|
2297
|
+
|
|
2298
|
+
// Validate configuration on startup
|
|
2299
|
+
config.validate();
|
|
2300
|
+
|
|
2301
|
+
module.exports = config;
|
|
2302
|
+
```
|
|
2303
|
+
|
|
2304
|
+
### Package.json with Modern Scripts
|
|
2305
|
+
|
|
2306
|
+
```json
|
|
2307
|
+
{
|
|
2308
|
+
"name": "my-javascript-project",
|
|
2309
|
+
"version": "1.0.0",
|
|
2310
|
+
"description": "Modern Node.js application",
|
|
2311
|
+
"main": "src/app.js",
|
|
2312
|
+
"type": "commonjs",
|
|
2313
|
+
"scripts": {
|
|
2314
|
+
"start": "node src/app.js",
|
|
2315
|
+
"dev": "nodemon src/app.js",
|
|
2316
|
+
"dev:debug": "nodemon --inspect src/app.js",
|
|
2317
|
+
"test": "jest",
|
|
2318
|
+
"test:watch": "jest --watch",
|
|
2319
|
+
"test:coverage": "jest --coverage",
|
|
2320
|
+
"test:integration": "jest --testPathPattern=test/integration",
|
|
2321
|
+
"lint": "eslint src/ test/",
|
|
2322
|
+
"lint:fix": "eslint src/ test/ --fix",
|
|
2323
|
+
"format": "prettier --write src/ test/",
|
|
2324
|
+
"validate": "npm run lint && npm run test",
|
|
2325
|
+
"build": "echo 'No build step required for JavaScript'",
|
|
2326
|
+
"clean": "rm -rf node_modules package-lock.json",
|
|
2327
|
+
"fresh": "npm run clean && npm install",
|
|
2328
|
+
"security:audit": "npm audit",
|
|
2329
|
+
"security:fix": "npm audit fix",
|
|
2330
|
+
"db:migrate": "node scripts/migrate.js",
|
|
2331
|
+
"db:seed": "node scripts/seed.js",
|
|
2332
|
+
"logs": "tail -f logs/app.log",
|
|
2333
|
+
"docker:build": "docker build -t my-app .",
|
|
2334
|
+
"docker:run": "docker run -p 3000:3000 my-app"
|
|
2335
|
+
},
|
|
2336
|
+
"dependencies": {
|
|
2337
|
+
"express": "^4.21.0",
|
|
2338
|
+
"helmet": "^7.1.0",
|
|
2339
|
+
"cors": "^2.8.5",
|
|
2340
|
+
"compression": "^1.7.5",
|
|
2341
|
+
"morgan": "^1.10.0",
|
|
2342
|
+
"express-rate-limit": "^7.4.0",
|
|
2343
|
+
"express-validator": "^7.1.0",
|
|
2344
|
+
"bcrypt": "^5.1.1",
|
|
2345
|
+
"jsonwebtoken": "^9.0.2",
|
|
2346
|
+
"mongoose": "^8.8.1",
|
|
2347
|
+
"redis": "^4.7.0",
|
|
2348
|
+
"dotenv": "^16.4.5",
|
|
2349
|
+
"winston": "^3.15.0",
|
|
2350
|
+
"joi": "^17.13.3",
|
|
2351
|
+
"multer": "^1.4.5-lts.1",
|
|
2352
|
+
"nodemailer": "^6.9.8",
|
|
2353
|
+
"apollo-server-express": "^3.12.1",
|
|
2354
|
+
"graphql": "^16.9.0",
|
|
2355
|
+
"ws": "^8.18.0",
|
|
2356
|
+
"uuid": "^10.0.0"
|
|
2357
|
+
},
|
|
2358
|
+
"devDependencies": {
|
|
2359
|
+
"nodemon": "^3.1.4",
|
|
2360
|
+
"jest": "^30.0.4",
|
|
2361
|
+
"supertest": "^7.0.0",
|
|
2362
|
+
"eslint": "^9.0.0",
|
|
2363
|
+
"eslint-config-standard": "^17.1.0",
|
|
2364
|
+
"eslint-plugin-import": "^2.31.0",
|
|
2365
|
+
"eslint-plugin-node": "^11.1.0",
|
|
2366
|
+
"eslint-plugin-promise": "^7.1.0",
|
|
2367
|
+
"prettier": "^3.3.3",
|
|
2368
|
+
"@types/jest": "^30.0.4",
|
|
2369
|
+
"mongodb-memory-server": "^9.16.0"
|
|
2370
|
+
},
|
|
2371
|
+
"engines": {
|
|
2372
|
+
"node": ">=18.0.0",
|
|
2373
|
+
"npm": ">=8.0.0"
|
|
2374
|
+
},
|
|
2375
|
+
"keywords": [
|
|
2376
|
+
"nodejs",
|
|
2377
|
+
"express",
|
|
2378
|
+
"javascript",
|
|
2379
|
+
"api",
|
|
2380
|
+
"backend"
|
|
2381
|
+
],
|
|
2382
|
+
"author": "Your Name",
|
|
2383
|
+
"license": "MIT"
|
|
2384
|
+
}
|
|
2385
|
+
```
|
|
2386
|
+
|
|
2387
|
+
---
|
|
89
2388
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
2389
|
+
**Created by**: MoAI Language Skill Factory
|
|
2390
|
+
**Last Updated**: 2025-11-06
|
|
2391
|
+
**Version**: 2.0.0
|
|
2392
|
+
**JavaScript Target**: Node.js 22.x + Modern ES2025 Features
|
|
93
2393
|
|
|
94
|
-
|
|
95
|
-
- Enable automatic validation by matching your linter with the language's official style guide.
|
|
96
|
-
- Fix test/build pipelines with reproducible commands in CI.
|
|
2394
|
+
This skill provides comprehensive JavaScript development guidance with 2025 best practices, covering everything from legacy code maintenance to modern Node.js backend development and browser compatibility strategies.
|