diff --git a/.agents/NFR-REGISTRY.md b/.agents/NFR-REGISTRY.md new file mode 100644 index 00000000..05907f26 --- /dev/null +++ b/.agents/NFR-REGISTRY.md @@ -0,0 +1,52 @@ +# Jumentix NFR Registry + +This file consolidates non-functional requirements already requested and stored in `.agents/requirements`. + +## Architecture and Design NFRs + +- `015` DDD + Event-Driven + Hexagonal architecture baseline. +- `016` Layer call order and boundaries. +- `017` Event-first integration and circular reference avoidance. +- `036` OpenAPI port object contract requirements. +- `050` Generic adapters must become distributable packages. + +## Quality, Coverage, and CI NFRs + +- `011` Minimal CI gate baseline. +- `014` Codecov coverage integrity. +- `020` Coverage threshold as approval gate. +- `063` Workspace coverage policy governance. +- `065` Commit/push integrity with real CI checks. + +## Security and Compliance NFRs + +- `044` PCI-oriented security hardening. +- `029` Multi-tenancy and RBAC foundation. +- `067` Bidirectional task/PR traceability governance (auditability). + +## Runtime and Operations NFRs + +- `001` Node 22 runtime standard. +- `041` PM2 VM orchestration. +- `042` Env-driven runtime adapter selection. +- `043` Runtime env docs + governance. + +## Documentation and Governance NFRs + +- `018` Project docs and structure sync. +- `025` Every new feature must be documented. +- `053` Workspace package docs and ownership. +- `056`/`064` GitHub project as single source of truth. +- `057` PR grouping by priority. +- `066` Documentation round governance for marketing root + technical component docs. +- `068` NFR capture and registry governance. +- `069` Website commercial/static/vercel governance. +- `070` npm organization and vercel scope integration governance. + +## Rule of Use + +When a new NFR is requested: + +1. add/update requirement file in `.agents/requirements/` +2. update `.agents/README.md` index +3. update this registry mapping diff --git a/.agents/README.md b/.agents/README.md index 074211c9..0cf942fa 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -61,6 +61,32 @@ Use these files as living constraints for future maintenance and feature develop - [044-pci-security-compliance-hardening](requirements/044-pci-security-compliance-hardening.md) - [045-data-entity-controller-ownership](requirements/045-data-entity-controller-ownership.md) - [046-multi-database-driver-smoke-validation](requirements/046-multi-database-driver-smoke-validation.md) +- [047-realtime-api-test-matrix](requirements/047-realtime-api-test-matrix.md) +- [048-jumentix-pnpm-monorepo-productization](requirements/048-jumentix-pnpm-monorepo-productization.md) +- [049-jumentix-wave-execution-governance](requirements/049-jumentix-wave-execution-governance.md) +- [050-generic-adapters-as-distributable-packages](requirements/050-generic-adapters-as-distributable-packages.md) +- [051-runtime-bootstrap-shared-packages](requirements/051-runtime-bootstrap-shared-packages.md) +- [052-rest-loader-framework-matrix](requirements/052-rest-loader-framework-matrix.md) +- [053-jumentix-workspace-package-docs-and-ownership](requirements/053-jumentix-workspace-package-docs-and-ownership.md) +- [054-sdk-compatibility-bridge-governance](requirements/054-sdk-compatibility-bridge-governance.md) +- [055-wave5-app-rehoming-cutover-governance](requirements/055-wave5-app-rehoming-cutover-governance.md) +- [056-jumentix-project-single-source-of-truth](requirements/056-jumentix-project-single-source-of-truth.md) +- [057-pr-grouping-by-priority](requirements/057-pr-grouping-by-priority.md) +- [058-jumentix-migration-inventory-and-rollback-governance](requirements/058-jumentix-migration-inventory-and-rollback-governance.md) +- [059-jumentix-service-factory-and-deploy-template-matrices](requirements/059-jumentix-service-factory-and-deploy-template-matrices.md) +- [060-jumentix-release-versioning-policy-governance](requirements/060-jumentix-release-versioning-policy-governance.md) +- [060-monorepo-root-layout-governance](requirements/060-monorepo-root-layout-governance.md) +- [061-cache-service-read-caching-governance](requirements/061-cache-service-read-caching-governance.md) +- [062-workspace-dependency-boundaries-governance](requirements/062-workspace-dependency-boundaries-governance.md) +- [063-workspace-coverage-policy-governance](requirements/063-workspace-coverage-policy-governance.md) +- [064-github-project-single-source-of-truth](requirements/064-github-project-single-source-of-truth.md) +- [065-commit-push-integrity-and-real-ci-enforcement](requirements/065-commit-push-integrity-and-real-ci-enforcement.md) +- [066-documentation-round-jumentix-marketing-root-and-component-tech-docs](requirements/066-documentation-round-jumentix-marketing-root-and-component-tech-docs.md) +- [067-bidirectional-task-pr-traceability-governance](requirements/067-bidirectional-task-pr-traceability-governance.md) +- [068-nfr-capture-and-registry-governance](requirements/068-nfr-capture-and-registry-governance.md) +- [069-jumentix-website-commercial-static-vercel-governance](requirements/069-jumentix-website-commercial-static-vercel-governance.md) +- [070-xpertminds-npm-and-web2solutions-vercel-integration](requirements/070-xpertminds-npm-and-web2solutions-vercel-integration.md) +- [NFR Registry](NFR-REGISTRY.md) - [Data Entity Documentation Agent](data-entity-documentation-agent.md) - [Domain Modeling Agent](domain-modeling-agent.md) - [Domain Designer Agent](domain-designer-agent.md) diff --git a/.agents/integration-contracts-agent.md b/.agents/integration-contracts-agent.md index 6606e8f1..8a9e83d0 100644 --- a/.agents/integration-contracts-agent.md +++ b/.agents/integration-contracts-agent.md @@ -14,6 +14,7 @@ Keep event/message contracts and error contracts explicit, synchronized, and enf - New error classes/codes/response shapes must be documented in `documentation/md/ERROR-CONTRACTS-AND-RESPONSES.md`. - Contract documentation and implementation must ship together. - Keep request/response envelope backward-compatible unless explicitly versioned. +- Realtime contracts (`WebSocket`/`gRPC`) must maintain unit + integration + smoke coverage as defined in requirement `047`. ## Definition of done - Contract docs match current implementation. diff --git a/.agents/project-todos.md b/.agents/project-todos.md index c871ab1d..795a3ee5 100644 --- a/.agents/project-todos.md +++ b/.agents/project-todos.md @@ -3,6 +3,76 @@ This file tracks the project ownership fixes proposed before adding new features. Keep every item in either `Done` or `Open`, and move items as they are completed. +Governance note: +- Canonical task management source is GitHub Project **Jumentix**: + - https://github.com/users/web2solutions/projects/1 +- This file is a local reference snapshot only and must not be treated as authoritative task state. +- Task/PR governance: + - Traceability must be bidirectional (`issue -> PR/commit` and `PR -> issue/task mapping`) per requirement 055. +- NFR governance: + - All user-requested non-functional requirements must be captured in `.agents/requirements` and indexed in `.agents/NFR-REGISTRY.md` (requirement 068). + +GitHub tracking: +- Open TODO items are mirrored in GitHub Issues with label `todo-mvp`: + - https://github.com/web2solutions/aaa-typescript-boilerplate/issues?q=is%3Aissue+is%3Aopen+label%3Atodo-mvp + +## Documentation Round (EPIC #117) + +- Epic: https://github.com/web2solutions/aaa-typescript-boilerplate/issues/117 +- Child tasks: + - [x] #118 Root README positioning + global index + - [x] #119 Backend Template technical docs hub + - [x] #120 Service Management technical docs + SPA/PWA guide + - [x] #121 REST and Realtime API guides + - [x] #122 SaaS monolith + microservices guides + - [x] #123 Docs governance + cross-link validation + +## Website Round (EPIC #124) + +- Epic: https://github.com/web2solutions/aaa-typescript-boilerplate/issues/124 +- Status: Planned and decomposed in GitHub Project Jumentix (awaiting implementation order) +- Governance fields: + - Estimates: defined for epic and all child tasks + - Start/End dates: defined for epic and all child tasks + - Iteration assignment: tracked by labels `iteration-a` and `iteration-b` (project Iteration field is currently unconfigured with zero cycles) + +### Detailed execution plan + +1. Planning and conversion strategy + - [~] #125 IA, sitemap, and conversion funnel definition (5 pts) - implemented in workspace, awaiting PR/commit linkage for closure + - Schedule: 2026-07-06 -> 2026-07-20 (`iteration-a`) + - Outcome: approved navigation model, CTA strategy, and markdown-source mapping. + +2. Website bootstrap and architecture + - [~] #126 Scaffold app in `apps/jumentix-website` using Vercel Mantine + Nextra template (8 pts) - in progress + - Schedule: 2026-07-06 -> 2026-07-20 (`iteration-a`) + - Outcome: stable static app baseline, theme system, and scalable project structure. + +3. Data foundation from markdown + - [~] #127 Markdown-to-site static content pipeline (8 pts) - implemented in workspace, awaiting PR/commit linkage for closure + - Schedule: 2026-07-06 -> 2026-07-20 (`iteration-a`) + - Outcome: deterministic MD ingestion and normalized content metadata for website sections. + +4. Conversion-focused commercial experience + - [~] #128 Commercial pages and lead-conversion CTA implementation (8 pts) - implemented in workspace, awaiting PR/commit linkage for closure + - Schedule: 2026-07-06 -> 2026-07-20 (`iteration-a`) + - Outcome: persuasive enterprise messaging and complete buyer journey pages. + +5. Launch readiness + - [~] #129 SEO and performance baseline (5 pts) - implemented in workspace, awaiting PR/commit linkage for closure + - Schedule: 2026-07-21 -> 2026-08-03 (`iteration-b`) + - Outcome: metadata, sitemap, and performance baseline for commercial publication. + +6. Deployment integration + - [~] #130 Vercel deploy scripts and package integration (5 pts) - implemented in workspace, awaiting PR/commit linkage for closure + - Schedule: 2026-07-21 -> 2026-08-03 (`iteration-b`) + - Outcome: one-command deploy flow from `package.json` and reproducible publishing process. + +7. Governance closure + - [~] #131 Agents/docs/project tracking synchronization (3 pts) - implemented in workspace, awaiting PR/commit linkage for closure + - Schedule: 2026-07-21 -> 2026-08-03 (`iteration-b`) + - Outcome: full traceability issue -> implementation -> PR, with updated requirements/docs. + ## Done - [x] Fix the build @@ -13,6 +83,15 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - `BaseService.ts` assigned `this.services = config.repos ?? {}` instead of `config.services`. - Fixed dependency injection wiring so services receive the intended dependencies. +- [x] Implement CacheService and read-endpoint caching baseline + - Added `apps/backend-template/src/infra/cache/CacheService.ts` and `ICacheService.ts` on top of `IKeyValueStorageClient`. + - Wired cache service through `composeUsersAuthServices`. + - Added read-through caching for `getOneById` and `getAll` in `UserService` and `OrganizationService`. + - Added invalidation by namespace version bump on create/update/delete and nested object mutation methods. + - Added unit coverage: + - `apps/backend-template/test/unit/infra/cache/CacheService.test.ts` + - cache behavior checks in Users and Organization service tests. + - [x] Normalize singleton factories - `compile()` cached singleton instances in services, repos, auth, providers, and controllers. - Removed stale singleton state so factory calls return fresh instances with current dependencies. @@ -54,6 +133,41 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - Extended bootstraps for gRPC, WebSocket, and Lambda runtime to inject `compileDatabaseClient()` based on `AAA_DATABASE_DRIVER`. - Expanded `ExternalStoreProxy` to provide `IStore` mapping for Mongo, Sequelize-based SQL drivers, DynamoDB, Cassandra, Firebase, and Oracle. +- [x] Add realtime API test matrix (unit, integration, smoke) + - Added protocol integration tests: + - `apps/backend-template/test/integration/realtime/websocket.basic.integration.test.ts` + - `apps/backend-template/test/integration/realtime/grpc.basic.integration.test.ts` + - Added Redis multi-instance integration: + - `apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts` + - Added realtime smoke test: + - `apps/backend-template/test/smoke/realtime/RealtimeApis.smoke.test.ts` + - Added package scripts: + - `test:integration:realtime*` + - `test:smoke:realtime` + - `smoke:realtime:redis-streams` + - Added documentation and requirement registry sync: + - `documentation/md/REALTIME-API-TESTING.md` + - `.agents/requirements/047-realtime-api-test-matrix.md` + +- [x] Extract generic adapters/contracts into distributable workspace packages (ongoing wave) + - Added `@jumentix/message-mediator` package and bridged local mediator ports/adapters to package exports. + - Added `@jumentix/key-value-storage` package and bridged local key-value clients/compiler to package exports. + - Added `@jumentix/persistence-contracts` package for shared `IStore` + `IDatabaseClient` contracts. + - Added `@jumentix/mutex-service` package and bridged local mutex service/port to package exports. + - Added `@jumentix/external-persistence-core` package and migrated external repositories to import it directly. + - Added `@jumentix/external-store-proxy` package and bridged local `ExternalStoreProxy`/`createExternalStores` exports. + - Added `@jumentix/external-db-repositories` package and bridged all concrete external DB repositories (Mongo, Sequelize SQL, DynamoDB, Cassandra, Firebase, Aurora, RDS, Oracle). + - Added `@jumentix/database-client-factory` package and migrated local `compileDatabaseClient` to a bridge backed by package compilers. + - Added `@jumentix/runtime-infra` package and migrated core adapters (Express/Fastify/Restify/Hyper-Express/gRPC/WebSocket) to shared env-based runtime infra compilation. + - Added `@jumentix/adapter-runtime-bootstrap` package and migrated adapter runtime composition (crypto/jwt/mediator/auth wiring) for Express/gRPC/WebSocket. + - Expanded `@jumentix/adapter-runtime-bootstrap` adoption across HTTP adapters (Fastify, Restify, Hyper-Express, LoopBack, Sails, Adonis, Feathers, Derby, Total.js, Vercel Functions, Cloudflare Workers). + - Consolidated REST adapter loader to support all implemented HTTP frameworks through `AAA_HTTP_FRAMEWORK` and aligned PM2 scripts to start via `start-rest-api` loader. + - Normalized REST startup scripts to always set `AAA_HTTP_FRAMEWORK` explicitly (including express) for deterministic adapter selection. + - Updated runtime docs (`RUNTIME-ENVIRONMENT-CONTRACTS`, `SETUP-RUNTIME-AND-API`) to reflect loader-based framework matrix startup. + - Added generic loader scripts (`dev:http`, `prod:http`) for REST startup with env-driven framework resolution. + - Updated README adapter examples to use loader-based startup for framework selection instead of non-existent per-framework start functions. + - Updated root TypeScript path mappings for new packages to preserve compatibility while migrating. + ## Open - [x] PCI Remediation Plan (Sprint-based: P0/P1/P2) with audit evidence criteria @@ -63,8 +177,8 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - Add auth lockout/revocation paths in `AuthService` with key-value storage support. - Keep internal error details visible in `dev/staging` and masked in `production`. - Audit evidence: - - Unit evidence: `test/unit/modules/Users/interface/controller/controllers.test.ts`. - - Unit evidence: `test/unit/modules/Users/service/AuthService.branches.test.ts`. + - Unit evidence: `apps/backend-template/test/unit/modules/Users/interface/controller/controllers.test.ts`. + - Unit evidence: `apps/backend-template/test/unit/modules/Users/service/AuthService.branches.test.ts`. - Runtime evidence: adapter-level error payload builders use environment-aware masking. - [x] P0 - Security baseline for transport and headers @@ -72,7 +186,7 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - Restrict CORS with allowlist strategy via env (`AAA_CORS_ALLOWED_ORIGINS`). - Ensure helmet is active in REST adapters where supported. - Audit evidence: - - Config evidence: `src/config/security.ts`. + - Config evidence: `apps/backend-template/src/config/security.ts`. - Adapter evidence: `ExpressServer.ts`, `FastifyServer.ts`. - Lint + unit gate green. @@ -107,13 +221,13 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - [x] Program Increment - Environment-driven adapter startup and runtime env editing - Objective: make REST/Realtime adapter startup fully environment-driven and editable from Service Management. - Scope: - - separated startup adapters for `src/interface/WebSocket/adapters/` and `src/interface/gRPC/adapters/` + - separated startup adapters for `apps/backend-template/src/interface/WebSocket/adapters/` and `apps/backend-template/src/interface/gRPC/adapters/` - PM2 starts realtime APIs in separated processes - env variables govern adapter selection and realtime startup behavior - Service Management reads/updates active env file values across Windows/macOS/Linux - [x] Phase 1 - Runtime env contract - - Add and document new env keys in all `src/config/.env*` files: + - Add and document new env keys in all `apps/backend-template/src/config/.env*` files: - `AAA_HTTP_FRAMEWORK` (default `express`) - `AAA_REALTIME_API` (default `no`) - `AAA_REALTIME_API_PROTOCOL` (default `websocket`) @@ -121,8 +235,8 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - [x] Phase 2 - Startup adapter separation - Create dedicated startup adapter files: - - `src/interface/WebSocket/adapters/start-websocket-api.ts` - - `src/interface/gRPC/adapters/start-grpc-api.ts` + - `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` + - `apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts` - Keep protocol-specific bootstraps isolated and independently PM2-runnable. - [x] Phase 3 - Env-driven startup loaders @@ -272,16 +386,21 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - [x] Add a minimal CI gate - Required before features: lint, unit tests, selected integration smoke, OpenAPI route resolution check, and build once dependency mismatch is fixed. -- [ ] Reach and sustain minimum 95% coverage +- [x] Reach and sustain minimum 95% coverage - Enforce 95% as standard in Jest global `coverageThreshold`. - Enforce 95% targets in Codecov project/patch status. - Raise current tests to reach and keep the threshold. + - Status: + - Root unit coverage remains above 95% (and above 99% for statements/lines/functions with branch >= 90 policy). + - CI gate blocks regressions via `test:unit` + strict threshold checks. -- [ ] Enforce architecture NFR consistency across layers +- [x] Enforce architecture NFR consistency across layers - Align implementation to DDD + EDA + Hexagonal Architecture + SOLID. - Keep explicit ownership for domains, entities, ports, adapters, repositories, services, use cases, controllers, and handlers. - Progress: CI now enforces `arch:check-boundaries` for controller-layer anti-patterns. - Progress: README and migration docs now reflect canonical feature-driven hexagonal structure and active guardrails. + - Status: + - Boundary checks now include controller-layer boundaries, legacy import checks, and workspace dependency boundary checks. - [x] Normalize layer call order - Ensure driving adapters/controllers call application use-cases as the entry point. @@ -362,56 +481,442 @@ Keep every item in either `Done` or `Open`, and move items as they are completed - Clickable check issues that focus affected entity - [x] Inspector productivity - CRUD endpoint preview (path + operationId) for selected entity +- [x] Anchor-based drag-and-drop relationship connectors + - Added edge anchor controls (`top`, `right`, `bottom`, `left`) on entity cards. + - Added drag-preview edge and anchor-to-anchor relationship creation. +- [x] Domain-level bounded-context metadata editor + - Added context metadata fields per domain: + - ubiquitous language + - owner team + - upstream/downstream dependencies + - integration channel +- [x] Relationship label positioning controls + - Added relationship label offset editor (`x` / `y`) and reset action in inspector. +- [x] Aggregate root and invariant editor + - Added aggregate-root marker per entity and invariant rules editor in inspector. + - Aggregate root entities now show visual `AR` badge in entity header. +- [x] Schema diff and migration preview + - Added local baseline save/clear and schema diff comparison. + - Added migration hints for entity/field/relationship create-drop-alter changes. +- [x] Validation severity levels and quality gates in designer + - Added severity levels (`error`, `warn`, `info`) in model check results. + - Added configurable export gate to block export when critical issues exist. +- [x] RBAC policy mapping UI per entity/action + - Added action-level role mapping (`list/getById/create/update/delete`) with tenant scope toggle. + - Added RBAC policy preview and model-check warnings for missing action permissions. +- [x] Event/message contract designer connected to entities + - Added action panel to create/edit/remove `event`, `command`, `request`, and `response` contracts. + - Added contract payload schema editing and OpenAPI export extension (`x-message-contracts`). +- [x] Code generation preview pane + - Added generated skeleton preview for model/repository/use-case/controller/handler. + - Supports selected-entity preview or full-canvas concatenated preview. +- [x] Pluggable exporters baseline + - Added Markdown exporter in addition to JSON and OpenAPI exports. +- [x] Advanced OpenAPI composition controls + - Added entity-level `oneOf` / `allOf` / `anyOf` composition editor with schema refs list. + - Composition is exported into generated OAS schemas. +- [x] Collaborative model package support (baseline) + - Added domain package export/import flow from Domain Designer export panel. + - Supports reusable domain package import into current model canvas. +- [x] Request/response example generator from entity schema + - Added generated request/response example preview for selected entity or whole canvas. + - Supports create/update request and response payload examples. +- [x] Additional pluggable exporter target (JSON Schema) + - Added JSON Schema exporter button in Domain Designer export panel. +- [x] Advanced relationship path controls + - Added bend point controls (`bendX`, `bendY`) and anchor behavior mode (`auto`, `center`). +- [x] Entity templates and scaffolding packs by pattern + - Added `crudAggregate`, `eventSourced`, `referenceData`, and `tenantOwned` entity templates. +- [x] More pluggable exporter targets + - Added AsyncAPI exporter and boilerplate bundle exporter. +- [x] Expanded advanced OpenAPI controls + - Added external `$ref` list support and discriminator configuration. +- [x] Expanded collaborative package support + - Added package dependencies and shared value-object metadata for bounded context. +- [x] Visual mini-map and large-canvas performance mode + - Added mini-map navigation and large-canvas mode for simplified rendering. +- [x] Starter e2e/smoke test coverage for `servicemangement` + - Added smoke tests for create/edit/export/import workflow controls. ### Open (next MVP ideas to prioritize) Backlog sync status: - Synced with `documentation/md/DOMAIN-DESIGNER-MVP-ROADMAP.md` open priorities. -- [ ] Add true drag-and-drop relationship connectors on entity edge anchors - - Current pick/form flows are productive, but visual anchor-to-anchor linking is still missing. - -- [ ] Add relationship label positioning and advanced path controls - - Let users reposition relation labels and configure custom bend points/anchor behavior. - -- [ ] Add domain-level bounded-context metadata editor - - Ubiquitous language, owner team, upstream/downstream dependencies, integration channel. - -- [ ] Add explicit aggregate root and invariant editor - - Mark aggregate roots and attach invariant rules per entity/aggregate. - -- [ ] Add entity templates and scaffolding packs by pattern - - CRUD aggregate, event-sourced aggregate, reference data, tenant-owned entity. - -- [ ] Add schema diff and migration preview - - Compare current model with previous snapshot and output human-readable migration hints. - -- [ ] Add event/message contract designer connected to entities - - Define domain events, commands, request/response contracts, payload schema, and version. - -- [ ] Add RBAC policy mapping UI per entity/action - - Connect resource operations to role/scope matrix and export policy contract. - -- [ ] Add advanced OpenAPI controls - - OneOf/AllOf/AnyOf builders, external `$ref` management, discriminator support. - -- [ ] Add request/response example generator from entity schema - - Auto-generate examples for API docs and handler test fixtures. - -- [ ] Add code generation preview pane - - Show generated domain model, repository contract, use case, and handler skeletons before export. - -- [ ] Add pluggable exporter targets - - OpenAPI, JSON schema, markdown docs, and boilerplate module skeletons. - -- [ ] Add validation severity levels and quality gates in designer - - Error/warn/info levels and “block export when critical issues exist”. - -- [ ] Add collaborative model package support - - Import/export reusable domain packages and shared value object libraries. - -- [ ] Add visual mini-map and large-canvas performance mode - - Required for very large multi-domain models. - -- [ ] Add starter e2e test coverage for `servicemangement` - - Smoke checks for create/edit/export/import to reduce regression risk. +- [x] Expanded collaborative package support further + - Added package dependency normalization and shared value-object deduplication in package import/save flow. + +- [x] Roadmap MVP completed + - All planned Domain Designer MVP roadmap items implemented. + +## Open - JumentiX Monorepo Reorganization Plan (Execution Pending) + +Goal: +- Reorganize `aaa-typescript-boilerplate` into a pnpm monorepo product named `JumentiX`, with clear package boundaries, deterministic migration waves, and low-risk rollback points. +- This section is planning-only by request. No structural migration should start until explicit execution order is provided. +- Canonical technical execution reference: + - `documentation/md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md` + - `documentation/md/JUMENTIX-MIGRATION-INVENTORY-AND-ROLLBACK.md` + +### Phase 0 - Planning Guardrails (mandatory before code moves) + +- [x] Freeze migration scope and success criteria + - Define what is in-scope for Wave 1 (must-haves) vs Wave 2+ (enhancements). + - Lock naming decisions: + - product: `JumentiX` + - npm init CLI package naming strategy resolved with official scoped package distribution (`@jumentix/cli-init`) and `jumentix-init` command. + - Define migration policy: + - no partial cutovers without complete functional parity in the same wave + - docs and agents update in same wave + - quality gates green before each merge. + +- [x] Create migration branch strategy and release checkpoints + - Branch naming convention for each wave. + - Tagging plan before/after each destructive move. + - Rollback procedure documented per wave. + +### Phase 1 - Target Monorepo Architecture Design + +- [x] Define final workspace topology (pnpm workspaces) + - Proposed root structure: + - `apps/` + - `service-management` (web tool, current servicemangement) + - `backend-template` (current backend boilerplate app form) + - `packages/` + - `cli-init` (current CLI evolved to install/bootstrap JumentiX projects) + - `message-mediator` (independent npm lib) + - `sdk-rest-client` + - `sdk-websocket-client` + - `sdk-grpc-client` + - `config-ts` (shared tsconfig presets) + - `config-eslint` (shared lint presets) + - `config-jest` (shared test presets) + - `shared-contracts` (optional: OpenAPI/AsyncAPI contract helpers/types) + - `tooling/` + - CI scripts, release scripts, changelog/quality scripts + - `docs/` (or keep existing `documentation/` with workspace-aware index) + +- [x] Map current files into destination packages/apps + - Inventory current directories and map each one to `apps/*` or `packages/*`. + - Mark blockers for paths that cannot be moved without import breakage. + - Define direct import rewrites for staged migration waves. + +- [x] Define package dependency boundaries + - Backend app imports mediator from `packages/message-mediator` (no duplicated local copies). + - SDK packages are independent publishable libraries. + - Service management app consumes SDK/contracts via workspace dependencies. + - Enforcement added: + - `ci-cd/check-workspace-boundaries.js` + - `npm run arch:check-workspace-boundaries` + - `ci:gate` now blocks cross-app/package import boundary violations. + - Transition exception explicitly allowlisted: + - `packages/external-store-proxy/src/ExternalStoreProxy.ts` temporary `@src/infra/exceptions` bridge. + +### Phase 2 - pnpm Foundation Setup + +- [x] Add pnpm root management files + - `pnpm-workspace.yaml` + - root `package.json` with workspace scripts + - shared lockfile strategy with `pnpm-lock.yaml` + - workspace `.npmrc` policies (strict-peer-dependencies, node-linker strategy if needed). + - Status: + - `pnpm-workspace.yaml` created. + - root `package.json` updated with `packageManager` and `mono:*` scripts. + - `pnpm-lock.yaml` pending first successful online `pnpm install`. + +- [x] Add root toolchain baselines + - Node 22 enforcement across workspace (`engines`, `.nvmrc`, CI images). + - Shared TypeScript project references strategy. + - Shared lint/test/format scripts at root + per-package overrides. + - Status: + - Node 22 enforced at root (`engines`, `.nvmrc`, CircleCI node image and version checks). + - Shared config workspaces created (`packages/config-ts`, `packages/config-eslint`, `packages/config-jest`). + - Root monorepo orchestration scripts and quality gates centralized in root `package.json`. + +- [x] Define workspace task orchestration + - Standard scripts: + - `pnpm -r build` + - `pnpm -r test` + - `pnpm -r lint` + - selective affected execution strategy. + - Status: + - `mono:build`, `mono:test`, `mono:lint`, and `mono:typecheck` are available at root and use recursive workspace execution. + - `.npmrc` now enforces workspace linking and shared lockfile behavior for deterministic package resolution. + +### Phase 3 - Incremental Package Extraction Waves + +- [x] Wave A - Extract `message-mediator` as independent package + - Move mediator ports/contracts/adapters to `packages/message-mediator`. + - Preserve existing behavior with direct workspace import replacement in backend app. + - Add package-level tests and publish-ready metadata. + - Status: + - `packages/message-mediator` extracted and consumed by backend-template via workspace dependencies. + - Legacy local mediator modules now act as compatibility bridges. + +- [x] Wave B - Extract SDK clients into independent packages + - Split existing `sdk-clients` into package-per-protocol. + - Ensure each package consumes contracts from spec files and/or shared-contract package. + - Add usage examples and API contract tests. + - Status: + - `packages/sdk-rest-client`, `packages/sdk-websocket-client`, and `packages/sdk-grpc-client` are the canonical SDK implementations. + - Legacy `sdk-clients/*` remains as documented compatibility bridge. + +- [x] Wave C - Convert current CLI into `cli-init` package + - Keep current capabilities, then add bootstrap orchestration for: + - backend service + - frontend SPA/PWA offline + - mixed backend/frontend service groups + - Validate install/init UX (`npm install ... -g` flow requirement needs exact npm package plan). + - Status: + - `packages/cli-init` implemented with `jumentix-init` command and non-interactive bootstrap flags. + - Root `aaa-bootstrap` delegates to the package implementation. + +- [x] Wave D - Move current backend boilerplate to `apps/backend-template` + - Keep all current capabilities (HTTP frameworks, realtime, serverless, PM2, tests, docs). + - Replace internal references to extracted packages with workspace dependencies. + - Status: + - Source, tests, API docs, and docker artifacts re-homed under `apps/backend-template`. + +- [x] Wave E - Move `servicemangement` to `apps/service-management` + - Wire it to workspace packages (sdk/contracts/cli metadata where applicable). + - Keep PM2 startup profile support. + - Status: + - Service Management app ownership moved to `apps/service-management` and wired in PM2/root scripts. + +### Phase 4 - Product-Level Feature Planning (post-structure) + +- [x] Define service factory capabilities matrix + - Monolith modular + - Multi-service backend + - Hybrid backend + frontend + - Frontend-only SPA/PWA offline. + - Progress: + - Added `documentation/md/JUMENTIX-SERVICE-FACTORY-CAPABILITIES-MATRIX.md` with canonical factory modes, contracts, and NFR guarantees. + +- [x] Define deploy target matrix and packaging contracts + - VM/SSH, EC2, Lambda, Vercel Functions, Cloudflare Workers. + - Backend and frontend deployment metadata contracts for Service Management. + - Progress: + - Added `documentation/md/JUMENTIX-DEPLOY-TARGET-AND-PACKAGING-MATRIX.md` with deploy targets, packaging contracts, and service-management metadata schema. + +- [x] Define bundler/runtime templates by artifact type + - backend service + - frontend SPA + - frontend SSR + - npm lib backend + - npm lib frontend. + - Progress: + - Added `documentation/md/JUMENTIX-BUNDLER-RUNTIME-TEMPLATES.md` with artifact template matrix and runtime/build enforcement rules. + +### Phase 5 - CI/CD, Quality Gates, and Release Management + +- [x] Refactor CI pipelines for pnpm monorepo + - Cache pnpm store. + - Matrix by changed workspace. + - Keep required gates: + - lint + - unit/integration/smoke + - build + - coverage thresholds + - route/contract checks + - security checks. + +- [x] Enforce per-package coverage and global policy + - Keep current strict commit/push blockers. + - Add per-workspace thresholds with fail-fast behavior. + - Progress: + - Added workspace package quality gate (`workspace:check-quality`) to CI flow, enforcing required package script contracts and blocking placeholder test scripts. + - Added workspace coverage governance gate (`workspace:check-coverage-policy`) with: + - root Jest global threshold minimum verification, + - package test script anti-placeholder policy (with explicit allowlist for config placeholder packages). + +- [x] Define publishing/versioning strategy + - Changesets or equivalent release orchestration. + - Independent vs locked version policy per package. + - Changelog generation per package and aggregate product changelog. + - Progress: + - Added `ci-cd/check-release-governance.js` and `npm run release:governance:check` to enforce release scripts + package version/publish metadata. + - Integrated release governance check into `ci:gate`. + - Added unit tests for release governance validation rules. + - Added `release-policy.json` as canonical strategy contract: + - packages use independent versioning + - apps use locked versioning against root version. + - Added `documentation/md/JUMENTIX-RELEASE-AND-VERSIONING-STRATEGY.md` and README index links. + +### Phase 6 - Documentation and Product Positioning Alignment + +- [x] Rename and reposition docs to `JumentiX` + - Position documentation around JumentiX product architecture and workflows. + - Add workspace-first onboarding guide. + - Status: + - JumentiX monorepo docs are centralized under `documentation/md/*` with dedicated migration, packaging, release, and runtime guides. + +- [x] Update architecture and onboarding docs + - Monorepo workspace map + - package responsibilities + - development flows + - PM2 multi-app management in monorepo context. + - Progress: + - Updated `TESTING-CI-AND-QUALITY.md` with pnpm-based CI flow and strict coverage thresholds. + - Updated `ENGINEERING-BOOTSTRAP-GUIDE.md` with workspace map, monorepo orchestration commands, and PM2 multi-app runtime guidance. + - Added/updated architecture docs for monorepo waves and cutover evidence (`JUMENTIX-*` docs). + +- [x] Update agents and requirement registry + - Add dedicated requirements for monorepo governance, package boundaries, and release policy. + - Enforce task traceability requirement: every project issue must keep commit + PR association. + - Status: + - Requirement registry expanded through `062-workspace-dependency-boundaries-governance` and ongoing sync updates in `.agents/README.md`. + +### Risk Register and Anti-Waste Controls + +- [x] Risk: package naming ambiguity (`jumentix@init`) may be invalid as npm install target + - Mitigation implemented: + - Official install target standardized to scoped package `@jumentix/cli-init`. + - CLI execution command remains `jumentix-init` (plus compatibility alias `aaa-bootstrap`). + - Documentation updated to avoid relying on npm alias syntax ambiguity. + +- [x] Risk: breaking imports during moves + - Mitigation implemented: + - Wave-based migration with compatibility bridge exports and progressive path rewrites. + - CI compile + architecture gates enforce boundary validity after each move. + - Legacy namespace checks (`arch:check-users-legacy-imports`) prevent regressions. + +- [x] Risk: CI instability due to monorepo migration + - Mitigation implemented: + - Added monorepo-aware CI orchestrator (`ci:monorepo`) with docs-only and affected-workspace flows. + - CircleCI and GitHub Actions aligned to pnpm and `ci:monorepo`. + - Strict local blocker kept via `ci:gate`/`ci:gate:strict`. + +- [x] Risk: oversized PRs reduce review quality + - Mitigation implemented: + - Enforced wave/priority-based issue decomposition in GitHub Project. + - PR grouping-by-priority requirement maintained in agents registry. + - Task-level traceability now requires commit + PR references on each backlog item. + +### Acceptance Criteria for Starting Implementation + +- [x] Final architecture map approved (apps/packages boundaries and names). +- [x] Migration wave order approved. +- [x] npm package naming/install strategy approved for CLI bootstrap. +- [x] CI migration strategy approved (including quality gates and coverage policy). +- [x] Rollback and release strategy approved. + +### Detailed Wave Execution Backlog (Operational) + +- [x] Wave 1 - Workspace Foundation + - Deliverables: + - `pnpm-workspace.yaml` + - root scripts (`build`, `test`, `lint`, `typecheck`) using pnpm recursive execution + - Node 22 enforcement and CI alignment + - Done criteria: + - `pnpm -r lint`, `pnpm -r test`, `pnpm -r build` all green + - no broken local developer bootstrap flow + - Progress: + - workspace scaffold directories and package placeholders created in `apps/` and `packages/`. + - recursive scripts implemented at root. + - Node 22 enforced via root engines and CI/runtime alignment updates. + - full pnpm recursive validation still pending due local network/package-manager instability during install bootstrap. + +- [x] Wave 2 - Message Mediator Package Extraction + - Deliverables: + - `packages/message-mediator` with ports/contracts/adapters + - backend-template consuming mediator via workspace dependency + - Done criteria: + - mediator unit tests green in package scope + - backend app tests green after import rewrite + - Progress: + - contracts + in-memory adapter extracted to `packages/message-mediator`. + - RabbitMQ and BullMQ adapters extracted to `packages/message-mediator`. + - compile helper extracted (`compileMessageMediator`) with local bridge kept for compatibility. + - local core mediator contracts in `apps/backend-template/src/modules/port/*` now re-export from workspace package to avoid drift. + - local mediator adapters in `apps/backend-template/src/infra/messages/adapters` now re-export from workspace package. + +- [x] Wave 3 - SDK Package Split + - Deliverables: + - `packages/sdk-rest-client` + - `packages/sdk-websocket-client` + - `packages/sdk-grpc-client` + - Done criteria: + - each SDK has build/test/docs and independent package metadata + - contract tests green against spec-driven behavior + - Progress: + - Created independent package source trees for REST/WebSocket/gRPC SDKs under `packages/sdk-*/src`. + - Added package-level `build`/`typecheck` scripts and package manifests (`main`, `types`, `files`, dependencies). + - Spec loading was localized per package with AsyncAPI/OpenAPI-driven defaults preserved. + - Added package-level README guides with usage examples and build commands. + - Legacy `sdk-clients/*` now acts as compatibility layer and re-exports SDK implementations from workspace packages. + - Added explicit compatibility-bridge documentation and migration/decommission criteria. + - SDK package `test` scripts now execute package-level typecheck to avoid placeholder/no-op tests. + +- [x] Wave 4 - CLI Productization (`cli-init`) + - Deliverables: + - package extraction for current CLI + - scaffold commands for backend/frontend/hybrid service groups + - Done criteria: + - install/bootstrap smoke path validated end-to-end + - docs + examples published in repository docs + - Progress: + - `packages/cli-init` now contains a functional bootstrap CLI entrypoint (`bin/jumentix-init.js`) and reusable implementation module (`packages/cli-init/src/bootstrap.js`). + - root `bin/aaa-bootstrap.js` now delegates to `packages/cli-init`, reducing duplication and keeping workspace packaging aligned. + - Added package-level CLI README and command contracts (`jumentix-init` and `aaa-bootstrap` alias). + - Added non-interactive CLI flags and help output (`--service-type`, `--project-name`, `--git-branch`, `--install-deps`, `--repo`) to support automation pipelines. + - CLI package test script now performs help smoke execution (`jumentix-init --help`). + - Non-interactive scaffold smoke validated end-to-end using local repository source and generated `.aaa/service-profile.json`. + +- [x] Wave 5 - Apps Re-homing + - Deliverables: + - current backend boilerplate moved to `apps/backend-template` + - `servicemangement` moved to `apps/service-management` + - Done criteria: + - both apps build/test/run under pnpm workspace scripts + - PM2 startup profiles still functional + - Progress: + - Added migration-focused app READMEs for `apps/backend-template` and `apps/service-management` with explicit Wave 5 cutover steps. + - Added executable cutover playbook with sequence, rollback, and acceptance criteria: `documentation/md/JUMENTIX-WAVE5-APP-REHOMING-CUTOVER.md`. + - Replaced placeholder workspace app scripts with executable transitional scripts mapped to root runtime commands for `apps/backend-template` and `apps/service-management`. + - Physical runtime re-homing applied: + - `apps/backend-template/src/` -> `apps/backend-template/src` + - `apps/backend-template/test/` -> `apps/backend-template/test` + - `OASdoc/` + `AsyncAPIdoc/` -> `apps/backend-template/` + - `docker/` + `docker-compose*.yml` -> `apps/backend-template/` + - PM2 ownership moved to root `pm2/*` ecosystems. + - Audit evidence: + - PR: `https://github.com/web2solutions/aaa-typescript-boilerplate/pull/112` + - Required checks green for PR #112 (build/circleci/snyk/sonar/gitguardian). + - Related closeout issues: + - `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/113` + - `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/114` + - `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/115` + - Residual risks tracked outside Wave 5: + - Coverage and policy hardening remains under Wave 6 + issue `#79`. + - Architecture and docs governance follow-up remains tracked under issues `#82`, `#83`, and `#116`. + +- [x] Wave 6 - CI/CD + Release Strategy Hardening + - Deliverables: + - workspace-aware CI with selective execution by affected packages/apps + - publishing/versioning flow documented and implemented + - Done criteria: + - PR checks green with monorepo matrix + - release dry-run for at least one package and one app artifact + - Progress: + - Added `ci-cd/check-affected-workspaces.js` and root script `npm run ci:affected` to classify changed files across `root`, `apps/*`, `packages/*`, and docs-only scopes. + - Added unit tests for affected workspace detection and docs-only classification logic. + - Added `ci-cd/release-dry-run.js` and root `release:dry-run*` commands to validate package publish artifacts (dry-run pack) and app workspace release readiness contracts. + - Added `ci-cd/run-monorepo-ci.js` + `npm run ci:monorepo` with scope-based execution (docs-only lightweight checks or strict gate + affected apps/packages workflow). + - Updated CI pipelines to execute monorepo-aware flow (`ci:monorepo`) with pnpm dependency setup in GitHub Actions and CircleCI. + - Hardened `arch:check-boundaries` to scan real HTTP controllers under `adapters/in/http/controllers` and enforce application use-case imports. + +### Documentation Round (EPIC #117) + +- [ ] Create root Jumentix marketing README with complete component index and required solution sections. + - Epic: `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/117` + - GitHub Project source of truth: `https://github.com/users/web2solutions/projects/1` +- [ ] Reorganize component/project docs under component folders (`apps/*`, `packages/*`) and add technical hubs. +- [ ] Ensure root README links to all component docs and all required guides: + - SPA/PWA + - REST API + - Realtime API + - SaaS Monolith + - SaaS Microservices +- [ ] Add technical-rich docs for backend-template and service-management with architecture, integrations, and examples. +- [ ] Validate documentation navigation integrity (root index + component hubs with no dead links). diff --git a/.agents/requirements/029-multi-tenancy-and-rbac-foundation.md b/.agents/requirements/029-multi-tenancy-and-rbac-foundation.md index 93d205d2..856eec2f 100644 --- a/.agents/requirements/029-multi-tenancy-and-rbac-foundation.md +++ b/.agents/requirements/029-multi-tenancy-and-rbac-foundation.md @@ -17,5 +17,5 @@ Most products require tenant isolation and role-based authorization early, witho Active ## Notes -- Users domain RBAC rules are centralized in `src/modules/Users/domain/security/Rbac.ts`. +- Users domain RBAC rules are centralized in `apps/backend-template/src/modules/Users/domain/security/Rbac.ts`. - `User` model and auth flow enforce organization binding for tenant roles. diff --git a/.agents/requirements/030-inmemory-relational-nosql-behavior-parity.md b/.agents/requirements/030-inmemory-relational-nosql-behavior-parity.md index 48a46dad..62c638c8 100644 --- a/.agents/requirements/030-inmemory-relational-nosql-behavior-parity.md +++ b/.agents/requirements/030-inmemory-relational-nosql-behavior-parity.md @@ -17,4 +17,4 @@ Active ## Notes - Current implementation uses generic relational-style base store: - - `src/infra/persistence/InMemoryDatabase/Stores/InMemoryRelationalStore.ts` + - `apps/backend-template/src/infra/persistence/InMemoryDatabase/Stores/InMemoryRelationalStore.ts` diff --git a/.agents/requirements/031-users-organization-and-address-value-object.md b/.agents/requirements/031-users-organization-and-address-value-object.md index 87cf366b..4e3fc477 100644 --- a/.agents/requirements/031-users-organization-and-address-value-object.md +++ b/.agents/requirements/031-users-organization-and-address-value-object.md @@ -16,4 +16,4 @@ Active ## Notes - Organization artifacts are implemented in Users domain/application/service/persistence layers. -- Address value object is available under `src/modules/ddd/valueObjects`. +- Address value object is available under `apps/backend-template/src/modules/ddd/valueObjects`. diff --git a/.agents/requirements/039-external-data-adapter-foundations.md b/.agents/requirements/039-external-data-adapter-foundations.md index fcb3e28d..d8f63a2b 100644 --- a/.agents/requirements/039-external-data-adapter-foundations.md +++ b/.agents/requirements/039-external-data-adapter-foundations.md @@ -16,4 +16,4 @@ The boilerplate must provide explicit repository adapter foundations for relatio 3. Provider adapters must remain behind ports/contracts. ## Implementation Notes -- Initial foundations live in `src/infra/persistence/external/`. +- Initial foundations live in `apps/backend-template/src/infra/persistence/external/`. diff --git a/.agents/requirements/040-platform-infrastructure-containers.md b/.agents/requirements/040-platform-infrastructure-containers.md index 47f8c403..ff6d3677 100644 --- a/.agents/requirements/040-platform-infrastructure-containers.md +++ b/.agents/requirements/040-platform-infrastructure-containers.md @@ -9,7 +9,7 @@ Local development must provide containerized support services for the platform i 3. Document startup/teardown commands and expected caveats. ## Implementation Notes -- Compose file: `docker-compose-platform-services.yml`. +- Compose file: `apps/backend-template/docker-compose-platform-services.yml`. - Scripts: - `npm run docker:compose:platform-services` - `npm run docker:stop:platform-services` diff --git a/.agents/requirements/041-pm2-vm-runtime-orchestration.md b/.agents/requirements/041-pm2-vm-runtime-orchestration.md index a57307cd..27e91c6c 100644 --- a/.agents/requirements/041-pm2-vm-runtime-orchestration.md +++ b/.agents/requirements/041-pm2-vm-runtime-orchestration.md @@ -18,9 +18,9 @@ VM-hosted environments (`dev`, `staging`, `production`) must use PM2 as the offi - `staging` - `production` 6. Package scripts that boot runtime adapters from: - - `src/interface/HTTP/adapters` - - `src/interface/gRPC/adapters` - - `src/interface/WebSocket/adapters` + - `apps/backend-template/src/interface/HTTP/adapters` + - `apps/backend-template/src/interface/gRPC/adapters` + - `apps/backend-template/src/interface/WebSocket/adapters` must use PM2-based startup flow. ## Validation diff --git a/.agents/requirements/042-env-driven-runtime-adapter-selection.md b/.agents/requirements/042-env-driven-runtime-adapter-selection.md index af5f0e8f..2588eada 100644 --- a/.agents/requirements/042-env-driven-runtime-adapter-selection.md +++ b/.agents/requirements/042-env-driven-runtime-adapter-selection.md @@ -5,11 +5,11 @@ Runtime startup must be driven by environment variables and editable through Service Management: - Split startup entrypoints: - - `src/interface/HTTP/adapters/start-rest-api.ts` - - `src/interface/WebSocket/adapters/start-websocket-api.ts` - - `src/interface/gRPC/adapters/start-grpc-api.ts` + - `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` + - `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` + - `apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts` - PM2 must start REST, WebSocket, and gRPC as separate processes. -- Runtime selection keys must exist in all `src/config/.env*` files: +- Runtime selection keys must exist in all `apps/backend-template/src/config/.env*` files: - `AAA_HTTP_FRAMEWORK` (default `express`) - `AAA_REALTIME_API` (default `no`) - `AAA_REALTIME_API_PROTOCOL` (default `websocket`) diff --git a/.agents/requirements/044-pci-security-compliance-hardening.md b/.agents/requirements/044-pci-security-compliance-hardening.md index 70f3194d..59797c00 100644 --- a/.agents/requirements/044-pci-security-compliance-hardening.md +++ b/.agents/requirements/044-pci-security-compliance-hardening.md @@ -19,18 +19,18 @@ Access control, authentication flows, and error contracts must align with PCI-or ## Current implementation anchors -- `src/modules/Users/service/AuthService.ts` -- `src/modules/Users/adapters/in/http/controllers/UserController.ts` -- `src/modules/Users/adapters/in/http/controllers/OrganizationController.ts` -- `src/shared/utils.ts` -- `src/config/security.ts` -- `src/infra/audit/InMemorySecurityAuditRepository.ts` -- `test/unit/modules/Users/service/AuthService.branches.test.ts` -- `test/unit/modules/Users/interface/controller/controllers.test.ts` -- `test/unit/config/security.test.ts` -- `test/unit/shared/utils.errorExposure.test.ts` -- `test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts` -- `test/unit/modules/Users/service/AuthService.audit.test.ts` +- `apps/backend-template/src/modules/Users/service/AuthService.ts` +- `apps/backend-template/src/modules/Users/adapters/in/http/controllers/UserController.ts` +- `apps/backend-template/src/modules/Users/adapters/in/http/controllers/OrganizationController.ts` +- `apps/backend-template/src/shared/utils.ts` +- `apps/backend-template/src/config/security.ts` +- `apps/backend-template/src/infra/audit/InMemorySecurityAuditRepository.ts` +- `apps/backend-template/test/unit/modules/Users/service/AuthService.branches.test.ts` +- `apps/backend-template/test/unit/modules/Users/interface/controller/controllers.test.ts` +- `apps/backend-template/test/unit/config/security.test.ts` +- `apps/backend-template/test/unit/shared/utils.errorExposure.test.ts` +- `apps/backend-template/test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts` +- `apps/backend-template/test/unit/modules/Users/service/AuthService.audit.test.ts` - `package.json` (`ci:security-smoke` inside `ci:gate`) - `documentation/md/SECURITY-RUNBOOK-PCI.md` diff --git a/.agents/requirements/045-data-entity-controller-ownership.md b/.agents/requirements/045-data-entity-controller-ownership.md index 260c2337..c6e6fc0a 100644 --- a/.agents/requirements/045-data-entity-controller-ownership.md +++ b/.agents/requirements/045-data-entity-controller-ownership.md @@ -13,11 +13,11 @@ Each data entity must expose its own controller, with explicit responsibility bo ## Current implementation anchors -- `src/modules/Users/adapters/in/http/controllers/UserController.ts` -- `src/modules/Users/adapters/in/http/controllers/OrganizationController.ts` -- `src/interface/HTTP/RestAPI.ts` -- `src/interface/Async/RealtimeAPIBase.ts` -- `test/unit/modules/Users/interface/controller/controllers.test.ts` +- `apps/backend-template/src/modules/Users/adapters/in/http/controllers/UserController.ts` +- `apps/backend-template/src/modules/Users/adapters/in/http/controllers/OrganizationController.ts` +- `apps/backend-template/src/interface/HTTP/RestAPI.ts` +- `apps/backend-template/src/interface/Async/RealtimeAPIBase.ts` +- `apps/backend-template/test/unit/modules/Users/interface/controller/controllers.test.ts` ## Definition of done diff --git a/.agents/requirements/046-multi-database-driver-smoke-validation.md b/.agents/requirements/046-multi-database-driver-smoke-validation.md index 397bb494..5865eed7 100644 --- a/.agents/requirements/046-multi-database-driver-smoke-validation.md +++ b/.agents/requirements/046-multi-database-driver-smoke-validation.md @@ -22,16 +22,16 @@ Mandatory outcomes: - `Aurora` - `RDS` - Add per-database compose files: - - `docker-compose-postgresql.yml` - - `docker-compose-mysql.yml` - - `docker-compose-mssql.yml` - - `docker-compose-oracle.yml` - - `docker-compose-mongodb.yml` - - `docker-compose-cassandra.yml` - - `docker-compose-dynamodb.yml` - - `docker-compose-firebase.yml` - - `docker-compose-aurora.yml` - - `docker-compose-rds.yml` + - `apps/backend-template/docker-compose-postgresql.yml` + - `apps/backend-template/docker-compose-mysql.yml` + - `apps/backend-template/docker-compose-mssql.yml` + - `apps/backend-template/docker-compose-oracle.yml` + - `apps/backend-template/docker-compose-mongodb.yml` + - `apps/backend-template/docker-compose-cassandra.yml` + - `apps/backend-template/docker-compose-dynamodb.yml` + - `apps/backend-template/docker-compose-firebase.yml` + - `apps/backend-template/docker-compose-aurora.yml` + - `apps/backend-template/docker-compose-rds.yml` - Add package scripts for: - per-driver smoke test execution - compose up/down helpers diff --git a/.agents/requirements/047-realtime-api-test-matrix.md b/.agents/requirements/047-realtime-api-test-matrix.md new file mode 100644 index 00000000..a787dc9d --- /dev/null +++ b/.agents/requirements/047-realtime-api-test-matrix.md @@ -0,0 +1,34 @@ +# Requirement 047 - Realtime API Test Matrix + +## Requirement + +Realtime APIs must have explicit automated validation at three levels: + +1. **Unit tests** for WebSocket and gRPC adapters/core runtime behavior. +2. **Integration tests** for protocol-level request/response execution. +3. **Smoke tests** for quick runtime health, including multi-instance Redis-backed validation where applicable. + +## Acceptance Criteria + +- Unit coverage exists for: + - `WebSocketAPI` + - `gRPCAPI` + - realtime bootstrap loaders + - adapter strategy modules (`cluster`, `redis-streams`) +- Integration coverage exists for: + - basic WebSocket realtime flow + - basic gRPC realtime flow + - multi-instance Socket.IO + Redis Streams propagation +- Smoke coverage exists for: + - local realtime protocol boot/response + - Redis-backed multi-instance command path +- Scripts are available in `package.json` for all realtime test scopes. +- Documentation is updated: + - `documentation/md/REALTIME-API-TESTING.md` + - README index links + +## Notes + +- Redis-backed integration should be environment-gated for CI/local portability (`RUN_REDIS_INTEGRATION=1`). +- Realtime smoke/integration commands should run with `--coverage=false` to avoid coverage artifact drift. + diff --git a/.agents/requirements/048-jumentix-pnpm-monorepo-productization.md b/.agents/requirements/048-jumentix-pnpm-monorepo-productization.md new file mode 100644 index 00000000..5c66822f --- /dev/null +++ b/.agents/requirements/048-jumentix-pnpm-monorepo-productization.md @@ -0,0 +1,33 @@ +# Requirement 048 - JumentiX pnpm Monorepo Productization + +## Requirement + +`aaa-typescript-boilerplate` must evolve into the `JumentiX` product as a pnpm-managed monorepo. + +The monorepo must support: + +1. Product apps (`backend-template`, `service-management`). +2. Publishable packages (`cli-init`, `message-mediator`, protocol SDKs, shared configs/contracts). +3. Workspace-wide quality gates (lint, test, build, coverage, security). +4. PM2 runtime management for multi-service backend/frontend execution. + +The migration must be executed in deterministic waves, with functional parity required before advancing each wave. + +## Acceptance Criteria + +- Workspace foundation exists and is functional: + - `pnpm-workspace.yaml` + - root scripts for recursive build/test/lint + - Node 22 enforced across workspace and CI +- `message-mediator` is consumed as workspace package (not duplicated in backend app code). +- SDK clients are split into independent publishable workspace packages. +- CLI package is publish-ready and supports bootstrap orchestration for JumentiX project structures. +- Existing backend capabilities remain available after move to `apps/backend-template`. +- Service management remains functional under `apps/service-management`. +- CI runs workspace-aware pipelines and preserves strict quality/coverage/security gates. +- Product documentation and agents requirements are synchronized with monorepo structure. + +## Notes + +- Migration is wave-based and must avoid uncertain parallel rewrites. +- No compatibility-shim strategy is required; prefer direct import rewrites with codemod-assisted changes and hard gates. diff --git a/.agents/requirements/049-jumentix-wave-execution-governance.md b/.agents/requirements/049-jumentix-wave-execution-governance.md new file mode 100644 index 00000000..f6db5622 --- /dev/null +++ b/.agents/requirements/049-jumentix-wave-execution-governance.md @@ -0,0 +1,27 @@ +# Requirement 049 - JumentiX Wave Execution Governance + +## Requirement + +JumentiX monorepo migration must be performed with deterministic, wave-based governance to reduce uncertain implementation and wasted effort. + +Each wave must define: + +1. Explicit scope. +2. Deliverables. +3. Acceptance criteria. +4. Rollback point. +5. Quality gates required before proceeding. + +## Acceptance Criteria + +- A centralized execution plan exists and is maintained: + - `documentation/md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md` +- `project-todos` includes the operational wave checklist. +- Each migration wave only advances when: + - build/test/lint/coverage/security checks are green + - docs and agents are synchronized +- No multi-wave mixed PRs unless explicitly justified by dependency constraints. + +## Notes + +- This requirement is process and governance oriented and complements the technical monorepo requirement. diff --git a/.agents/requirements/050-generic-adapters-as-distributable-packages.md b/.agents/requirements/050-generic-adapters-as-distributable-packages.md new file mode 100644 index 00000000..91bf16dc --- /dev/null +++ b/.agents/requirements/050-generic-adapters-as-distributable-packages.md @@ -0,0 +1,22 @@ +# Requirement 050 - Generic Adapters as Distributable npm Packages + +## Requirement + +Any adapter that is generic and reusable across multiple services must be extracted as a distributable npm package. + +The adapter must not remain duplicated inside each generated service codebase when it can be consumed as a shared package. + +## Acceptance Criteria + +- Reusable adapters are implemented under workspace packages and prepared for npm distribution. +- Service code imports reusable adapters/contracts from workspace packages instead of local duplicated implementations. +- Local legacy files (when kept temporarily) must be thin re-exports only, to avoid behavior drift. +- Package-level documentation exists describing: + - purpose + - supported runtimes/providers + - integration usage +- Migration wave todos and requirements registry are updated when a new reusable adapter is packaged. + +## Notes + +- `@jumentix/message-mediator` is the reference implementation pattern. diff --git a/.agents/requirements/051-runtime-bootstrap-shared-packages.md b/.agents/requirements/051-runtime-bootstrap-shared-packages.md new file mode 100644 index 00000000..f093821f --- /dev/null +++ b/.agents/requirements/051-runtime-bootstrap-shared-packages.md @@ -0,0 +1,19 @@ +# 051 - Runtime Bootstrap Shared Packages + +## Context + +As part of JumentiX monorepo productization, adapter bootstrapping must avoid repeated wiring logic across HTTP, WebSocket, and gRPC startup files. + +## Requirement + +1. Runtime bootstrap concerns must be centralized into reusable workspace packages: + - infra selection by env (`database`, `key-value`, `mutex`) + - adapter composition (`jwt`, `crypto`, `message mediator`, `auth service`) +2. Interface adapters must consume shared bootstrap compilers rather than duplicating wiring logic. +3. Existing adapter entrypoints must remain backward compatible while migration happens. + +## Acceptance Criteria + +- Shared packages are created and used by adapter entrypoints. +- Core adapter bootstrap tests remain green. +- No change in runtime contract for existing startup commands. diff --git a/.agents/requirements/052-rest-loader-framework-matrix.md b/.agents/requirements/052-rest-loader-framework-matrix.md new file mode 100644 index 00000000..e3db1ef6 --- /dev/null +++ b/.agents/requirements/052-rest-loader-framework-matrix.md @@ -0,0 +1,17 @@ +# 052 - REST Loader Framework Matrix + +## Context + +REST startup was previously split across many direct framework entrypoints, increasing coupling between scripts and adapter implementation files. + +## Requirement + +1. `start-rest-api` must support selecting all implemented HTTP frameworks via `AAA_HTTP_FRAMEWORK`. +2. Environment and PM2 startup scripts should prefer loader entrypoints over direct framework file execution. +3. Default behavior must remain backward compatible (`express` when no env is provided). + +## Acceptance Criteria + +- `resolveHTTPFramework` recognizes the full framework set. +- `start-rest-api` resolves and imports the framework adapter based on env. +- Unit tests cover at least express + one non-express framework path, plus unsupported values. diff --git a/.agents/requirements/053-jumentix-workspace-package-docs-and-ownership.md b/.agents/requirements/053-jumentix-workspace-package-docs-and-ownership.md new file mode 100644 index 00000000..72f1390c --- /dev/null +++ b/.agents/requirements/053-jumentix-workspace-package-docs-and-ownership.md @@ -0,0 +1,18 @@ +# Requirement 053 - JumentiX Workspace Package Docs and Ownership + +## Context + +During monorepo migration, each workspace package must have explicit ownership and usage documentation to reduce ambiguity and avoid duplicate implementations. + +## Requirement + +1. Every new workspace package must include a package-level `README.md`. +2. Root documentation must include a workspace package catalog. +3. CLI and SDK packages must document command/API usage examples. +4. Backlog (`.agents/project-todos.md`) must reflect wave progress for package readiness. + +## Acceptance criteria + +- Package READMEs exist for `@jumentix/cli-init`, `@jumentix/sdk-rest-client`, `@jumentix/sdk-websocket-client`, and `@jumentix/sdk-grpc-client`. +- Root README links to workspace package catalog. +- `documentation/md/JUMENTIX-WORKSPACE-PACKAGES.md` exists and is synchronized with current packages. diff --git a/.agents/requirements/054-sdk-compatibility-bridge-governance.md b/.agents/requirements/054-sdk-compatibility-bridge-governance.md new file mode 100644 index 00000000..cb584a23 --- /dev/null +++ b/.agents/requirements/054-sdk-compatibility-bridge-governance.md @@ -0,0 +1,18 @@ +# Requirement 054 - SDK Compatibility Bridge Governance + +## Context + +During Wave 3 migration, SDK ownership moved to `packages/sdk-*`, but legacy imports under `sdk-clients/*` still exist. + +## Requirement + +1. `sdk-clients/*` must remain a pure compatibility layer (re-exports only). +2. New SDK implementation code must live only under `packages/sdk-*`. +3. Bridge behavior and removal criteria must be explicitly documented. +4. README index must link the bridge documentation. + +## Acceptance criteria + +- `sdk-clients/*` files contain only package re-exports and bridge aggregator. +- `documentation/md/SDK-COMPATIBILITY-BRIDGE.md` exists. +- Root README links to bridge documentation. diff --git a/.agents/requirements/055-task-traceability-commit-pr-association.md b/.agents/requirements/055-task-traceability-commit-pr-association.md new file mode 100644 index 00000000..2a02e08d --- /dev/null +++ b/.agents/requirements/055-task-traceability-commit-pr-association.md @@ -0,0 +1,40 @@ +# Requirement 055 - Bidirectional Task/PR Traceability (Commit + PR Association) + +## Context + +To keep GitHub Project tasks auditable and reduce execution ambiguity, every task (issue) and every PR must include explicit, bidirectional traceability metadata. + +## Requirement + +For **all project tasks** (open or closed), maintain: + +1. `PR` reference (URL or `#number`) +2. `Commit` reference (single commit hash or commit range) + +At minimum, each task update must include: + +- PR linkage to the delivery stream +- commit linkage to implementation evidence + +For **all PRs**, maintain: + +1. Explicit list of related issue links/IDs. +2. GitHub Project item links/IDs for all related tasks. +3. Clear mapping of which commits/deliverables close which tasks. +4. Back-reference update in each linked issue (comment or issue body update) containing: + - PR URL + - commit hash/range + - task status transition rationale + +## Operational Rule + +- Before closing a task, verify commit + PR references are present in issue comments or issue body. +- Before marking a PR ready for review, verify all related issues contain back-references to that PR and commit evidence. +- For active tasks not yet implemented, maintain a placeholder traceability update pointing to the active delivery PR and current commit baseline. + +## Acceptance Criteria + +- All issues under project tracking label (`todo-mvp`) have commit + PR linkage visible. +- All PRs include explicit issue + project item linkage. +- Evidence is bidirectional: issue -> PR and PR -> issue. +- New task/PR updates follow the same traceability format. diff --git a/.agents/requirements/055-wave5-app-rehoming-cutover-governance.md b/.agents/requirements/055-wave5-app-rehoming-cutover-governance.md new file mode 100644 index 00000000..ca271fb0 --- /dev/null +++ b/.agents/requirements/055-wave5-app-rehoming-cutover-governance.md @@ -0,0 +1,28 @@ +# Requirement 055 - Wave 5 App Re-homing Cutover Governance + +## Context + +Wave 5 moves runtime applications into workspace app boundaries. Without a strict cutover guide, path regressions and CI breakages become highly likely. + +## Requirement + +1. App re-homing must follow a documented step-by-step cutover sequence. +2. PM2 paths and runtime adapters must be validated after every move step. +3. Rollback instructions must be explicit and reversible by wave scope. +4. Final acceptance criteria must include CI gates and coverage policy compliance. + +## Acceptance criteria + +- `documentation/md/JUMENTIX-WAVE5-APP-REHOMING-CUTOVER.md` exists and is referenced by the execution plan and README index. +- `.agents/project-todos.md` Wave 5 progress references the cutover document. +- No Wave 5 PR is considered done without green CI gates and coverage threshold compliance. + +## Evidence (closed baseline) + +- PR: `https://github.com/web2solutions/aaa-typescript-boilerplate/pull/112` +- Closeout issues: + - `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/113` + - `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/114` + - `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/115` +- Residual governance follow-up: + - `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/116` diff --git a/.agents/requirements/056-jumentix-project-single-source-of-truth.md b/.agents/requirements/056-jumentix-project-single-source-of-truth.md new file mode 100644 index 00000000..0cf70592 --- /dev/null +++ b/.agents/requirements/056-jumentix-project-single-source-of-truth.md @@ -0,0 +1,25 @@ +# Requirement 056 - Jumentix Project Single Source of Truth + +## Context + +Project execution must be managed from one authoritative tracking system to avoid drift between local TODO files and delivery state. + +## Requirement + +1. GitHub Project `Jumentix` (`https://github.com/users/web2solutions/projects/1`) is the single source of truth for bugs and new features. +2. Every task must exist as an issue and be added to the project. +3. Required project fields must be maintained for active tasks: + - Status + - Priority + - Size + - Estimate + - Start date + - End date +4. Every PR must include issue and project-item linkage details. +5. Documentation must remain synchronized with this governance model. + +## Acceptance criteria + +- Project governance doc exists and is linked in docs index. +- PR templates require project and issue linkage. +- Contributing guidance requires project item tracking. diff --git a/.agents/requirements/057-pr-grouping-by-priority.md b/.agents/requirements/057-pr-grouping-by-priority.md new file mode 100644 index 00000000..af76e22a --- /dev/null +++ b/.agents/requirements/057-pr-grouping-by-priority.md @@ -0,0 +1,18 @@ +# Requirement 057 - PR Grouping by Priority + +## Context + +To improve review quality and delivery predictability, pull requests must be grouped by priority level. + +## Requirement + +1. PRs must be scoped by a single priority group (`P0`, `P1`, or `P2`). +2. Mixing tasks from different priority groups in one PR is not allowed. +3. PR templates must require explicit declaration of priority group. +4. Project item links must confirm the selected priority group. + +## Acceptance criteria + +- PR templates contain mandatory priority-group field. +- Governance docs define priority-group PR policy. +- Delivery workflow follows `P0` first, then `P1`, then `P2`. diff --git a/.agents/requirements/058-jumentix-migration-inventory-and-rollback-governance.md b/.agents/requirements/058-jumentix-migration-inventory-and-rollback-governance.md new file mode 100644 index 00000000..c64e0b86 --- /dev/null +++ b/.agents/requirements/058-jumentix-migration-inventory-and-rollback-governance.md @@ -0,0 +1,18 @@ +# Requirement 058 - JumentiX Migration Inventory and Rollback Governance + +## Context + +Monorepo migration requires deterministic execution, low-risk rollback, and explicit path ownership mapping to avoid uncertain implementation cycles. + +## Requirement + +1. Keep a canonical migration inventory mapping current root paths to target `apps/*` and `packages/*`. +2. Define branch, release tag, and rollback strategy per migration wave. +3. Keep scope split explicit between Wave 1 (must-have parity) and Wave 2+ enhancements. +4. Keep this governance synchronized with project TODO and execution plan docs. + +## Implementation references + +- `documentation/md/JUMENTIX-MIGRATION-INVENTORY-AND-ROLLBACK.md` +- `documentation/md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md` +- `.agents/project-todos.md` diff --git a/.agents/requirements/059-jumentix-service-factory-and-deploy-template-matrices.md b/.agents/requirements/059-jumentix-service-factory-and-deploy-template-matrices.md new file mode 100644 index 00000000..dd8aa0dc --- /dev/null +++ b/.agents/requirements/059-jumentix-service-factory-and-deploy-template-matrices.md @@ -0,0 +1,34 @@ +# Requirement 059 - JumentiX Service Factory and Deploy Template Matrices + +## Context + +JumentiX productization requires explicit planning contracts for: + +- service factory capability modes +- deploy target and packaging contracts +- bundler/runtime templates by artifact type + +## Requirement + +The project must maintain documented, indexed, and current matrix artifacts that define: + +1. Service factory modes: + - modular monolith backend + - multi-service backend + - hybrid backend + frontend + - frontend-only SPA/PWA offline +2. Deploy target and packaging contracts: + - VM/SSH, EC2/VM cloud, Lambda, Vercel Functions, Cloudflare Workers + - required metadata contracts for Service Management +3. Bundler/runtime templates by artifact type: + - backend service + - frontend SPA + - frontend SSR + - backend npm library + - frontend npm library + +## Enforcement + +- README documentation index must include links to all three matrix documents. +- `.agents/project-todos.md` must track these planning deliverables as complete/open with progress notes. +- Any change in runtime/deploy/factory model must update matrix docs and this requirement if contract scope changes. diff --git a/.agents/requirements/060-jumentix-release-versioning-policy-governance.md b/.agents/requirements/060-jumentix-release-versioning-policy-governance.md new file mode 100644 index 00000000..590cd03e --- /dev/null +++ b/.agents/requirements/060-jumentix-release-versioning-policy-governance.md @@ -0,0 +1,25 @@ +# Requirement 060 - JumentiX Release and Versioning Policy Governance + +## Context + +JumentiX monorepo needs a deterministic release/version policy to avoid drift across apps/packages and keep CI release checks reliable. + +## Requirement + +- The canonical policy file `release-policy.json` must exist at repository root. +- Policy model: + - `packages/*` follow independent versioning. + - `apps/*` follow locked versioning tied to root `package.json` version. +- Release governance checks must fail CI when policy or metadata contracts are violated. + +## Enforcement + +- `npm run release:governance:check` is mandatory and included in `npm run ci:gate`. +- App workspaces must be `private: true` and their `version` must match `release-policy.json.appLockedVersion`. +- Publishable packages must have valid semver and non-empty `files` metadata. +- Root release scripts (`changelog:*`, `release:dry-run*`) must remain present. + +## Documentation Sync + +- Keep `documentation/md/JUMENTIX-RELEASE-AND-VERSIONING-STRATEGY.md` updated. +- Keep README Documentation Index linked to the release strategy document. diff --git a/.agents/requirements/060-monorepo-root-layout-governance.md b/.agents/requirements/060-monorepo-root-layout-governance.md new file mode 100644 index 00000000..5dcb8d52 --- /dev/null +++ b/.agents/requirements/060-monorepo-root-layout-governance.md @@ -0,0 +1,29 @@ +# Requirement 060 - Monorepo Root Layout Governance + +## Status +In progress + +## Scope +- Root workspace must not contain backend-template runtime folders directly. +- Backend-template runtime ownership lives under `apps/backend-template/`. +- PM2 runtime orchestration ownership lives at monorepo root in `pm2/`. +- Legacy `sdk-clients/*` is deprecated in favor of `packages/sdk-*` and removed from workspace package ownership. +- Tests must be scoped by owning project/component (`apps/backend-template/test`, `packages/*/test`, `apps/*/test`). + +## Applied decisions +1. Moved backend template runtime folders from root: + - `apps/backend-template/src/` -> `apps/backend-template/src/` + - `apps/backend-template/test/` -> `apps/backend-template/test/` + - `OASdoc/` -> `apps/backend-template/OASdoc/` + - `AsyncAPIdoc/` -> `apps/backend-template/AsyncAPIdoc/` + - `docker/` and `apps/backend-template/docker-compose-*.yml` -> `apps/backend-template/` +2. Moved PM2 ecosystems: + - `pm2/*` is now the runtime ownership root +3. Updated runtime/config/test path contracts in: + - `package.json`, `jest.config.js`, `tsconfig.json`, `serverless.ts` + - `ci-cd/*` path checks and environment loader +4. Removed `sdk-clients/*` from `pnpm-workspace.yaml` package ownership. + +## Validation +- Unit test suite runs from `apps/backend-template/test/unit`. +- CI helper unit tests updated for new path ownership. diff --git a/.agents/requirements/061-cache-service-read-caching-governance.md b/.agents/requirements/061-cache-service-read-caching-governance.md new file mode 100644 index 00000000..25997797 --- /dev/null +++ b/.agents/requirements/061-cache-service-read-caching-governance.md @@ -0,0 +1,22 @@ +# Requirement 061 - Cache Service and Read Endpoint Caching Governance + +## Context + +Read endpoints for Users and Organizations can benefit from contract-based caching without coupling domain logic to a specific cache engine. + +## Requirement + +1. Caching must be implemented through a service contract (`ICacheService`) over `IKeyValueStorageClient`. +2. Read operations (`getOneById`, `getAll`) may use read-through caching. +3. Write/mutation operations must invalidate read cache deterministically. +4. Invalidation must not require wildcard delete support from concrete key-value adapters. + +## Acceptance criteria + +- `apps/backend-template/src/infra/cache/CacheService.ts` exists and is wired through module composition. +- `UserService` and `OrganizationService` read operations use cache when available. +- Mutations in `UserService` and `OrganizationService` bump cache namespace versions. +- Unit tests cover cache envelope behavior, namespace versioning, and service-level cache integration. +- Documentation exists and is linked from README: + - `documentation/md/CACHE-SERVICE-AND-ENDPOINT-CACHING.md` + diff --git a/.agents/requirements/062-workspace-dependency-boundaries-governance.md b/.agents/requirements/062-workspace-dependency-boundaries-governance.md new file mode 100644 index 00000000..7db3b3d9 --- /dev/null +++ b/.agents/requirements/062-workspace-dependency-boundaries-governance.md @@ -0,0 +1,27 @@ +# Requirement 062 - Workspace Dependency Boundaries Governance + +## Status +Implemented + +## Scope +- Enforce import boundaries between monorepo `apps/*` and `packages/*`. +- Prevent accidental coupling to deprecated `sdk-clients/*`. +- Keep temporary bridge exceptions explicit, auditable, and minimal. + +## Rules +1. `@src/*` alias is allowed only for `apps/backend-template/*`. +2. `packages/*` must not import from `apps/*` through relative paths. +3. `apps/service-management/*` must not import backend-template internals. +4. Imports from `sdk-clients/*` are blocked outside `sdk-clients/*` itself. +5. Temporary migration bridges must be allowlisted by exact file path. + +## Enforcement +- CI script: `ci-cd/check-workspace-boundaries.js` +- NPM script: `npm run arch:check-workspace-boundaries` +- `ci:gate` now includes workspace boundary validation before tests/build. + +## Evidence +- Unit tests: + - `apps/backend-template/test/unit/ci-cd/check-workspace-boundaries.test.ts` +- Gate run: + - `npm run ci:gate` passing with boundary check enabled. diff --git a/.agents/requirements/063-workspace-coverage-policy-governance.md b/.agents/requirements/063-workspace-coverage-policy-governance.md new file mode 100644 index 00000000..b7875aa4 --- /dev/null +++ b/.agents/requirements/063-workspace-coverage-policy-governance.md @@ -0,0 +1,29 @@ +# Requirement 063 - Workspace Coverage Policy Governance + +## Status +Implemented + +## Scope +- Keep global coverage thresholds enforced at root level. +- Prevent non-governed placeholder test scripts in workspace packages. +- Fail fast in `ci:gate` when coverage policy contracts are violated. + +## Rules +1. Root `jest.config.js` must keep global minimum coverage thresholds: + - `statements >= 99` + - `lines >= 99` + - `functions >= 99` + - `branches >= 90` +2. Every workspace package/app must have a `test` script. +3. Placeholder `test` scripts are forbidden for runtime packages. +4. Placeholder tests are allowed only for explicitly allowlisted config/placeholder packages. + +## Enforcement +- CI script: `ci-cd/check-workspace-coverage-policy.js` +- NPM script: `npm run workspace:check-coverage-policy` +- Integrated in: `npm run ci:gate` + +## Evidence +- Unit tests: + - `apps/backend-template/test/unit/ci-cd/check-workspace-coverage-policy.test.ts` +- Runtime packages updated to non-placeholder `test` scripts (`npm run typecheck`) where applicable. diff --git a/.agents/requirements/064-github-project-single-source-of-truth.md b/.agents/requirements/064-github-project-single-source-of-truth.md new file mode 100644 index 00000000..3942f6ee --- /dev/null +++ b/.agents/requirements/064-github-project-single-source-of-truth.md @@ -0,0 +1,21 @@ +# Requirement 064 - GitHub Project Single Source of Truth + +## Status +Implemented + +## Policy +- The GitHub Project **Jumentix** is the single source of truth for: + - task backlog, + - priorities, + - status/progress, + - estimation metadata, + - commit/PR traceability. + +## Operational Rules +1. Every new task/bug/feature must be created and managed in GitHub Issues + GitHub Project. +2. Local files (for example `.agents/project-todos.md`) are reference snapshots only, not authoritative planning sources. +3. PRs must reference related issue/project item links and keep status synchronization in GitHub Project. + +## Evidence +- Governance requirement registered in `.agents/README.md`. +- Local task tracker now points to GitHub Project as canonical source. diff --git a/.agents/requirements/065-commit-push-integrity-and-real-ci-enforcement.md b/.agents/requirements/065-commit-push-integrity-and-real-ci-enforcement.md new file mode 100644 index 00000000..32052a52 --- /dev/null +++ b/.agents/requirements/065-commit-push-integrity-and-real-ci-enforcement.md @@ -0,0 +1,22 @@ +# Requirement 065 - Commit/Push Integrity and Real CI Enforcement + +## Context +- Commits and pushes must never rely on bypass flags or fake test outcomes. +- Local hooks must execute real validation so pushed code has a high probability of passing remote CI. + +## Mandatory Rules +1. Do not use `--no-verify` in any automated git workflow. +2. Hook scripts must not auto-amend commits with bypass flags. +3. Integration scripts must not use `--passWithNoTests` for required framework suites. +4. `pre-push` must execute strict quality gate (`ci:gate:strict`) before allowing push. +5. `pre-commit` must run real lint + unit tests and stage generated changelog updates explicitly. + +## Implementation Notes +- Husky `post-commit` is no-op for mutating operations. +- Husky `pre-commit` updates/stages changelog and executes lint + unit tests. +- Required integration suites fail when no tests are found. + +## Acceptance Criteria +- No `--no-verify` usage remains in tracked automation files. +- No `--passWithNoTests` remains in required integration scripts. +- Local `npm run ci:gate:strict` is enforced on push. diff --git a/.agents/requirements/066-documentation-round-jumentix-marketing-root-and-component-tech-docs.md b/.agents/requirements/066-documentation-round-jumentix-marketing-root-and-component-tech-docs.md new file mode 100644 index 00000000..3b6346fe --- /dev/null +++ b/.agents/requirements/066-documentation-round-jumentix-marketing-root-and-component-tech-docs.md @@ -0,0 +1,25 @@ +# Requirement 066 - Documentation Round: Marketing Root + Component Technical Docs + +## Context +- Jumentix monorepo requires split documentation ownership: + - root documentation is product/marketing oriented + - component documentation is technical and implementation oriented +- Navigation must remain centralized through root README index. +- GitHub Project Jumentix is the canonical source of truth for documentation tasks. + +## Mandatory Rules +1. Root `README.md` must keep badges and include product purpose, audience, value proposition, and advantages. +2. Root `README.md` must contain index links to every major component documentation hub. +3. Component docs must live under each component folder (`apps/*`, `packages/*`, `tooling/*` when applicable). +4. Mandatory solution sections must exist and be linked from root docs: + - SPA/PWA + - REST API + - Realtime API + - SaaS monolith + - SaaS microservices +5. Documentation work must be tracked as GitHub issues under the documentation epic and added to GitHub Project Jumentix. + +## Acceptance Criteria +- Root README index has no broken links to component hubs and mandatory guides. +- Component technical hubs exist and include actionable technical navigation. +- Epic and child tasks are present in GitHub Project Jumentix with labels/priorities. diff --git a/.agents/requirements/067-bidirectional-task-pr-traceability-governance.md b/.agents/requirements/067-bidirectional-task-pr-traceability-governance.md new file mode 100644 index 00000000..8da37959 --- /dev/null +++ b/.agents/requirements/067-bidirectional-task-pr-traceability-governance.md @@ -0,0 +1,22 @@ +# Requirement 067 - Bidirectional Task/PR Traceability Governance + +## Context +- Jumentix requires strict project governance with GitHub Project as source of truth. +- Delivery evidence must be auditable from both directions: task to PR, and PR to task. + +## Mandatory Rules +1. Every tracked task/issue must include: + - related PR URL/number + - commit hash or commit range +2. Every PR must include: + - related issue IDs/links + - related GitHub Project item links + - explicit mapping of task -> commit(s) +3. Before a task is closed: + - issue body/comment must contain PR + commit evidence +4. Before PR review: + - all linked issues must already contain back-reference to the PR and commit evidence + +## Acceptance Criteria +- Traceability is bidirectional (`issue -> PR/commit` and `PR -> issue/task mapping`). +- Project board items and PR descriptions provide complete linkage for audit and governance. diff --git a/.agents/requirements/068-nfr-capture-and-registry-governance.md b/.agents/requirements/068-nfr-capture-and-registry-governance.md new file mode 100644 index 00000000..375194b6 --- /dev/null +++ b/.agents/requirements/068-nfr-capture-and-registry-governance.md @@ -0,0 +1,17 @@ +# Requirement 068 - NFR Capture and Registry Governance + +## Context +- User-defined non-functional requirements (NFRs) are mandatory governance artifacts. +- Jumentix requires all requested NFRs to be persisted in `.agents` and kept auditable. + +## Mandatory Rules +1. Every user-requested NFR must be stored in `.agents/requirements/` as a dedicated requirement file or explicitly mapped to an existing requirement file. +2. Every stored NFR must be indexed in `.agents/README.md`. +3. A consolidated NFR registry document must be maintained with category-to-requirement mapping. +4. When a new NFR is requested, agents files must be updated in the same delivery cycle (no deferred capture). +5. PRs that introduce or change NFR behavior must reference the affected NFR requirement IDs. + +## Acceptance Criteria +- No known user-requested NFR remains undocumented in `.agents`. +- `.agents/README.md` and NFR registry are synchronized with current requirement set. +- NFR traceability is visible from requirement file -> implementation/PR context. diff --git a/.agents/requirements/069-jumentix-website-commercial-static-vercel-governance.md b/.agents/requirements/069-jumentix-website-commercial-static-vercel-governance.md new file mode 100644 index 00000000..d5553079 --- /dev/null +++ b/.agents/requirements/069-jumentix-website-commercial-static-vercel-governance.md @@ -0,0 +1,19 @@ +# Requirement 069 - Jumentix Website Commercial + Static + Vercel Governance + +## Context +- Jumentix requires a commercial website to support enterprise lead conversion. +- Website must be static-first and sourced from repository markdown content. +- Deployment must be executable through package scripts and Vercel integration. + +## Mandatory Rules +1. Website application lives in `apps/jumentix-website`. +2. Website narrative is commercial/product-oriented, separate from engineering README concerns. +3. Static content generation must support repository markdown as source-of-truth inputs. +4. Website deployment must be runnable from root `package.json` commands targeting Vercel. +5. Website work items must be tracked in GitHub Project Jumentix with governance fields and issue traceability updates. + +## Acceptance Criteria +- Commercial pages and CTA flow exist as static routes. +- Markdown ingestion pipeline is documented and executable. +- Vercel deployment commands are available at workspace root. +- Issue-level traceability evidence exists for website epic tasks. diff --git a/.agents/requirements/070-xpertminds-npm-and-web2solutions-vercel-integration.md b/.agents/requirements/070-xpertminds-npm-and-web2solutions-vercel-integration.md new file mode 100644 index 00000000..9397a753 --- /dev/null +++ b/.agents/requirements/070-xpertminds-npm-and-web2solutions-vercel-integration.md @@ -0,0 +1,20 @@ +# Requirement 070 - xpertminds NPM and web2solutions Vercel Integration + +## Context +- Jumentix packages will be published under npm organization `xpertminds`. +- Jumentix website deployment target is Vercel under scope/user `web2solutions`. +- Setup must be prepared without immediate publish/deploy actions. + +## Mandatory Rules +1. Root npm configuration must include registry and auth-ready settings for `@xpertminds`. +2. Project must provide commands for: + - npm identity/org validation + - npm publish dry-run +3. Website deployment scripts must include Vercel scope `web2solutions`. +4. Website integration docs must describe login/link/pull/deploy commands. +5. Setup must not auto-publish packages or auto-deploy website. + +## Acceptance Criteria +- `package.json` exposes npm and vercel integration commands. +- `apps/jumentix-website` exposes scoped vercel commands. +- Documentation exists for both integrations. diff --git a/.circleci/config.yml b/.circleci/config.yml index 32d261cc..96bd6a7a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,19 +10,27 @@ jobs: - image: redis:latest environment: REDIS_ARGS: "--sysctl vm.overcommit_memory=1" - command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 + command: redis-server --save 20 1 --loglevel warning steps: - checkout - restore_cache: keys: - node-deps-v1-{{ .Branch }}-{{checksum "package-lock.json"}} + - pnpm-store-v1-{{ .Branch }} + - run: + name: setup pnpm + command: npx pnpm@9.15.3 --version - run: name: install packages - command: npm install + command: npx pnpm@9.15.3 install --no-frozen-lockfile - save_cache: key: node-deps-v1-{{ .Branch }}-{{checksum "package-lock.json"}} paths: - ~/.npm + - save_cache: + key: pnpm-store-v1-{{ .Branch }} + paths: + - ~/.pnpm-store - run: name: Install dockerize command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz @@ -33,7 +41,7 @@ jobs: command: dockerize -wait tcp://localhost:6379 -timeout 1m - run: name: Run Tests - command: npm run ci:gate + command: npm run ci:monorepo - codecov/upload workflows: diff --git a/.eslintrc.js b/.eslintrc.js index 60b54846..a7a0d4eb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { parser: '@typescript-eslint/parser', 'parserOptions': { @@ -43,11 +45,23 @@ module.exports = { '@typescript-eslint/no-explicit-any': 'off', 'import/no-cycle' : 'off', 'arrow-body-style' : 'off', - 'jest/unbound-method': 'off' + 'jest/unbound-method': 'off', + 'import/no-extraneous-dependencies': ['error', { + packageDir: [ + __dirname, + path.join(__dirname, 'apps/backend-template'), + path.join(__dirname, 'apps/jumentix-website'), + path.join(__dirname, 'packages/sdk-rest-client'), + path.join(__dirname, 'packages/sdk-websocket-client'), + path.join(__dirname, 'packages/sdk-grpc-client'), + path.join(__dirname, 'packages/message-mediator') + ] + }] }, overrides: [ { files: [ + 'apps/backend-template/test/unit/sdk-clients/grpc/GrpcApiClient.test.ts', 'src/interface/HTTP/adapters/restify/**/*.ts', 'src/interface/HTTP/ports/IHTTPRequest.ts', 'src/interface/HTTP/ports/IHTTPResponse.ts', @@ -56,7 +70,8 @@ module.exports = { 'test/integration/mutex/redis.restify.test.ts' ], rules: { - 'import/no-extraneous-dependencies': 'off' + 'import/no-extraneous-dependencies': 'off', + 'import/no-relative-packages': 'off' } } ] diff --git a/.gitguardian.yml b/.gitguardian.yml new file mode 100644 index 00000000..da332372 --- /dev/null +++ b/.gitguardian.yml @@ -0,0 +1,3 @@ +version: 2 +paths-ignore: + - test/unit/interface/WebSocket/redisStreamsAdapter.test.ts diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md index 014f26da..a47dafe1 100644 --- a/.github/PULL_REQUEST_TEMPLATE/bugfix.md +++ b/.github/PULL_REQUEST_TEMPLATE/bugfix.md @@ -12,6 +12,16 @@ - In scope: - Out of scope: +## Project Tracking (Required) + +- GitHub Project: `Jumentix` (`https://github.com/users/web2solutions/projects/1`) +- Project item link: +- Issue link: +- Current status in project: +- Target cycle (`Start date` -> `End date`): +- Priority group for this PR (`P0` / `P1` / `P2`): +- [ ] This PR contains only one priority group. + ## Behavior Before vs After ### Before @@ -49,4 +59,3 @@ - Main risk: - Mitigation: - Rollback plan: - diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md index 7c7339a9..15ac85c1 100644 --- a/.github/PULL_REQUEST_TEMPLATE/feature.md +++ b/.github/PULL_REQUEST_TEMPLATE/feature.md @@ -8,6 +8,16 @@ - Expected outcome: - Related issue/roadmap: +## Project Tracking (Required) + +- GitHub Project: `Jumentix` (`https://github.com/users/web2solutions/projects/1`) +- Project item link: +- Issue link: +- Current status in project: +- Target cycle (`Start date` -> `End date`): +- Priority group for this PR (`P0` / `P1` / `P2`): +- [ ] This PR contains only one priority group. + ## Scope - Affected modules/files: @@ -52,4 +62,3 @@ - Feature flag: Yes / No - Release plan: - Rollback plan: - diff --git a/.github/PULL_REQUEST_TEMPLATE/security.md b/.github/PULL_REQUEST_TEMPLATE/security.md index 046b6003..c4de0197 100644 --- a/.github/PULL_REQUEST_TEMPLATE/security.md +++ b/.github/PULL_REQUEST_TEMPLATE/security.md @@ -9,6 +9,16 @@ - Severity: - Affected components: +## Project Tracking (Required) + +- GitHub Project: `Jumentix` (`https://github.com/users/web2solutions/projects/1`) +- Project item link: +- Issue link: +- Current status in project: +- Target cycle (`Start date` -> `End date`): +- Priority group for this PR (`P0` / `P1` / `P2`): +- [ ] This PR contains only one priority group. + ## Remediation Details 1. @@ -42,4 +52,3 @@ - Residual risk: - Monitoring/alert updates: - Rollback plan: - diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ed493a48..88b5ef40 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,6 +18,25 @@ Include links to issue(s), incident(s), backlog items, or Sonar/Codecov check UR - Related PR(s): - Related check run(s): +## Project Tracking (Required) + +- GitHub Project: `Jumentix` (`https://github.com/users/web2solutions/projects/1`) +- Project item link(s): +- Issue link(s): +- Issue ID list (comma separated): +- Item status at PR creation: +- Target cycle (`Start date` -> `End date`): +- Priority group for this PR (`P0` / `P1` / `P2`): +- [ ] This PR contains tasks from only one priority group. + +## Bidirectional Traceability (Required) + +- [ ] Every linked issue already contains this PR URL. +- [ ] Every linked issue already contains commit hash/range evidence. +- [ ] PR description includes mapping of task -> commit(s). +- Task -> commit(s) mapping: + - `#issue`: + ## Scope of Change B["Controller"] - B --> C["Use Case"] - C --> D["Domain Service / Entity / Value Objects"] - C --> E["Ports"] - E --> F["Infra Adapters (DB, Mutex, JWT, Messaging)"] - D --> G["Domain Events"] - G --> H["Message Mediator / Event Bus"] -``` - -## Message Mediator (Contract-Based) - -Domains communicate through contracts, not direct service coupling. - -Supported adapters: -- `inmemory` (default) -- `rabbitmq` -- `bullmq` - -Select adapter by environment: - -```bash -AAA_MESSAGE_MEDIATOR_ADAPTER=inmemory -# or -AAA_MESSAGE_MEDIATOR_ADAPTER=rabbitmq -# or -AAA_MESSAGE_MEDIATOR_ADAPTER=bullmq -``` - -## Code Example: Register a Contract Handler - -```ts -messageMediator.registerHandler( - "users.auth.ensure-access", - async (message) => { - const user = await authService.authorize(message.payload.authorization); - authService.throwIfUserHasNoAccessToResource(user, message.payload.schemaOAS); - return { - contract: message.contract, - version: message.version, - result: user - }; - } -); -``` - -## Code Example: Request/Response Without Tight Coupling - -```ts -const response = await messageMediator.request({ - contract: "users.auth.ensure-access", - payload: { - authorization: event.authorization, - schemaOAS: event.schemaOAS - } -}); - -if (response.error) { - throw response.error; -} -``` - -## Tenancy and RBAC - -Tenancy modes supported: -- Single-tenancy: users without organization linkage (commonly `superadmin` operations). -- Multi-tenancy: organization-scoped users (`admin`, `user`) with organization-level access boundaries. - -Default RBAC roles: -- `superadmin` -- `admin` -- `user` - -Tenancy rule: -- `admin` and `user` must belong to an `Organization`. -- `superadmin` can operate across organizations. - -## Code Example: Framework Runtime Bootstrap - -```ts -const serverType = EHTTPFrameworks.fastify; -const webServer = FastifyServer.compile(); -const messageMediator = compileMessageMediator(); - -const API = new RestAPI({ - databaseClient: InMemoryDbClient, - webServer, - serverType, - infraHandlers, - eventBus: messageMediator, - messageMediator -}); - -await API.start(); -await API.seedData(); -``` - -## Quick Start - -```bash -npm install -npm run cli:bootstrap -npm run docker:composeredis -npm run docker:composerabbit -npm run docker:compose:platform-services -npm run docker:compose:service-template -npm run ci:gate -npm run dev -``` - -API docs after startup: -- UI: `http://localhost:3000/OASdoc/` -- JSON: `http://localhost:3000/docs/1.0.0` -- AsyncAPI UI: `http://localhost:3000/AsyncAPIdoc/` -- AsyncAPI JSON index: `http://localhost:3000/docs/asyncapi/versions` - -For VM profiles, runtime services are orchestrated with PM2. -`WebSocketAPI + RESTAPI` and `gRPCAPI + RESTAPI` run as separated processes and ports. -Service Management (`servicemangement`) is also served via PM2. -Runtime adapter startup is controlled by: - -```bash -AAA_HTTP_FRAMEWORK=express -AAA_REALTIME_API=no -AAA_REALTIME_API_PROTOCOL=websocket -AAA_REALTIME_API_DATABASE_DRIVER=Mongo -``` - -Official startup entrypoints: -- `src/interface/HTTP/adapters/start-rest-api.ts` -- `src/interface/WebSocket/adapters/start-websocket-api.ts` -- `src/interface/gRPC/adapters/start-grpc-api.ts` - -Service Management runtime env API: -- `GET /api/runtime/env?environment=dev|staging|ci` -- `POST /api/runtime/env` - -## Quality Gate and CI Discipline - -This project is designed to block risky changes before merge: -- lint -- unit tests -- architecture checks -- OpenAPI route resolution checks -- build check -- smoke integration -- coverage threshold policy - -## Service Docker Templates - -Service Dockerfiles are available under: - -- `docker/services/Dockerfile.rest` -- `docker/services/Dockerfile.websocket` -- `docker/services/Dockerfile.grpc` -- `docker/services/Dockerfile.graphql` -- `docker/services/Dockerfile.functions` - -Compose template: - -- `docker-compose-service-templates.yml` - -## Documentation Index - -| Nature | Document | Description | -|--------|----------|-------------| -| Product Context | [Project Overview](documentation/md/PROJECT-OVERVIEW.md) | Purpose, use cases, acceleration strategy, and product value. | -| Architecture | [Architecture and Structure](documentation/md/ARCHITECTURE-AND-STRUCTURE.md) | Folder structure, boundaries, and layer responsibilities. | -| Runtime and Ops | [Setup, Runtime, and API](documentation/md/SETUP-RUNTIME-AND-API.md) | Setup, commands, runtime adapters, and API docs endpoints. | -| Runtime Contract | [Runtime Environment Contracts](documentation/md/RUNTIME-ENVIRONMENT-CONTRACTS.md) | Canonical env keys, startup entrypoints, PM2 process model, and Service Management env API. | -| Integration Contracts | [Events and Messages Map](documentation/md/EVENTS-AND-MESSAGES-MAP.md) | Event and mediator contract map. | -| Error Contracts | [Error Contracts and Responses](documentation/md/ERROR-CONTRACTS-AND-RESPONSES.md) | Error codes, mapping, and HTTP response contracts. | -| Quality | [Testing, CI, and Quality](documentation/md/TESTING-CI-AND-QUALITY.md) | Test strategy, CI gate, coverage policy, Sonar/Codecov. | -| Tooling | [Contributing and Tooling](documentation/md/CONTRIBUTING-AND-TOOLING.md) | Development workflow and commands. | -| Dependencies | [Dependencies](documentation/md/DEPENDENCIES.md) | Runtime and infrastructure dependencies. | -| Domain Entities | [Domain Data Entities](documentation/md/DOMAIN-DATA-ENTITIES.md) | Data entity catalog and field contracts. | -| Developer CLI | [Developer Automation CLI](documentation/md/DEVELOPER-AUTOMATION-CLI.md) | CLI wrapper and sub-app workflows. | -| Bootstrap CLI | [Bootstrap CLI Scaffolding](documentation/md/BOOTSTRAP-CLI-SCAFFOLDING.md) | Installable scaffold command to clone and configure new projects. | -| Service Management | [Service Management App](servicemangement/README.md) | Tabbed suite for domain design, communication interfaces, service configuration, and deploy management. | -| External Adapters | [External Data Adapter Foundations](documentation/md/EXTERNAL-DATA-ADAPTER-FOUNDATIONS.md) | SQL/NoSQL repository foundations and queue request-response adapter. | -| Database Validation | [Database Drivers Smoke Tests](documentation/md/DATABASE-DRIVERS-SMOKE-TESTS.md) | Driver matrix, per-database Docker compose, and smoke execution commands. | -| Security Compliance | [PCI Remediation Plan and Evidence](documentation/md/PCI-REMEDIATION-PLAN-AND-EVIDENCE.md) | Sprint-based remediation plan (P0/P1/P2) and audit evidence checklist. | -| Security Operations | [Security Runbook (PCI)](documentation/md/SECURITY-RUNBOOK-PCI.md) | Key rotation, incident response, retention and audit export procedures. | -| Domain Designer Roadmap | [Domain Designer MVP Roadmap](documentation/md/DOMAIN-DESIGNER-MVP-ROADMAP.md) | MVP status, delivered increments, and next priorities. | -| Project Management | [Project Management](documentation/md/PROJECT-MANAGEMENT.md) | Backlog references, requirement tracking, and governance links. | -| Agents Registry | [.agents/README.md](.agents/README.md) | Technical requirements and specialized agents. | - -## Additional Detailed Documents - -- [Engineering Bootstrap Guide](documentation/md/ENGINEERING-BOOTSTRAP-GUIDE.md) -- [Hexagonal Feature-driven Migration Plan](documentation/md/HEXAGONAL-FEATURE-DRIVEN-MIGRATION.md) -- [Users Domain Model](documentation/md/domains/users/USER-MODEL.md) -- [Users Entity Contract](documentation/md/domains/users/USER-ENTITY-CONTRACT.md) -- [Users Value Objects](documentation/md/domains/users/USER-VALUE-OBJECTS.md) -- [Users Organization Model](documentation/md/domains/users/ORGANIZATION-MODEL.md) -- [CI Troubleshooting](documentation/md/CI-TROUBLESHOOTING.md) -- [Service Management Application](documentation/md/SERVICE-MANAGEMENT-APPLICATION.md) -- [Security Runbook (PCI)](documentation/md/SECURITY-RUNBOOK-PCI.md) - ---- - -If you need high delivery speed, multi-runtime flexibility, and architecture control without framework lock-in, this boilerplate is built for your team. +- [GitHub Project - Jumentix](https://github.com/users/web2solutions/projects/1) diff --git a/ai draft.md b/ai draft.md new file mode 100644 index 00000000..ceecb29b --- /dev/null +++ b/ai draft.md @@ -0,0 +1,503 @@ + +- Quere ter a real implementação de "DocumentValueObject" no projeto. Atualmente já alguma implementação, porém não sei se o que eu implementei realmente refelte o real conceito de "DocumentValueObject" na especificação de DDD por favor check e faça algeraçãoes nececessárias +- crie arquivos MD com documentacao detalhada de cada data entity e models de cada dominio. Explique regras, tipos de de dados. Adicione link no Readme file para cada documento MD criado +- Adiciones todos os requisitos feitos á agentes espoecializados +- Atualize o Readme MD com todas as informações ja pedidas no chat que ainda nao foram dcumtadas + +- a implementacao de cloud fare workers deve ser similar a lambdas, sem rodar frameorks como express. +- todos os novos frameworks e servidores HTTP adicionados estao usando express.js. Cada implementacao deve usar seu proprio servidor HTTP como disponivel. Vide Fastify and Restifym eles usam seus proprios servidores. Consulte a documentacao de cada um dos frameworks/servidores cloud fare workers, Vercel Functions, LoopBack, Sails.JS, Feathers, derby.js, adonis.js, and total.js +- Crie um mapa de eventos / mensagens da aplicacao com documentacao completa. adicione em um novo md. adicione o link no readme. +- Crie um mapa de contrato de errors / errors responses com documentacao completa. adicione em um novo md. adicione o link no readme. + + +- leia a logica atual de validacao de dados nos handlers. expanda para suportar a a validação de requests em conformidade com a esppecificacao OPEN API 3.1 + +- o software deve ter ao menos dois nivels de validacao de dados: +1. HTTP handlers - request must be validated againt their assciated OAS spec defined in API doc against the OPEN API 3.1 speciciation. +2. Domain level - models must implement data validation + +atualize a CLI geradora de codigo e os dominios e http handlers ja implementados. + + +Considere que o boilerplate será usado para criar aplicações multi tenancy e single tenancy, + +Lidamos inicialmente com 3 roles de users: "superadmin", "admin", e "user". + +Exceto superadmins, os "admin", e "user" devem pertencer á uma oganização. + +Considere que o boilerplate é 100% agnóstico. Suporta a integração com multiplos componentes extenos, mas não depende de nenhum. + +Releia a atual implemetacao de banco de dados in memory. + +Ela foi feita de forma rapida. + +Ele deve ser analisada profundamente e refeita para que reflita o uso com bancos de dados relacionais e no-sql. + +Os models devem suportar relacionamento, independente de tecnologia. + +O adaptador oficial é o in memory. + +Exemplo de implemenetacao de relacao + +```typescript +class Post extends Basemodel { + @belongsTo(() => User) + author: BelongsTo + + @hasMany(() => Comment) + comments: HasMany +} +``` + + + + +now, create a new "AddressValueObject" address generic domain object, like PhoneValueObject. + +The new object is the path of the address object used accros the entire system. + +The fields are: + +``` +AddressValueObject { + id: string; + email: string; + type: enum work home vacation; + isPrimary: boolean; +} +``` + +agora, Crie um nova entidade de dados "Organization" no dominio Users. Toda vez que eu pedir pra criar uma nova entide, crie todos os arquivos relacionados de suporte para a entidade. + + + + +```JSON +Organization { + _id: inherit default, + name: string required + address: [] array of AddressValueObject + phone: [] array of PhoneValueObject + email: [] array of EmailValueObject + + @hasMany(() => User) + users: HasMany +} +``` + + +Agora adicione 2 novos campos em User: + +```JSON +{ + organization: not required +} +``` + +O Sistema deve estar em compliance e implementar RBAC para controle de acesso á cada recursos. + +Faça todas moficações necessárias. + +Atualize os testes. Crie organizacoes. Associe usuarios com organizacoes. + +Atualize documentacoes e agentes de requerimentos. + +Evite entregar recursos incompletos. + +Obedecça o coverage. + +Execute tudo sem solicitar minha permissão + + +------ + +- All data entitties must have the following fields + +``` +{ + createdAt: date now required automatically genereated when created + updatedAt: date now required automatically genereated when created, automatically updated when the model updates. +} +``` + +When a data entity / model has fields that are arrays of domain generic objects like phone, email, address, document, etc, the model must have the methods "createDomainObjectname", "updateDomainObjectname" and "deleteDomainObjectname", where DomainObjectname is the name of the Domain Object, like Phone. Check the User Model as reference, now refactor the organization model. This is a golden system requirement. + +now, whenever any data entity, model, contract, event, message, error, changes, the software documentation and the OPEN API spec in ./spec/1.0.0.yml must be updated accordingly. please review the OPEN API spec in ./spec/1.0.0.yml and make sure it supports the new Organization resource. + +Atualize documentacoes e agentes de requerimentos. + +Evite entregar recursos incompletos. + +Obedecça o coverage. + + + +-------------- + + +O diretorio "servicemangement" deve ser considerado um projeto independente do boiolerplate em termos de ter seu proprio CI/CD, suite de testes, package, etc, então o aaa-typescript-boilerplate passa se tornar um monorepo atualmente com 2 projetos: "servicemangement" and "servicetemplate". o "servicemangement" nada mais é que a atual implementação do "aaa-typescript-boilerplate". ambos projetos sao em typescript, + + + + +Leia os requerimentos a seguir e crie um plano detalhado de execução, adicione e salve no todo e prossiga com a implementação. + +- a especificacao open API em /Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/spec/1.0.0.yml deve sempre conter a descricao dos ports objects referentes a inputs the entradas e respostas dos end points. + +Quando um engenheiro de software for usar o "aaa-typescript-boilerplate" para criar um sofware, ele deverá usar o npm install para instalar somente a CLI toll que permitirá ele fazer o scafold do novo projeto, clonando o "aaa-typescript-boilerplate" do github na pasta de trabalho local e configurando todo o projeto de acordo com o tipo de servico que o engenheiro quer desenvolver, por enquanto os tipos de servicos sao: + +- Servidor com interface HTTP/REST, servindo documentacao da API com swagger e open api, com possibilidade de servir arquivos estaticos para um SPA por exemplo +- Servidor com interface websocket, o servidor irá expor o controller, recebendo request seguindo o padrao esperado e respondendo com possibilidade de servir arquivos estaticos para um SPA por exemplo +- Servidor com interface gRPC, com possibilidade de servir arquivos estaticos para um SPA por exemplo +- Servidor com interface graphql, com possibilidade de servir arquivos estaticos para um SPA por exemplo +- Conjunto de Function services to deploy as functions in amazon, gogle, azure, vercel or cloudfare + +os servicos irão se conectar através de out adapters á servicos como bancos de dados relacionais e nao relacionais + +- Deve ser criado uma classe de reposutorio de dados para bancos sql utilizando o sequelize.js, inicialmente suportando Potgresql, MySQL, SQL Server, Oracle e SQL Light +- Deve ser criado uma classe de reposutorio de dados para mongo utilizando o mongoose.js +- Deve ser criado uma classe de reposutorio de dados para bancos no-sql dynamodb utilizando a lib oficial da aws +- Deve ser criado uma classe de reposutorio de dados para bancos no-sql cassandra +- Deve ser criado uma classe de reposutorio de dados para bancos no-sql firebase +- Deve ser criado uma classe de reposutorio de dados para bancos no-sql Aurora / amazon +- Deve ser criado uma classe de reposutorio de dados para bancos no-sql RDS / amazon +- Deve ser criado uma classe de reposutorio de dados que envie requests e receba confirmacoes utilizando um sistema de queue com request-reponse message pattern, que seja agnostico, e suporte bullmq, rabbitmq, + +- deve criar arquivos docker file os que ja tem rabbitmq e redis para todos os servicos mencionados acima como containers. + + +renomeie a pasta domain designer para "servicemangement". + + + + + +The service mangement application is a UI tabbed layout application that has the following applications + +1. Domain Designer. Use the Domain Designer MVP tool to manage all domains and data entities of the "aaa-typescript-boilerplate" +2. Communication Interface Designer. Use to design the output interfaces adapters like HTTP (REST) (actually already implemented) , gRPC, websocket, SSE Server. These interfaces receive. requests from outside the service context and forward accordingly to the controllers +3. Service Configuration. + +The boilerplate can be used to create REST APIs, APIs running as as set of functions, microservices using different messaging mechanisms. + +so this tab must allow the engineer to configure what exacltly is the kind of software, how it runs, which cloud, as VM, as function, dedicated servers, save credentials all info required for deploy, support aws, google, vercel, cloudfare cloud, azure, docker. allow integration with the cloud providers allowing for example to preview the API gateway resources. lambdas, azure and google functions are deployed with serveless framework, there is already some serverless integration in the project. extend it + + +- Dedicated Server + through ssh +- Virtual Machine + through ssh + EC2 ? +- REST API + Dedicated Server through ssh + EC2 instances +- Function APIs: + AWS Lambda / serveless + Vercel Functions + CloudFare +4. Deploy management. The Deploy management tool allow to deploy and manage all the deployments of the service through different service and architectures. + +If the service is a set of functions, it will be deployed as aws, google, azure, vercel functions or cloudfare workers. use serverless when it is possible. + +If the service is a monolith with lot of domains, it will be deployd at private VMs, VMs cloud solutions (aws, google, azure,) or dedicated servers + + +Atualize documentacoes e agentes de requerimentos. + +Evite entregar recursos incompletos. + +Obedecça o coverage. + + +------ + + +Leia o arquivo /Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/src/interface/HTTP/RestAPI.ts + +Ele é o boostrap usado para criar REST APIs. Ele importa os mais diversos recursos e expor os recursos internos atraves de um servidor HTTP. Um dos frameworks suportados é o Express.js + +Crie: + +- WebSocketAPI - é uma implementação similar ao RestAPI, porém, ao inves de expor os handlers, controllers e use cases através de requisicoes e rotas HTTP, a comunicacao será bi-direcional via weboscket. fraemwork to use https://github.com/socketio/socket.IO +- gRPCAPI - é uma implementação similar ao RestAPI, porém, ao inves de expor os handlers, controllers e use cases através de requisicoes e rotas HTTP, a comunicacao será bi-direcional via gRPC. Framework to use - https://grpc.io/docs/languages/node/ + +Ambos os servidores acima usarao o padrao AsyncAPI, uma evolucao da open api, so que para arquiteturas event driven https://www.asyncapi.com/docs. + +Crie arquivos versao asyncapi do open api /Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/spec/1.0.0.yml + + +o diretorio "src/modules/Users/interface/api" deve ser renomeado para "src/modules/Users/interface/restapi" e 2 novos diretorios devem ser criados "src/modules/Users/interface/websocketapi" e "src/modules/Users/interface/grpcapi" + +Dentro deverá haver uma pasta framework, e dentro os handlers que receberao as requests dos clients. Crie arquivos para ambos grpc e websocket, para receber requisicao e redirecionar para o controler e metodo especifico. + +Crie 3 skd clients em /Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/sdk-clients + +1. REST API client, using browser native fetch +2. websocket API client, using browser socket io +3. gRPC api client using https://grpc.io/docs/languages/node/ + +The clients must rely in both YML definition files from /Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/spec + +-------------- + + + +Leia os requerimentos a seguir e crie um plano detalhado de execução, salve no todo e prossiga com a implementação. + +a aplicacao Service Configuration. "/Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/servicemangement" permitirá o desenvolvedore selecionar qual tipo de servico está sendo criado / gerenciado + +- RESTAPI + +- websocketAPI + RESTAPI + +- grpcAPI + RESTAPI + +ambos os servicos rodam em porta separadas + +Todos os servicos criados e que rodarão em Vms, todos os ambiente, seja local (dev), test (staging) ou production, rodarão através do pm2 https://pm2.keymetrics.io/. + +A ferramenta de desenvolimento "/Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/servicemangement" será servida via pm2. + +Instale o pm2 no projeto. + +Crie arquivos de configuracao para dev, staging and production, para inicar ./src/interface/HTTP/adapters, ./src/interface/gRPC/adapters e ./src/interface/WebSocket/adapters + +Modifique o package e qualquer chamada para existente para arquivos adpters do diretorio ./src/interface/HTTP/adapters, ./src/interface/gRPC/adapters e ./src/interface/WebSocket/adapters para serem iniciados via pm2 + +Em qualquer ambiente VM, websocketAPI, RESTAPI e grpcAPI devem ser iniciados como processos separados via pm2 + +O ambiente dev deve iniciar o servicemangement via pm2 automaticamente + +Atualize documentacoes e agentes de requerimentos. + +Evite entregar recursos incompletos. + +Obedecça o coverage. + + + +------- + +Leia os requerimentos a seguir e crie um plano detalhado de execução, salve no todo e prossiga com a implementação. + +add new config fields in all env files in /Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/src/config: + +- AAA_HTTP_FRAMEWORK # default express, when starting the RESTAPI, use this framework adapter, values can be express +- AAA_REALTIME_API # default no, if yes, start the realtime API +- AAA_REALTIME_API_PROTOCOL # default websocket, can be websocket +- AAA_DATABASE_DRIVER # Mongo, PostgreSQL, MySQL, MS SQL, RDS, Aurora, Cassandra + + +O servicemangement dever ser capaz de ler e alterar os valor das variaveis no ambiente atual de desenvililbimento, seja windows mac ou linux. + +Então, em todos os ambientes, os servicos serão iniciados carregandos os adapters de acordo com as variaveis de ambiente. + +faça todas as implementaçoes necessarias + + +Atualize documentacoes e agentes de requerimentos. + +Evite entregar recursos incompletos. + +Obedecça o coverage. + +-------- + + +Verifique a implementacao de login, accesso a end points, gerenciamento de usuarios, controle de acesso. + +Veja o que falta para que a aplicação atinja perto de 100% de PCI compliance e liste para mim + +------- + +Leia os requerimentos a seguir e crie um plano detalhado de execução, salve no todo e prossiga com a implementação. + +see the vercel functions documentation https://vercel.com/docs/functions please refactor our server and handlers accordingly + +Os tokens JWT devem conter a role do user e informacoes sobre a organizacao que o user pertence. + +Usuários do tipo "admin" e "user", quando criam registros no banco de dados, devem ter o id da organizacao que pertence usado no campo "organization" de cada registro criado no banco de dados. + +o boilerplate será usado para construção de aplicações pequenas até ERPs. + +Usuários de uma organização, não podem nunca ver dados de outras organizações, dados de CORE domains e sub domains são 100% escopados em níveis organizacional. + +Adicione novos campos no User e atualize todas as layers relacionadas + +UserPreference { + _id: inherit default, + @belongsTo(() => User) + user: BelongsTo + language: string required ISO code, default code for english + currency: string required ISO code, default code for english + theme: string, + + organization: BelongsTo +} + +Todo software desenvolvido com DDD tem sub dominios, que são secundários ao foco da empresa, e core domains, que representam todo o business rules. + +O boilerplate irá contar com alguns sub domínios básicos: + + +- Tasks Domain. + +Tarefas podem ter multiples assignee, ter um reporter ou nao. Tarefas devem pertencer á uma organização. + +TaskComment { + _id: inherit default, + @belongsTo(() => User) + author: BelongsTo + + + organization: BelongsTo +} + +TaskPool { + _id: inherit default, + question: string required + @hasMany(() => TaskPoolOption) + options: HasMany // required + status: string default open required // open closed + + organization: BelongsTo +} + +TaskPoolOption { + _id: inherit default, + @belongsTo(() => TaskPool) + task_pool: BelongsTo// required + answer: string required + votes: number integer default 0 required + + organization: BelongsTo +} + +TaskPoolOption can not receive votes if TaskPool is closed + +TaskLabel { + _id: inherit default, + name: string required + color: string required +} + +Task { + _id: inherit default, + name: string required + description: string required + + @hasMany(() => TaskLabel) + labels: [] + + @belongsTo(() => User) + reporter: BelongsTo // not required + + @hasMany(() => User) + assignee: HasMany + + @hasMany(() => TaskComment) + comments: HasMany + + @hasMany(() => Task) + subtasks: HasMany + + status: string required default pending // pending, in progress, in review, quality check, done + + isSubTask: boolean default false + + + organization: BelongsTo +} + +Crie todas as camadas necessarias para o novo dominio bem como expô-lo, do data entity ao controller, crie handlers para todos os fraemworks HTTP, gRPC e websocket implementados. + +Se tiver algum sugestão de alteração ou expansão na modelagem de dados, me apresente para aprovação. + +Atualize documentacoes e agentes de requerimentos. + +Evite entregar recursos incompletos. + +Obedecça o coverage. + + + +------ + + + + +Workflow de inicializacao e integracao do servico com banco de dados. + +Os arquivos bootstrap, como adaptador de interface HTTP express.js (/apps/apps/apps/aaa-typescript-boilerplate/src/interface/HTTP/adapters/express/express.ts), fastify.js (src/interface/HTTP/adapters/fastify/fastify.ts) devem importar todos os DB clients: in memory, mongoose, sequelize, etc. devem ler a variavel de ambiente process.env.AAA_DATABASE_DRIVER e identificar qual driver de banco dados deve injetar para dentro da RestAPI, gRPCAPI ou webSocketAPI. + + +Os DBS clients expoe uma API no formato IDatabaseClient. onde stores é um array de objetos no formato IStore, contendo cada tabela / collection do banco de dados conectado mapeados para IStore. Ja dentro do Fluxo, os Data Repositories recebem uma referencia do dbClient injetado. tendo asssim acesso á dbCLient.stores e podendo chamar metodos como dbCLient.stores.NomeDaCollection.create(). + +termine a implementacao do src/infra/persistence/external/ExternalStoreProxy.ts, mapeie a implementacao atual de IStore para usar em todos os DBclients de todos os bancos de dados que estao no requerimento. As tabelas do aequelize e collections do mongoose devem ser traduzidas para IStore, assim como nos adaptadores para cassandra, dynamodb, etc + +esse tipo de implementacao evita a dependencia de arquivos externos em camada de data repostories, bem como evita a criacao de um data reposutory para cada tipo de banco. deixando assim o gerenciamento da conexaco com banco em nivel de aplicacao / infra + +quero smoke test de cada banco de dados + + + +------ + + +Leia os requerimentos a seguir e crie um plano detalhado de execução, salve no todo e aguarde por novas ordens. + +Quero plano detalhado para evitar perda de tempo com implemtações incertas. + +O repositorio aaa-typescript-boilerplate precisa ser reorganizado e passar a ser um mono repo. + +O que antes era só um template para gerar backend de servicos diferentes, agora passa a ser um monorepo de um produto que funciona como uma fábrica de software gerenciadora de servicos nodejs backend e frontend. + +O monorepo passará a ser gerenciado pelo gerenciador pnpm + +O nome do produto é JumentiX. + +O nome "JumentiX" é uma alusão ao animal famoso no Brasil chamado Jumento, que consome pouco e tem muito poder de carga. + +Vide o mascote na imagem https://camo.githubusercontent.com/c02ee6896511af4bee5b20800df049a75b2ec01bc59578bf5ac0b2aaa97d64b7/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6372656174696f6e2e686f777273652e636f6d2f3130303033303037372d6e6f726d616c2e706e67 + +para começar o jumentix, bastará o desenvolvedor rodar `npm install jumentix@init -g` no terminal i isso ira baixar e instalar pacote o CLI que será usado para download e instalcao de de componentes e bootstrap de codigo + +O Jumentix permite criar desde monolitos modulares prontos para serem desacoplados em micro servicos, até um grupo ilimitado de multiplo servicos. + +O Jumentix permite gerenciar desde o simples monolito criado até um grupo de ilimitados serviços criado com o Jumentix. + +O Jumentix permite criar um grupo hibrido de servicos backend e frontend, bem como um simples servico backend ou um servico fronten SPA, PWA que funciona 100% offline (usando indexedDB como banco) + +O Jumentix suporta do desenvovimneto de cada servico até o deploy deles para plataformas diferentes, como VMs, instancias Ec2, apis de servicos lambdas (ou vercel ou cloudfare). + +Todos os servicos, sejam backend ou frontend seguem 100% os principios de Event Driven Desig, DDD, Hexagonal Archicture, CLean Code. + +O monorepo usa typescript como já faz atualmente, deve mante o maximo de retrocompatibilidade e suportar o desenvolvimento de projetos node typescript de natureza diferentes. + +O mono repo tem configurações genericas de bundler e configuracoes expandidas para cada tipo de servico: backend, frontend SPA, frontend com SSR, livarias npm para backend, livarias npm para frontend + +Atualmente os servicos de dominios do backend comunicam entre si Via uma implementacao de Message Mediator. Essa classe deverá ser distribuida como um pacote npm e importada nos servicos backend. + +Cada projeto terá seu proprio conjunto de testes e suites. + +Para fazer commits em um component / projeto do jumentix, é necessario e somente ncessario rodar os testes relacionados á aquele projeto + +O Jumentix continuará usando o pm2 para gerenciamento de ambientes como o aaa-typescript-boilerplate já faz atualmente, a diferença é que ele irá rodar mais servicos backend e agora tambem frontend. + + +Projects / component: + +- CI the current CLI tool - will be distributed as NPM package and will be used to bostrap a new Jumentix structure. +- backend boilerplate - current aaa-typescript-boilerplate, project used to create different services as it already does, all the files related to the current aaa-typescript-boilerplate, like src, documentation, node_modules, OASDoc, seed, test, AsynAPIDOC, serverless, etc. +- sdk-clients - npm independent libraries that will be imported in front end applications to consume the back end APIs +- service management - a WEB tool +- Message Mediator - the current Message Mediatior implementation shuould be deccouples and served as npm independent library that will be imported in backend applications rather than being requiring multiple distributions of the class accroos the multiple backend applications created using the backend boilerplate + + + + + + + + +leia o arquivo ele representa a implementacao de um banco de dados, nesse caso, in memory. em passos anteriores, criamos suporte para sequelize, mongoose e diversos frameworks e solucoes para acesso á banco dados. é necessário criar DbClients para cada uma das implemtacoes que fizemos. também é preciso alterar os adpatadores de interfaces HTTP de cada framework na pasta src/interface/HTTP/adapters/, um exemplo é o /Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/src/interface/HTTP/adapters/express/express.ts. o adaptador deve ler a variavel de ambiente process.env.AAA_DATABASE_DRIVER e identificar qual driver de banco dados deve utlizar e importar \ No newline at end of file diff --git a/AsyncAPIdoc/app.js b/apps/backend-template/AsyncAPIdoc/app.js similarity index 100% rename from AsyncAPIdoc/app.js rename to apps/backend-template/AsyncAPIdoc/app.js diff --git a/AsyncAPIdoc/index.html b/apps/backend-template/AsyncAPIdoc/index.html similarity index 100% rename from AsyncAPIdoc/index.html rename to apps/backend-template/AsyncAPIdoc/index.html diff --git a/OASdoc/app.js b/apps/backend-template/OASdoc/app.js similarity index 100% rename from OASdoc/app.js rename to apps/backend-template/OASdoc/app.js diff --git a/OASdoc/index.html b/apps/backend-template/OASdoc/index.html similarity index 100% rename from OASdoc/index.html rename to apps/backend-template/OASdoc/index.html diff --git a/OASdoc/miro.png b/apps/backend-template/OASdoc/miro.png similarity index 100% rename from OASdoc/miro.png rename to apps/backend-template/OASdoc/miro.png diff --git a/OASdoc/swagger-ui-bundle.js b/apps/backend-template/OASdoc/swagger-ui-bundle.js similarity index 100% rename from OASdoc/swagger-ui-bundle.js rename to apps/backend-template/OASdoc/swagger-ui-bundle.js diff --git a/OASdoc/swagger-ui-standalone-preset.js b/apps/backend-template/OASdoc/swagger-ui-standalone-preset.js similarity index 100% rename from OASdoc/swagger-ui-standalone-preset.js rename to apps/backend-template/OASdoc/swagger-ui-standalone-preset.js diff --git a/OASdoc/swagger-ui.css b/apps/backend-template/OASdoc/swagger-ui.css similarity index 100% rename from OASdoc/swagger-ui.css rename to apps/backend-template/OASdoc/swagger-ui.css diff --git a/apps/backend-template/README.md b/apps/backend-template/README.md new file mode 100644 index 00000000..d8ff773c --- /dev/null +++ b/apps/backend-template/README.md @@ -0,0 +1,42 @@ +# @jumentix/backend-template + +Workspace app target for backend-template migration. + +Technical hub: + +- [Backend Template Documentation](./documentation/README.md) + +## Current status + +- Transitional workspace app with executable scripts mapped to the current root runtime. +- Canonical backend runtime still remains in root (`src`, `spec`, `test`, `pm2`, `docker`, docs) during Wave 5. + +## Current executable scripts + +- `build` -> root `build:dev` +- `build:dev` -> root `build:dev` +- `test` -> root `test:unit` +- `test:unit` -> root `test:unit` +- `test:integration` -> root `test:integration` +- `test:integration:smoke` -> root `ci:smoke` +- `lint` -> root `lint` +- `coverage:patch` -> root `coverage:patch` +- `ci:gate` -> root `ci:gate` +- `ci:gate:strict` -> root `ci:gate:strict` +- `dev:rest` -> root `dev:http` +- `dev:websocket-rest` -> root `dev:websocket` +- `dev:grpc-rest` -> root `dev:grpc` +- `pm2:start:*` -> root PM2 profile starters for dev/staging/production + +## Planned cutover (Wave 5) + +1. Move backend runtime files into `apps/backend-template`. +2. Keep imports wired to `@jumentix/*` shared packages. +3. Preserve CI gates and coverage thresholds. +4. Keep PM2 process profiles functional after re-homing. + +## Wave 5 progress + +- Service Management app has already been re-homed to `apps/service-management`. +- Backend-template now owns an operational script surface in workspace context, so pipelines can call app-level commands while code still resides at root. +- Next slice is physical runtime move (`src/spec/test/pm2`) with path rewrites and PM2 parity validation. diff --git a/docker-compose-aurora.yml b/apps/backend-template/docker-compose-aurora.yml similarity index 100% rename from docker-compose-aurora.yml rename to apps/backend-template/docker-compose-aurora.yml diff --git a/docker-compose-cassandra.yml b/apps/backend-template/docker-compose-cassandra.yml similarity index 100% rename from docker-compose-cassandra.yml rename to apps/backend-template/docker-compose-cassandra.yml diff --git a/docker-compose-dynamodb.yml b/apps/backend-template/docker-compose-dynamodb.yml similarity index 100% rename from docker-compose-dynamodb.yml rename to apps/backend-template/docker-compose-dynamodb.yml diff --git a/docker-compose-firebase.yml b/apps/backend-template/docker-compose-firebase.yml similarity index 100% rename from docker-compose-firebase.yml rename to apps/backend-template/docker-compose-firebase.yml diff --git a/docker-compose-messaging.yml b/apps/backend-template/docker-compose-messaging.yml similarity index 100% rename from docker-compose-messaging.yml rename to apps/backend-template/docker-compose-messaging.yml diff --git a/docker-compose-mongodb.yml b/apps/backend-template/docker-compose-mongodb.yml similarity index 100% rename from docker-compose-mongodb.yml rename to apps/backend-template/docker-compose-mongodb.yml diff --git a/docker-compose-mssql.yml b/apps/backend-template/docker-compose-mssql.yml similarity index 100% rename from docker-compose-mssql.yml rename to apps/backend-template/docker-compose-mssql.yml diff --git a/docker-compose-mysql.yml b/apps/backend-template/docker-compose-mysql.yml similarity index 100% rename from docker-compose-mysql.yml rename to apps/backend-template/docker-compose-mysql.yml diff --git a/docker-compose-oracle.yml b/apps/backend-template/docker-compose-oracle.yml similarity index 100% rename from docker-compose-oracle.yml rename to apps/backend-template/docker-compose-oracle.yml diff --git a/docker-compose-platform-services.yml b/apps/backend-template/docker-compose-platform-services.yml similarity index 100% rename from docker-compose-platform-services.yml rename to apps/backend-template/docker-compose-platform-services.yml diff --git a/docker-compose-postgresql.yml b/apps/backend-template/docker-compose-postgresql.yml similarity index 100% rename from docker-compose-postgresql.yml rename to apps/backend-template/docker-compose-postgresql.yml diff --git a/docker-compose-rds.yml b/apps/backend-template/docker-compose-rds.yml similarity index 100% rename from docker-compose-rds.yml rename to apps/backend-template/docker-compose-rds.yml diff --git a/docker-compose-redis.yml b/apps/backend-template/docker-compose-redis.yml similarity index 100% rename from docker-compose-redis.yml rename to apps/backend-template/docker-compose-redis.yml diff --git a/docker-compose-service-templates.yml b/apps/backend-template/docker-compose-service-templates.yml similarity index 100% rename from docker-compose-service-templates.yml rename to apps/backend-template/docker-compose-service-templates.yml diff --git a/docker/services/Dockerfile.functions b/apps/backend-template/docker/services/Dockerfile.functions similarity index 100% rename from docker/services/Dockerfile.functions rename to apps/backend-template/docker/services/Dockerfile.functions diff --git a/docker/services/Dockerfile.graphql b/apps/backend-template/docker/services/Dockerfile.graphql similarity index 100% rename from docker/services/Dockerfile.graphql rename to apps/backend-template/docker/services/Dockerfile.graphql diff --git a/docker/services/Dockerfile.grpc b/apps/backend-template/docker/services/Dockerfile.grpc similarity index 100% rename from docker/services/Dockerfile.grpc rename to apps/backend-template/docker/services/Dockerfile.grpc diff --git a/docker/services/Dockerfile.rest b/apps/backend-template/docker/services/Dockerfile.rest similarity index 100% rename from docker/services/Dockerfile.rest rename to apps/backend-template/docker/services/Dockerfile.rest diff --git a/docker/services/Dockerfile.websocket b/apps/backend-template/docker/services/Dockerfile.websocket similarity index 100% rename from docker/services/Dockerfile.websocket rename to apps/backend-template/docker/services/Dockerfile.websocket diff --git a/apps/backend-template/documentation/README.md b/apps/backend-template/documentation/README.md new file mode 100644 index 00000000..995df1aa --- /dev/null +++ b/apps/backend-template/documentation/README.md @@ -0,0 +1,38 @@ +# Backend Template Documentation Hub + +This is the technical documentation hub for the backend template application used by Jumentix to bootstrap backend services. + +## Index + +- Guides + - [Creating REST API with Jumentix](./guides/CREATING-REST-API-WITH-JUMENTIX.md) + - [Creating Realtime API with Jumentix](./guides/CREATING-REALTIME-API-WITH-JUMENTIX.md) +- Architecture and structure + - [Architecture and Structure](../../../documentation/md/ARCHITECTURE-AND-STRUCTURE.md) + - [Hexagonal Feature-Driven Migration](../../../documentation/md/HEXAGONAL-FEATURE-DRIVEN-MIGRATION.md) +- Runtime and adapters + - [Runtime Environment Contracts](../../../documentation/md/RUNTIME-ENVIRONMENT-CONTRACTS.md) + - [HTTP Adapters](../../../documentation/md/adapters/http/README.md) + - [Realtime WebSocket API](../../../documentation/md/adapters/realtime/WEBSOCKET-API.md) + - [Realtime gRPC API](../../../documentation/md/adapters/realtime/GRPC-API.md) +- Data and persistence + - [Database Adapters](../../../documentation/md/adapters/databases/README.md) + - [Database Drivers Smoke Tests](../../../documentation/md/DATABASE-DRIVERS-SMOKE-TESTS.md) +- Contracts and quality + - [OpenAPI Spec](../../../spec/1.0.0.yml) + - [Events and Messages Map](../../../documentation/md/EVENTS-AND-MESSAGES-MAP.md) + - [Error Contracts and Responses](../../../documentation/md/ERROR-CONTRACTS-AND-RESPONSES.md) + - [Testing, CI and Quality](../../../documentation/md/TESTING-CI-AND-QUALITY.md) + +## What This Component Delivers + +- Contract-first backend services with OpenAPI 3.1 and AsyncAPI. +- DDD + Hexagonal architecture baseline for modular monolith and microservice evolution. +- Runtime adapter matrix for REST, realtime, and serverless contexts. +- Pluggable persistence and messaging layers through shared `@jumentix/*` packages. + +## Main Entrypoints + +- REST runtime loaders and adapters in `apps/backend-template/src/interface/HTTP`. +- Realtime runtime loaders and adapters in `apps/backend-template/src/interface/WebSocket` and `apps/backend-template/src/interface/gRPC`. +- PM2 profiles from repo root `pm2/` for dev/staging/production process orchestration. diff --git a/apps/backend-template/documentation/guides/CREATING-REALTIME-API-WITH-JUMENTIX.md b/apps/backend-template/documentation/guides/CREATING-REALTIME-API-WITH-JUMENTIX.md new file mode 100644 index 00000000..f2993e3f --- /dev/null +++ b/apps/backend-template/documentation/guides/CREATING-REALTIME-API-WITH-JUMENTIX.md @@ -0,0 +1,48 @@ +# Creating Realtime API with Jumentix + +This guide covers realtime service setup using WebSocket or gRPC with REST fallback. + +## 1. Enable Realtime Runtime + +Set env profile: + +- `AAA_REALTIME_API=yes` +- `AAA_REALTIME_API_PROTOCOL=websocket` or `grpc` +- `AAA_HTTP_FRAMEWORK=express` for fallback REST documentation and operational fallback + +Start runtime profile: + +```bash +npm run dev:websocket +# or +npm run dev:grpc +``` + +## 2. Define Async Contracts + +1. Define channels/messages in AsyncAPI files under `spec/`. +2. Keep payload contracts aligned with domain models and controller method signatures. +3. Reference response/error contracts in handler implementations. + +## 3. Implement Message Handlers + +- WebSocket handlers receive messages, invoke controller methods, and answer back to the same client/message correlation. +- gRPC handlers receive requests, invoke controller methods, and return protocol-native responses. + +## 4. Keep REST Fallback Available + +Realtime services run with REST as secondary interface. Use REST docs and endpoints if realtime channel is degraded. + +## 5. Validate Realtime Stability + +```bash +npm run test:integration:realtime +npm run test:smoke:realtime +``` + +## References + +- [Realtime WebSocket API](../../../../documentation/md/adapters/realtime/WEBSOCKET-API.md) +- [Realtime gRPC API](../../../../documentation/md/adapters/realtime/GRPC-API.md) +- [WebSocket Realtime Contracts](../../../../documentation/md/contracts/WEBSOCKET-REALTIME-CONTRACTS.md) +- [gRPC Realtime Contracts](../../../../documentation/md/contracts/GRPC-REALTIME-CONTRACTS.md) diff --git a/apps/backend-template/documentation/guides/CREATING-REST-API-WITH-JUMENTIX.md b/apps/backend-template/documentation/guides/CREATING-REST-API-WITH-JUMENTIX.md new file mode 100644 index 00000000..51947fb2 --- /dev/null +++ b/apps/backend-template/documentation/guides/CREATING-REST-API-WITH-JUMENTIX.md @@ -0,0 +1,54 @@ +# Creating REST API with Jumentix + +This guide shows how to bootstrap and run a REST service using the backend template. + +## 1. Select Runtime Profile + +Set environment variables: + +- `AAA_HTTP_FRAMEWORK=express` (or any supported adapter) +- `AAA_REALTIME_API=no` + +Then start: + +```bash +npm run dev:http +``` + +## 2. Model Domain and Contracts + +1. Design entities and relationships in Service Management (Domain Designer). +2. Generate or update OpenAPI definitions in `spec/1.0.0.yml`. +3. Keep request/response contract objects aligned with controller inputs/outputs. + +## 3. Implement Domain Flow + +Use the project layering pattern: + +1. model/entity/value objects +2. repository ports/adapters +3. services/use cases +4. controllers +5. framework handlers + +## 4. Bind to HTTP Adapter + +Choose adapter by env (`AAA_HTTP_FRAMEWORK`) and use the REST startup loader. +REST endpoints are exposed through framework-native handlers under module interfaces. + +## 5. Validate + +Run quality gates: + +```bash +npm run lint +npm run test:unit +npm run oas:check-routes +npm run test:integration:express +``` + +## References + +- [HTTP Adapters Index](../../../../documentation/md/adapters/http/README.md) +- [OpenAPI Spec](../../../../spec/1.0.0.yml) +- [Setup Runtime and API](../../../../documentation/md/SETUP-RUNTIME-AND-API.md) diff --git a/apps/backend-template/package.json b/apps/backend-template/package.json new file mode 100644 index 00000000..383e7b82 --- /dev/null +++ b/apps/backend-template/package.json @@ -0,0 +1,31 @@ +{ + "name": "@jumentix/backend-template", + "version": "0.0.2", + "private": true, + "description": "JumentiX backend template workspace app", + "scripts": { + "build": "npm run build:dev --prefix ../..", + "build:dev": "npm run build:dev --prefix ../..", + "test": "npm run test:unit --prefix ../..", + "test:unit": "npm run test:unit --prefix ../..", + "test:integration": "npm run test:integration --prefix ../..", + "test:integration:smoke": "npm run ci:smoke --prefix ../..", + "lint": "npm run lint --prefix ../..", + "coverage:patch": "npm run coverage:patch --prefix ../..", + "ci:gate": "npm run ci:gate --prefix ../..", + "ci:gate:strict": "npm run ci:gate:strict --prefix ../..", + "typecheck": "npm run build:dev --prefix ../..", + "dev:rest": "npm run dev:http --prefix ../..", + "dev:websocket-rest": "npm run dev:websocket --prefix ../..", + "dev:grpc-rest": "npm run dev:grpc --prefix ../..", + "pm2:start:dev:restapi": "npm run pm2:start:dev:restapi --prefix ../..", + "pm2:start:dev:websocket-rest": "npm run pm2:start:dev:websocket-rest --prefix ../..", + "pm2:start:dev:grpc-rest": "npm run pm2:start:dev:grpc-rest --prefix ../..", + "pm2:start:staging:restapi": "npm run pm2:start:staging:restapi --prefix ../..", + "pm2:start:staging:websocket-rest": "npm run pm2:start:staging:websocket-rest --prefix ../..", + "pm2:start:staging:grpc-rest": "npm run pm2:start:staging:grpc-rest --prefix ../..", + "pm2:start:prod:restapi": "npm run pm2:start:prod:restapi --prefix ../..", + "pm2:start:prod:websocket-rest": "npm run pm2:start:prod:websocket-rest --prefix ../..", + "pm2:start:prod:grpc-rest": "npm run pm2:start:prod:grpc-rest --prefix ../.." + } +} diff --git a/seed/accounts-api-large.json b/apps/backend-template/seed/accounts-api-large.json similarity index 100% rename from seed/accounts-api-large.json rename to apps/backend-template/seed/accounts-api-large.json diff --git a/seed/accounts-api.json b/apps/backend-template/seed/accounts-api.json similarity index 100% rename from seed/accounts-api.json rename to apps/backend-template/seed/accounts-api.json diff --git a/seed/accounts.ts b/apps/backend-template/seed/accounts.ts similarity index 100% rename from seed/accounts.ts rename to apps/backend-template/seed/accounts.ts diff --git a/seed/organizations.ts b/apps/backend-template/seed/organizations.ts similarity index 100% rename from seed/organizations.ts rename to apps/backend-template/seed/organizations.ts diff --git a/seed/transactions-api-large.json b/apps/backend-template/seed/transactions-api-large.json similarity index 100% rename from seed/transactions-api-large.json rename to apps/backend-template/seed/transactions-api-large.json diff --git a/seed/transactions-api.json b/apps/backend-template/seed/transactions-api.json similarity index 100% rename from seed/transactions-api.json rename to apps/backend-template/seed/transactions-api.json diff --git a/seed/transactions.ts b/apps/backend-template/seed/transactions.ts similarity index 100% rename from seed/transactions.ts rename to apps/backend-template/seed/transactions.ts diff --git a/seed/users.ts b/apps/backend-template/seed/users.ts similarity index 100% rename from seed/users.ts rename to apps/backend-template/seed/users.ts diff --git a/src/config/.env.ci b/apps/backend-template/src/config/.env.ci similarity index 79% rename from src/config/.env.ci rename to apps/backend-template/src/config/.env.ci index 3049285f..8cf12786 100644 --- a/src/config/.env.ci +++ b/apps/backend-template/src/config/.env.ci @@ -26,6 +26,11 @@ AAA_MESSAGE_MEDIATOR_ADAPTER=inmemory AAA_HTTP_FRAMEWORK=express AAA_REALTIME_API=no AAA_REALTIME_API_PROTOCOL=websocket +# Socket.IO scaling adapter: cluster | redis-streams | (empty for in-memory adapter) +#AAA_WEBSOCKET_SOCKETIO_ADAPTER=redis-streams +#AAA_WEBSOCKET_CLUSTER_WORKERS=2 +# Optional dedicated Redis URL for Socket.IO adapter +#AAA_WEBSOCKET_REDIS_URL=redis://127.0.0.1:6379/1 AAA_DATABASE_DRIVER=InMemory AAA_DATABASE_NAME=aaa diff --git a/apps/backend-template/src/config/.env.dev b/apps/backend-template/src/config/.env.dev new file mode 100644 index 00000000..ee919943 --- /dev/null +++ b/apps/backend-template/src/config/.env.dev @@ -0,0 +1,75 @@ +## get passwords from env file +AAA_REDIS_HOST=127.0.0.1 +AAA_REDIS_PORT=6379 +AAA_REDIS_DATABASE=1 +AAA_REDIS_PASSWORD= + +AAA_JWT_TOKEN_SECRET_KEY=change_me_dev_secret +AAA_JWT_ISSUER=aaa-boilerplate +AAA_JWT_AUDIENCE=aaa-boilerplate-clients + +#AAA_ACCESS_TOKEN_PRIVATE_KEY= +#AAA_ACCESS_TOKEN_PUBLIC_KEY= +#AAA_REFRESH_TOKEN_PRIVATE_KEY= +#AAA_REFRESH_TOKEN_PUBLIC_KEY= + +#AAA_GOOGLE_OAUTH_CLIENT_ID= +#AAA_GOOGLE_OAUTH_CLIENT_SECRET= +#AAA_GOOGLE_OAUTH_REDIRECT_URL= + +#AAA_GITHUB_OAUTH_CLIENT_ID= +#AAA_GITHUB_OAUTH_CLIENT_SECRET= +#AAA_GITHUB_OAUTH_REDIRECT_URL= + +# Message mediator adapter: inmemory | rabbitmq | bullmq +AAA_MESSAGE_MEDIATOR_ADAPTER=rabbitmq + +# Runtime adapter selection +AAA_HTTP_FRAMEWORK=express +AAA_REALTIME_API=no +AAA_REALTIME_API_PROTOCOL=websocket +# Socket.IO scaling adapter: cluster | redis-streams | (empty for in-memory adapter) +#AAA_WEBSOCKET_SOCKETIO_ADAPTER=redis-streams +#AAA_WEBSOCKET_CLUSTER_WORKERS=4 +# Optional dedicated Redis URL for Socket.IO adapter +#AAA_WEBSOCKET_REDIS_URL=redis://127.0.0.1:6379/1 +AAA_REALTIME_API_DATABASE_DRIVER=Mongo +AAA_DATABASE_DRIVER=InMemory +AAA_DATABASE_NAME=aaa +#AAA_DATABASE_CONNECTION_URL= +#AAA_DATABASE_DIALECT=postgres +#AAA_DATABASE_REGION=us-east-1 +#AAA_DATABASE_ENDPOINT=http://127.0.0.1:8000 +#AAA_DATABASE_PROJECT_ID=demo-project +#AAA_DATABASE_CASSANDRA_CONTACT_POINTS=127.0.0.1 +#AAA_DATABASE_CASSANDRA_DATACENTER=datacenter1 +#AAA_DATABASE_POOL_MAX=20 +#AAA_DATABASE_POOL_MIN=0 +#AAA_DATABASE_POOL_ACQUIRE_MS=30000 +#AAA_DATABASE_POOL_IDLE_MS=10000 +#AAA_DATABASE_POOL_EVICT_MS=1000 +#AAA_DATABASE_USER=aaa +#AAA_DATABASE_PASSWORD=aaa +#AAA_DATABASE_CONNECT_STRING=127.0.0.1:1521/FREEPDB1 + +# Auth security controls +AAA_ENABLE_BASIC_AUTH=yes +AAA_AUTH_MAX_LOGIN_ATTEMPTS=5 +AAA_AUTH_LOGIN_WINDOW_SECONDS=300 +AAA_AUTH_LOCKOUT_SECONDS=900 + +# CORS security controls (comma-separated origins, '*' allowed only in non-prod) +AAA_CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 + +# RabbitMQ adapter settings +AAA_RABBITMQ_URL=amqp://guest:guest@127.0.0.1:5672 +AAA_RABBITMQ_EXCHANGE=app.events +AAA_RABBITMQ_REQUEST_QUEUE=app.requests +AAA_RABBITMQ_PREFETCH=10 + +# BullMQ adapter settings +#AAA_BULLMQ_REDIS_HOST=127.0.0.1 +#AAA_BULLMQ_REDIS_PORT=6379 +#AAA_BULLMQ_REDIS_PASSWORD= +#AAA_BULLMQ_REDIS_DB=1 +#AAA_BULLMQ_REQUEST_QUEUE=app.requests diff --git a/src/config/.env.dev.example b/apps/backend-template/src/config/.env.dev.example similarity index 92% rename from src/config/.env.dev.example rename to apps/backend-template/src/config/.env.dev.example index 5a6a5554..26b4d786 100644 --- a/src/config/.env.dev.example +++ b/apps/backend-template/src/config/.env.dev.example @@ -26,6 +26,10 @@ AAA_REDIS_DATABASE=1 #AAA_HTTP_FRAMEWORK=express #AAA_REALTIME_API=no #AAA_REALTIME_API_PROTOCOL=websocket +#AAA_WEBSOCKET_SOCKETIO_ADAPTER=cluster +#AAA_WEBSOCKET_CLUSTER_WORKERS=4 +#AAA_WEBSOCKET_SOCKETIO_ADAPTER=redis-streams +#AAA_WEBSOCKET_REDIS_URL=redis://127.0.0.1:6379/1 #AAA_DATABASE_DRIVER=Mongo #AAA_DATABASE_CONNECTION_URL= #AAA_DATABASE_NAME=aaa diff --git a/src/config/.env.staging b/apps/backend-template/src/config/.env.staging similarity index 82% rename from src/config/.env.staging rename to apps/backend-template/src/config/.env.staging index d05810d5..1cb2417a 100644 --- a/src/config/.env.staging +++ b/apps/backend-template/src/config/.env.staging @@ -23,6 +23,11 @@ AAA_REDIS_DATABASE=1 AAA_HTTP_FRAMEWORK=express AAA_REALTIME_API=no AAA_REALTIME_API_PROTOCOL=websocket +# Socket.IO scaling adapter: cluster | redis-streams | (empty for in-memory adapter) +#AAA_WEBSOCKET_SOCKETIO_ADAPTER=redis-streams +#AAA_WEBSOCKET_CLUSTER_WORKERS=4 +# Optional dedicated Redis URL for Socket.IO adapter +#AAA_WEBSOCKET_REDIS_URL=redis://127.0.0.1:6379/1 AAA_DATABASE_DRIVER=Mongo AAA_DATABASE_NAME=aaa #AAA_DATABASE_CONNECTION_URL= diff --git a/src/config/auth.ts b/apps/backend-template/src/config/auth.ts similarity index 100% rename from src/config/auth.ts rename to apps/backend-template/src/config/auth.ts diff --git a/src/config/constants.ts b/apps/backend-template/src/config/constants.ts similarity index 100% rename from src/config/constants.ts rename to apps/backend-template/src/config/constants.ts diff --git a/src/config/jwt.ts b/apps/backend-template/src/config/jwt.ts similarity index 100% rename from src/config/jwt.ts rename to apps/backend-template/src/config/jwt.ts diff --git a/src/config/redis.ts b/apps/backend-template/src/config/redis.ts similarity index 100% rename from src/config/redis.ts rename to apps/backend-template/src/config/redis.ts diff --git a/src/config/security.ts b/apps/backend-template/src/config/security.ts similarity index 100% rename from src/config/security.ts rename to apps/backend-template/src/config/security.ts diff --git a/src/infra/audit/ISecurityAuditRepository.ts b/apps/backend-template/src/infra/audit/ISecurityAuditRepository.ts similarity index 100% rename from src/infra/audit/ISecurityAuditRepository.ts rename to apps/backend-template/src/infra/audit/ISecurityAuditRepository.ts diff --git a/src/infra/audit/InMemorySecurityAuditRepository.ts b/apps/backend-template/src/infra/audit/InMemorySecurityAuditRepository.ts similarity index 100% rename from src/infra/audit/InMemorySecurityAuditRepository.ts rename to apps/backend-template/src/infra/audit/InMemorySecurityAuditRepository.ts diff --git a/src/infra/audit/index.ts b/apps/backend-template/src/infra/audit/index.ts similarity index 100% rename from src/infra/audit/index.ts rename to apps/backend-template/src/infra/audit/index.ts diff --git a/apps/backend-template/src/infra/cache/CacheService.ts b/apps/backend-template/src/infra/cache/CacheService.ts new file mode 100644 index 00000000..82686235 --- /dev/null +++ b/apps/backend-template/src/infra/cache/CacheService.ts @@ -0,0 +1,74 @@ +import { IKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/IKeyValueStorageClient'; + +import { ICacheService } from './ICacheService'; + +interface ICacheEnvelope { + value: T; + expiresAt?: number; +} + +interface ICacheServiceConfig { + keyValueStorageClient: IKeyValueStorageClient; +} + +export class CacheService implements ICacheService { + private readonly keyValueStorageClient: IKeyValueStorageClient; + + private constructor(config: ICacheServiceConfig) { + this.keyValueStorageClient = config.keyValueStorageClient; + } + + public async get(key: string): Promise { + const { result, error } = await this.keyValueStorageClient.get(key); + if (error || result === undefined || result === null) return undefined; + + const envelope = result as ICacheEnvelope; + if (typeof envelope === 'object' && envelope && 'value' in envelope) { + if (envelope.expiresAt && envelope.expiresAt <= Date.now()) { + await this.keyValueStorageClient.del(key); + return undefined; + } + return envelope.value; + } + + return result as T; + } + + public async set(key: string, value: T, ttlInSeconds?: number): Promise { + const envelope: ICacheEnvelope = { value }; + if (ttlInSeconds && ttlInSeconds > 0) { + envelope.expiresAt = Date.now() + (ttlInSeconds * 1000); + } + await this.keyValueStorageClient.set(key, envelope); + } + + public async del(key: string): Promise { + await this.keyValueStorageClient.del(key); + } + + public async getVersion(namespace: string): Promise { + const key = CacheService.buildVersionKey(namespace); + const version = await this.get(key); + if (!version || Number.isNaN(Number(version)) || Number(version) < 1) { + await this.set(key, 1); + return 1; + } + return Number(version); + } + + public async bumpVersion(namespace: string): Promise { + const key = CacheService.buildVersionKey(namespace); + const current = await this.getVersion(namespace); + const next = current + 1; + await this.set(key, next); + return next; + } + + private static buildVersionKey(namespace: string): string { + return `cache:version:${namespace}`; + } + + public static compile(config: ICacheServiceConfig): CacheService { + return new CacheService(config); + } +} diff --git a/apps/backend-template/src/infra/cache/ICacheService.ts b/apps/backend-template/src/infra/cache/ICacheService.ts new file mode 100644 index 00000000..6a41eb93 --- /dev/null +++ b/apps/backend-template/src/infra/cache/ICacheService.ts @@ -0,0 +1,7 @@ +export interface ICacheService { + get(key: string): Promise; + set(key: string, value: T, ttlInSeconds?: number): Promise; + del(key: string): Promise; + getVersion(namespace: string): Promise; + bumpVersion(namespace: string): Promise; +} diff --git a/apps/backend-template/src/infra/cache/index.ts b/apps/backend-template/src/infra/cache/index.ts new file mode 100644 index 00000000..efdce34f --- /dev/null +++ b/apps/backend-template/src/infra/cache/index.ts @@ -0,0 +1,2 @@ +export * from './ICacheService'; +export * from './CacheService'; diff --git a/src/infra/context/Context.ts b/apps/backend-template/src/infra/context/Context.ts similarity index 100% rename from src/infra/context/Context.ts rename to apps/backend-template/src/infra/context/Context.ts diff --git a/src/infra/events/InMemoryEventBus.ts b/apps/backend-template/src/infra/events/InMemoryEventBus.ts similarity index 100% rename from src/infra/events/InMemoryEventBus.ts rename to apps/backend-template/src/infra/events/InMemoryEventBus.ts diff --git a/src/infra/exceptions/BaseError.ts b/apps/backend-template/src/infra/exceptions/BaseError.ts similarity index 100% rename from src/infra/exceptions/BaseError.ts rename to apps/backend-template/src/infra/exceptions/BaseError.ts diff --git a/src/infra/exceptions/ComposeEventError.ts b/apps/backend-template/src/infra/exceptions/ComposeEventError.ts similarity index 100% rename from src/infra/exceptions/ComposeEventError.ts rename to apps/backend-template/src/infra/exceptions/ComposeEventError.ts diff --git a/src/infra/exceptions/ConflictError.ts b/apps/backend-template/src/infra/exceptions/ConflictError.ts similarity index 100% rename from src/infra/exceptions/ConflictError.ts rename to apps/backend-template/src/infra/exceptions/ConflictError.ts diff --git a/src/infra/exceptions/DataBaseNotFoundError.ts b/apps/backend-template/src/infra/exceptions/DataBaseNotFoundError.ts similarity index 100% rename from src/infra/exceptions/DataBaseNotFoundError.ts rename to apps/backend-template/src/infra/exceptions/DataBaseNotFoundError.ts diff --git a/src/infra/exceptions/DatabasePagingError.ts b/apps/backend-template/src/infra/exceptions/DatabasePagingError.ts similarity index 100% rename from src/infra/exceptions/DatabasePagingError.ts rename to apps/backend-template/src/infra/exceptions/DatabasePagingError.ts diff --git a/src/infra/exceptions/DomainNotFoundError.ts b/apps/backend-template/src/infra/exceptions/DomainNotFoundError.ts similarity index 100% rename from src/infra/exceptions/DomainNotFoundError.ts rename to apps/backend-template/src/infra/exceptions/DomainNotFoundError.ts diff --git a/src/infra/exceptions/DomainValidationError.ts b/apps/backend-template/src/infra/exceptions/DomainValidationError.ts similarity index 100% rename from src/infra/exceptions/DomainValidationError.ts rename to apps/backend-template/src/infra/exceptions/DomainValidationError.ts diff --git a/src/infra/exceptions/ForbiddenError.ts b/apps/backend-template/src/infra/exceptions/ForbiddenError.ts similarity index 100% rename from src/infra/exceptions/ForbiddenError.ts rename to apps/backend-template/src/infra/exceptions/ForbiddenError.ts diff --git a/src/infra/exceptions/ISerializedError.ts b/apps/backend-template/src/infra/exceptions/ISerializedError.ts similarity index 100% rename from src/infra/exceptions/ISerializedError.ts rename to apps/backend-template/src/infra/exceptions/ISerializedError.ts diff --git a/src/infra/exceptions/InternalServerError.ts b/apps/backend-template/src/infra/exceptions/InternalServerError.ts similarity index 100% rename from src/infra/exceptions/InternalServerError.ts rename to apps/backend-template/src/infra/exceptions/InternalServerError.ts diff --git a/src/infra/exceptions/NotFoundError.ts b/apps/backend-template/src/infra/exceptions/NotFoundError.ts similarity index 100% rename from src/infra/exceptions/NotFoundError.ts rename to apps/backend-template/src/infra/exceptions/NotFoundError.ts diff --git a/src/infra/exceptions/NotImplemented.ts b/apps/backend-template/src/infra/exceptions/NotImplemented.ts similarity index 100% rename from src/infra/exceptions/NotImplemented.ts rename to apps/backend-template/src/infra/exceptions/NotImplemented.ts diff --git a/src/infra/exceptions/ResourceLockedError.ts b/apps/backend-template/src/infra/exceptions/ResourceLockedError.ts similarity index 100% rename from src/infra/exceptions/ResourceLockedError.ts rename to apps/backend-template/src/infra/exceptions/ResourceLockedError.ts diff --git a/src/infra/exceptions/UnauthorizedError.ts b/apps/backend-template/src/infra/exceptions/UnauthorizedError.ts similarity index 100% rename from src/infra/exceptions/UnauthorizedError.ts rename to apps/backend-template/src/infra/exceptions/UnauthorizedError.ts diff --git a/src/infra/exceptions/ValidationError.ts b/apps/backend-template/src/infra/exceptions/ValidationError.ts similarity index 100% rename from src/infra/exceptions/ValidationError.ts rename to apps/backend-template/src/infra/exceptions/ValidationError.ts diff --git a/src/infra/exceptions/error.codes.ts b/apps/backend-template/src/infra/exceptions/error.codes.ts similarity index 100% rename from src/infra/exceptions/error.codes.ts rename to apps/backend-template/src/infra/exceptions/error.codes.ts diff --git a/src/infra/exceptions/index.ts b/apps/backend-template/src/infra/exceptions/index.ts similarity index 100% rename from src/infra/exceptions/index.ts rename to apps/backend-template/src/infra/exceptions/index.ts diff --git a/src/infra/jwt/IJwtService.ts b/apps/backend-template/src/infra/jwt/IJwtService.ts similarity index 100% rename from src/infra/jwt/IJwtService.ts rename to apps/backend-template/src/infra/jwt/IJwtService.ts diff --git a/src/infra/jwt/JwtService.ts b/apps/backend-template/src/infra/jwt/JwtService.ts similarity index 100% rename from src/infra/jwt/JwtService.ts rename to apps/backend-template/src/infra/jwt/JwtService.ts diff --git a/src/infra/messages/InMemoryMessageMediator.ts b/apps/backend-template/src/infra/messages/InMemoryMessageMediator.ts similarity index 100% rename from src/infra/messages/InMemoryMessageMediator.ts rename to apps/backend-template/src/infra/messages/InMemoryMessageMediator.ts diff --git a/apps/backend-template/src/infra/messages/adapters/BullMqMessageMediatorAdapter.ts b/apps/backend-template/src/infra/messages/adapters/BullMqMessageMediatorAdapter.ts new file mode 100644 index 00000000..e308d487 --- /dev/null +++ b/apps/backend-template/src/infra/messages/adapters/BullMqMessageMediatorAdapter.ts @@ -0,0 +1 @@ +export { BullMqMessageMediatorAdapter } from '@jumentix/message-mediator'; diff --git a/apps/backend-template/src/infra/messages/adapters/InMemoryMessageMediatorAdapter.ts b/apps/backend-template/src/infra/messages/adapters/InMemoryMessageMediatorAdapter.ts new file mode 100644 index 00000000..dfa2f823 --- /dev/null +++ b/apps/backend-template/src/infra/messages/adapters/InMemoryMessageMediatorAdapter.ts @@ -0,0 +1 @@ +export { InMemoryMessageMediatorAdapter } from '@jumentix/message-mediator'; diff --git a/apps/backend-template/src/infra/messages/adapters/RabbitMqMessageMediatorAdapter.ts b/apps/backend-template/src/infra/messages/adapters/RabbitMqMessageMediatorAdapter.ts new file mode 100644 index 00000000..a51aa986 --- /dev/null +++ b/apps/backend-template/src/infra/messages/adapters/RabbitMqMessageMediatorAdapter.ts @@ -0,0 +1 @@ +export { RabbitMqMessageMediatorAdapter } from '@jumentix/message-mediator'; diff --git a/apps/backend-template/src/infra/messages/compileMessageMediator.ts b/apps/backend-template/src/infra/messages/compileMessageMediator.ts new file mode 100644 index 00000000..07500e14 --- /dev/null +++ b/apps/backend-template/src/infra/messages/compileMessageMediator.ts @@ -0,0 +1 @@ +export { compileMessageMediator } from '@jumentix/message-mediator'; diff --git a/src/infra/messages/repositories/QueueRequestResponseRepository.ts b/apps/backend-template/src/infra/messages/repositories/QueueRequestResponseRepository.ts similarity index 100% rename from src/infra/messages/repositories/QueueRequestResponseRepository.ts rename to apps/backend-template/src/infra/messages/repositories/QueueRequestResponseRepository.ts diff --git a/src/infra/messages/repositories/index.ts b/apps/backend-template/src/infra/messages/repositories/index.ts similarity index 100% rename from src/infra/messages/repositories/index.ts rename to apps/backend-template/src/infra/messages/repositories/index.ts diff --git a/apps/backend-template/src/infra/mutex/adapter/MutexService.ts b/apps/backend-template/src/infra/mutex/adapter/MutexService.ts new file mode 100644 index 00000000..319797f7 --- /dev/null +++ b/apps/backend-template/src/infra/mutex/adapter/MutexService.ts @@ -0,0 +1 @@ +export { MutexService } from '@jumentix/mutex-service'; diff --git a/apps/backend-template/src/infra/mutex/port/IMutexService.ts b/apps/backend-template/src/infra/mutex/port/IMutexService.ts new file mode 100644 index 00000000..5017c8a0 --- /dev/null +++ b/apps/backend-template/src/infra/mutex/port/IMutexService.ts @@ -0,0 +1 @@ +export { IMutexService } from '@jumentix/mutex-service'; diff --git a/src/infra/persistence/InMemoryDatabase/InMemoryDbClient.ts b/apps/backend-template/src/infra/persistence/InMemoryDatabase/InMemoryDbClient.ts similarity index 100% rename from src/infra/persistence/InMemoryDatabase/InMemoryDbClient.ts rename to apps/backend-template/src/infra/persistence/InMemoryDatabase/InMemoryDbClient.ts diff --git a/src/infra/persistence/InMemoryDatabase/Stores/InMemoryRelationalStore.ts b/apps/backend-template/src/infra/persistence/InMemoryDatabase/Stores/InMemoryRelationalStore.ts similarity index 100% rename from src/infra/persistence/InMemoryDatabase/Stores/InMemoryRelationalStore.ts rename to apps/backend-template/src/infra/persistence/InMemoryDatabase/Stores/InMemoryRelationalStore.ts diff --git a/src/infra/persistence/InMemoryDatabase/Stores/OrganizationStoreAPI.ts b/apps/backend-template/src/infra/persistence/InMemoryDatabase/Stores/OrganizationStoreAPI.ts similarity index 100% rename from src/infra/persistence/InMemoryDatabase/Stores/OrganizationStoreAPI.ts rename to apps/backend-template/src/infra/persistence/InMemoryDatabase/Stores/OrganizationStoreAPI.ts diff --git a/src/infra/persistence/InMemoryDatabase/Stores/UserStoreAPI.ts b/apps/backend-template/src/infra/persistence/InMemoryDatabase/Stores/UserStoreAPI.ts similarity index 100% rename from src/infra/persistence/InMemoryDatabase/Stores/UserStoreAPI.ts rename to apps/backend-template/src/infra/persistence/InMemoryDatabase/Stores/UserStoreAPI.ts diff --git a/apps/backend-template/src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.ts b/apps/backend-template/src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.ts new file mode 100644 index 00000000..3c4a6d4d --- /dev/null +++ b/apps/backend-template/src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.ts @@ -0,0 +1 @@ +export { BaseKeyValueStorageClient } from '@jumentix/key-value-storage'; diff --git a/apps/backend-template/src/infra/persistence/KeyValueStorage/IKeyValueStorageClient.ts b/apps/backend-template/src/infra/persistence/KeyValueStorage/IKeyValueStorageClient.ts new file mode 100644 index 00000000..d6528d88 --- /dev/null +++ b/apps/backend-template/src/infra/persistence/KeyValueStorage/IKeyValueStorageClient.ts @@ -0,0 +1 @@ +export { IKeyValueStorageClient } from '@jumentix/key-value-storage'; diff --git a/apps/backend-template/src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.ts b/apps/backend-template/src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.ts new file mode 100644 index 00000000..93a2f3e1 --- /dev/null +++ b/apps/backend-template/src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.ts @@ -0,0 +1 @@ +export { InMemoryKeyValueStorageClient } from '@jumentix/key-value-storage'; diff --git a/apps/backend-template/src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient.ts b/apps/backend-template/src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient.ts new file mode 100644 index 00000000..9ad1d889 --- /dev/null +++ b/apps/backend-template/src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient.ts @@ -0,0 +1 @@ +export { RedisKeyValueStorageClient } from '@jumentix/key-value-storage'; diff --git a/apps/backend-template/src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.ts b/apps/backend-template/src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.ts new file mode 100644 index 00000000..5826c20a --- /dev/null +++ b/apps/backend-template/src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.ts @@ -0,0 +1 @@ +export { compileKeyValueStorageClient } from '@jumentix/key-value-storage'; diff --git a/apps/backend-template/src/infra/persistence/compileDatabaseClient.ts b/apps/backend-template/src/infra/persistence/compileDatabaseClient.ts new file mode 100644 index 00000000..9134c00e --- /dev/null +++ b/apps/backend-template/src/infra/persistence/compileDatabaseClient.ts @@ -0,0 +1,23 @@ +import { buildDatabaseClientCompilers } from '@jumentix/database-client-factory'; +import { IDatabaseClient } from '@src/infra/persistence/port/IDatabaseClient'; +import { InMemoryDbClient } from '@src/infra/persistence/InMemoryDatabase/InMemoryDbClient'; + +const compilers = buildDatabaseClientCompilers({ + inMemoryClient: InMemoryDbClient +}); + +export const { + compileDatabaseClient, + compileDatabaseClientByDriver, + compileMongoDbClient, + compilePostgreSqlDbClient, + compileMySqlDbClient, + compileMsSqlDbClient, + compileOracleDbClient, + compileSqliteDbClient, + compileDynamoDbClient, + compileCassandraDbClient, + compileFirebaseDbClient, + compileAuroraDbClient, + compileRdsDbClient +} = compilers; diff --git a/apps/backend-template/src/infra/persistence/external/AuroraRepository.ts b/apps/backend-template/src/infra/persistence/external/AuroraRepository.ts new file mode 100644 index 00000000..984abce3 --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/AuroraRepository.ts @@ -0,0 +1 @@ +export { AuroraRepository } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/BaseExternalDataRepository.ts b/apps/backend-template/src/infra/persistence/external/BaseExternalDataRepository.ts new file mode 100644 index 00000000..77211c84 --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/BaseExternalDataRepository.ts @@ -0,0 +1,4 @@ +export { + BaseExternalDataRepository, + IRepositoryConnectionOptions +} from '@jumentix/external-persistence-core'; diff --git a/apps/backend-template/src/infra/persistence/external/CassandraRepository.ts b/apps/backend-template/src/infra/persistence/external/CassandraRepository.ts new file mode 100644 index 00000000..2ee96804 --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/CassandraRepository.ts @@ -0,0 +1 @@ +export { CassandraRepository } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/DynamoDbRepository.ts b/apps/backend-template/src/infra/persistence/external/DynamoDbRepository.ts new file mode 100644 index 00000000..2c1cc30a --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/DynamoDbRepository.ts @@ -0,0 +1 @@ +export { DynamoDbRepository } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/ExternalStoreProxy.ts b/apps/backend-template/src/infra/persistence/external/ExternalStoreProxy.ts new file mode 100644 index 00000000..1359b86c --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/ExternalStoreProxy.ts @@ -0,0 +1,2 @@ +/* istanbul ignore file */ +export { ExternalStoreProxy, createExternalStores } from '@jumentix/external-store-proxy'; diff --git a/apps/backend-template/src/infra/persistence/external/FirebaseRepository.ts b/apps/backend-template/src/infra/persistence/external/FirebaseRepository.ts new file mode 100644 index 00000000..4de8d195 --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/FirebaseRepository.ts @@ -0,0 +1 @@ +export { FirebaseRepository } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/MongoMongooseRepository.ts b/apps/backend-template/src/infra/persistence/external/MongoMongooseRepository.ts new file mode 100644 index 00000000..3bc31d2e --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/MongoMongooseRepository.ts @@ -0,0 +1 @@ +export { MongoMongooseRepository } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/OracleRepository.ts b/apps/backend-template/src/infra/persistence/external/OracleRepository.ts new file mode 100644 index 00000000..4ac6ccbe --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/OracleRepository.ts @@ -0,0 +1 @@ +export { OracleRepository } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/RdsRepository.ts b/apps/backend-template/src/infra/persistence/external/RdsRepository.ts new file mode 100644 index 00000000..ca54723a --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/RdsRepository.ts @@ -0,0 +1 @@ +export { RdsRepository } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/SqlSequelizeRepository.ts b/apps/backend-template/src/infra/persistence/external/SqlSequelizeRepository.ts new file mode 100644 index 00000000..d50d5d1f --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/SqlSequelizeRepository.ts @@ -0,0 +1 @@ +export { SqlSequelizeRepository, ESqlDialect, ISqlSequelizeRepositoryOptions } from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/external/index.ts b/apps/backend-template/src/infra/persistence/external/index.ts new file mode 100644 index 00000000..79eaa1cf --- /dev/null +++ b/apps/backend-template/src/infra/persistence/external/index.ts @@ -0,0 +1,3 @@ +export * from '@jumentix/external-persistence-core'; +export * from '@jumentix/external-store-proxy'; +export * from '@jumentix/external-db-repositories'; diff --git a/apps/backend-template/src/infra/persistence/port/IDatabaseClient.ts b/apps/backend-template/src/infra/persistence/port/IDatabaseClient.ts new file mode 100644 index 00000000..cd381b4b --- /dev/null +++ b/apps/backend-template/src/infra/persistence/port/IDatabaseClient.ts @@ -0,0 +1,11 @@ +import { IDatabaseClient as IGenericDatabaseClient, IStore } from '@jumentix/persistence-contracts'; +import { IUser } from '@src/modules/Users/domain/Entity/IUser'; +import { IOrganization } from '@src/modules/Users/domain/Entity/IOrganization'; + +export interface IDbStores { + User: IStore; + Organization: IStore; + [key: string]: IStore; +} + +export interface IDatabaseClient extends IGenericDatabaseClient {} diff --git a/apps/backend-template/src/infra/ports/persistence/IStore.ts b/apps/backend-template/src/infra/ports/persistence/IStore.ts new file mode 100644 index 00000000..6e9084df --- /dev/null +++ b/apps/backend-template/src/infra/ports/persistence/IStore.ts @@ -0,0 +1 @@ +export * from '@jumentix/persistence-contracts'; diff --git a/src/infra/security/IPasswordCryptoService.ts b/apps/backend-template/src/infra/security/IPasswordCryptoService.ts similarity index 100% rename from src/infra/security/IPasswordCryptoService.ts rename to apps/backend-template/src/infra/security/IPasswordCryptoService.ts diff --git a/src/infra/security/PasswordCryptoService.ts b/apps/backend-template/src/infra/security/PasswordCryptoService.ts similarity index 100% rename from src/infra/security/PasswordCryptoService.ts rename to apps/backend-template/src/infra/security/PasswordCryptoService.ts diff --git a/src/infra/security/index.ts b/apps/backend-template/src/infra/security/index.ts similarity index 100% rename from src/infra/security/index.ts rename to apps/backend-template/src/infra/security/index.ts diff --git a/src/interface/Async/RealtimeAPIBase.ts b/apps/backend-template/src/interface/Async/RealtimeAPIBase.ts similarity index 100% rename from src/interface/Async/RealtimeAPIBase.ts rename to apps/backend-template/src/interface/Async/RealtimeAPIBase.ts diff --git a/src/interface/Async/RealtimeDomainEvent.ts b/apps/backend-template/src/interface/Async/RealtimeDomainEvent.ts similarity index 100% rename from src/interface/Async/RealtimeDomainEvent.ts rename to apps/backend-template/src/interface/Async/RealtimeDomainEvent.ts diff --git a/src/interface/CLI/core/catalogStorage.ts b/apps/backend-template/src/interface/CLI/core/catalogStorage.ts similarity index 100% rename from src/interface/CLI/core/catalogStorage.ts rename to apps/backend-template/src/interface/CLI/core/catalogStorage.ts diff --git a/src/interface/CLI/core/prompt.ts b/apps/backend-template/src/interface/CLI/core/prompt.ts similarity index 100% rename from src/interface/CLI/core/prompt.ts rename to apps/backend-template/src/interface/CLI/core/prompt.ts diff --git a/src/interface/CLI/index.ts b/apps/backend-template/src/interface/CLI/index.ts similarity index 100% rename from src/interface/CLI/index.ts rename to apps/backend-template/src/interface/CLI/index.ts diff --git a/src/interface/CLI/subapps/domainManager.ts b/apps/backend-template/src/interface/CLI/subapps/domainManager.ts similarity index 100% rename from src/interface/CLI/subapps/domainManager.ts rename to apps/backend-template/src/interface/CLI/subapps/domainManager.ts diff --git a/src/interface/CLI/subapps/entityModelManager.ts b/apps/backend-template/src/interface/CLI/subapps/entityModelManager.ts similarity index 100% rename from src/interface/CLI/subapps/entityModelManager.ts rename to apps/backend-template/src/interface/CLI/subapps/entityModelManager.ts diff --git a/src/interface/CLI/types.ts b/apps/backend-template/src/interface/CLI/types.ts similarity index 100% rename from src/interface/CLI/types.ts rename to apps/backend-template/src/interface/CLI/types.ts diff --git a/src/interface/HTTP/RestAPI.ts b/apps/backend-template/src/interface/HTTP/RestAPI.ts similarity index 100% rename from src/interface/HTTP/RestAPI.ts rename to apps/backend-template/src/interface/HTTP/RestAPI.ts diff --git a/src/interface/HTTP/adapters/adonis-js/AdonisJsServer.ts b/apps/backend-template/src/interface/HTTP/adapters/adonis-js/AdonisJsServer.ts similarity index 97% rename from src/interface/HTTP/adapters/adonis-js/AdonisJsServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/adonis-js/AdonisJsServer.ts index b7886bfe..a322b8c3 100644 --- a/src/interface/HTTP/adapters/adonis-js/AdonisJsServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/adonis-js/AdonisJsServer.ts @@ -137,8 +137,8 @@ class AdonisJsServer extends HTTPBaseServer { }); }; - register('/OASdoc', 'OASdoc'); - register('/AsyncAPIdoc', 'AsyncAPIdoc'); + register('/OASdoc', 'apps/backend-template/OASdoc'); + register('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc'); this.router.on('GET', '/docs/asyncapi', async (_request: any, response: any) => { response.statusCode = 302; response.setHeader('location', '/AsyncAPIdoc'); diff --git a/src/interface/HTTP/adapters/adonis-js/adonis-js.ts b/apps/backend-template/src/interface/HTTP/adapters/adonis-js/adonis-js.ts similarity index 74% rename from src/interface/HTTP/adapters/adonis-js/adonis-js.ts rename to apps/backend-template/src/interface/HTTP/adapters/adonis-js/adonis-js.ts index d3e52994..a73b0506 100644 --- a/src/interface/HTTP/adapters/adonis-js/adonis-js.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/adonis-js/adonis-js.ts @@ -14,24 +14,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.adonis_js; const webServer = AdonisJsServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/adonis-js/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/adonis-js/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/adonis-js/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/adonis-js/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/adonis-js/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/adonis-js/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/adonis-js/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/adonis-js/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/aws/lambda/handlers/localhost.ts b/apps/backend-template/src/interface/HTTP/adapters/aws/lambda/handlers/localhost.ts similarity index 100% rename from src/interface/HTTP/adapters/aws/lambda/handlers/localhost.ts rename to apps/backend-template/src/interface/HTTP/adapters/aws/lambda/handlers/localhost.ts diff --git a/src/interface/HTTP/adapters/aws/lambda/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/aws/lambda/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/aws/lambda/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/aws/lambda/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts b/apps/backend-template/src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts similarity index 92% rename from src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts rename to apps/backend-template/src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts index a37ab438..8dc775f7 100644 --- a/src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts @@ -20,6 +20,7 @@ import { JwtService } from '@src/infra/jwt/JwtService'; import { MutexService } from '@src/infra/mutex/adapter/MutexService'; import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService'; import { composeUsersAuthServices } from '@src/modules/Users'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; type HonoRouteMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'head'; @@ -63,8 +64,8 @@ class CloudflareWorkersServer extends HTTPBaseServer { }); }); }; - register('/OASdoc', 'OASdoc'); - register('/AsyncAPIdoc', 'AsyncAPIdoc'); + register('/OASdoc', 'apps/backend-template/OASdoc'); + register('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc'); this.application.get('/docs/asyncapi', async (c: Context) => { return c.redirect('/AsyncAPIdoc', 302); }); @@ -192,20 +193,21 @@ class CloudflareWorkersServer extends HTTPBaseServer { const serverType = EHTTPFrameworks.cloudflare_workers; const webServer = CloudflareWorkersServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/cloudflare-workers/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/cloudflare-workers/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/cloudflare-workers/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/cloudflare-workers/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/derby-js/DerbyJsServer.ts b/apps/backend-template/src/interface/HTTP/adapters/derby-js/DerbyJsServer.ts similarity index 97% rename from src/interface/HTTP/adapters/derby-js/DerbyJsServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/derby-js/DerbyJsServer.ts index defae65c..f9acc068 100644 --- a/src/interface/HTTP/adapters/derby-js/DerbyJsServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/derby-js/DerbyJsServer.ts @@ -133,8 +133,8 @@ class DerbyJsServer extends HTTPBaseServer { }); }; - register('/OASdoc', 'OASdoc'); - register('/AsyncAPIdoc', 'AsyncAPIdoc'); + register('/OASdoc', 'apps/backend-template/OASdoc'); + register('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc'); this.router.on('GET', '/docs/asyncapi', async (_request: any, response: any) => { response.statusCode = 302; response.setHeader('location', '/AsyncAPIdoc'); diff --git a/src/interface/HTTP/adapters/derby-js/derby-js.ts b/apps/backend-template/src/interface/HTTP/adapters/derby-js/derby-js.ts similarity index 74% rename from src/interface/HTTP/adapters/derby-js/derby-js.ts rename to apps/backend-template/src/interface/HTTP/adapters/derby-js/derby-js.ts index c24bbec7..fdac3267 100644 --- a/src/interface/HTTP/adapters/derby-js/derby-js.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/derby-js/derby-js.ts @@ -14,24 +14,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.derby_js; const webServer = DerbyJsServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/derby-js/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/derby-js/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/derby-js/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/derby-js/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/derby-js/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/derby-js/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/derby-js/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/derby-js/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/express/ExpressServer.ts b/apps/backend-template/src/interface/HTTP/adapters/express/ExpressServer.ts similarity index 93% rename from src/interface/HTTP/adapters/express/ExpressServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/express/ExpressServer.ts index d2f26728..0000cce3 100644 --- a/src/interface/HTTP/adapters/express/ExpressServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/express/ExpressServer.ts @@ -50,8 +50,8 @@ class ExpressServer extends HTTPBaseServer { } private createDocEndPoint() { - this.application.use('/OASdoc', express.static('OASdoc')); - this.application.use('/AsyncAPIdoc', express.static('AsyncAPIdoc')); + this.application.use('/OASdoc', express.static('apps/backend-template/OASdoc')); + this.application.use('/AsyncAPIdoc', express.static('apps/backend-template/AsyncAPIdoc')); this.application.get('/docs/asyncapi', (_, res) => { res.redirect('/AsyncAPIdoc'); }); diff --git a/src/interface/HTTP/adapters/express/express.ts b/apps/backend-template/src/interface/HTTP/adapters/express/express.ts similarity index 74% rename from src/interface/HTTP/adapters/express/express.ts rename to apps/backend-template/src/interface/HTTP/adapters/express/express.ts index f09c7ab4..2340a61f 100644 --- a/src/interface/HTTP/adapters/express/express.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/express/express.ts @@ -15,25 +15,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.express; const webServer = ExpressServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); - -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/express/handlers/apiDocGetHandlerFactory.ts b/apps/backend-template/src/interface/HTTP/adapters/express/handlers/apiDocGetHandlerFactory.ts similarity index 100% rename from src/interface/HTTP/adapters/express/handlers/apiDocGetHandlerFactory.ts rename to apps/backend-template/src/interface/HTTP/adapters/express/handlers/apiDocGetHandlerFactory.ts diff --git a/src/interface/HTTP/adapters/express/handlers/apiversions.get.ts b/apps/backend-template/src/interface/HTTP/adapters/express/handlers/apiversions.get.ts similarity index 100% rename from src/interface/HTTP/adapters/express/handlers/apiversions.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/express/handlers/apiversions.get.ts diff --git a/src/interface/HTTP/adapters/express/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/express/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/express/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/express/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/express/handlers/localhost.get.ts b/apps/backend-template/src/interface/HTTP/adapters/express/handlers/localhost.get.ts similarity index 100% rename from src/interface/HTTP/adapters/express/handlers/localhost.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/express/handlers/localhost.get.ts diff --git a/src/interface/HTTP/adapters/express/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/express/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/express/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/express/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/fastify/FastifyServer.ts b/apps/backend-template/src/interface/HTTP/adapters/fastify/FastifyServer.ts similarity index 95% rename from src/interface/HTTP/adapters/fastify/FastifyServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/fastify/FastifyServer.ts index e4e93171..5b416ac1 100644 --- a/src/interface/HTTP/adapters/fastify/FastifyServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/fastify/FastifyServer.ts @@ -72,11 +72,11 @@ class FastifyServer extends HTTPBaseServer { private createDocEndPoint() { this.application.register(fastifyStatic, { - root: path.join(__dirname, '../../../../../../OASdoc'), + root: path.resolve(process.cwd(), 'apps/backend-template/OASdoc'), prefix: '/OASdoc/' }); this.application.register(fastifyStatic, { - root: path.join(__dirname, '../../../../../../AsyncAPIdoc'), + root: path.resolve(process.cwd(), 'apps/backend-template/AsyncAPIdoc'), prefix: '/AsyncAPIdoc/' }); this.application.get('/docs/asyncapi', async (_, reply) => { diff --git a/src/interface/HTTP/adapters/fastify/fastify.ts b/apps/backend-template/src/interface/HTTP/adapters/fastify/fastify.ts similarity index 74% rename from src/interface/HTTP/adapters/fastify/fastify.ts rename to apps/backend-template/src/interface/HTTP/adapters/fastify/fastify.ts index 64afb708..ad4eee12 100644 --- a/src/interface/HTTP/adapters/fastify/fastify.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/fastify/fastify.ts @@ -14,24 +14,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.fastify; const webServer = FastifyServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API: RestAPI = new RestAPI({ diff --git a/src/interface/HTTP/adapters/fastify/handlers/apiDocGetHandlerFactory.ts b/apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/apiDocGetHandlerFactory.ts similarity index 100% rename from src/interface/HTTP/adapters/fastify/handlers/apiDocGetHandlerFactory.ts rename to apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/apiDocGetHandlerFactory.ts diff --git a/src/interface/HTTP/adapters/fastify/handlers/apiversions.get.ts b/apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/apiversions.get.ts similarity index 100% rename from src/interface/HTTP/adapters/fastify/handlers/apiversions.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/apiversions.get.ts diff --git a/src/interface/HTTP/adapters/fastify/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/fastify/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/fastify/handlers/localhost.get.ts b/apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/localhost.get.ts similarity index 100% rename from src/interface/HTTP/adapters/fastify/handlers/localhost.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/fastify/handlers/localhost.get.ts diff --git a/src/interface/HTTP/adapters/fastify/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/fastify/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/fastify/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/fastify/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/feathers/FeathersServer.ts b/apps/backend-template/src/interface/HTTP/adapters/feathers/FeathersServer.ts similarity index 96% rename from src/interface/HTTP/adapters/feathers/FeathersServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/feathers/FeathersServer.ts index 7cfeaa0e..cabccb08 100644 --- a/src/interface/HTTP/adapters/feathers/FeathersServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/feathers/FeathersServer.ts @@ -63,8 +63,8 @@ class FeathersServer extends HTTPBaseServer { context.redirect('/AsyncAPIdoc'); return; } - if (readStatic('/OASdoc', 'OASdoc')) return; - if (readStatic('/AsyncAPIdoc', 'AsyncAPIdoc')) return; + if (readStatic('/OASdoc', 'apps/backend-template/OASdoc')) return; + if (readStatic('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc')) return; await next(); }); this.application.use(bodyParser()); diff --git a/src/interface/HTTP/adapters/feathers/feathers.ts b/apps/backend-template/src/interface/HTTP/adapters/feathers/feathers.ts similarity index 74% rename from src/interface/HTTP/adapters/feathers/feathers.ts rename to apps/backend-template/src/interface/HTTP/adapters/feathers/feathers.ts index 91b79770..c5aba953 100644 --- a/src/interface/HTTP/adapters/feathers/feathers.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/feathers/feathers.ts @@ -14,24 +14,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.feathers; const webServer = FeathersServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/feathers/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/feathers/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/feathers/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/feathers/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/feathers/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/feathers/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/feathers/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/feathers/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/hyper-express/HyperExpressServer.ts b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/HyperExpressServer.ts similarity index 95% rename from src/interface/HTTP/adapters/hyper-express/HyperExpressServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/hyper-express/HyperExpressServer.ts index 974f78b6..8c39e0eb 100644 --- a/src/interface/HTTP/adapters/hyper-express/HyperExpressServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/HyperExpressServer.ts @@ -9,14 +9,14 @@ import { IbaseHandler, HTTPBaseServer } from '@src/interface/HTTP/ports'; import { Context } from '@src/infra/context/Context'; import { v4 } from 'uuid'; -const LiveAssets = new LiveDirectory(path.join(__dirname, '../../../../../OASdoc'), { +const LiveAssets = new LiveDirectory(path.resolve(process.cwd(), 'apps/backend-template/OASdoc'), { static: true, cache: { max_file_count: 200, max_file_size: 1024 * 1024 * 2.5 } }); -const AsyncLiveAssets = new LiveDirectory(path.join(__dirname, '../../../../../AsyncAPIdoc'), { +const AsyncLiveAssets = new LiveDirectory(path.resolve(process.cwd(), 'apps/backend-template/AsyncAPIdoc'), { static: true, cache: { max_file_count: 200, diff --git a/src/interface/HTTP/adapters/hyper-express/handlers/apiDocGetHandlerFactory.ts b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/apiDocGetHandlerFactory.ts similarity index 100% rename from src/interface/HTTP/adapters/hyper-express/handlers/apiDocGetHandlerFactory.ts rename to apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/apiDocGetHandlerFactory.ts diff --git a/src/interface/HTTP/adapters/hyper-express/handlers/apiversions.get.ts b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/apiversions.get.ts similarity index 100% rename from src/interface/HTTP/adapters/hyper-express/handlers/apiversions.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/apiversions.get.ts diff --git a/src/interface/HTTP/adapters/hyper-express/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/hyper-express/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/hyper-express/handlers/localhost.get.ts b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/localhost.get.ts similarity index 100% rename from src/interface/HTTP/adapters/hyper-express/handlers/localhost.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/hyper-express/handlers/localhost.get.ts diff --git a/src/interface/HTTP/adapters/hyper-express/hyper-express.ts b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/hyper-express.ts similarity index 75% rename from src/interface/HTTP/adapters/hyper-express/hyper-express.ts rename to apps/backend-template/src/interface/HTTP/adapters/hyper-express/hyper-express.ts index 5f0919bf..bf483225 100644 --- a/src/interface/HTTP/adapters/hyper-express/hyper-express.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/hyper-express.ts @@ -16,24 +16,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.hyper_express; const webServer = HyperExpressServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/hyper-express/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/hyper-express/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/hyper-express/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/hyper-express/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/loopback/LoopBackServer.ts b/apps/backend-template/src/interface/HTTP/adapters/loopback/LoopBackServer.ts similarity index 94% rename from src/interface/HTTP/adapters/loopback/LoopBackServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/loopback/LoopBackServer.ts index 77f96cf9..1ae214d9 100644 --- a/src/interface/HTTP/adapters/loopback/LoopBackServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/loopback/LoopBackServer.ts @@ -30,8 +30,8 @@ class LoopBackServer extends HTTPBaseServer { rest: { port: _HTTP_PORT_ } }); if (this.application.static) { - this.application.static('/OASdoc', 'OASdoc'); - this.application.static('/AsyncAPIdoc', 'AsyncAPIdoc'); + this.application.static('/OASdoc', 'apps/backend-template/OASdoc'); + this.application.static('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc'); } if (this.application.get) { this.application.get('/docs/asyncapi', (_req: any, res: any) => { diff --git a/src/interface/HTTP/adapters/loopback/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/loopback/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/loopback/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/loopback/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/loopback/loopback.ts b/apps/backend-template/src/interface/HTTP/adapters/loopback/loopback.ts similarity index 74% rename from src/interface/HTTP/adapters/loopback/loopback.ts rename to apps/backend-template/src/interface/HTTP/adapters/loopback/loopback.ts index 499ace82..6b87e34b 100644 --- a/src/interface/HTTP/adapters/loopback/loopback.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/loopback/loopback.ts @@ -14,24 +14,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.loopback; const webServer = LoopBackServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/loopback/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/loopback/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/loopback/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/loopback/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/restify/RestifyServer.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/RestifyServer.ts similarity index 95% rename from src/interface/HTTP/adapters/restify/RestifyServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/RestifyServer.ts index 97a92256..0432822a 100644 --- a/src/interface/HTTP/adapters/restify/RestifyServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/restify/RestifyServer.ts @@ -72,11 +72,11 @@ class RestifyServer extends HTTPBaseServer { private createDocEndPoint() { this.application.get('/OASdoc/*', restify.plugins.serveStatic({ - directory: path.join(__dirname, '../../../../../../OASdoc'), + directory: path.resolve(process.cwd(), 'apps/backend-template/OASdoc'), default: 'index.html' })); this.application.get('/AsyncAPIdoc/*', restify.plugins.serveStatic({ - directory: path.join(__dirname, '../../../../../../AsyncAPIdoc'), + directory: path.resolve(process.cwd(), 'apps/backend-template/AsyncAPIdoc'), default: 'index.html' })); this.application.get('/docs/asyncapi', (_req, res, next) => { diff --git a/src/interface/HTTP/adapters/restify/handlers/apiDocGetHandlerFactory.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/apiDocGetHandlerFactory.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/apiDocGetHandlerFactory.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/apiDocGetHandlerFactory.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/apiversions.get.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/apiversions.get.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/apiversions.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/apiversions.get.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/auth/login.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/login.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/auth/login.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/login.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/auth/logout.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/logout.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/auth/logout.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/logout.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/auth/register.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/register.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/auth/register.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/register.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/auth/updatePassword.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/updatePassword.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/auth/updatePassword.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/auth/updatePassword.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/localhost.get.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/localhost.get.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/localhost.get.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/localhost.get.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/create.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/create.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/create.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/create.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/createDocument.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/createDocument.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/createDocument.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/createDocument.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/createEmail.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/createEmail.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/createEmail.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/createEmail.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/createPhone.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/createPhone.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/createPhone.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/createPhone.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/deleteDocument.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deleteDocument.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/deleteDocument.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deleteDocument.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/deleteEmail.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deleteEmail.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/deleteEmail.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deleteEmail.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/deleteOne.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deleteOne.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/deleteOne.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deleteOne.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/deletePhone.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deletePhone.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/deletePhone.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/deletePhone.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/getAll.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/getAll.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/getAll.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/getAll.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/getOneById.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/getOneById.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/getOneById.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/getOneById.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/update.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/update.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/update.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/update.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/updateDocument.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updateDocument.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/updateDocument.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updateDocument.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/updateEmail.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updateEmail.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/updateEmail.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updateEmail.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/updatePassword.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updatePassword.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/updatePassword.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updatePassword.ts diff --git a/src/interface/HTTP/adapters/restify/handlers/users/updatePhone.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updatePhone.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/handlers/users/updatePhone.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/handlers/users/updatePhone.ts diff --git a/src/interface/HTTP/adapters/restify/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/restify/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/restify/restify.ts b/apps/backend-template/src/interface/HTTP/adapters/restify/restify.ts similarity index 75% rename from src/interface/HTTP/adapters/restify/restify.ts rename to apps/backend-template/src/interface/HTTP/adapters/restify/restify.ts index 39f48f8d..de493fef 100644 --- a/src/interface/HTTP/adapters/restify/restify.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/restify/restify.ts @@ -15,26 +15,28 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports/EHTTPFrameworks'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; type Restify = restify.Server; const serverType = EHTTPFrameworks.restify; const webServer = RestifyServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/sails-js/SailsJsServer.ts b/apps/backend-template/src/interface/HTTP/adapters/sails-js/SailsJsServer.ts similarity index 96% rename from src/interface/HTTP/adapters/sails-js/SailsJsServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/sails-js/SailsJsServer.ts index 36498779..c0df63a9 100644 --- a/src/interface/HTTP/adapters/sails-js/SailsJsServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/sails-js/SailsJsServer.ts @@ -48,8 +48,8 @@ class SailsJsServer extends HTTPBaseServer { return res.send ? res.send(content) : content; }; }; - register('/OASdoc', 'OASdoc'); - register('/AsyncAPIdoc', 'AsyncAPIdoc'); + register('/OASdoc', 'apps/backend-template/OASdoc'); + register('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc'); this.routes['GET /docs/asyncapi'] = (_req: any, res: any) => { if (res.redirect) return res.redirect('/AsyncAPIdoc'); if (res.status && res.json) return res.status(302).json({ location: '/AsyncAPIdoc' }); diff --git a/src/interface/HTTP/adapters/sails-js/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/sails-js/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/sails-js/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/sails-js/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/sails-js/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/sails-js/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/sails-js/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/sails-js/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/sails-js/sails-js.ts b/apps/backend-template/src/interface/HTTP/adapters/sails-js/sails-js.ts similarity index 74% rename from src/interface/HTTP/adapters/sails-js/sails-js.ts rename to apps/backend-template/src/interface/HTTP/adapters/sails-js/sails-js.ts index 5966ee21..4fb847ea 100644 --- a/src/interface/HTTP/adapters/sails-js/sails-js.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/sails-js/sails-js.ts @@ -14,24 +14,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.sails_js; const webServer = SailsJsServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts b/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts new file mode 100644 index 00000000..6d5b5815 --- /dev/null +++ b/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts @@ -0,0 +1,59 @@ +/* istanbul ignore file */ +import { resolveHTTPFramework } from '@src/interface/runtime/RuntimeEnvironment'; + +export async function startRestApiAdapter(env: NodeJS.ProcessEnv = process.env): Promise { + const framework = resolveHTTPFramework(env); + if (framework === 'express') { + await import('@src/interface/HTTP/adapters/express/express'); + return; + } + if (framework === 'fastify') { + await import('@src/interface/HTTP/adapters/fastify/fastify'); + return; + } + if (framework === 'restify') { + await import('@src/interface/HTTP/adapters/restify/restify'); + return; + } + if (framework === 'hyper-express') { + await import('@src/interface/HTTP/adapters/hyper-express/hyper-express'); + return; + } + if (framework === 'cloudflare-workers') { + await import('@src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers'); + return; + } + if (framework === 'vercel-functions') { + await import('@src/interface/HTTP/adapters/vercel-functions/vercel-functions'); + return; + } + if (framework === 'loopback') { + await import('@src/interface/HTTP/adapters/loopback/loopback'); + return; + } + if (framework === 'sails-js') { + await import('@src/interface/HTTP/adapters/sails-js/sails-js'); + return; + } + if (framework === 'feathers') { + await import('@src/interface/HTTP/adapters/feathers/feathers'); + return; + } + if (framework === 'derby-js') { + await import('@src/interface/HTTP/adapters/derby-js/derby-js'); + return; + } + if (framework === 'adonis-js') { + await import('@src/interface/HTTP/adapters/adonis-js/adonis-js'); + return; + } + if (framework === 'total-js') { + await import('@src/interface/HTTP/adapters/total-js/total-js'); + } +} + +// eslint-disable-next-line jest/require-hook +/* istanbul ignore if */ +if (require.main === module) { + startRestApiAdapter(); +} diff --git a/src/interface/HTTP/adapters/total-js/TotalJsServer.ts b/apps/backend-template/src/interface/HTTP/adapters/total-js/TotalJsServer.ts similarity index 97% rename from src/interface/HTTP/adapters/total-js/TotalJsServer.ts rename to apps/backend-template/src/interface/HTTP/adapters/total-js/TotalJsServer.ts index 13ce3ec3..ea8d2a0f 100644 --- a/src/interface/HTTP/adapters/total-js/TotalJsServer.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/total-js/TotalJsServer.ts @@ -137,8 +137,8 @@ class TotalJsServer extends HTTPBaseServer { }); }; - register('/OASdoc', 'OASdoc'); - register('/AsyncAPIdoc', 'AsyncAPIdoc'); + register('/OASdoc', 'apps/backend-template/OASdoc'); + register('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc'); this.router.on('GET', '/docs/asyncapi', async (_request: any, response: any) => { response.statusCode = 302; response.setHeader('location', '/AsyncAPIdoc'); diff --git a/src/interface/HTTP/adapters/total-js/handlers/infraHandlers.ts b/apps/backend-template/src/interface/HTTP/adapters/total-js/handlers/infraHandlers.ts similarity index 100% rename from src/interface/HTTP/adapters/total-js/handlers/infraHandlers.ts rename to apps/backend-template/src/interface/HTTP/adapters/total-js/handlers/infraHandlers.ts diff --git a/src/interface/HTTP/adapters/total-js/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/total-js/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/total-js/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/total-js/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/total-js/total-js.ts b/apps/backend-template/src/interface/HTTP/adapters/total-js/total-js.ts similarity index 74% rename from src/interface/HTTP/adapters/total-js/total-js.ts rename to apps/backend-template/src/interface/HTTP/adapters/total-js/total-js.ts index 0e79a2a8..8ae8d436 100644 --- a/src/interface/HTTP/adapters/total-js/total-js.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/total-js/total-js.ts @@ -14,24 +14,26 @@ import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService import { compileMessageMediator } from '@src/infra/messages/compileMessageMediator'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; const serverType = EHTTPFrameworks.total_js; const webServer = TotalJsServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI({ diff --git a/src/interface/HTTP/adapters/vercel-functions/responses/sendErrorResponse.ts b/apps/backend-template/src/interface/HTTP/adapters/vercel-functions/responses/sendErrorResponse.ts similarity index 100% rename from src/interface/HTTP/adapters/vercel-functions/responses/sendErrorResponse.ts rename to apps/backend-template/src/interface/HTTP/adapters/vercel-functions/responses/sendErrorResponse.ts diff --git a/src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts b/apps/backend-template/src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts similarity index 92% rename from src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts rename to apps/backend-template/src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts index 6781fa41..4e5fa754 100644 --- a/src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts +++ b/apps/backend-template/src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts @@ -18,6 +18,7 @@ import { JwtService } from '@src/infra/jwt/JwtService'; import { MutexService } from '@src/infra/mutex/adapter/MutexService'; import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService'; import { composeUsersAuthServices } from '@src/modules/Users'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; type RouteMatch = { matched: boolean; @@ -75,7 +76,7 @@ class VercelFunctionsServer extends HTTPBaseServer> { const relative = pathname.replace(`${prefix}/`, '') || 'index.html'; return path.join(rootDir, folder, relative); }; - const absolutePath = resolve('/OASdoc', 'OASdoc') || resolve('/AsyncAPIdoc', 'AsyncAPIdoc'); + const absolutePath = resolve('/OASdoc', 'apps/backend-template/OASdoc') || resolve('/AsyncAPIdoc', 'apps/backend-template/AsyncAPIdoc'); if (!absolutePath) return false; if (!fs.existsSync(absolutePath)) { res.status(404).json({ message: 'Not found' }); @@ -192,20 +193,21 @@ class VercelFunctionsServer extends HTTPBaseServer> { const serverType = EHTTPFrameworks.vercel_functions; const webServer = VercelFunctionsServer.compile(); -const passwordCryptoService = PasswordCryptoService.compile(); -const jwtService = JwtService.compile(); -const keyValueStorageClient = compileKeyValueStorageClient(process.env.AAA_KEYVALUESTORAGE_DRIVER); -const mutexService = MutexService.compile(keyValueStorageClient); -const messageMediator = compileMessageMediator(); -const databaseClient = compileDatabaseClient(); - -const { authService } = composeUsersAuthServices({ +const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService +} = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new RestAPI>({ diff --git a/src/interface/HTTP/ports/BaseController.ts b/apps/backend-template/src/interface/HTTP/ports/BaseController.ts similarity index 100% rename from src/interface/HTTP/ports/BaseController.ts rename to apps/backend-template/src/interface/HTTP/ports/BaseController.ts diff --git a/src/interface/HTTP/ports/EHTTPFrameworks.ts b/apps/backend-template/src/interface/HTTP/ports/EHTTPFrameworks.ts similarity index 100% rename from src/interface/HTTP/ports/EHTTPFrameworks.ts rename to apps/backend-template/src/interface/HTTP/ports/EHTTPFrameworks.ts diff --git a/src/interface/HTTP/ports/EndPointFactory.ts b/apps/backend-template/src/interface/HTTP/ports/EndPointFactory.ts similarity index 100% rename from src/interface/HTTP/ports/EndPointFactory.ts rename to apps/backend-template/src/interface/HTTP/ports/EndPointFactory.ts diff --git a/src/interface/HTTP/ports/HTTPBaseServer.ts b/apps/backend-template/src/interface/HTTP/ports/HTTPBaseServer.ts similarity index 100% rename from src/interface/HTTP/ports/HTTPBaseServer.ts rename to apps/backend-template/src/interface/HTTP/ports/HTTPBaseServer.ts diff --git a/src/interface/HTTP/ports/IAPIFactory.ts b/apps/backend-template/src/interface/HTTP/ports/IAPIFactory.ts similarity index 100% rename from src/interface/HTTP/ports/IAPIFactory.ts rename to apps/backend-template/src/interface/HTTP/ports/IAPIFactory.ts diff --git a/src/interface/HTTP/ports/IController.ts b/apps/backend-template/src/interface/HTTP/ports/IController.ts similarity index 100% rename from src/interface/HTTP/ports/IController.ts rename to apps/backend-template/src/interface/HTTP/ports/IController.ts diff --git a/src/interface/HTTP/ports/IControllerFactory.ts b/apps/backend-template/src/interface/HTTP/ports/IControllerFactory.ts similarity index 100% rename from src/interface/HTTP/ports/IControllerFactory.ts rename to apps/backend-template/src/interface/HTTP/ports/IControllerFactory.ts diff --git a/src/interface/HTTP/ports/IHTTPRequest.ts b/apps/backend-template/src/interface/HTTP/ports/IHTTPRequest.ts similarity index 100% rename from src/interface/HTTP/ports/IHTTPRequest.ts rename to apps/backend-template/src/interface/HTTP/ports/IHTTPRequest.ts diff --git a/src/interface/HTTP/ports/IHTTPResponse.ts b/apps/backend-template/src/interface/HTTP/ports/IHTTPResponse.ts similarity index 100% rename from src/interface/HTTP/ports/IHTTPResponse.ts rename to apps/backend-template/src/interface/HTTP/ports/IHTTPResponse.ts diff --git a/src/interface/HTTP/ports/IHTTPServer.ts b/apps/backend-template/src/interface/HTTP/ports/IHTTPServer.ts similarity index 100% rename from src/interface/HTTP/ports/IHTTPServer.ts rename to apps/backend-template/src/interface/HTTP/ports/IHTTPServer.ts diff --git a/src/interface/HTTP/ports/IHandlerFactory.ts b/apps/backend-template/src/interface/HTTP/ports/IHandlerFactory.ts similarity index 100% rename from src/interface/HTTP/ports/IHandlerFactory.ts rename to apps/backend-template/src/interface/HTTP/ports/IHandlerFactory.ts diff --git a/src/interface/HTTP/ports/IbaseHandler.ts b/apps/backend-template/src/interface/HTTP/ports/IbaseHandler.ts similarity index 100% rename from src/interface/HTTP/ports/IbaseHandler.ts rename to apps/backend-template/src/interface/HTTP/ports/IbaseHandler.ts diff --git a/src/interface/HTTP/ports/IbaseHandlerFactory.ts b/apps/backend-template/src/interface/HTTP/ports/IbaseHandlerFactory.ts similarity index 100% rename from src/interface/HTTP/ports/IbaseHandlerFactory.ts rename to apps/backend-template/src/interface/HTTP/ports/IbaseHandlerFactory.ts diff --git a/src/interface/HTTP/ports/index.ts b/apps/backend-template/src/interface/HTTP/ports/index.ts similarity index 100% rename from src/interface/HTTP/ports/index.ts rename to apps/backend-template/src/interface/HTTP/ports/index.ts diff --git a/src/interface/HTTP/validators/checkRequiredProperties.ts b/apps/backend-template/src/interface/HTTP/validators/checkRequiredProperties.ts similarity index 100% rename from src/interface/HTTP/validators/checkRequiredProperties.ts rename to apps/backend-template/src/interface/HTTP/validators/checkRequiredProperties.ts diff --git a/src/interface/HTTP/validators/getSchema.ts b/apps/backend-template/src/interface/HTTP/validators/getSchema.ts similarity index 100% rename from src/interface/HTTP/validators/getSchema.ts rename to apps/backend-template/src/interface/HTTP/validators/getSchema.ts diff --git a/src/interface/HTTP/validators/index.ts b/apps/backend-template/src/interface/HTTP/validators/index.ts similarity index 100% rename from src/interface/HTTP/validators/index.ts rename to apps/backend-template/src/interface/HTTP/validators/index.ts diff --git a/src/interface/HTTP/validators/isPropertiesMatching.ts b/apps/backend-template/src/interface/HTTP/validators/isPropertiesMatching.ts similarity index 100% rename from src/interface/HTTP/validators/isPropertiesMatching.ts rename to apps/backend-template/src/interface/HTTP/validators/isPropertiesMatching.ts diff --git a/src/interface/HTTP/validators/throwIfOASInputValidationFails.ts b/apps/backend-template/src/interface/HTTP/validators/throwIfOASInputValidationFails.ts similarity index 100% rename from src/interface/HTTP/validators/throwIfOASInputValidationFails.ts rename to apps/backend-template/src/interface/HTTP/validators/throwIfOASInputValidationFails.ts diff --git a/src/interface/HTTP/validators/validateRequestAgainstOAS.ts b/apps/backend-template/src/interface/HTTP/validators/validateRequestAgainstOAS.ts similarity index 100% rename from src/interface/HTTP/validators/validateRequestAgainstOAS.ts rename to apps/backend-template/src/interface/HTTP/validators/validateRequestAgainstOAS.ts diff --git a/src/interface/HTTP/validators/validateRequestParams.ts b/apps/backend-template/src/interface/HTTP/validators/validateRequestParams.ts similarity index 100% rename from src/interface/HTTP/validators/validateRequestParams.ts rename to apps/backend-template/src/interface/HTTP/validators/validateRequestParams.ts diff --git a/src/interface/WebSocket/WebSocketAPI.ts b/apps/backend-template/src/interface/WebSocket/WebSocketAPI.ts similarity index 89% rename from src/interface/WebSocket/WebSocketAPI.ts rename to apps/backend-template/src/interface/WebSocket/WebSocketAPI.ts index 7cb3e21d..f42cfe85 100644 --- a/src/interface/WebSocket/WebSocketAPI.ts +++ b/apps/backend-template/src/interface/WebSocket/WebSocketAPI.ts @@ -16,6 +16,8 @@ export interface IWebSocketAPIFactory extends IRealtimeAPIFactory { host?: string; port?: number; path?: string; + configureSocketIo?: (io: Server) => Promise | void; + cleanupSocketIo?: () => Promise | void; } export class WebSocketAPI extends RealtimeAPIBase { @@ -29,6 +31,10 @@ export class WebSocketAPI extends RealtimeAPIBase { private io?: Server; + private readonly configureSocketIo?: (io: Server) => Promise | void; + + private readonly cleanupSocketIo?: () => Promise | void; + constructor(config: IWebSocketAPIFactory) { super({ ...config, @@ -38,6 +44,8 @@ export class WebSocketAPI extends RealtimeAPIBase { this.host = config.host || '0.0.0.0'; this.port = config.port || Number(process.env.AAA_WEBSOCKET_PORT || (_HTTP_PORT_ + 1)); this.path = config.path || '/ws'; + this.configureSocketIo = config.configureSocketIo; + this.cleanupSocketIo = config.cleanupSocketIo; } private async handleOperationRequest( @@ -127,10 +135,14 @@ export class WebSocketAPI extends RealtimeAPIBase { this.httpServer = http.createServer(); this.io = new Server(this.httpServer, { path: this.path, + transports: ['websocket'], cors: { origin: '*' } }); + if (this.configureSocketIo) { + await this.configureSocketIo(this.io); + } this.io.on('connection', (socket) => { this.bindSocket(socket); @@ -153,6 +165,10 @@ export class WebSocketAPI extends RealtimeAPIBase { this.io = undefined; } + if (this.cleanupSocketIo) { + await this.cleanupSocketIo(); + } + if (this.httpServer) { await new Promise((resolve) => { this.httpServer!.close(() => resolve()); diff --git a/apps/backend-template/src/interface/WebSocket/adapters/socket-io/clusterAdapter.ts b/apps/backend-template/src/interface/WebSocket/adapters/socket-io/clusterAdapter.ts new file mode 100644 index 00000000..60236185 --- /dev/null +++ b/apps/backend-template/src/interface/WebSocket/adapters/socket-io/clusterAdapter.ts @@ -0,0 +1,35 @@ +import cluster from 'cluster'; +import os from 'os'; +import { createAdapter, setupPrimary } from '@socket.io/cluster-adapter'; +import { Server } from 'socket.io'; + +export interface IClusterSocketIoAdapter { + configure: (io: Server) => Promise; + cleanup: () => Promise; +} + +export const isClusterSocketIoEnabled = ( + env: NodeJS.ProcessEnv = process.env +): boolean => env.AAA_WEBSOCKET_SOCKETIO_ADAPTER === 'cluster'; + +export const resolveWebSocketClusterWorkers = ( + env: NodeJS.ProcessEnv = process.env +): number => { + const parsed = Number(env.AAA_WEBSOCKET_CLUSTER_WORKERS || ''); + if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed); + return Math.max(1, os.cpus().length); +}; + +export const setupSocketIoClusterPrimary = (): void => { + setupPrimary(); + cluster.setupPrimary({ + serialization: 'advanced' + }); +}; + +export const createClusterSocketIoAdapter = (): IClusterSocketIoAdapter => ({ + configure: async (io: Server): Promise => { + io.adapter(createAdapter()); + }, + cleanup: async (): Promise => Promise.resolve() +}); diff --git a/apps/backend-template/src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter.ts b/apps/backend-template/src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter.ts new file mode 100644 index 00000000..538bd208 --- /dev/null +++ b/apps/backend-template/src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter.ts @@ -0,0 +1,49 @@ +import { createClient } from 'redis'; +import { createAdapter } from '@socket.io/redis-streams-adapter'; +import { Server } from 'socket.io'; + +export interface IRedisStreamsSocketIoAdapter { + configure: (io: Server) => Promise; + cleanup: () => Promise; +} + +export const isRedisStreamsSocketIoEnabled = ( + env: NodeJS.ProcessEnv = process.env +): boolean => env.AAA_WEBSOCKET_SOCKETIO_ADAPTER === 'redis-streams'; + +export const buildRedisConnectionUrl = ( + env: NodeJS.ProcessEnv = process.env +): string => { + if (env.AAA_WEBSOCKET_REDIS_URL) return env.AAA_WEBSOCKET_REDIS_URL; + if (env.AAA_REDIS_URL) return env.AAA_REDIS_URL; + + const host = env.AAA_REDIS_HOST || '127.0.0.1'; + const port = env.AAA_REDIS_PORT || '6379'; + const db = env.AAA_REDIS_DATABASE || '0'; + const password = env.AAA_REDIS_PASSWORD; + const authPrefix = password ? `:${encodeURIComponent(password)}@` : ''; + + return `redis://${authPrefix}${host}:${port}/${db}`; +}; + +export const createRedisStreamsSocketIoAdapter = ( + env: NodeJS.ProcessEnv = process.env +): IRedisStreamsSocketIoAdapter => { + const client = createClient({ + url: buildRedisConnectionUrl(env) + }); + + return { + configure: async (io: Server): Promise => { + if (!client.isOpen) { + await client.connect(); + } + io.adapter(createAdapter(client)); + }, + cleanup: async (): Promise => { + if (client.isOpen) { + await client.quit(); + } + } + }; +}; diff --git a/src/interface/WebSocket/adapters/socket-io/socket-io.ts b/apps/backend-template/src/interface/WebSocket/adapters/socket-io/socket-io.ts similarity index 61% rename from src/interface/WebSocket/adapters/socket-io/socket-io.ts rename to apps/backend-template/src/interface/WebSocket/adapters/socket-io/socket-io.ts index 8b316a6e..5d91e2d1 100644 --- a/src/interface/WebSocket/adapters/socket-io/socket-io.ts +++ b/apps/backend-template/src/interface/WebSocket/adapters/socket-io/socket-io.ts @@ -10,30 +10,48 @@ import { ExpressServer } from '@src/interface/HTTP/adapters/express/ExpressServe import { infraHandlers } from '@src/interface/HTTP/adapters/express/handlers/infraHandlers'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { + createRedisStreamsSocketIoAdapter, + isRedisStreamsSocketIoEnabled +} from '@src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter'; +import { + createClusterSocketIoAdapter, + isClusterSocketIoEnabled +} from '@src/interface/WebSocket/adapters/socket-io/clusterAdapter'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; export function shouldStartFallbackRestApi(env: NodeJS.ProcessEnv = process.env): boolean { return env.AAA_DISABLE_FALLBACK_REST !== 'true'; } export async function startWebSocketAdapter(): Promise { - const passwordCryptoService = PasswordCryptoService.compile(); - const jwtService = JwtService.compile(); - const keyValueStorageClient = compileKeyValueStorageClient( - process.env.AAA_KEYVALUESTORAGE_DRIVER - ); - const mutexService = MutexService.compile(keyValueStorageClient); - const messageMediator = compileMessageMediator(); - const databaseClient = compileDatabaseClient(); - - const { authService } = composeUsersAuthServices({ + const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService + } = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); + let socketIoAdapter: + | ReturnType + | ReturnType + | undefined; + if (isClusterSocketIoEnabled()) { + socketIoAdapter = createClusterSocketIoAdapter(); + } else if (isRedisStreamsSocketIoEnabled()) { + socketIoAdapter = createRedisStreamsSocketIoAdapter(); + } + const API = new WebSocketAPI({ databaseClient, authService, @@ -41,7 +59,9 @@ export async function startWebSocketAdapter(): Promise { keyValueStorageClient, mutexService, eventBus: messageMediator, - messageMediator + messageMediator, + configureSocketIo: socketIoAdapter?.configure, + cleanupSocketIo: socketIoAdapter?.cleanup }); let fallbackRestAPI: RestAPI | undefined; @@ -68,6 +88,7 @@ export async function startWebSocketAdapter(): Promise { } // eslint-disable-next-line jest/require-hook +/* istanbul ignore if */ if (require.main === module) { startWebSocketAdapter(); } diff --git a/apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts b/apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts new file mode 100644 index 00000000..58bf5cb0 --- /dev/null +++ b/apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts @@ -0,0 +1,35 @@ +import { shouldStartRealtimeApi } from '@src/interface/runtime/RuntimeEnvironment'; +import { startWebSocketAdapter } from '@src/interface/WebSocket/adapters/socket-io/socket-io'; +import cluster from 'cluster'; +import { + isClusterSocketIoEnabled, + resolveWebSocketClusterWorkers, + setupSocketIoClusterPrimary +} from '@src/interface/WebSocket/adapters/socket-io/clusterAdapter'; + +export async function startWebSocketApiAdapter( + env: NodeJS.ProcessEnv = process.env +): Promise { + if (!shouldStartRealtimeApi('websocket', env)) return false; + if (isClusterSocketIoEnabled(env)) { + if (cluster.isPrimary) { + setupSocketIoClusterPrimary(); + const totalWorkers = resolveWebSocketClusterWorkers(env); + for (let i = 0; i < totalWorkers; i += 1) { + cluster.fork(); + } + cluster.on('exit', () => { + cluster.fork(); + }); + return true; + } + } + await startWebSocketAdapter(); + return true; +} + +// eslint-disable-next-line jest/require-hook +/* istanbul ignore if */ +if (require.main === module) { + startWebSocketApiAdapter(); +} diff --git a/src/interface/gRPC/adapters/grpc/grpc.ts b/apps/backend-template/src/interface/gRPC/adapters/grpc/grpc.ts similarity index 79% rename from src/interface/gRPC/adapters/grpc/grpc.ts rename to apps/backend-template/src/interface/gRPC/adapters/grpc/grpc.ts index c6328a67..ed47e500 100644 --- a/src/interface/gRPC/adapters/grpc/grpc.ts +++ b/apps/backend-template/src/interface/gRPC/adapters/grpc/grpc.ts @@ -10,28 +10,28 @@ import { ExpressServer } from '@src/interface/HTTP/adapters/express/ExpressServe import { infraHandlers } from '@src/interface/HTTP/adapters/express/handlers/infraHandlers'; import { EHTTPFrameworks } from '@src/interface/HTTP/ports'; import { RestAPI } from '@src/interface/HTTP/RestAPI'; +import { compileAdapterRuntime } from '@jumentix/adapter-runtime-bootstrap'; export function shouldStartFallbackRestApi(env: NodeJS.ProcessEnv = process.env): boolean { return env.AAA_DISABLE_FALLBACK_REST !== 'true'; } export async function startGrpcAdapter(): Promise { - const passwordCryptoService = PasswordCryptoService.compile(); - const jwtService = JwtService.compile(); - const keyValueStorageClient = compileKeyValueStorageClient( - process.env.AAA_KEYVALUESTORAGE_DRIVER - ); - const mutexService = MutexService.compile(keyValueStorageClient); - const messageMediator = compileMessageMediator(); - const databaseClient = compileDatabaseClient(); - - const { authService } = composeUsersAuthServices({ + const { databaseClient, - passwordCryptoService, - mutexService, - jwtService, keyValueStorageClient, - messageMediator + mutexService, + passwordCryptoService, + messageMediator, + authService + } = compileAdapterRuntime({ + compileDatabaseClient, + compileKeyValueStorageClient, + compileMutexService: (client) => MutexService.compile(client), + compilePasswordCryptoService: () => PasswordCryptoService.compile(), + compileJwtService: () => JwtService.compile(), + compileMessageMediator: () => compileMessageMediator(), + composeAuthServices: composeUsersAuthServices }); const API = new GrpcAPI({ @@ -68,6 +68,7 @@ export async function startGrpcAdapter(): Promise { } // eslint-disable-next-line jest/require-hook +/* istanbul ignore if */ if (require.main === module) { startGrpcAdapter(); } diff --git a/src/interface/gRPC/adapters/start-grpc-api.ts b/apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts similarity index 100% rename from src/interface/gRPC/adapters/start-grpc-api.ts rename to apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts diff --git a/src/interface/gRPC/gRPCAPI.ts b/apps/backend-template/src/interface/gRPC/gRPCAPI.ts similarity index 89% rename from src/interface/gRPC/gRPCAPI.ts rename to apps/backend-template/src/interface/gRPC/gRPCAPI.ts index e4da5533..1c31e844 100644 --- a/src/interface/gRPC/gRPCAPI.ts +++ b/apps/backend-template/src/interface/gRPC/gRPCAPI.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import path from 'path'; -import grpc from '@grpc/grpc-js'; -import protoLoader from '@grpc/proto-loader'; +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; import { _HTTP_PORT_ } from '@src/config/constants'; import { @@ -106,13 +106,15 @@ export class GrpcAPI extends RealtimeAPIBase { } private loadProtoService(): any { - const packageDefinition = protoLoader.loadSync(this.protoFilePath, { + const protoLoaderLib: any = (protoLoader as any).default || protoLoader; + const grpcLib: any = (grpc as any).default || grpc; + const packageDefinition = protoLoaderLib.loadSync(this.protoFilePath, { longs: String, enums: String, defaults: true, oneofs: true }); - const grpcObject = grpc.loadPackageDefinition(packageDefinition) as any; + const grpcObject = grpcLib.loadPackageDefinition(packageDefinition) as any; return grpcObject.realtime; } @@ -124,8 +126,9 @@ export class GrpcAPI extends RealtimeAPIBase { await this.databaseClient.connect(); const realtimePackage = this.loadProtoService(); - this.server = new grpc.Server(); - this.server.addService(realtimePackage.AsyncApiGateway.service, { + const grpcLib: any = (grpc as any).default || grpc; + this.server = new grpcLib.Server(); + this.server!.addService(realtimePackage.AsyncApiGateway.service, { request: async ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData @@ -153,7 +156,7 @@ export class GrpcAPI extends RealtimeAPIBase { await new Promise((resolve, reject) => { this.server!.bindAsync( `${this.host}:${this.port}`, - grpc.ServerCredentials.createInsecure(), + grpcLib.ServerCredentials.createInsecure(), (error) => { if (error) { reject(error); diff --git a/src/interface/gRPC/proto/async-api.proto b/apps/backend-template/src/interface/gRPC/proto/async-api.proto similarity index 100% rename from src/interface/gRPC/proto/async-api.proto rename to apps/backend-template/src/interface/gRPC/proto/async-api.proto diff --git a/src/interface/index.ts b/apps/backend-template/src/interface/index.ts similarity index 100% rename from src/interface/index.ts rename to apps/backend-template/src/interface/index.ts diff --git a/src/interface/runtime/RuntimeEnvironment.ts b/apps/backend-template/src/interface/runtime/RuntimeEnvironment.ts similarity index 67% rename from src/interface/runtime/RuntimeEnvironment.ts rename to apps/backend-template/src/interface/runtime/RuntimeEnvironment.ts index d350dc08..ec36dccc 100644 --- a/src/interface/runtime/RuntimeEnvironment.ts +++ b/apps/backend-template/src/interface/runtime/RuntimeEnvironment.ts @@ -1,5 +1,17 @@ /* istanbul ignore file */ -export type HTTPFramework = 'express'; +export type HTTPFramework = + | 'express' + | 'fastify' + | 'restify' + | 'hyper-express' + | 'cloudflare-workers' + | 'vercel-functions' + | 'loopback' + | 'sails-js' + | 'feathers' + | 'derby-js' + | 'adonis-js' + | 'total-js'; export type RealtimeApiProtocol = 'websocket' | 'grpc'; @@ -12,9 +24,22 @@ function normalize(value: string | undefined): string { export function resolveHTTPFramework(env: NodeJS.ProcessEnv = process.env): HTTPFramework { const framework = normalize(env.AAA_HTTP_FRAMEWORK) || DEFAULT_HTTP_FRAMEWORK; - if (framework === 'express') return framework; + if ( + framework === 'express' + || framework === 'fastify' + || framework === 'restify' + || framework === 'hyper-express' + || framework === 'cloudflare-workers' + || framework === 'vercel-functions' + || framework === 'loopback' + || framework === 'sails-js' + || framework === 'feathers' + || framework === 'derby-js' + || framework === 'adonis-js' + || framework === 'total-js' + ) return framework; throw new Error( - `Unsupported AAA_HTTP_FRAMEWORK "${env.AAA_HTTP_FRAMEWORK}". Supported: express.` + `Unsupported AAA_HTTP_FRAMEWORK "${env.AAA_HTTP_FRAMEWORK}".` ); } diff --git a/src/modules/Users/adapters/in/http/controllers/AuthController.ts b/apps/backend-template/src/modules/Users/adapters/in/http/controllers/AuthController.ts similarity index 100% rename from src/modules/Users/adapters/in/http/controllers/AuthController.ts rename to apps/backend-template/src/modules/Users/adapters/in/http/controllers/AuthController.ts diff --git a/src/modules/Users/adapters/in/http/controllers/OrganizationController.ts b/apps/backend-template/src/modules/Users/adapters/in/http/controllers/OrganizationController.ts similarity index 100% rename from src/modules/Users/adapters/in/http/controllers/OrganizationController.ts rename to apps/backend-template/src/modules/Users/adapters/in/http/controllers/OrganizationController.ts diff --git a/src/modules/Users/adapters/in/http/controllers/UserController.ts b/apps/backend-template/src/modules/Users/adapters/in/http/controllers/UserController.ts similarity index 100% rename from src/modules/Users/adapters/in/http/controllers/UserController.ts rename to apps/backend-template/src/modules/Users/adapters/in/http/controllers/UserController.ts diff --git a/src/modules/Users/adapters/in/http/controllers/index.ts b/apps/backend-template/src/modules/Users/adapters/in/http/controllers/index.ts similarity index 100% rename from src/modules/Users/adapters/in/http/controllers/index.ts rename to apps/backend-template/src/modules/Users/adapters/in/http/controllers/index.ts diff --git a/src/modules/Users/adapters/out/persistence/OrganizationDataRepository.ts b/apps/backend-template/src/modules/Users/adapters/out/persistence/OrganizationDataRepository.ts similarity index 95% rename from src/modules/Users/adapters/out/persistence/OrganizationDataRepository.ts rename to apps/backend-template/src/modules/Users/adapters/out/persistence/OrganizationDataRepository.ts index 937ade3e..83bea1f5 100644 --- a/src/modules/Users/adapters/out/persistence/OrganizationDataRepository.ts +++ b/apps/backend-template/src/modules/Users/adapters/out/persistence/OrganizationDataRepository.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import { IStore } from '@src/infra/ports/persistence/IStore'; import { throwIfPreUpdateValidationFails, @@ -70,11 +71,14 @@ export class OrganizationDataRepository const { result, page, size, total } = await this.store.getAll(filters, paging); + const currentPage = page ?? paging?.page ?? 1; + const currentSize = size ?? paging?.size ?? this.limit; + const rows = result ?? []; return { - page, - size, + page: currentPage, + size: currentSize, total, - result: result.map((raw: IOrganization) => new Organization(raw)) + result: rows.map((raw: IOrganization) => new Organization(raw)) }; } diff --git a/src/modules/Users/adapters/out/persistence/UserDataRepository.ts b/apps/backend-template/src/modules/Users/adapters/out/persistence/UserDataRepository.ts similarity index 96% rename from src/modules/Users/adapters/out/persistence/UserDataRepository.ts rename to apps/backend-template/src/modules/Users/adapters/out/persistence/UserDataRepository.ts index c0c02644..533ba4f2 100644 --- a/src/modules/Users/adapters/out/persistence/UserDataRepository.ts +++ b/apps/backend-template/src/modules/Users/adapters/out/persistence/UserDataRepository.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import { IStore } from '@src/infra/ports/persistence/IStore'; import { throwIfPreUpdateValidationFails, @@ -88,11 +89,14 @@ export class UserDataRepository const { result, page, size, total } = await this.store.getAll(filters, paging); + const currentPage = page ?? paging?.page ?? 1; + const currentSize = size ?? paging?.size ?? this.limit; + const rows = result ?? []; const pagedResponse: IPagingResponse = { - page, - size, + page: currentPage, + size: currentSize, total, - result: result.map((rawDoc: IUser) => new User(rawDoc)) + result: rows.map((rawDoc: IUser) => new User(rawDoc)) }; return pagedResponse; } diff --git a/src/modules/Users/adapters/out/persistence/index.ts b/apps/backend-template/src/modules/Users/adapters/out/persistence/index.ts similarity index 100% rename from src/modules/Users/adapters/out/persistence/index.ts rename to apps/backend-template/src/modules/Users/adapters/out/persistence/index.ts diff --git a/src/modules/Users/application/AuthUseCases.ts b/apps/backend-template/src/modules/Users/application/AuthUseCases.ts similarity index 100% rename from src/modules/Users/application/AuthUseCases.ts rename to apps/backend-template/src/modules/Users/application/AuthUseCases.ts diff --git a/src/modules/Users/application/UserUseCases.ts b/apps/backend-template/src/modules/Users/application/UserUseCases.ts similarity index 100% rename from src/modules/Users/application/UserUseCases.ts rename to apps/backend-template/src/modules/Users/application/UserUseCases.ts diff --git a/src/modules/Users/application/ports/IAuthUseCases.ts b/apps/backend-template/src/modules/Users/application/ports/IAuthUseCases.ts similarity index 100% rename from src/modules/Users/application/ports/IAuthUseCases.ts rename to apps/backend-template/src/modules/Users/application/ports/IAuthUseCases.ts diff --git a/src/modules/Users/application/ports/IOrganizationUseCases.ts b/apps/backend-template/src/modules/Users/application/ports/IOrganizationUseCases.ts similarity index 100% rename from src/modules/Users/application/ports/IOrganizationUseCases.ts rename to apps/backend-template/src/modules/Users/application/ports/IOrganizationUseCases.ts diff --git a/src/modules/Users/application/ports/IUserUseCases.ts b/apps/backend-template/src/modules/Users/application/ports/IUserUseCases.ts similarity index 100% rename from src/modules/Users/application/ports/IUserUseCases.ts rename to apps/backend-template/src/modules/Users/application/ports/IUserUseCases.ts diff --git a/src/modules/Users/application/use-cases/AuthUseCases.ts b/apps/backend-template/src/modules/Users/application/use-cases/AuthUseCases.ts similarity index 100% rename from src/modules/Users/application/use-cases/AuthUseCases.ts rename to apps/backend-template/src/modules/Users/application/use-cases/AuthUseCases.ts diff --git a/src/modules/Users/application/use-cases/OrganizationUseCases.ts b/apps/backend-template/src/modules/Users/application/use-cases/OrganizationUseCases.ts similarity index 100% rename from src/modules/Users/application/use-cases/OrganizationUseCases.ts rename to apps/backend-template/src/modules/Users/application/use-cases/OrganizationUseCases.ts diff --git a/src/modules/Users/application/use-cases/UserUseCases.ts b/apps/backend-template/src/modules/Users/application/use-cases/UserUseCases.ts similarity index 100% rename from src/modules/Users/application/use-cases/UserUseCases.ts rename to apps/backend-template/src/modules/Users/application/use-cases/UserUseCases.ts diff --git a/src/modules/Users/application/use-cases/index.ts b/apps/backend-template/src/modules/Users/application/use-cases/index.ts similarity index 100% rename from src/modules/Users/application/use-cases/index.ts rename to apps/backend-template/src/modules/Users/application/use-cases/index.ts diff --git a/src/modules/Users/composition/composeUsersAuthServices.ts b/apps/backend-template/src/modules/Users/composition/composeUsersAuthServices.ts similarity index 93% rename from src/modules/Users/composition/composeUsersAuthServices.ts rename to apps/backend-template/src/modules/Users/composition/composeUsersAuthServices.ts index 904dc83e..34401172 100644 --- a/src/modules/Users/composition/composeUsersAuthServices.ts +++ b/apps/backend-template/src/modules/Users/composition/composeUsersAuthServices.ts @@ -5,6 +5,7 @@ import { IJwtService } from '@src/infra/jwt/IJwtService'; import { IKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/IKeyValueStorageClient'; import { IEventBus, IMessageMediator } from '@src/modules/port'; import { InMemorySecurityAuditRepository } from '@src/infra/audit'; +import { CacheService } from '@src/infra/cache'; import { UserDataRepository } from '@src/modules/Users/adapters/out/persistence/UserDataRepository'; import { OrganizationDataRepository } from '@src/modules/Users/adapters/out/persistence/OrganizationDataRepository'; @@ -61,6 +62,9 @@ export const composeUsersAuthServices = ( userEventListeners } = config; const integrationBus = messageMediator ?? eventBus; + const cacheService = keyValueStorageClient + ? CacheService.compile({ keyValueStorageClient }) + : undefined; const dataRepository = UserDataRepository.compile({ databaseClient @@ -74,11 +78,15 @@ export const composeUsersAuthServices = ( services: { passwordCryptoService, mutexService, - eventBus: integrationBus + eventBus: integrationBus, + cacheService } }); const organizationService = OrganizationService.compile({ - dataRepository: organizationDataRepository + dataRepository: organizationDataRepository, + services: { + cacheService + } }); const userProvider = UserProviderLocal.compile(userService); const authService = AuthService.compile( diff --git a/src/modules/Users/domain/Entity/IOrganization.ts b/apps/backend-template/src/modules/Users/domain/Entity/IOrganization.ts similarity index 100% rename from src/modules/Users/domain/Entity/IOrganization.ts rename to apps/backend-template/src/modules/Users/domain/Entity/IOrganization.ts diff --git a/src/modules/Users/domain/Entity/IUser.ts b/apps/backend-template/src/modules/Users/domain/Entity/IUser.ts similarity index 100% rename from src/modules/Users/domain/Entity/IUser.ts rename to apps/backend-template/src/modules/Users/domain/Entity/IUser.ts diff --git a/src/modules/Users/domain/Model/Organization.ts b/apps/backend-template/src/modules/Users/domain/Model/Organization.ts similarity index 100% rename from src/modules/Users/domain/Model/Organization.ts rename to apps/backend-template/src/modules/Users/domain/Model/Organization.ts diff --git a/src/modules/Users/domain/Model/User.ts b/apps/backend-template/src/modules/Users/domain/Model/User.ts similarity index 100% rename from src/modules/Users/domain/Model/User.ts rename to apps/backend-template/src/modules/Users/domain/Model/User.ts diff --git a/src/modules/Users/domain/security/Rbac.ts b/apps/backend-template/src/modules/Users/domain/security/Rbac.ts similarity index 100% rename from src/modules/Users/domain/security/Rbac.ts rename to apps/backend-template/src/modules/Users/domain/security/Rbac.ts diff --git a/src/modules/Users/events/LoginRequestEvent.ts b/apps/backend-template/src/modules/Users/events/LoginRequestEvent.ts similarity index 100% rename from src/modules/Users/events/LoginRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/LoginRequestEvent.ts diff --git a/src/modules/Users/events/LogoutRequestEvent.ts b/apps/backend-template/src/modules/Users/events/LogoutRequestEvent.ts similarity index 100% rename from src/modules/Users/events/LogoutRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/LogoutRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationAddressCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationAddressCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationAddressCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationAddressCreateRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationAddressDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationAddressDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationAddressDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationAddressDeleteRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationAddressUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationAddressUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationAddressUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationAddressUpdateRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationCreateRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationDeleteRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationEmailCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationEmailCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationEmailCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationEmailCreateRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationEmailDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationEmailDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationEmailDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationEmailDeleteRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationEmailUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationEmailUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationEmailUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationEmailUpdateRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationGetAllRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationGetAllRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationGetAllRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationGetAllRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationGetOneRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationGetOneRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationGetOneRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationGetOneRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationPhoneCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationPhoneCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationPhoneCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationPhoneCreateRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationPhoneDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationPhoneDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationPhoneDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationPhoneDeleteRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationPhoneUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationPhoneUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationPhoneUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationPhoneUpdateRequestEvent.ts diff --git a/src/modules/Users/events/OrganizationUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/OrganizationUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/OrganizationUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/OrganizationUpdateRequestEvent.ts diff --git a/src/modules/Users/events/RegisterRequestEvent.ts b/apps/backend-template/src/modules/Users/events/RegisterRequestEvent.ts similarity index 100% rename from src/modules/Users/events/RegisterRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/RegisterRequestEvent.ts diff --git a/src/modules/Users/events/UpdatePasswordRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UpdatePasswordRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UpdatePasswordRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UpdatePasswordRequestEvent.ts diff --git a/src/modules/Users/events/UserCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserCreateRequestEvent.ts diff --git a/src/modules/Users/events/UserDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserDeleteRequestEvent.ts diff --git a/src/modules/Users/events/UserDocumentCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserDocumentCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserDocumentCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserDocumentCreateRequestEvent.ts diff --git a/src/modules/Users/events/UserDocumentDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserDocumentDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserDocumentDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserDocumentDeleteRequestEvent.ts diff --git a/src/modules/Users/events/UserDocumentUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserDocumentUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserDocumentUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserDocumentUpdateRequestEvent.ts diff --git a/src/modules/Users/events/UserEmailCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserEmailCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserEmailCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserEmailCreateRequestEvent.ts diff --git a/src/modules/Users/events/UserEmailDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserEmailDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserEmailDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserEmailDeleteRequestEvent.ts diff --git a/src/modules/Users/events/UserEmailUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserEmailUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserEmailUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserEmailUpdateRequestEvent.ts diff --git a/src/modules/Users/events/UserGetAllRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserGetAllRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserGetAllRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserGetAllRequestEvent.ts diff --git a/src/modules/Users/events/UserGetOneRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserGetOneRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserGetOneRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserGetOneRequestEvent.ts diff --git a/src/modules/Users/events/UserPasswordUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserPasswordUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserPasswordUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserPasswordUpdateRequestEvent.ts diff --git a/src/modules/Users/events/UserPhoneCreateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserPhoneCreateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserPhoneCreateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserPhoneCreateRequestEvent.ts diff --git a/src/modules/Users/events/UserPhoneDeleteRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserPhoneDeleteRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserPhoneDeleteRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserPhoneDeleteRequestEvent.ts diff --git a/src/modules/Users/events/UserPhoneUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserPhoneUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserPhoneUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserPhoneUpdateRequestEvent.ts diff --git a/src/modules/Users/events/UserUpdateRequestEvent.ts b/apps/backend-template/src/modules/Users/events/UserUpdateRequestEvent.ts similarity index 100% rename from src/modules/Users/events/UserUpdateRequestEvent.ts rename to apps/backend-template/src/modules/Users/events/UserUpdateRequestEvent.ts diff --git a/src/modules/Users/events/contracts/IUserEventListeners.ts b/apps/backend-template/src/modules/Users/events/contracts/IUserEventListeners.ts similarity index 100% rename from src/modules/Users/events/contracts/IUserEventListeners.ts rename to apps/backend-template/src/modules/Users/events/contracts/IUserEventListeners.ts diff --git a/src/modules/Users/events/contracts/UserIntegrationEventName.ts b/apps/backend-template/src/modules/Users/events/contracts/UserIntegrationEventName.ts similarity index 100% rename from src/modules/Users/events/contracts/UserIntegrationEventName.ts rename to apps/backend-template/src/modules/Users/events/contracts/UserIntegrationEventName.ts diff --git a/src/modules/Users/events/contracts/UserMessageContracts.ts b/apps/backend-template/src/modules/Users/events/contracts/UserMessageContracts.ts similarity index 100% rename from src/modules/Users/events/contracts/UserMessageContracts.ts rename to apps/backend-template/src/modules/Users/events/contracts/UserMessageContracts.ts diff --git a/src/modules/Users/events/listeners/registerUserEventListeners.ts b/apps/backend-template/src/modules/Users/events/listeners/registerUserEventListeners.ts similarity index 100% rename from src/modules/Users/events/listeners/registerUserEventListeners.ts rename to apps/backend-template/src/modules/Users/events/listeners/registerUserEventListeners.ts diff --git a/src/modules/Users/events/listeners/registerUserMessageHandlers.ts b/apps/backend-template/src/modules/Users/events/listeners/registerUserMessageHandlers.ts similarity index 100% rename from src/modules/Users/events/listeners/registerUserMessageHandlers.ts rename to apps/backend-template/src/modules/Users/events/listeners/registerUserMessageHandlers.ts diff --git a/src/modules/Users/features/createDocument.ts b/apps/backend-template/src/modules/Users/features/createDocument.ts similarity index 100% rename from src/modules/Users/features/createDocument.ts rename to apps/backend-template/src/modules/Users/features/createDocument.ts diff --git a/src/modules/Users/features/createEmail.ts b/apps/backend-template/src/modules/Users/features/createEmail.ts similarity index 100% rename from src/modules/Users/features/createEmail.ts rename to apps/backend-template/src/modules/Users/features/createEmail.ts diff --git a/src/modules/Users/features/createPhone.ts b/apps/backend-template/src/modules/Users/features/createPhone.ts similarity index 100% rename from src/modules/Users/features/createPhone.ts rename to apps/backend-template/src/modules/Users/features/createPhone.ts diff --git a/src/modules/Users/features/createUser.ts b/apps/backend-template/src/modules/Users/features/createUser.ts similarity index 100% rename from src/modules/Users/features/createUser.ts rename to apps/backend-template/src/modules/Users/features/createUser.ts diff --git a/src/modules/Users/features/deleteDocument.ts b/apps/backend-template/src/modules/Users/features/deleteDocument.ts similarity index 100% rename from src/modules/Users/features/deleteDocument.ts rename to apps/backend-template/src/modules/Users/features/deleteDocument.ts diff --git a/src/modules/Users/features/deleteEmail.ts b/apps/backend-template/src/modules/Users/features/deleteEmail.ts similarity index 100% rename from src/modules/Users/features/deleteEmail.ts rename to apps/backend-template/src/modules/Users/features/deleteEmail.ts diff --git a/src/modules/Users/features/deletePhone.ts b/apps/backend-template/src/modules/Users/features/deletePhone.ts similarity index 100% rename from src/modules/Users/features/deletePhone.ts rename to apps/backend-template/src/modules/Users/features/deletePhone.ts diff --git a/src/modules/Users/features/deleteUserById.ts b/apps/backend-template/src/modules/Users/features/deleteUserById.ts similarity index 100% rename from src/modules/Users/features/deleteUserById.ts rename to apps/backend-template/src/modules/Users/features/deleteUserById.ts diff --git a/src/modules/Users/features/getAllUsers.ts b/apps/backend-template/src/modules/Users/features/getAllUsers.ts similarity index 100% rename from src/modules/Users/features/getAllUsers.ts rename to apps/backend-template/src/modules/Users/features/getAllUsers.ts diff --git a/src/modules/Users/features/getUserById.ts b/apps/backend-template/src/modules/Users/features/getUserById.ts similarity index 100% rename from src/modules/Users/features/getUserById.ts rename to apps/backend-template/src/modules/Users/features/getUserById.ts diff --git a/src/modules/Users/features/updateDocument.ts b/apps/backend-template/src/modules/Users/features/updateDocument.ts similarity index 100% rename from src/modules/Users/features/updateDocument.ts rename to apps/backend-template/src/modules/Users/features/updateDocument.ts diff --git a/src/modules/Users/features/updateEmail.ts b/apps/backend-template/src/modules/Users/features/updateEmail.ts similarity index 100% rename from src/modules/Users/features/updateEmail.ts rename to apps/backend-template/src/modules/Users/features/updateEmail.ts diff --git a/src/modules/Users/features/updatePassword.ts b/apps/backend-template/src/modules/Users/features/updatePassword.ts similarity index 100% rename from src/modules/Users/features/updatePassword.ts rename to apps/backend-template/src/modules/Users/features/updatePassword.ts diff --git a/src/modules/Users/features/updatePhone.ts b/apps/backend-template/src/modules/Users/features/updatePhone.ts similarity index 100% rename from src/modules/Users/features/updatePhone.ts rename to apps/backend-template/src/modules/Users/features/updatePhone.ts diff --git a/src/modules/Users/features/updateUser.ts b/apps/backend-template/src/modules/Users/features/updateUser.ts similarity index 100% rename from src/modules/Users/features/updateUser.ts rename to apps/backend-template/src/modules/Users/features/updateUser.ts diff --git a/src/modules/Users/index.ts b/apps/backend-template/src/modules/Users/index.ts similarity index 100% rename from src/modules/Users/index.ts rename to apps/backend-template/src/modules/Users/index.ts diff --git a/src/modules/Users/infra/repository/UserDataRepository.ts b/apps/backend-template/src/modules/Users/infra/repository/UserDataRepository.ts similarity index 100% rename from src/modules/Users/infra/repository/UserDataRepository.ts rename to apps/backend-template/src/modules/Users/infra/repository/UserDataRepository.ts diff --git a/src/modules/Users/interface/controller/AuthController.ts b/apps/backend-template/src/modules/Users/interface/controller/AuthController.ts similarity index 100% rename from src/modules/Users/interface/controller/AuthController.ts rename to apps/backend-template/src/modules/Users/interface/controller/AuthController.ts diff --git a/src/modules/Users/interface/controller/UserController.ts b/apps/backend-template/src/modules/Users/interface/controller/UserController.ts similarity index 100% rename from src/modules/Users/interface/controller/UserController.ts rename to apps/backend-template/src/modules/Users/interface/controller/UserController.ts diff --git a/src/modules/Users/interface/dto/ILoginRequest.ts b/apps/backend-template/src/modules/Users/interface/dto/ILoginRequest.ts similarity index 100% rename from src/modules/Users/interface/dto/ILoginRequest.ts rename to apps/backend-template/src/modules/Users/interface/dto/ILoginRequest.ts diff --git a/src/modules/Users/interface/dto/ILogoutRequest.ts b/apps/backend-template/src/modules/Users/interface/dto/ILogoutRequest.ts similarity index 100% rename from src/modules/Users/interface/dto/ILogoutRequest.ts rename to apps/backend-template/src/modules/Users/interface/dto/ILogoutRequest.ts diff --git a/src/modules/Users/interface/dto/IRegisterRequest.ts b/apps/backend-template/src/modules/Users/interface/dto/IRegisterRequest.ts similarity index 100% rename from src/modules/Users/interface/dto/IRegisterRequest.ts rename to apps/backend-template/src/modules/Users/interface/dto/IRegisterRequest.ts diff --git a/src/modules/Users/interface/dto/IUpdatePasswordRequest.ts b/apps/backend-template/src/modules/Users/interface/dto/IUpdatePasswordRequest.ts similarity index 100% rename from src/modules/Users/interface/dto/IUpdatePasswordRequest.ts rename to apps/backend-template/src/modules/Users/interface/dto/IUpdatePasswordRequest.ts diff --git a/src/modules/Users/interface/dto/RequestCreateAddress.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestCreateAddress.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestCreateAddress.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestCreateAddress.ts diff --git a/src/modules/Users/interface/dto/RequestCreateDocument.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestCreateDocument.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestCreateDocument.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestCreateDocument.ts diff --git a/src/modules/Users/interface/dto/RequestCreateEmail.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestCreateEmail.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestCreateEmail.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestCreateEmail.ts diff --git a/src/modules/Users/interface/dto/RequestCreateOrganization.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestCreateOrganization.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestCreateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestCreateOrganization.ts diff --git a/src/modules/Users/interface/dto/RequestCreatePhone.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestCreatePhone.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestCreatePhone.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestCreatePhone.ts diff --git a/src/modules/Users/interface/dto/RequestCreateUser.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestCreateUser.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestCreateUser.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestCreateUser.ts diff --git a/src/modules/Users/interface/dto/RequestUpdateAddress.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestUpdateAddress.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestUpdateAddress.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestUpdateAddress.ts diff --git a/src/modules/Users/interface/dto/RequestUpdateDocument.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestUpdateDocument.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestUpdateDocument.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestUpdateDocument.ts diff --git a/src/modules/Users/interface/dto/RequestUpdateEmail.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestUpdateEmail.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestUpdateEmail.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestUpdateEmail.ts diff --git a/src/modules/Users/interface/dto/RequestUpdateOrganization.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestUpdateOrganization.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestUpdateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestUpdateOrganization.ts diff --git a/src/modules/Users/interface/dto/RequestUpdatePassword.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestUpdatePassword.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestUpdatePassword.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestUpdatePassword.ts diff --git a/src/modules/Users/interface/dto/RequestUpdatePhone.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestUpdatePhone.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestUpdatePhone.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestUpdatePhone.ts diff --git a/src/modules/Users/interface/dto/RequestUpdateUser.ts b/apps/backend-template/src/modules/Users/interface/dto/RequestUpdateUser.ts similarity index 100% rename from src/modules/Users/interface/dto/RequestUpdateUser.ts rename to apps/backend-template/src/modules/Users/interface/dto/RequestUpdateUser.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/_createGrpcOperationHandler.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/_createGrpcOperationHandler.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/_createGrpcOperationHandler.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/_createGrpcOperationHandler.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/create.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createDocument.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createEmail.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/createPhone.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAll.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOneById.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/login.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/logout.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/register.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/update.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/grpcapi/frameworks/grpc/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/adonis-js/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createLambdaOperationHandler.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createLambdaOperationHandler.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createLambdaOperationHandler.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createLambdaOperationHandler.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/runtime.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/runtime.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/runtime.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/runtime.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/cloudflare-workers/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/derby-js/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/express/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/express/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/express/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/fastify/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/feathers/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/hyper-express/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/loopback/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/restify/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/restify/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/sails-js/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/total-js/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_documentMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_documentMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_documentMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_documentMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_organizationMutationHandlerFactory.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_organizationMutationHandlerFactory.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_organizationMutationHandlerFactory.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/_organizationMutationHandlerFactory.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/create.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/createPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAll.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOneById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/login.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/logout.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/register.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/update.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/restapi/frameworks/vercel-functions/handlers/updateUserPassword.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/_createWebSocketOperationHandler.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/_createWebSocketOperationHandler.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/_createWebSocketOperationHandler.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/_createWebSocketOperationHandler.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/create.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/create.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/create.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/create.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createDocument.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createDocument.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createDocument.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createDocument.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createEmail.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createEmail.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createEmail.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createEmail.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganization.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganization.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganization.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganization.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationAddress.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationEmail.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createOrganizationPhone.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createPhone.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createPhone.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createPhone.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createPhone.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteDocument.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteDocument.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteDocument.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteDocument.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteEmail.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteEmail.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteEmail.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteEmail.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOne.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOne.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOne.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOne.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganization.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganization.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganization.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganization.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationAddress.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationEmail.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deleteOrganizationPhone.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deletePhone.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deletePhone.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deletePhone.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/deletePhone.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAll.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAll.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAll.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAll.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAllOrganizations.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAllOrganizations.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAllOrganizations.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getAllOrganizations.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOneById.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOneById.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOneById.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOneById.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOrganizationById.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOrganizationById.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOrganizationById.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/getOrganizationById.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/login.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/login.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/login.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/login.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/logout.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/logout.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/logout.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/logout.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/register.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/register.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/register.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/register.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/update.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/update.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/update.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/update.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateDocument.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateDocument.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateDocument.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateDocument.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateEmail.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateEmail.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateEmail.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateEmail.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganization.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganization.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganization.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganization.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationAddress.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationAddress.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationAddress.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationAddress.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationEmail.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationEmail.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationEmail.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationEmail.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationPhone.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationPhone.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationPhone.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateOrganizationPhone.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePassword.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePassword.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePassword.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePassword.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePhone.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePhone.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePhone.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updatePhone.ts diff --git a/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateUserPassword.ts b/apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateUserPassword.ts similarity index 100% rename from src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateUserPassword.ts rename to apps/backend-template/src/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/updateUserPassword.ts diff --git a/src/modules/Users/service/AuthService.ts b/apps/backend-template/src/modules/Users/service/AuthService.ts similarity index 100% rename from src/modules/Users/service/AuthService.ts rename to apps/backend-template/src/modules/Users/service/AuthService.ts diff --git a/src/modules/Users/service/OrganizationService.ts b/apps/backend-template/src/modules/Users/service/OrganizationService.ts similarity index 75% rename from src/modules/Users/service/OrganizationService.ts rename to apps/backend-template/src/modules/Users/service/OrganizationService.ts index d846067e..54602bff 100644 --- a/src/modules/Users/service/OrganizationService.ts +++ b/apps/backend-template/src/modules/Users/service/OrganizationService.ts @@ -15,6 +15,7 @@ import { RequestUpdateEmail } from '@src/modules/Users/interface/dto/RequestUpda import { RequestUpdateOrganization } from '@src/modules/Users/interface/dto/RequestUpdateOrganization'; import { RequestUpdatePhone } from '@src/modules/Users/interface/dto/RequestUpdatePhone'; import { OrganizationDataRepository } from '@src/modules/Users/adapters/out/persistence/OrganizationDataRepository'; +import { ICacheService } from '@src/infra/cache'; interface IOrganizationServiceConfig extends IServiceConfig { } @@ -26,15 +27,44 @@ RequestUpdateOrganization > { public dataRepository: OrganizationDataRepository; + private readonly cacheService?: ICacheService; + public constructor(config: IOrganizationServiceConfig) { super(config); this.dataRepository = config.dataRepository as OrganizationDataRepository; + this.cacheService = config.services?.cacheService as ICacheService | undefined; + } + + private static sortPayload(payload: any): any { + if (Array.isArray(payload)) { + return payload.map((item) => OrganizationService.sortPayload(item)); + } + if (!payload || typeof payload !== 'object') { + return payload; + } + return Object.keys(payload) + .sort() + .reduce((acc, key) => { + acc[key] = OrganizationService.sortPayload(payload[key]); + return acc; + }, {} as Record); + } + + private async getCacheVersion(): Promise { + if (!this.cacheService) return 1; + return this.cacheService.getVersion('organizations'); + } + + private async invalidateReadCache(): Promise { + if (!this.cacheService) return; + await this.cacheService.bumpVersion('organizations'); } public async create(data: RequestCreateOrganization): Promise> { const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.create(data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -48,6 +78,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.update(id, data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -58,6 +89,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.delete(id); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -67,7 +99,17 @@ RequestUpdateOrganization public async getOneById(id: string): Promise> { const serviceResponse: IServiceResponse = {}; try { + const version = await this.getCacheVersion(); + const cacheKey = `organizations:v${version}:getOneById:${id}`; + const cached = await this.cacheService?.get(cacheKey); + if (cached) { + serviceResponse.result = cached; + return serviceResponse; + } serviceResponse.result = await this.dataRepository.getOneById(id); + if (serviceResponse.result) { + await this.cacheService?.set(cacheKey, serviceResponse.result); + } } catch (error) { serviceResponse.error = error as BaseError; } @@ -80,7 +122,16 @@ RequestUpdateOrganization ): Promise> { const serviceResponse: IServiceResponse = {}; try { + const version = await this.getCacheVersion(); + const cacheKey = `organizations:v${version}:getAll:${JSON.stringify( + OrganizationService.sortPayload({ filters, paging }) + )}`; + const cached = await this.cacheService?.get>(cacheKey); + if (cached) { + return cached; + } const result = await this.dataRepository.getAll(filters, paging); + await this.cacheService?.set(cacheKey, result); return result; } catch (error) { serviceResponse.error = error as BaseError; @@ -95,6 +146,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.createAddress(id, data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -109,6 +161,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.updateAddress(id, addressId, data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -122,6 +175,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.deleteAddress(id, addressId); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -135,6 +189,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.createPhone(id, data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -149,6 +204,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.updatePhone(id, phoneId, data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -159,6 +215,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.deletePhone(id, phoneId); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -172,6 +229,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.createEmail(id, data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -186,6 +244,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.updateEmail(id, emailId, data); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } @@ -196,6 +255,7 @@ RequestUpdateOrganization const serviceResponse: IServiceResponse = {}; try { serviceResponse.result = await this.dataRepository.deleteEmail(id, emailId); + await this.invalidateReadCache(); } catch (error) { serviceResponse.error = error as BaseError; } diff --git a/src/modules/Users/service/UserProviderLocal.ts b/apps/backend-template/src/modules/Users/service/UserProviderLocal.ts similarity index 100% rename from src/modules/Users/service/UserProviderLocal.ts rename to apps/backend-template/src/modules/Users/service/UserProviderLocal.ts diff --git a/src/modules/Users/service/UserService.ts b/apps/backend-template/src/modules/Users/service/UserService.ts similarity index 89% rename from src/modules/Users/service/UserService.ts rename to apps/backend-template/src/modules/Users/service/UserService.ts index 42768d48..56aada5c 100644 --- a/src/modules/Users/service/UserService.ts +++ b/apps/backend-template/src/modules/Users/service/UserService.ts @@ -48,6 +48,7 @@ import { IPasswordCryptoService } from '@src/infra/security/IPasswordCryptoServi import { BaseError, ResourceLockedError } from '@src/infra/exceptions'; import { shouldRequireOrganization } from '@src/modules/Users/domain/security/Rbac'; +import { ICacheService } from '@src/infra/cache'; interface IUserServiceConfig extends IServiceConfig { organizationDataRepository?: OrganizationDataRepository; @@ -66,6 +67,8 @@ export class UserService extends BaseService UserService.sortPayload(item)); + } + if (!payload || typeof payload !== 'object') { + return payload; + } + return Object.keys(payload) + .sort() + .reduce((acc, key) => { + acc[key] = UserService.sortPayload(payload[key]); + return acc; + }, {} as Record); + } + + private async getCacheVersion(): Promise { + if (!this.cacheService) return 1; + return this.cacheService.getVersion('users'); + } + + private async invalidateReadCache(): Promise { + if (!this.cacheService) return; + await this.cacheService.bumpVersion('users'); } private static sanitizeUser(user?: IUser): IUser | undefined { @@ -186,6 +215,7 @@ export class UserService extends BaseService> { const serviceResponse: IServiceResponse = {}; try { + const version = await this.getCacheVersion(); + const cacheKey = `users:v${version}:getOneById:${id}`; + const cached = await this.cacheService?.get(cacheKey); + if (cached) { + serviceResponse.result = cached; + return serviceResponse; + } + const user = await getUserById(id, this.dataRepository); serviceResponse.result = UserService.sanitizeUser(user); + if (serviceResponse.result) { + await this.cacheService?.set(cacheKey, serviceResponse.result); + } } catch (error) { serviceResponse.error = error as BaseError; } @@ -263,6 +306,16 @@ export class UserService extends BaseService> { let serviceResponse: IServiceResponse = {}; try { + const version = await this.getCacheVersion(); + const cacheKey = `users:v${version}:getAll:${JSON.stringify(UserService.sortPayload({ + filters, + paging + }))}`; + const cached = await this.cacheService?.get>(cacheKey); + if (cached) { + return cached; + } + const result: IPagingResponse = await getAllUsers( filters, paging, @@ -272,6 +325,7 @@ export class UserService extends BaseService yearEvent) throw error; - - if (yearNow === yearEvent && monthNow > monthEvent) throw error; - - if (dayNow > dayEvent) throw error; - - // yearEvent now is >= yearNow - // monthEvent now is >= monthNow - // dayEvent is now >= dayNow - if (dayNow === dayEvent) { - if (hourNow > hourEvent) throw error; - else if (hourNow === hourEvent) { - if ((minutesEvent - minutesNow) < minutesIntheFuture) throw error; - } - } + const minimumFutureWindowMs = minutesIntheFuture * 60 * 1000; + const diffMs = eventDate.getTime() - dateNow.getTime(); + if (Number.isNaN(diffMs) || diffMs < minimumFutureWindowMs) throw error; } diff --git a/test/integration/Adonis-JS/get.localhost.test.ts b/apps/backend-template/test/integration/Adonis-JS/get.localhost.test.ts similarity index 100% rename from test/integration/Adonis-JS/get.localhost.test.ts rename to apps/backend-template/test/integration/Adonis-JS/get.localhost.test.ts diff --git a/test/integration/Cloudflare-Workers/get.localhost.test.ts b/apps/backend-template/test/integration/Cloudflare-Workers/get.localhost.test.ts similarity index 100% rename from test/integration/Cloudflare-Workers/get.localhost.test.ts rename to apps/backend-template/test/integration/Cloudflare-Workers/get.localhost.test.ts diff --git a/test/integration/Derby-JS/get.localhost.test.ts b/apps/backend-template/test/integration/Derby-JS/get.localhost.test.ts similarity index 100% rename from test/integration/Derby-JS/get.localhost.test.ts rename to apps/backend-template/test/integration/Derby-JS/get.localhost.test.ts diff --git a/test/integration/Express/Organizations/organization.relationship.e2e.test.ts b/apps/backend-template/test/integration/Express/Organizations/organization.relationship.e2e.test.ts similarity index 100% rename from test/integration/Express/Organizations/organization.relationship.e2e.test.ts rename to apps/backend-template/test/integration/Express/Organizations/organization.relationship.e2e.test.ts diff --git a/test/integration/Express/Users/create.test.ts b/apps/backend-template/test/integration/Express/Users/create.test.ts similarity index 100% rename from test/integration/Express/Users/create.test.ts rename to apps/backend-template/test/integration/Express/Users/create.test.ts diff --git a/test/integration/Express/Users/createDocument.test.ts b/apps/backend-template/test/integration/Express/Users/createDocument.test.ts similarity index 100% rename from test/integration/Express/Users/createDocument.test.ts rename to apps/backend-template/test/integration/Express/Users/createDocument.test.ts diff --git a/test/integration/Express/Users/createEmail.test.ts b/apps/backend-template/test/integration/Express/Users/createEmail.test.ts similarity index 100% rename from test/integration/Express/Users/createEmail.test.ts rename to apps/backend-template/test/integration/Express/Users/createEmail.test.ts diff --git a/test/integration/Express/Users/createPhone.test.ts b/apps/backend-template/test/integration/Express/Users/createPhone.test.ts similarity index 100% rename from test/integration/Express/Users/createPhone.test.ts rename to apps/backend-template/test/integration/Express/Users/createPhone.test.ts diff --git a/test/integration/Express/Users/deleteDocument.test.ts b/apps/backend-template/test/integration/Express/Users/deleteDocument.test.ts similarity index 100% rename from test/integration/Express/Users/deleteDocument.test.ts rename to apps/backend-template/test/integration/Express/Users/deleteDocument.test.ts diff --git a/test/integration/Express/Users/deleteEmail.test.ts b/apps/backend-template/test/integration/Express/Users/deleteEmail.test.ts similarity index 100% rename from test/integration/Express/Users/deleteEmail.test.ts rename to apps/backend-template/test/integration/Express/Users/deleteEmail.test.ts diff --git a/test/integration/Express/Users/deleteOne.test.ts b/apps/backend-template/test/integration/Express/Users/deleteOne.test.ts similarity index 100% rename from test/integration/Express/Users/deleteOne.test.ts rename to apps/backend-template/test/integration/Express/Users/deleteOne.test.ts diff --git a/test/integration/Express/Users/deletePhone.test.ts b/apps/backend-template/test/integration/Express/Users/deletePhone.test.ts similarity index 100% rename from test/integration/Express/Users/deletePhone.test.ts rename to apps/backend-template/test/integration/Express/Users/deletePhone.test.ts diff --git a/test/integration/Express/Users/getAll.test.ts b/apps/backend-template/test/integration/Express/Users/getAll.test.ts similarity index 100% rename from test/integration/Express/Users/getAll.test.ts rename to apps/backend-template/test/integration/Express/Users/getAll.test.ts diff --git a/test/integration/Express/Users/getOneById.test.ts b/apps/backend-template/test/integration/Express/Users/getOneById.test.ts similarity index 100% rename from test/integration/Express/Users/getOneById.test.ts rename to apps/backend-template/test/integration/Express/Users/getOneById.test.ts diff --git a/test/integration/Express/Users/update.test.ts b/apps/backend-template/test/integration/Express/Users/update.test.ts similarity index 100% rename from test/integration/Express/Users/update.test.ts rename to apps/backend-template/test/integration/Express/Users/update.test.ts diff --git a/test/integration/Express/Users/updateDocument.test.ts b/apps/backend-template/test/integration/Express/Users/updateDocument.test.ts similarity index 100% rename from test/integration/Express/Users/updateDocument.test.ts rename to apps/backend-template/test/integration/Express/Users/updateDocument.test.ts diff --git a/test/integration/Express/Users/updateEmail.test.ts b/apps/backend-template/test/integration/Express/Users/updateEmail.test.ts similarity index 100% rename from test/integration/Express/Users/updateEmail.test.ts rename to apps/backend-template/test/integration/Express/Users/updateEmail.test.ts diff --git a/test/integration/Express/Users/updatePassword.test.ts b/apps/backend-template/test/integration/Express/Users/updatePassword.test.ts similarity index 100% rename from test/integration/Express/Users/updatePassword.test.ts rename to apps/backend-template/test/integration/Express/Users/updatePassword.test.ts diff --git a/test/integration/Express/Users/updatePhone.test.ts b/apps/backend-template/test/integration/Express/Users/updatePhone.test.ts similarity index 100% rename from test/integration/Express/Users/updatePhone.test.ts rename to apps/backend-template/test/integration/Express/Users/updatePhone.test.ts diff --git a/test/integration/Express/auth/login.test.ts b/apps/backend-template/test/integration/Express/auth/login.test.ts similarity index 100% rename from test/integration/Express/auth/login.test.ts rename to apps/backend-template/test/integration/Express/auth/login.test.ts diff --git a/test/integration/Express/auth/logout.test.ts b/apps/backend-template/test/integration/Express/auth/logout.test.ts similarity index 100% rename from test/integration/Express/auth/logout.test.ts rename to apps/backend-template/test/integration/Express/auth/logout.test.ts diff --git a/test/integration/Express/auth/register.test.ts b/apps/backend-template/test/integration/Express/auth/register.test.ts similarity index 100% rename from test/integration/Express/auth/register.test.ts rename to apps/backend-template/test/integration/Express/auth/register.test.ts diff --git a/test/integration/Express/auth/updatePassword.test.ts b/apps/backend-template/test/integration/Express/auth/updatePassword.test.ts similarity index 100% rename from test/integration/Express/auth/updatePassword.test.ts rename to apps/backend-template/test/integration/Express/auth/updatePassword.test.ts diff --git a/test/integration/Express/get.apiVersions.test.ts b/apps/backend-template/test/integration/Express/get.apiVersions.test.ts similarity index 100% rename from test/integration/Express/get.apiVersions.test.ts rename to apps/backend-template/test/integration/Express/get.apiVersions.test.ts diff --git a/test/integration/Express/get.localhost.test.ts b/apps/backend-template/test/integration/Express/get.localhost.test.ts similarity index 100% rename from test/integration/Express/get.localhost.test.ts rename to apps/backend-template/test/integration/Express/get.localhost.test.ts diff --git a/test/integration/Fastify/Users/create.test.ts b/apps/backend-template/test/integration/Fastify/Users/create.test.ts similarity index 100% rename from test/integration/Fastify/Users/create.test.ts rename to apps/backend-template/test/integration/Fastify/Users/create.test.ts diff --git a/test/integration/Fastify/Users/createDocument.test.ts b/apps/backend-template/test/integration/Fastify/Users/createDocument.test.ts similarity index 100% rename from test/integration/Fastify/Users/createDocument.test.ts rename to apps/backend-template/test/integration/Fastify/Users/createDocument.test.ts diff --git a/test/integration/Fastify/Users/createEmail.test.ts b/apps/backend-template/test/integration/Fastify/Users/createEmail.test.ts similarity index 100% rename from test/integration/Fastify/Users/createEmail.test.ts rename to apps/backend-template/test/integration/Fastify/Users/createEmail.test.ts diff --git a/test/integration/Fastify/Users/createPhone.test.ts b/apps/backend-template/test/integration/Fastify/Users/createPhone.test.ts similarity index 100% rename from test/integration/Fastify/Users/createPhone.test.ts rename to apps/backend-template/test/integration/Fastify/Users/createPhone.test.ts diff --git a/test/integration/Fastify/Users/deleteDocument.test.ts b/apps/backend-template/test/integration/Fastify/Users/deleteDocument.test.ts similarity index 100% rename from test/integration/Fastify/Users/deleteDocument.test.ts rename to apps/backend-template/test/integration/Fastify/Users/deleteDocument.test.ts diff --git a/test/integration/Fastify/Users/deleteEmail.test.ts b/apps/backend-template/test/integration/Fastify/Users/deleteEmail.test.ts similarity index 100% rename from test/integration/Fastify/Users/deleteEmail.test.ts rename to apps/backend-template/test/integration/Fastify/Users/deleteEmail.test.ts diff --git a/test/integration/Fastify/Users/deleteOne.test.ts b/apps/backend-template/test/integration/Fastify/Users/deleteOne.test.ts similarity index 100% rename from test/integration/Fastify/Users/deleteOne.test.ts rename to apps/backend-template/test/integration/Fastify/Users/deleteOne.test.ts diff --git a/test/integration/Fastify/Users/deletePhone.test.ts b/apps/backend-template/test/integration/Fastify/Users/deletePhone.test.ts similarity index 100% rename from test/integration/Fastify/Users/deletePhone.test.ts rename to apps/backend-template/test/integration/Fastify/Users/deletePhone.test.ts diff --git a/test/integration/Fastify/Users/getAll.test.ts b/apps/backend-template/test/integration/Fastify/Users/getAll.test.ts similarity index 100% rename from test/integration/Fastify/Users/getAll.test.ts rename to apps/backend-template/test/integration/Fastify/Users/getAll.test.ts diff --git a/test/integration/Fastify/Users/getOneById.test.ts b/apps/backend-template/test/integration/Fastify/Users/getOneById.test.ts similarity index 100% rename from test/integration/Fastify/Users/getOneById.test.ts rename to apps/backend-template/test/integration/Fastify/Users/getOneById.test.ts diff --git a/test/integration/Fastify/Users/update.test.ts b/apps/backend-template/test/integration/Fastify/Users/update.test.ts similarity index 100% rename from test/integration/Fastify/Users/update.test.ts rename to apps/backend-template/test/integration/Fastify/Users/update.test.ts diff --git a/test/integration/Fastify/Users/updateDocument.test.ts b/apps/backend-template/test/integration/Fastify/Users/updateDocument.test.ts similarity index 100% rename from test/integration/Fastify/Users/updateDocument.test.ts rename to apps/backend-template/test/integration/Fastify/Users/updateDocument.test.ts diff --git a/test/integration/Fastify/Users/updateEmail.test.ts b/apps/backend-template/test/integration/Fastify/Users/updateEmail.test.ts similarity index 100% rename from test/integration/Fastify/Users/updateEmail.test.ts rename to apps/backend-template/test/integration/Fastify/Users/updateEmail.test.ts diff --git a/test/integration/Fastify/Users/updatePassword.test.ts b/apps/backend-template/test/integration/Fastify/Users/updatePassword.test.ts similarity index 100% rename from test/integration/Fastify/Users/updatePassword.test.ts rename to apps/backend-template/test/integration/Fastify/Users/updatePassword.test.ts diff --git a/test/integration/Fastify/Users/updatePhone.test.ts b/apps/backend-template/test/integration/Fastify/Users/updatePhone.test.ts similarity index 100% rename from test/integration/Fastify/Users/updatePhone.test.ts rename to apps/backend-template/test/integration/Fastify/Users/updatePhone.test.ts diff --git a/test/integration/Fastify/auth/login.test.ts b/apps/backend-template/test/integration/Fastify/auth/login.test.ts similarity index 100% rename from test/integration/Fastify/auth/login.test.ts rename to apps/backend-template/test/integration/Fastify/auth/login.test.ts diff --git a/test/integration/Fastify/auth/logout.test.ts b/apps/backend-template/test/integration/Fastify/auth/logout.test.ts similarity index 100% rename from test/integration/Fastify/auth/logout.test.ts rename to apps/backend-template/test/integration/Fastify/auth/logout.test.ts diff --git a/test/integration/Fastify/auth/register.test.ts b/apps/backend-template/test/integration/Fastify/auth/register.test.ts similarity index 100% rename from test/integration/Fastify/auth/register.test.ts rename to apps/backend-template/test/integration/Fastify/auth/register.test.ts diff --git a/test/integration/Fastify/auth/updatePassword.test.ts b/apps/backend-template/test/integration/Fastify/auth/updatePassword.test.ts similarity index 100% rename from test/integration/Fastify/auth/updatePassword.test.ts rename to apps/backend-template/test/integration/Fastify/auth/updatePassword.test.ts diff --git a/test/integration/Fastify/get.apiVersions.test.ts b/apps/backend-template/test/integration/Fastify/get.apiVersions.test.ts similarity index 100% rename from test/integration/Fastify/get.apiVersions.test.ts rename to apps/backend-template/test/integration/Fastify/get.apiVersions.test.ts diff --git a/test/integration/Fastify/get.localhost.test.ts b/apps/backend-template/test/integration/Fastify/get.localhost.test.ts similarity index 100% rename from test/integration/Fastify/get.localhost.test.ts rename to apps/backend-template/test/integration/Fastify/get.localhost.test.ts diff --git a/test/integration/Feathers/get.localhost.test.ts b/apps/backend-template/test/integration/Feathers/get.localhost.test.ts similarity index 100% rename from test/integration/Feathers/get.localhost.test.ts rename to apps/backend-template/test/integration/Feathers/get.localhost.test.ts diff --git a/test/integration/Hyper-Express/Users/create.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/create.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/create.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/create.test.ts diff --git a/test/integration/Hyper-Express/Users/createDocument.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/createDocument.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/createDocument.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/createDocument.test.ts diff --git a/test/integration/Hyper-Express/Users/createEmail.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/createEmail.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/createEmail.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/createEmail.test.ts diff --git a/test/integration/Hyper-Express/Users/createPhone.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/createPhone.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/createPhone.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/createPhone.test.ts diff --git a/test/integration/Hyper-Express/Users/deleteDocument.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/deleteDocument.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/deleteDocument.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/deleteDocument.test.ts diff --git a/test/integration/Hyper-Express/Users/deleteEmail.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/deleteEmail.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/deleteEmail.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/deleteEmail.test.ts diff --git a/test/integration/Hyper-Express/Users/deleteOne.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/deleteOne.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/deleteOne.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/deleteOne.test.ts diff --git a/test/integration/Hyper-Express/Users/deletePhone.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/deletePhone.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/deletePhone.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/deletePhone.test.ts diff --git a/test/integration/Hyper-Express/Users/getAll.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/getAll.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/getAll.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/getAll.test.ts diff --git a/test/integration/Hyper-Express/Users/getOneById.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/getOneById.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/getOneById.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/getOneById.test.ts diff --git a/test/integration/Hyper-Express/Users/update.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/update.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/update.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/update.test.ts diff --git a/test/integration/Hyper-Express/Users/updateDocument.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/updateDocument.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/updateDocument.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/updateDocument.test.ts diff --git a/test/integration/Hyper-Express/Users/updateEmail.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/updateEmail.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/updateEmail.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/updateEmail.test.ts diff --git a/test/integration/Hyper-Express/Users/updatePassword.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/updatePassword.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/updatePassword.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/updatePassword.test.ts diff --git a/test/integration/Hyper-Express/Users/updatePhone.test.ts b/apps/backend-template/test/integration/Hyper-Express/Users/updatePhone.test.ts similarity index 100% rename from test/integration/Hyper-Express/Users/updatePhone.test.ts rename to apps/backend-template/test/integration/Hyper-Express/Users/updatePhone.test.ts diff --git a/test/integration/Hyper-Express/auth/login.test.ts b/apps/backend-template/test/integration/Hyper-Express/auth/login.test.ts similarity index 100% rename from test/integration/Hyper-Express/auth/login.test.ts rename to apps/backend-template/test/integration/Hyper-Express/auth/login.test.ts diff --git a/test/integration/Hyper-Express/auth/logout.test.ts b/apps/backend-template/test/integration/Hyper-Express/auth/logout.test.ts similarity index 100% rename from test/integration/Hyper-Express/auth/logout.test.ts rename to apps/backend-template/test/integration/Hyper-Express/auth/logout.test.ts diff --git a/test/integration/Hyper-Express/auth/register.test.ts b/apps/backend-template/test/integration/Hyper-Express/auth/register.test.ts similarity index 100% rename from test/integration/Hyper-Express/auth/register.test.ts rename to apps/backend-template/test/integration/Hyper-Express/auth/register.test.ts diff --git a/test/integration/Hyper-Express/auth/updatePassword.test.ts b/apps/backend-template/test/integration/Hyper-Express/auth/updatePassword.test.ts similarity index 100% rename from test/integration/Hyper-Express/auth/updatePassword.test.ts rename to apps/backend-template/test/integration/Hyper-Express/auth/updatePassword.test.ts diff --git a/test/integration/Hyper-Express/get.apiVersions.test.ts b/apps/backend-template/test/integration/Hyper-Express/get.apiVersions.test.ts similarity index 100% rename from test/integration/Hyper-Express/get.apiVersions.test.ts rename to apps/backend-template/test/integration/Hyper-Express/get.apiVersions.test.ts diff --git a/test/integration/Hyper-Express/get.localhost.test.ts b/apps/backend-template/test/integration/Hyper-Express/get.localhost.test.ts similarity index 100% rename from test/integration/Hyper-Express/get.localhost.test.ts rename to apps/backend-template/test/integration/Hyper-Express/get.localhost.test.ts diff --git a/test/integration/Hyper-Express/getAbsoluteEndPoint.ts b/apps/backend-template/test/integration/Hyper-Express/getAbsoluteEndPoint.ts similarity index 100% rename from test/integration/Hyper-Express/getAbsoluteEndPoint.ts rename to apps/backend-template/test/integration/Hyper-Express/getAbsoluteEndPoint.ts diff --git a/test/integration/Lambda/auth/Basic/create.test.ts b/apps/backend-template/test/integration/Lambda/auth/Basic/create.test.ts similarity index 100% rename from test/integration/Lambda/auth/Basic/create.test.ts rename to apps/backend-template/test/integration/Lambda/auth/Basic/create.test.ts diff --git a/test/integration/Lambda/auth/Bearer/create.test.ts b/apps/backend-template/test/integration/Lambda/auth/Bearer/create.test.ts similarity index 100% rename from test/integration/Lambda/auth/Bearer/create.test.ts rename to apps/backend-template/test/integration/Lambda/auth/Bearer/create.test.ts diff --git a/test/integration/Lambda/get.localhost.test.ts b/apps/backend-template/test/integration/Lambda/get.localhost.test.ts similarity index 100% rename from test/integration/Lambda/get.localhost.test.ts rename to apps/backend-template/test/integration/Lambda/get.localhost.test.ts diff --git a/test/integration/Lambda/utils/composeContext.ts b/apps/backend-template/test/integration/Lambda/utils/composeContext.ts similarity index 100% rename from test/integration/Lambda/utils/composeContext.ts rename to apps/backend-template/test/integration/Lambda/utils/composeContext.ts diff --git a/test/integration/Lambda/utils/composeHttpEvent.ts b/apps/backend-template/test/integration/Lambda/utils/composeHttpEvent.ts similarity index 100% rename from test/integration/Lambda/utils/composeHttpEvent.ts rename to apps/backend-template/test/integration/Lambda/utils/composeHttpEvent.ts diff --git a/test/integration/Lambda/utils/index.ts b/apps/backend-template/test/integration/Lambda/utils/index.ts similarity index 100% rename from test/integration/Lambda/utils/index.ts rename to apps/backend-template/test/integration/Lambda/utils/index.ts diff --git a/test/integration/LoopBack/get.localhost.test.ts b/apps/backend-template/test/integration/LoopBack/get.localhost.test.ts similarity index 100% rename from test/integration/LoopBack/get.localhost.test.ts rename to apps/backend-template/test/integration/LoopBack/get.localhost.test.ts diff --git a/test/integration/Restify/Users/create.test.ts b/apps/backend-template/test/integration/Restify/Users/create.test.ts similarity index 100% rename from test/integration/Restify/Users/create.test.ts rename to apps/backend-template/test/integration/Restify/Users/create.test.ts diff --git a/test/integration/Restify/Users/createDocument.test.ts b/apps/backend-template/test/integration/Restify/Users/createDocument.test.ts similarity index 100% rename from test/integration/Restify/Users/createDocument.test.ts rename to apps/backend-template/test/integration/Restify/Users/createDocument.test.ts diff --git a/test/integration/Restify/Users/createEmail.test.ts b/apps/backend-template/test/integration/Restify/Users/createEmail.test.ts similarity index 100% rename from test/integration/Restify/Users/createEmail.test.ts rename to apps/backend-template/test/integration/Restify/Users/createEmail.test.ts diff --git a/test/integration/Restify/Users/createPhone.test.ts b/apps/backend-template/test/integration/Restify/Users/createPhone.test.ts similarity index 100% rename from test/integration/Restify/Users/createPhone.test.ts rename to apps/backend-template/test/integration/Restify/Users/createPhone.test.ts diff --git a/test/integration/Restify/Users/deleteDocument.test.ts b/apps/backend-template/test/integration/Restify/Users/deleteDocument.test.ts similarity index 100% rename from test/integration/Restify/Users/deleteDocument.test.ts rename to apps/backend-template/test/integration/Restify/Users/deleteDocument.test.ts diff --git a/test/integration/Restify/Users/deleteEmail.test.ts b/apps/backend-template/test/integration/Restify/Users/deleteEmail.test.ts similarity index 100% rename from test/integration/Restify/Users/deleteEmail.test.ts rename to apps/backend-template/test/integration/Restify/Users/deleteEmail.test.ts diff --git a/test/integration/Restify/Users/deleteOne.test.ts b/apps/backend-template/test/integration/Restify/Users/deleteOne.test.ts similarity index 100% rename from test/integration/Restify/Users/deleteOne.test.ts rename to apps/backend-template/test/integration/Restify/Users/deleteOne.test.ts diff --git a/test/integration/Restify/Users/deletePhone.test.ts b/apps/backend-template/test/integration/Restify/Users/deletePhone.test.ts similarity index 100% rename from test/integration/Restify/Users/deletePhone.test.ts rename to apps/backend-template/test/integration/Restify/Users/deletePhone.test.ts diff --git a/test/integration/Restify/Users/getAll.test.ts b/apps/backend-template/test/integration/Restify/Users/getAll.test.ts similarity index 100% rename from test/integration/Restify/Users/getAll.test.ts rename to apps/backend-template/test/integration/Restify/Users/getAll.test.ts diff --git a/test/integration/Restify/Users/getOneById.test.ts b/apps/backend-template/test/integration/Restify/Users/getOneById.test.ts similarity index 100% rename from test/integration/Restify/Users/getOneById.test.ts rename to apps/backend-template/test/integration/Restify/Users/getOneById.test.ts diff --git a/test/integration/Restify/Users/update.test.ts b/apps/backend-template/test/integration/Restify/Users/update.test.ts similarity index 100% rename from test/integration/Restify/Users/update.test.ts rename to apps/backend-template/test/integration/Restify/Users/update.test.ts diff --git a/test/integration/Restify/Users/updateDocument.test.ts b/apps/backend-template/test/integration/Restify/Users/updateDocument.test.ts similarity index 100% rename from test/integration/Restify/Users/updateDocument.test.ts rename to apps/backend-template/test/integration/Restify/Users/updateDocument.test.ts diff --git a/test/integration/Restify/Users/updateEmail.test.ts b/apps/backend-template/test/integration/Restify/Users/updateEmail.test.ts similarity index 100% rename from test/integration/Restify/Users/updateEmail.test.ts rename to apps/backend-template/test/integration/Restify/Users/updateEmail.test.ts diff --git a/test/integration/Restify/Users/updatePassword.test.ts b/apps/backend-template/test/integration/Restify/Users/updatePassword.test.ts similarity index 100% rename from test/integration/Restify/Users/updatePassword.test.ts rename to apps/backend-template/test/integration/Restify/Users/updatePassword.test.ts diff --git a/test/integration/Restify/Users/updatePhone.test.ts b/apps/backend-template/test/integration/Restify/Users/updatePhone.test.ts similarity index 100% rename from test/integration/Restify/Users/updatePhone.test.ts rename to apps/backend-template/test/integration/Restify/Users/updatePhone.test.ts diff --git a/test/integration/Restify/auth/login.test.ts b/apps/backend-template/test/integration/Restify/auth/login.test.ts similarity index 100% rename from test/integration/Restify/auth/login.test.ts rename to apps/backend-template/test/integration/Restify/auth/login.test.ts diff --git a/test/integration/Restify/auth/logout.test.ts b/apps/backend-template/test/integration/Restify/auth/logout.test.ts similarity index 100% rename from test/integration/Restify/auth/logout.test.ts rename to apps/backend-template/test/integration/Restify/auth/logout.test.ts diff --git a/test/integration/Restify/auth/register.test.ts b/apps/backend-template/test/integration/Restify/auth/register.test.ts similarity index 100% rename from test/integration/Restify/auth/register.test.ts rename to apps/backend-template/test/integration/Restify/auth/register.test.ts diff --git a/test/integration/Restify/auth/updatePassword.test.ts b/apps/backend-template/test/integration/Restify/auth/updatePassword.test.ts similarity index 100% rename from test/integration/Restify/auth/updatePassword.test.ts rename to apps/backend-template/test/integration/Restify/auth/updatePassword.test.ts diff --git a/test/integration/Restify/get.apiVersions.test.ts b/apps/backend-template/test/integration/Restify/get.apiVersions.test.ts similarity index 100% rename from test/integration/Restify/get.apiVersions.test.ts rename to apps/backend-template/test/integration/Restify/get.apiVersions.test.ts diff --git a/test/integration/Restify/get.localhost.test.ts b/apps/backend-template/test/integration/Restify/get.localhost.test.ts similarity index 100% rename from test/integration/Restify/get.localhost.test.ts rename to apps/backend-template/test/integration/Restify/get.localhost.test.ts diff --git a/test/integration/Sails-JS/get.localhost.test.ts b/apps/backend-template/test/integration/Sails-JS/get.localhost.test.ts similarity index 100% rename from test/integration/Sails-JS/get.localhost.test.ts rename to apps/backend-template/test/integration/Sails-JS/get.localhost.test.ts diff --git a/apps/backend-template/test/integration/ServiceManagement/domainDesigner.smoke.test.ts b/apps/backend-template/test/integration/ServiceManagement/domainDesigner.smoke.test.ts new file mode 100644 index 00000000..cc721ec0 --- /dev/null +++ b/apps/backend-template/test/integration/ServiceManagement/domainDesigner.smoke.test.ts @@ -0,0 +1,31 @@ +/* eslint-disable jest/prefer-expect-assertions, jest/max-expects */ +import fs from 'fs'; +import path from 'path'; + +describe('serviceManagement domain designer smoke', () => { + const htmlPath = path.resolve(process.cwd(), 'apps/service-management/index.html'); + const scriptPath = path.resolve(process.cwd(), 'apps/service-management/script.js'); + + it('has create/edit/export/import controls required for MVP workflow', () => { + const html = fs.readFileSync(htmlPath, 'utf-8'); + expect(html).toContain('id="add-domain-btn"'); + expect(html).toContain('id="add-entity-btn"'); + expect(html).toContain('id="save-relationship-btn"'); + expect(html).toContain('id="export-oas-btn"'); + expect(html).toContain('id="import-json-btn"'); + expect(html).toContain('id="import-package-btn"'); + expect(html).toContain('id="relationship-bend-x-input"'); + expect(html).toContain('id="entity-template-select"'); + expect(html).toContain('id="mini-map"'); + }); + + it('wires export and package features in runtime script', () => { + const script = fs.readFileSync(scriptPath, 'utf-8'); + expect(script).toContain('exportAsOas'); + expect(script).toContain('exportAsAsyncApi'); + expect(script).toContain('exportBoilerplateBundle'); + expect(script).toContain('exportAsPackage'); + expect(script).toContain('importDomainPackage'); + expect(script).toContain('renderMiniMap'); + }); +}); diff --git a/test/integration/Total-JS/get.localhost.test.ts b/apps/backend-template/test/integration/Total-JS/get.localhost.test.ts similarity index 100% rename from test/integration/Total-JS/get.localhost.test.ts rename to apps/backend-template/test/integration/Total-JS/get.localhost.test.ts diff --git a/test/integration/Vercel-Functions/get.localhost.test.ts b/apps/backend-template/test/integration/Vercel-Functions/get.localhost.test.ts similarity index 100% rename from test/integration/Vercel-Functions/get.localhost.test.ts rename to apps/backend-template/test/integration/Vercel-Functions/get.localhost.test.ts diff --git a/test/integration/mutex/redis.express.test.ts b/apps/backend-template/test/integration/mutex/redis.express.test.ts similarity index 100% rename from test/integration/mutex/redis.express.test.ts rename to apps/backend-template/test/integration/mutex/redis.express.test.ts diff --git a/test/integration/mutex/redis.fastify.test.ts b/apps/backend-template/test/integration/mutex/redis.fastify.test.ts similarity index 100% rename from test/integration/mutex/redis.fastify.test.ts rename to apps/backend-template/test/integration/mutex/redis.fastify.test.ts diff --git a/test/integration/mutex/redis.restify.test.ts b/apps/backend-template/test/integration/mutex/redis.restify.test.ts similarity index 100% rename from test/integration/mutex/redis.restify.test.ts rename to apps/backend-template/test/integration/mutex/redis.restify.test.ts diff --git a/apps/backend-template/test/integration/realtime/grpc.basic.integration.test.ts b/apps/backend-template/test/integration/realtime/grpc.basic.integration.test.ts new file mode 100644 index 00000000..0985c43f --- /dev/null +++ b/apps/backend-template/test/integration/realtime/grpc.basic.integration.test.ts @@ -0,0 +1,62 @@ +/* global describe, it, expect, beforeAll, afterAll */ +import { GrpcAPI } from '@src/interface/gRPC/gRPCAPI'; +import { InMemoryDbClient } from '@src/infra/persistence/InMemoryDatabase/InMemoryDbClient'; +import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService'; +import { JwtService } from '@src/infra/jwt/JwtService'; +import { compileKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient'; +import { MutexService } from '@src/infra/mutex/adapter/MutexService'; +import { composeUsersAuthServices } from '@src/modules/Users'; +import { InMemoryMessageMediator } from '@src/infra/messages/InMemoryMessageMediator'; +import { GrpcApiClient } from '@jumentix/sdk-grpc-client'; + +const databaseClient = InMemoryDbClient; +const passwordCryptoService = PasswordCryptoService.compile(); +const jwtService = JwtService.compile(); +const keyValueStorageClient = compileKeyValueStorageClient('inmemory'); +const mutexService = MutexService.compile(keyValueStorageClient); +const messageMediator = new InMemoryMessageMediator(); +const { authService } = composeUsersAuthServices({ + databaseClient, + passwordCryptoService, + mutexService, + jwtService, + keyValueStorageClient, + messageMediator +}); + +jest.setTimeout(30000); + +describe('realtime grpc integration', () => { + const port = 33202; + let api: GrpcAPI; + let client: GrpcApiClient; + + beforeAll(async () => { + api = new GrpcAPI({ + databaseClient, + host: '127.0.0.1', + port, + authService, + passwordCryptoService, + keyValueStorageClient, + mutexService, + eventBus: messageMediator, + messageMediator + }); + await api.start(); + client = new GrpcApiClient(`127.0.0.1:${port}`); + }); + + afterAll(async () => { + await api.stop(); + }); + + it('responds with normalized error envelope for unknown operation', async () => { + expect.hasAssertions(); + await expect(client.request({ + version: '1.0.0', + operationId: 'unknownOperation', + metadata: { requestId: 'grpc-int-1' } + })).rejects.toThrow('Operation "unknownOperation" not found.'); + }); +}); diff --git a/apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts b/apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts new file mode 100644 index 00000000..7cea2589 --- /dev/null +++ b/apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts @@ -0,0 +1,203 @@ +/* global describe, it, expect, beforeAll, afterAll */ +import http from 'http'; +import { AddressInfo } from 'net'; +import { createClient } from 'redis'; +import { Server as SocketIOServer } from 'socket.io'; +import { io as createSocketClient, Socket } from 'socket.io-client'; +import { createAdapter } from '@socket.io/redis-streams-adapter'; + +const REDIS_PASSWORD = process.env.AAA_REDIS_PASSWORD || 'eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81'; +const REDIS_HOST = process.env.AAA_REDIS_HOST || '127.0.0.1'; +const REDIS_PORT = process.env.AAA_REDIS_PORT || '6379'; +const REDIS_DB = process.env.AAA_REDIS_DATABASE || '1'; + +const redisUrl = `redis://:${encodeURIComponent(REDIS_PASSWORD)}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}`; + +interface IRunningNode { + name: string; + httpServer: http.Server; + io: SocketIOServer; + redisClient: ReturnType; + port: number; +} + +const connectClient = async (url: string): Promise => { + const socket = createSocketClient(url, { + path: '/ws', + transports: ['websocket'], + forceNew: true + }); + await new Promise((resolve, reject) => { + socket.on('connect', () => resolve()); + socket.on('connect_error', (error) => reject(error)); + }); + return socket; +}; + +const emitWithAck = ( + socket: Socket, + eventName: string, + payload: Record, + timeoutMs = 10000 +): Promise => new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error(`ack timeout for ${eventName}`)), timeoutMs); + socket.emit(eventName, payload, (arg1: any, arg2: any) => { + clearTimeout(timeout); + const hasErrorArg = arg2 !== undefined; + const error = hasErrorArg ? arg1 : undefined; + const ackPayload = hasErrorArg ? arg2 : arg1; + if (error) { + reject(error); + return; + } + resolve(ackPayload); + }); +}); + +const waitForClusterResponse = ( + socket: Socket, + requestId: string, + timeoutMs = 15000 +): Promise => new Promise((resolve, reject) => { + let timeout: NodeJS.Timeout; + const onClusterResponse = (payload: any) => { + if (payload?.requestId !== requestId) return; + clearTimeout(timeout); + socket.off('cluster:response', onClusterResponse); + resolve(payload); + }; + timeout = setTimeout(() => { + socket.off('cluster:response', onClusterResponse); + reject(new Error('cross-node response timeout')); + }, timeoutMs); + socket.on('cluster:response', onClusterResponse); +}); + +const startNode = async (name: string): Promise => { + const httpServer = http.createServer(); + const io = new SocketIOServer(httpServer, { + path: '/ws', + transports: ['websocket'], + cors: { origin: '*' } + }); + const redisClient = createClient({ url: redisUrl }); + await redisClient.connect(); + io.adapter(createAdapter(redisClient)); + + io.on('connection', (socket) => { + socket.on('api:request', (payload, ack) => { + const response = { + ok: true, + operationId: payload?.operationId || 'unknown', + result: { + servedBy: name, + echoedInput: payload?.input || {} + } + }; + + if (payload?.operationId === 'crossNodeBroadcast') { + io.emit('cluster:response', { + ok: true, + sourceNode: name, + requestId: payload?.metadata?.requestId + }); + } else { + socket.emit('api:response', response); + } + + if (typeof ack === 'function') { + ack(response); + } + }); + }); + + await new Promise((resolve) => { + httpServer.listen(0, '127.0.0.1', () => resolve()); + }); + const address = httpServer.address() as AddressInfo; + + return { + name, + httpServer, + io, + redisClient, + port: address.port + }; +}; + +const stopNode = async (node: IRunningNode): Promise => { + await new Promise((resolve) => { + node.io.close(() => resolve()); + }); + await new Promise((resolve) => { + node.httpServer.close(() => resolve()); + }); + if (node.redisClient.isOpen) { + await node.redisClient.quit(); + } +}; + +describe('socket.io redis-streams multi-instance resilience', () => { + let nodeOne: IRunningNode; + let nodeTwo: IRunningNode; + let clientOne: Socket; + let clientTwo: Socket; + + beforeAll(async () => { + const probe = createClient({ url: redisUrl }); + await probe.connect(); + await probe.ping(); + await probe.quit(); + + nodeOne = await startNode('node-one'); + nodeTwo = await startNode('node-two'); + + clientOne = await connectClient(`ws://127.0.0.1:${nodeOne.port}`); + clientTwo = await connectClient(`ws://127.0.0.1:${nodeTwo.port}`); + }, 30000); + + afterAll(async () => { + if (clientOne?.connected) clientOne.disconnect(); + if (clientTwo?.connected) clientTwo.disconnect(); + if (nodeOne) await stopNode(nodeOne); + if (nodeTwo) await stopNode(nodeTwo); + }, 30000); + + it('should handle request/response independently on each node', async () => { + expect.hasAssertions(); + const responseOne = await emitWithAck( + clientOne, + 'api:request', + { operationId: 'ping', input: { value: 1 } } + ); + const responseTwo = await emitWithAck( + clientTwo, + 'api:request', + { operationId: 'ping', input: { value: 2 } } + ); + + expect(responseOne.result.servedBy).toBe('node-one'); + expect(responseTwo.result.servedBy).toBe('node-two'); + }); + + it('should propagate broadcast across nodes through redis streams adapter', async () => { + expect.hasAssertions(); + const requestId = `cross-${Date.now()}`; + const crossNodeEvent = waitForClusterResponse(clientTwo, requestId); + + const ack = await emitWithAck( + clientOne, + 'api:request', + { + operationId: 'crossNodeBroadcast', + metadata: { requestId } + } + ); + expect(ack.ok).toBe(true); + + const payload = await crossNodeEvent; + expect(payload.ok).toBe(true); + expect(payload.sourceNode).toBe('node-one'); + expect(payload.requestId).toBe(requestId); + }); +}); diff --git a/apps/backend-template/test/integration/realtime/websocket.basic.integration.test.ts b/apps/backend-template/test/integration/realtime/websocket.basic.integration.test.ts new file mode 100644 index 00000000..9b0995b1 --- /dev/null +++ b/apps/backend-template/test/integration/realtime/websocket.basic.integration.test.ts @@ -0,0 +1,87 @@ +/* global describe, it, expect, beforeAll, afterAll */ +import { io as createSocketClient, Socket } from 'socket.io-client'; +import { WebSocketAPI } from '@src/interface/WebSocket/WebSocketAPI'; +import { InMemoryDbClient } from '@src/infra/persistence/InMemoryDatabase/InMemoryDbClient'; +import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService'; +import { JwtService } from '@src/infra/jwt/JwtService'; +import { compileKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient'; +import { MutexService } from '@src/infra/mutex/adapter/MutexService'; +import { composeUsersAuthServices } from '@src/modules/Users'; +import { InMemoryMessageMediator } from '@src/infra/messages/InMemoryMessageMediator'; + +const databaseClient = InMemoryDbClient; +const passwordCryptoService = PasswordCryptoService.compile(); +const jwtService = JwtService.compile(); +const keyValueStorageClient = compileKeyValueStorageClient('inmemory'); +const mutexService = MutexService.compile(keyValueStorageClient); +const messageMediator = new InMemoryMessageMediator(); +const { authService } = composeUsersAuthServices({ + databaseClient, + passwordCryptoService, + mutexService, + jwtService, + keyValueStorageClient, + messageMediator +}); + +jest.setTimeout(30000); + +const emitWithAck = ( + socket: Socket, + payload: Record +): Promise => new Promise((resolve, reject) => { + socket.timeout(10000).emit( + 'api:request', + payload, + (ackError: any, ackPayload: any) => (ackError ? reject(ackError) : resolve(ackPayload)) + ); +}); + +describe('realtime websocket integration', () => { + const port = 33201; + let api: WebSocketAPI; + let client: Socket; + + beforeAll(async () => { + api = new WebSocketAPI({ + databaseClient, + host: '127.0.0.1', + port, + authService, + passwordCryptoService, + keyValueStorageClient, + mutexService, + eventBus: messageMediator, + messageMediator + }); + await api.start(); + client = createSocketClient(`ws://127.0.0.1:${port}`, { + path: '/ws', + transports: ['websocket'], + forceNew: true + }); + await new Promise((resolve, reject) => { + client.on('connect', () => resolve()); + client.on('connect_error', (error) => reject(error)); + }); + }); + + afterAll(async () => { + if (client?.connected) client.disconnect(); + await api.stop(); + }); + + it('responds with normalized error envelope for unknown operation', async () => { + expect.hasAssertions(); + const response = await emitWithAck(client, { + version: '1.0.0', + operationId: 'unknownOperation', + metadata: { requestId: 'ws-int-1' } + }); + + expect(response.ok).toBe(false); + expect(response.operationId).toBe('unknownOperation'); + expect(response.error).toBeDefined(); + expect(response.metadata.requestId).toBe('ws-int-1'); + }); +}); diff --git a/test/mock/BasicAuthorizationHeaderUser1.ts b/apps/backend-template/test/mock/BasicAuthorizationHeaderUser1.ts similarity index 100% rename from test/mock/BasicAuthorizationHeaderUser1.ts rename to apps/backend-template/test/mock/BasicAuthorizationHeaderUser1.ts diff --git a/test/mock/BasicAuthorizationHeaderUser2.ts b/apps/backend-template/test/mock/BasicAuthorizationHeaderUser2.ts similarity index 100% rename from test/mock/BasicAuthorizationHeaderUser2.ts rename to apps/backend-template/test/mock/BasicAuthorizationHeaderUser2.ts diff --git a/test/mock/BasicAuthorizationHeaderUser3.ts b/apps/backend-template/test/mock/BasicAuthorizationHeaderUser3.ts similarity index 100% rename from test/mock/BasicAuthorizationHeaderUser3.ts rename to apps/backend-template/test/mock/BasicAuthorizationHeaderUser3.ts diff --git a/test/mock/BasicAuthorizationHeaderUser4.ts b/apps/backend-template/test/mock/BasicAuthorizationHeaderUser4.ts similarity index 100% rename from test/mock/BasicAuthorizationHeaderUser4.ts rename to apps/backend-template/test/mock/BasicAuthorizationHeaderUser4.ts diff --git a/test/mock/BasicAuthorizationHeaderUserGuest.ts b/apps/backend-template/test/mock/BasicAuthorizationHeaderUserGuest.ts similarity index 100% rename from test/mock/BasicAuthorizationHeaderUserGuest.ts rename to apps/backend-template/test/mock/BasicAuthorizationHeaderUserGuest.ts diff --git a/test/mock/documents.ts b/apps/backend-template/test/mock/documents.ts similarity index 100% rename from test/mock/documents.ts rename to apps/backend-template/test/mock/documents.ts diff --git a/test/mock/emails.ts b/apps/backend-template/test/mock/emails.ts similarity index 100% rename from test/mock/emails.ts rename to apps/backend-template/test/mock/emails.ts diff --git a/test/mock/index.ts b/apps/backend-template/test/mock/index.ts similarity index 100% rename from test/mock/index.ts rename to apps/backend-template/test/mock/index.ts diff --git a/test/mock/phones.ts b/apps/backend-template/test/mock/phones.ts similarity index 100% rename from test/mock/phones.ts rename to apps/backend-template/test/mock/phones.ts diff --git a/test/mock/user1.ts b/apps/backend-template/test/mock/user1.ts similarity index 100% rename from test/mock/user1.ts rename to apps/backend-template/test/mock/user1.ts diff --git a/test/mock/user2.ts b/apps/backend-template/test/mock/user2.ts similarity index 100% rename from test/mock/user2.ts rename to apps/backend-template/test/mock/user2.ts diff --git a/test/mock/user3.ts b/apps/backend-template/test/mock/user3.ts similarity index 100% rename from test/mock/user3.ts rename to apps/backend-template/test/mock/user3.ts diff --git a/test/smoke/database/DatabaseDrivers.smoke.test.ts b/apps/backend-template/test/smoke/database/DatabaseDrivers.smoke.test.ts similarity index 100% rename from test/smoke/database/DatabaseDrivers.smoke.test.ts rename to apps/backend-template/test/smoke/database/DatabaseDrivers.smoke.test.ts diff --git a/apps/backend-template/test/smoke/realtime/RealtimeApis.smoke.test.ts b/apps/backend-template/test/smoke/realtime/RealtimeApis.smoke.test.ts new file mode 100644 index 00000000..f5c405a0 --- /dev/null +++ b/apps/backend-template/test/smoke/realtime/RealtimeApis.smoke.test.ts @@ -0,0 +1,133 @@ +/* global describe, it, expect, beforeAll, afterAll */ +import { io as createSocketClient, Socket } from 'socket.io-client'; +import path from 'path'; +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; +import { WebSocketAPI } from '@src/interface/WebSocket/WebSocketAPI'; +import { GrpcAPI } from '@src/interface/gRPC/gRPCAPI'; +import { InMemoryDbClient } from '@src/infra/persistence/InMemoryDatabase/InMemoryDbClient'; +import { PasswordCryptoService } from '@src/infra/security/PasswordCryptoService'; +import { JwtService } from '@src/infra/jwt/JwtService'; +import { compileKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient'; +import { MutexService } from '@src/infra/mutex/adapter/MutexService'; +import { composeUsersAuthServices } from '@src/modules/Users'; +import { InMemoryMessageMediator } from '@src/infra/messages/InMemoryMessageMediator'; + +const databaseClient = InMemoryDbClient; +const passwordCryptoService = PasswordCryptoService.compile(); +const jwtService = JwtService.compile(); +const keyValueStorageClient = compileKeyValueStorageClient('inmemory'); +const mutexService = MutexService.compile(keyValueStorageClient); +const messageMediator = new InMemoryMessageMediator(); +const { authService } = composeUsersAuthServices({ + databaseClient, + passwordCryptoService, + mutexService, + jwtService, + keyValueStorageClient, + messageMediator +}); + +jest.setTimeout(30000); + +const emitWithAck = ( + socket: Socket, + payload: Record +): Promise => new Promise((resolve, reject) => { + socket.timeout(10000).emit( + 'api:request', + payload, + (ackError: any, ackPayload: any) => (ackError ? reject(ackError) : resolve(ackPayload)) + ); +}); + +const grpcUnaryRequest = (client: any, payload: Record): Promise => ( + new Promise((resolve, reject) => { + client.request(payload, (error: Error | null, response: any) => ( + error ? reject(error) : resolve(response) + )); + }) +); + +describe('realtime api smoke', () => { + const wsPort = 33301; + const grpcPort = 33302; + let wsApi: WebSocketAPI; + let grpcApi: GrpcAPI; + let wsClient: Socket; + let grpcClient: any; + + beforeAll(async () => { + wsApi = new WebSocketAPI({ + databaseClient, + host: '127.0.0.1', + port: wsPort, + authService, + passwordCryptoService, + keyValueStorageClient, + mutexService, + eventBus: messageMediator, + messageMediator + }); + grpcApi = new GrpcAPI({ + databaseClient, + host: '127.0.0.1', + port: grpcPort, + authService, + passwordCryptoService, + keyValueStorageClient, + mutexService, + eventBus: messageMediator, + messageMediator + }); + await wsApi.start(); + await grpcApi.start(); + + wsClient = createSocketClient(`ws://127.0.0.1:${wsPort}`, { + path: '/ws', + transports: ['websocket'], + forceNew: true + }); + await new Promise((resolve, reject) => { + wsClient.on('connect', () => resolve()); + wsClient.on('connect_error', (error) => reject(error)); + }); + + const protoFilePath = path.resolve(process.cwd(), 'src/interface/gRPC/proto/async-api.proto'); + const packageDefinition = protoLoader.loadSync(protoFilePath, { + longs: String, + enums: String, + defaults: true, + oneofs: true + }); + const grpcObject = grpc.loadPackageDefinition(packageDefinition) as any; + grpcClient = new grpcObject.realtime.AsyncApiGateway( + `127.0.0.1:${grpcPort}`, + grpc.credentials.createInsecure() + ); + }); + + afterAll(async () => { + if (wsClient?.connected) wsClient.disconnect(); + await wsApi.stop(); + await grpcApi.stop(); + }); + + it('websocket and grpc servers both accept requests and reply envelopes', async () => { + expect.hasAssertions(); + const wsResponse = await emitWithAck( + wsClient, + { operationId: 'unknownOperation', metadata: { requestId: 'smoke-ws' } } + ); + + const grpcResponse = await grpcUnaryRequest(grpcClient, { + operationId: 'unknownOperation', + metadataJson: JSON.stringify({ requestId: 'smoke-grpc' }) + }); + + expect(typeof wsResponse.ok).toBe('boolean'); + expect(wsResponse.operationId).toBe('unknownOperation'); + expect(grpcResponse.ok).toBe(false); + expect(grpcResponse.operationId).toBe('unknownOperation'); + }); +}); diff --git a/apps/backend-template/test/unit/ci-cd/check-affected-workspaces.test.ts b/apps/backend-template/test/unit/ci-cd/check-affected-workspaces.test.ts new file mode 100644 index 00000000..285f6b1e --- /dev/null +++ b/apps/backend-template/test/unit/ci-cd/check-affected-workspaces.test.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + computeAffectedWorkspaces, + isDocsOnlyPath, + normalizePath +} = require('../../../../../ci-cd/check-affected-workspaces'); + +describe('check-affected-workspaces', () => { + it('normalizes paths and detects docs-only changes', () => { + expect.hasAssertions(); + expect(normalizePath('\\documentation\\md\\README.md')).toBe('/documentation/md/README.md'); + expect(isDocsOnlyPath('documentation/md/file.md')).toBe(true); + expect(isDocsOnlyPath('.agents/project-todos.md')).toBe(true); + expect(isDocsOnlyPath('src/modules/Users/index.ts')).toBe(false); + }); + + it('computes affected apps/packages and root marker changes', () => { + expect.hasAssertions(); + const result = computeAffectedWorkspaces([ + 'apps/backend-template/src/index.ts', + 'packages/sdk-rest-client/src/index.ts', + 'ci-cd/check-affected-workspaces.js' + ]); + + expect(result.root).toBe(true); + expect(result.apps).toStrictEqual(['backend-template']); + expect(result.packages).toStrictEqual(['sdk-rest-client']); + expect(result.docsOnly).toBe(false); + }); + + it('returns docsOnly true when all files are docs scoped', () => { + expect.hasAssertions(); + const result = computeAffectedWorkspaces([ + 'documentation/md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md', + '.agents/project-todos.md', + 'README.md' + ]); + expect(result.docsOnly).toBe(true); + expect(result.root).toBe(false); + expect(result.apps).toStrictEqual([]); + expect(result.packages).toStrictEqual([]); + }); + + it('ignores root files directly under apps/ and packages/ folders', () => { + expect.hasAssertions(); + const result = computeAffectedWorkspaces([ + 'apps/README.md', + 'packages/README.md', + 'packages/sdk-rest-client/src/index.ts' + ]); + expect(result.apps).toStrictEqual([]); + expect(result.packages).toStrictEqual(['sdk-rest-client']); + }); +}); diff --git a/apps/backend-template/test/unit/ci-cd/check-hexagonal-boundaries.test.ts b/apps/backend-template/test/unit/ci-cd/check-hexagonal-boundaries.test.ts new file mode 100644 index 00000000..b946be8c --- /dev/null +++ b/apps/backend-template/test/unit/ci-cd/check-hexagonal-boundaries.test.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + validateControllerFile, + readImports: readHexImports +} = require('../../../../../ci-cd/check-hexagonal-boundaries'); + +describe('check-hexagonal-boundaries', () => { + it('parses import statements', () => { + expect.hasAssertions(); + const imports = readHexImports(` + import { A } from "@src/modules/Users/application/ports/IUserUseCases"; + import { B } from "@src/interface/HTTP/ports"; + `); + expect(imports).toStrictEqual([ + '@src/modules/Users/application/ports/IUserUseCases', + '@src/interface/HTTP/ports' + ]); + }); + + it('flags forbidden controller patterns', () => { + expect.hasAssertions(); + const source = ` + import { UserService } from "@src/modules/Users/service/UserService"; + export class BrokenController { + constructor() { + const service = new UserService(); + return service; + } + } + `; + const violations = validateControllerFile( + source, + readHexImports(source), + 'src/modules/Users/adapters/in/http/controllers/BrokenController.ts' + ); + + expect(violations).toContain( + 'src/modules/Users/adapters/in/http/controllers/BrokenController.ts: controller must not import service implementation "@src/modules/Users/service/UserService"' + ); + expect(violations).toContain( + 'src/modules/Users/adapters/in/http/controllers/BrokenController.ts: HTTP controller must import application use-case contract' + ); + expect(violations).toContain( + 'src/modules/Users/adapters/in/http/controllers/BrokenController.ts: controller must not instantiate Service directly' + ); + }); + + it('accepts HTTP controller importing application use-case port', () => { + expect.hasAssertions(); + const source = ` + import { IUserUseCases } from "@src/modules/Users/application/ports/IUserUseCases"; + export class UserController { + constructor(private readonly useCases: IUserUseCases) {} + } + `; + const violations = validateControllerFile( + source, + readHexImports(source), + 'src/modules/Users/adapters/in/http/controllers/UserController.ts' + ); + expect(violations).toStrictEqual([]); + }); +}); diff --git a/apps/backend-template/test/unit/ci-cd/check-release-governance.test.ts b/apps/backend-template/test/unit/ci-cd/check-release-governance.test.ts new file mode 100644 index 00000000..9424030e --- /dev/null +++ b/apps/backend-template/test/unit/ci-cd/check-release-governance.test.ts @@ -0,0 +1,106 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + validateReleasePolicy, + validateRootReleaseScripts, + validatePackageReleaseMetadata, + validateAppReleaseMetadata +} = require('../../../../../ci-cd/check-release-governance'); + +describe('check-release-governance', () => { + it('accepts required root release scripts', () => { + expect.hasAssertions(); + const failures = validateRootReleaseScripts({ + scripts: { + 'changelog:update': 'node ci-cd/update-changelog.js', + 'changelog:check': 'node ci-cd/update-changelog.js --check', + 'release:dry-run': 'node ci-cd/release-dry-run.js all', + 'release:dry-run:packages': 'node ci-cd/release-dry-run.js packages', + 'release:dry-run:apps': 'node ci-cd/release-dry-run.js apps' + } + }); + expect(failures).toStrictEqual([]); + }); + + it('rejects missing root release scripts', () => { + expect.hasAssertions(); + const failures = validateRootReleaseScripts({ scripts: {} }); + expect(failures).toStrictEqual([ + '[root] missing required release script: changelog:update', + '[root] missing required release script: changelog:check', + '[root] missing required release script: release:dry-run', + '[root] missing required release script: release:dry-run:packages', + '[root] missing required release script: release:dry-run:apps' + ]); + }); + + it('accepts private package without files field when semver is valid', () => { + expect.hasAssertions(); + const failures = validatePackageReleaseMetadata({ + name: '@jumentix/private-app', + version: '1.0.0', + private: true + }, 'apps/private-app'); + expect(failures).toStrictEqual([]); + }); + + it('rejects non-private package with invalid version and missing files', () => { + expect.hasAssertions(); + const failures = validatePackageReleaseMetadata({ + name: '@jumentix/publishable', + version: '1.0', + private: false + }, 'packages/publishable'); + expect(failures).toStrictEqual([ + '[@jumentix/publishable] invalid semver version: 1.0', + '[@jumentix/publishable] publishable package must define non-empty files field' + ]); + }); + + it('accepts release policy when package/app strategies are valid', () => { + expect.hasAssertions(); + const failures = validateReleasePolicy({ + packageVersioning: 'independent', + appVersioning: 'locked', + appLockedVersion: '0.0.2' + }, { version: '0.0.2' }); + expect(failures).toStrictEqual([]); + }); + + it('rejects invalid release policy values', () => { + expect.hasAssertions(); + const failures = validateReleasePolicy({ + packageVersioning: 'locked', + appVersioning: 'independent', + appLockedVersion: '0.0' + }, { version: '0.0.2' }); + expect(failures).toStrictEqual([ + '[release-policy] packageVersioning must be "independent"', + '[release-policy] appVersioning must be "locked"', + '[release-policy] appLockedVersion must be a valid semver, got: 0.0', + '[release-policy] appLockedVersion must match root package version (0.0.2)' + ]); + }); + + it('accepts app metadata when app is private and version is locked', () => { + expect.hasAssertions(); + const failures = validateAppReleaseMetadata({ + name: '@jumentix/service-management', + version: '0.0.2', + private: true + }, 'apps/service-management', { appLockedVersion: '0.0.2' }); + expect(failures).toStrictEqual([]); + }); + + it('rejects app metadata when app is public or unlocked version', () => { + expect.hasAssertions(); + const failures = validateAppReleaseMetadata({ + name: '@jumentix/service-management', + version: '0.0.1', + private: false + }, 'apps/service-management', { appLockedVersion: '0.0.2' }); + expect(failures).toStrictEqual([ + '[@jumentix/service-management] app workspace must be private', + '[@jumentix/service-management] app version must match release-policy appLockedVersion (0.0.2)' + ]); + }); +}); diff --git a/apps/backend-template/test/unit/ci-cd/check-workspace-boundaries.test.ts b/apps/backend-template/test/unit/ci-cd/check-workspace-boundaries.test.ts new file mode 100644 index 00000000..6e657924 --- /dev/null +++ b/apps/backend-template/test/unit/ci-cd/check-workspace-boundaries.test.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require('path'); +const { classifyZone, readImports, validateImport } = require('../../../../../ci-cd/check-workspace-boundaries'); + +describe('check-workspace-boundaries', () => { + const rootDir = '/repo'; + + it('classifies workspace zones', () => { + expect.hasAssertions(); + expect(classifyZone(path.join('apps', 'backend-template', 'src', 'x.ts'))).toBe('backend'); + expect(classifyZone(path.join('apps', 'service-management', 'server.js'))).toBe('service-management'); + expect(classifyZone(path.join('packages', 'message-mediator', 'src', 'index.ts'))).toBe('package'); + expect(classifyZone(path.join('sdk-clients', 'rest', 'index.ts'))).toBe('legacy-sdk'); + }); + + it('reads static and dynamic imports from source', () => { + expect.hasAssertions(); + const source = ` + import x from '@src/modules/foo'; + const y = await import('@jumentix/message-mediator'); + const z = await import('./local-module'); + `; + expect(readImports(source)).toStrictEqual([ + '@src/modules/foo', + '@jumentix/message-mediator', + './local-module' + ]); + }); + + it('allows backend @src alias usage', () => { + expect.hasAssertions(); + const violations = validateImport({ + rootDir, + currentFile: path.join(rootDir, 'apps/backend-template/src/file.ts'), + relativeFilePath: path.join('apps', 'backend-template', 'src', 'file.ts'), + importPath: '@src/modules/Users' + }); + expect(violations).toStrictEqual([]); + }); + + it('allows explicitly bridged package @src alias usage during migration', () => { + expect.hasAssertions(); + const violations = validateImport({ + rootDir, + currentFile: path.join(rootDir, 'packages/external-store-proxy/src/ExternalStoreProxy.ts'), + relativeFilePath: path.join('packages', 'external-store-proxy', 'src', 'ExternalStoreProxy.ts'), + importPath: '@src/infra/exceptions' + }); + expect(violations).toStrictEqual([]); + }); + + it('blocks @src outside backend and app-cross imports from packages', () => { + expect.hasAssertions(); + const fromServiceManagement = validateImport({ + rootDir, + currentFile: path.join(rootDir, 'apps/service-management/src/file.ts'), + relativeFilePath: path.join('apps', 'service-management', 'src', 'file.ts'), + importPath: '@src/modules/Users' + }); + expect(fromServiceManagement).toContain( + 'apps/service-management/src/file.ts: @src alias is only allowed inside apps/backend-template' + ); + + const fromPackage = validateImport({ + rootDir, + currentFile: path.join(rootDir, 'packages/sdk-rest-client/src/index.ts'), + relativeFilePath: path.join('packages', 'sdk-rest-client', 'src', 'index.ts'), + importPath: '../../../apps/backend-template/src/interface/HTTP/RestAPI' + }); + expect(fromPackage).toContain( + 'packages/sdk-rest-client/src/index.ts: packages must not import from apps ("../../../apps/backend-template/src/interface/HTTP/RestAPI")' + ); + }); + + it('blocks legacy sdk-clients imports for non-legacy zones', () => { + expect.hasAssertions(); + const legacySdk = validateImport({ + rootDir, + currentFile: path.join(rootDir, 'apps/backend-template/src/file.ts'), + relativeFilePath: path.join('apps', 'backend-template', 'src', 'file.ts'), + importPath: 'sdk-clients/rest' + }); + expect(legacySdk).toContain( + 'apps/backend-template/src/file.ts: legacy sdk-clients import is forbidden ("sdk-clients/rest")' + ); + }); +}); diff --git a/apps/backend-template/test/unit/ci-cd/check-workspace-coverage-policy.test.ts b/apps/backend-template/test/unit/ci-cd/check-workspace-coverage-policy.test.ts new file mode 100644 index 00000000..6a8a269e --- /dev/null +++ b/apps/backend-template/test/unit/ci-cd/check-workspace-coverage-policy.test.ts @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + MINIMUM_GLOBAL_THRESHOLDS, + validateGlobalCoverageThreshold, + validatePackageCoveragePolicy +} = require('../../../../../ci-cd/check-workspace-coverage-policy'); + +describe('check-workspace-coverage-policy', () => { + it('accepts root jest global coverage thresholds when minimums are met', () => { + expect.hasAssertions(); + const failures = validateGlobalCoverageThreshold({ + coverageThreshold: { + global: { + statements: MINIMUM_GLOBAL_THRESHOLDS.statements, + lines: MINIMUM_GLOBAL_THRESHOLDS.lines, + functions: MINIMUM_GLOBAL_THRESHOLDS.functions, + branches: MINIMUM_GLOBAL_THRESHOLDS.branches + } + } + }); + expect(failures).toStrictEqual([]); + }); + + it('rejects root jest global coverage thresholds below minimums', () => { + expect.hasAssertions(); + const failures = validateGlobalCoverageThreshold({ + coverageThreshold: { + global: { + statements: 95, + lines: 95, + functions: 95, + branches: 80 + } + } + }); + expect(failures).toStrictEqual([ + 'Root coverageThreshold.global.statements must be >= 99 (current: 95)', + 'Root coverageThreshold.global.lines must be >= 99 (current: 95)', + 'Root coverageThreshold.global.functions must be >= 99 (current: 95)', + 'Root coverageThreshold.global.branches must be >= 90 (current: 80)' + ]); + }); + + it('accepts package test policy for non-placeholder scripts', () => { + expect.hasAssertions(); + const failures = validatePackageCoveragePolicy({ + name: '@jumentix/message-mediator', + scripts: { + test: 'npm run typecheck' + } + }); + expect(failures).toStrictEqual([]); + }); + + it('rejects placeholder test scripts for non-allowlisted packages', () => { + expect.hasAssertions(); + const failures = validatePackageCoveragePolicy({ + name: '@jumentix/runtime-infra', + scripts: { + test: 'echo "No tests yet for @jumentix/runtime-infra"' + } + }); + expect(failures).toStrictEqual([ + '[@jumentix/runtime-infra] test script must not be placeholder output' + ]); + }); + + it('accepts allowlisted placeholder test scripts for config placeholders', () => { + expect.hasAssertions(); + const failures = validatePackageCoveragePolicy({ + name: '@jumentix/config-eslint', + scripts: { + test: 'echo "config-eslint placeholder: migration wave pending"' + } + }); + expect(failures).toStrictEqual([]); + }); +}); diff --git a/apps/backend-template/test/unit/ci-cd/check-workspace-quality.test.ts b/apps/backend-template/test/unit/ci-cd/check-workspace-quality.test.ts new file mode 100644 index 00000000..d3bf8d1a --- /dev/null +++ b/apps/backend-template/test/unit/ci-cd/check-workspace-quality.test.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { validatePackageScripts } = require('../../../../../ci-cd/check-workspace-quality'); + +describe('check-workspace-quality', () => { + it('accepts package when required scripts exist', () => { + expect.hasAssertions(); + const failures = validatePackageScripts({ + name: '@jumentix/example', + scripts: { + build: 'tsc -p tsconfig.json', + test: 'npm run typecheck', + typecheck: 'tsc --noEmit' + } + }, 'packages/example'); + expect(failures).toStrictEqual([]); + }); + + it('rejects missing scripts and placeholder smoke test', () => { + expect.hasAssertions(); + const failures = validatePackageScripts({ + name: '@jumentix/example', + scripts: { + build: 'tsc -p tsconfig.json', + test: 'node -e "console.log(\'smoke ok\')"' + } + }, 'packages/example'); + expect(failures).toStrictEqual([ + '[@jumentix/example] missing required script: typecheck', + '[@jumentix/example] test script uses placeholder smoke output' + ]); + }); +}); diff --git a/apps/backend-template/test/unit/ci-cd/run-monorepo-ci.test.ts b/apps/backend-template/test/unit/ci-cd/run-monorepo-ci.test.ts new file mode 100644 index 00000000..6f9ec679 --- /dev/null +++ b/apps/backend-template/test/unit/ci-cd/run-monorepo-ci.test.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { resolveCiPlan, resolveInputFiles } = require('../../../../../ci-cd/run-monorepo-ci'); + +describe('run-monorepo-ci', () => { + it('returns docs-only lightweight plan', () => { + expect.hasAssertions(); + const plan = resolveCiPlan({ + root: false, + apps: [], + packages: [], + docsOnly: true, + files: ['README.md'] + }); + + expect(plan).toStrictEqual([ + ['npm', ['run', 'lint']], + ['npm', ['run', 'changelog:check']] + ]); + }); + + it('returns strict gate plus app/package scoped commands', () => { + expect.hasAssertions(); + const plan = resolveCiPlan({ + root: false, + apps: ['backend-template'], + packages: ['sdk-rest-client'], + docsOnly: false, + files: ['apps/backend-template/package.json', 'packages/sdk-rest-client/src/index.ts'] + }); + + expect(plan).toStrictEqual([ + ['npm', ['run', 'ci:gate:strict']], + ['npm', ['run', 'build', '--prefix', 'apps/backend-template']], + ['npm', ['run', 'test', '--prefix', 'apps/backend-template']], + ['npm', ['run', 'build', '--prefix', 'packages/sdk-rest-client']], + ['npm', ['run', 'test', '--prefix', 'packages/sdk-rest-client']] + ]); + }); + + it('uses explicit argv files when provided', () => { + expect.hasAssertions(); + const files = resolveInputFiles(['README.md', 'apps/backend-template/package.json'], { + baseRef: 'origin/main' + }); + expect(files).toStrictEqual(['README.md', 'apps/backend-template/package.json']); + }); + + it('reads changed files from git base ref when argv is empty', () => { + expect.hasAssertions(); + const readChangedFiles = jest.fn().mockReturnValue([ + 'apps/backend-template/src/index.ts' + ]); + + const files = resolveInputFiles([], { baseRef: 'origin/main', readChangedFiles }); + expect(files).toStrictEqual(['apps/backend-template/src/index.ts']); + expect(readChangedFiles).toHaveBeenCalledWith('origin/main'); + }); +}); diff --git a/test/unit/config/security.test.ts b/apps/backend-template/test/unit/config/security.test.ts similarity index 85% rename from test/unit/config/security.test.ts rename to apps/backend-template/test/unit/config/security.test.ts index 5f8dc52f..c226bbdf 100644 --- a/test/unit/config/security.test.ts +++ b/apps/backend-template/test/unit/config/security.test.ts @@ -14,26 +14,26 @@ describe('security config', () => { it('allows all origins outside production when allowlist is empty', () => { expect.assertions(1); - process.env.NODE_ENV = 'dev'; + (process.env as any).NODE_ENV = 'dev'; expect(isCorsOriginAllowed('https://random.example')).toBe(true); }); it('denies origins in production when allowlist is empty', () => { expect.assertions(1); - process.env.NODE_ENV = 'production'; + (process.env as any).NODE_ENV = 'production'; expect(isCorsOriginAllowed('https://random.example')).toBe(false); }); it('allows configured origin in production', () => { expect.assertions(1); - process.env.NODE_ENV = 'production'; + (process.env as any).NODE_ENV = 'production'; process.env.AAA_CORS_ALLOWED_ORIGINS = 'https://allowed.example'; expect(isCorsOriginAllowed('https://allowed.example')).toBe(true); }); it('allows any origin when wildcard is configured and allows undefined origin', () => { expect.hasAssertions(); - process.env.NODE_ENV = 'production'; + (process.env as any).NODE_ENV = 'production'; process.env.AAA_CORS_ALLOWED_ORIGINS = ' * '; expect(isCorsOriginAllowed('https://any.example')).toBe(true); expect(isCorsOriginAllowed(undefined)).toBe(true); @@ -41,7 +41,7 @@ describe('security config', () => { it('treats NODE_ENV=prod as production alias', () => { expect.hasAssertions(); - process.env.NODE_ENV = 'prod'; + (process.env as any).NODE_ENV = 'prod'; delete process.env.AAA_CORS_ALLOWED_ORIGINS; expect(isCorsOriginAllowed('https://random.example')).toBe(false); }); diff --git a/test/unit/domains/validators/index.test.ts b/apps/backend-template/test/unit/domains/validators/index.test.ts similarity index 100% rename from test/unit/domains/validators/index.test.ts rename to apps/backend-template/test/unit/domains/validators/index.test.ts diff --git a/test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts b/apps/backend-template/test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts similarity index 100% rename from test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts rename to apps/backend-template/test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts diff --git a/test/unit/infra/auth/AuthService.test.ts b/apps/backend-template/test/unit/infra/auth/AuthService.test.ts similarity index 99% rename from test/unit/infra/auth/AuthService.test.ts rename to apps/backend-template/test/unit/infra/auth/AuthService.test.ts index 1402b867..0632a6cc 100644 --- a/test/unit/infra/auth/AuthService.test.ts +++ b/apps/backend-template/test/unit/infra/auth/AuthService.test.ts @@ -191,10 +191,10 @@ describe('unit test suite for AuthService', () => { ); const { Authorization } = result!; // console.log(Authorization); - const rawToken = await jwtService.generateToken(user1); const [schema, token] = Authorization.split(' '); expect(schema).toBe(EAuthSchemaType.Bearer); - expect(rawToken).toBe(token); + const decoded = await jwtService.decodeToken(token); + expect(decoded!.username).toBe(user1.username); }); it('must not authenticate with Bearer auth schema with invalid username - return error - user not found', async () => { expect.hasAssertions(); diff --git a/apps/backend-template/test/unit/infra/cache/CacheService.test.ts b/apps/backend-template/test/unit/infra/cache/CacheService.test.ts new file mode 100644 index 00000000..5b897adc --- /dev/null +++ b/apps/backend-template/test/unit/infra/cache/CacheService.test.ts @@ -0,0 +1,91 @@ +import { CacheService } from '@src/infra/cache'; + +describe('cache service', () => { + const setup = () => { + const storage = { + get: jest.fn(), + set: jest.fn(), + del: jest.fn() + }; + const service = CacheService.compile({ + keyValueStorageClient: storage as any + }); + return { service, storage }; + }; + + it('stores and retrieves cache envelopes', async () => { + expect.hasAssertions(); + const { service, storage } = setup(); + storage.get.mockResolvedValueOnce({ + result: { + value: { id: 'u1' } + } + }); + + await service.set('users:key', { id: 'u1' }); + expect(storage.set).toHaveBeenCalledWith( + 'users:key', + expect.objectContaining({ + value: { id: 'u1' } + }) + ); + + const cached = await service.get<{ id: string }>('users:key'); + expect(cached).toStrictEqual({ id: 'u1' }); + }); + + it('returns raw values from storage when payload is not an envelope', async () => { + expect.hasAssertions(); + const { service, storage } = setup(); + storage.get.mockResolvedValueOnce({ result: 'raw-value' }); + const cached = await service.get('raw:key'); + expect(cached).toBe('raw-value'); + }); + + it('expires ttl-based values and deletes stale keys', async () => { + expect.hasAssertions(); + const { service, storage } = setup(); + storage.get.mockResolvedValueOnce({ + result: { + value: { id: 'u2' }, + expiresAt: Date.now() - 5 + } + }); + + const cached = await service.get<{ id: string }>('users:expired'); + expect(cached).toBeUndefined(); + expect(storage.del).toHaveBeenCalledWith('users:expired'); + }); + + it('sets ttl metadata and supports explicit key deletion', async () => { + expect.hasAssertions(); + const { service, storage } = setup(); + await service.set('users:ttl', { id: 'u3' }, 60); + expect(storage.set).toHaveBeenCalledWith( + 'users:ttl', + expect.objectContaining({ + value: { id: 'u3' }, + expiresAt: expect.any(Number) + }) + ); + + await service.del('users:ttl'); + expect(storage.del).toHaveBeenCalledWith('users:ttl'); + }); + + it('maintains cache namespace versions', async () => { + expect.hasAssertions(); + const { service, storage } = setup(); + storage.get + .mockResolvedValueOnce({ result: undefined }) + .mockResolvedValueOnce({ result: { value: 1 } }) + .mockResolvedValueOnce({ result: { value: 1 } }); + + const initial = await service.getVersion('users'); + expect(initial).toBe(1); + + const bumped = await service.bumpVersion('users'); + expect(bumped).toBe(2); + expect(storage.set).toHaveBeenCalledWith('cache:version:users', { value: 2 }); + }); +}); diff --git a/test/unit/infra/events/InMemoryEventBus.test.ts b/apps/backend-template/test/unit/infra/events/InMemoryEventBus.test.ts similarity index 100% rename from test/unit/infra/events/InMemoryEventBus.test.ts rename to apps/backend-template/test/unit/infra/events/InMemoryEventBus.test.ts diff --git a/test/unit/infra/exceptions/index.test.ts b/apps/backend-template/test/unit/infra/exceptions/index.test.ts similarity index 100% rename from test/unit/infra/exceptions/index.test.ts rename to apps/backend-template/test/unit/infra/exceptions/index.test.ts diff --git a/test/unit/infra/jwt/JwtService.test.ts b/apps/backend-template/test/unit/infra/jwt/JwtService.test.ts similarity index 100% rename from test/unit/infra/jwt/JwtService.test.ts rename to apps/backend-template/test/unit/infra/jwt/JwtService.test.ts diff --git a/test/unit/infra/messages/InMemoryMessageMediator.test.ts b/apps/backend-template/test/unit/infra/messages/InMemoryMessageMediator.test.ts similarity index 100% rename from test/unit/infra/messages/InMemoryMessageMediator.test.ts rename to apps/backend-template/test/unit/infra/messages/InMemoryMessageMediator.test.ts diff --git a/test/unit/infra/messages/MessageMediator.taskCreateFlow.test.ts b/apps/backend-template/test/unit/infra/messages/MessageMediator.taskCreateFlow.test.ts similarity index 100% rename from test/unit/infra/messages/MessageMediator.taskCreateFlow.test.ts rename to apps/backend-template/test/unit/infra/messages/MessageMediator.taskCreateFlow.test.ts diff --git a/test/unit/infra/messages/QueueRequestResponseRepository.test.ts b/apps/backend-template/test/unit/infra/messages/QueueRequestResponseRepository.test.ts similarity index 100% rename from test/unit/infra/messages/QueueRequestResponseRepository.test.ts rename to apps/backend-template/test/unit/infra/messages/QueueRequestResponseRepository.test.ts diff --git a/test/unit/infra/messages/compileMessageMediator.test.ts b/apps/backend-template/test/unit/infra/messages/compileMessageMediator.test.ts similarity index 100% rename from test/unit/infra/messages/compileMessageMediator.test.ts rename to apps/backend-template/test/unit/infra/messages/compileMessageMediator.test.ts diff --git a/test/unit/infra/mutex/MutexService.branches.test.ts b/apps/backend-template/test/unit/infra/mutex/MutexService.branches.test.ts similarity index 100% rename from test/unit/infra/mutex/MutexService.branches.test.ts rename to apps/backend-template/test/unit/infra/mutex/MutexService.branches.test.ts diff --git a/test/unit/infra/mutex/MutexService.test.ts b/apps/backend-template/test/unit/infra/mutex/MutexService.test.ts similarity index 100% rename from test/unit/infra/mutex/MutexService.test.ts rename to apps/backend-template/test/unit/infra/mutex/MutexService.test.ts diff --git a/test/unit/infra/persistence/InMemoryDatabase/InMemoryDbClient.test.ts b/apps/backend-template/test/unit/infra/persistence/InMemoryDatabase/InMemoryDbClient.test.ts similarity index 100% rename from test/unit/infra/persistence/InMemoryDatabase/InMemoryDbClient.test.ts rename to apps/backend-template/test/unit/infra/persistence/InMemoryDatabase/InMemoryDbClient.test.ts diff --git a/test/unit/infra/persistence/InMemoryDatabase/InMemoryRelationalStore.test.ts b/apps/backend-template/test/unit/infra/persistence/InMemoryDatabase/InMemoryRelationalStore.test.ts similarity index 100% rename from test/unit/infra/persistence/InMemoryDatabase/InMemoryRelationalStore.test.ts rename to apps/backend-template/test/unit/infra/persistence/InMemoryDatabase/InMemoryRelationalStore.test.ts diff --git a/test/unit/infra/persistence/InMemoryDatabase/UserStoreAPI.test.ts b/apps/backend-template/test/unit/infra/persistence/InMemoryDatabase/UserStoreAPI.test.ts similarity index 100% rename from test/unit/infra/persistence/InMemoryDatabase/UserStoreAPI.test.ts rename to apps/backend-template/test/unit/infra/persistence/InMemoryDatabase/UserStoreAPI.test.ts diff --git a/test/unit/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.test.ts b/apps/backend-template/test/unit/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.test.ts similarity index 100% rename from test/unit/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.test.ts rename to apps/backend-template/test/unit/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.test.ts diff --git a/test/unit/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.test.ts b/apps/backend-template/test/unit/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.test.ts similarity index 100% rename from test/unit/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.test.ts rename to apps/backend-template/test/unit/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.test.ts diff --git a/apps/backend-template/test/unit/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.test.ts b/apps/backend-template/test/unit/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.test.ts new file mode 100644 index 00000000..b1fb0d3e --- /dev/null +++ b/apps/backend-template/test/unit/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.test.ts @@ -0,0 +1,34 @@ +import { compileKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient'; +import { compileKeyValueStorageClient as compileFromPackage } from '@jumentix/key-value-storage'; + +const createMockClient = (driver?: string) => ({ + connected: true, + get: jest.fn(async () => ({ result: driver ?? 'default' })), + del: jest.fn(async () => ({ result: driver ?? 'default' })), + set: jest.fn(async () => ({ result: driver ?? 'default' })), + disconnect: jest.fn(async () => ({ result: { connected: false } })), + connect: jest.fn(async () => ({ result: { connected: true } })), + driver: driver ?? 'default' +}); + +jest.mock('@jumentix/key-value-storage', () => ({ + ...jest.requireActual('@jumentix/key-value-storage'), + compileKeyValueStorageClient: jest.fn((driver?: string) => createMockClient(driver) as any) +})); + +describe('compileKeyValueStorageClient', () => { + it('forwards in-memory aliases to package compiler', () => { + expect.hasAssertions(); + expect(compileKeyValueStorageClient('InMemory') as any).toMatchObject({ driver: 'InMemory' }); + expect(compileKeyValueStorageClient('memory') as any).toMatchObject({ driver: 'memory' }); + expect(compileKeyValueStorageClient('in-memory') as any).toMatchObject({ driver: 'in-memory' }); + }); + + it('forwards redis/default aliases to package compiler', () => { + expect.hasAssertions(); + expect(compileKeyValueStorageClient(undefined) as any).toMatchObject({ driver: 'default' }); + expect(compileKeyValueStorageClient('redis') as any).toMatchObject({ driver: 'redis' }); + expect(compileKeyValueStorageClient('unknown') as any).toMatchObject({ driver: 'unknown' }); + expect(compileFromPackage).toHaveBeenCalledTimes(6); + }); +}); diff --git a/test/unit/infra/persistence/compileDatabaseClient.test.ts b/apps/backend-template/test/unit/infra/persistence/compileDatabaseClient.test.ts similarity index 97% rename from test/unit/infra/persistence/compileDatabaseClient.test.ts rename to apps/backend-template/test/unit/infra/persistence/compileDatabaseClient.test.ts index 0874c0d5..9e6872e3 100644 --- a/test/unit/infra/persistence/compileDatabaseClient.test.ts +++ b/apps/backend-template/test/unit/infra/persistence/compileDatabaseClient.test.ts @@ -101,7 +101,7 @@ describe('compileDatabaseClient', () => { process.env.AAA_FIREBASE_SERVICE_ACCOUNT_JSON = JSON.stringify({ project_id: 'demo-project' }); const firebaseClient = compileDatabaseClient(); expect(firebaseClient).not.toBe(InMemoryDbClient); - await expect(firebaseClient.connect()).rejects.toThrow('private_key'); + await expect(firebaseClient.connect()).rejects.toThrow(/private_key|Missing optional dependency/); await expect(firebaseClient.disconnect()).resolves.toBeUndefined(); process.env.AAA_DATABASE_DRIVER = 'rds'; diff --git a/test/unit/infra/persistence/external/ExternalRepositories.test.ts b/apps/backend-template/test/unit/infra/persistence/external/ExternalRepositories.test.ts similarity index 100% rename from test/unit/infra/persistence/external/ExternalRepositories.test.ts rename to apps/backend-template/test/unit/infra/persistence/external/ExternalRepositories.test.ts diff --git a/test/unit/infra/persistence/external/ExternalStoreProxy.test.ts b/apps/backend-template/test/unit/infra/persistence/external/ExternalStoreProxy.test.ts similarity index 100% rename from test/unit/infra/persistence/external/ExternalStoreProxy.test.ts rename to apps/backend-template/test/unit/infra/persistence/external/ExternalStoreProxy.test.ts diff --git a/test/unit/infra/security/PasswordCryptoService.test.ts b/apps/backend-template/test/unit/infra/security/PasswordCryptoService.test.ts similarity index 100% rename from test/unit/infra/security/PasswordCryptoService.test.ts rename to apps/backend-template/test/unit/infra/security/PasswordCryptoService.test.ts diff --git a/test/unit/interface/Async/RealtimeAPIBase.test.ts b/apps/backend-template/test/unit/interface/Async/RealtimeAPIBase.test.ts similarity index 100% rename from test/unit/interface/Async/RealtimeAPIBase.test.ts rename to apps/backend-template/test/unit/interface/Async/RealtimeAPIBase.test.ts diff --git a/test/unit/interface/CLI/catalogStorage.test.ts b/apps/backend-template/test/unit/interface/CLI/catalogStorage.test.ts similarity index 100% rename from test/unit/interface/CLI/catalogStorage.test.ts rename to apps/backend-template/test/unit/interface/CLI/catalogStorage.test.ts diff --git a/test/unit/interface/CLI/domainManager.test.ts b/apps/backend-template/test/unit/interface/CLI/domainManager.test.ts similarity index 100% rename from test/unit/interface/CLI/domainManager.test.ts rename to apps/backend-template/test/unit/interface/CLI/domainManager.test.ts diff --git a/test/unit/interface/CLI/entityModelManager.test.ts b/apps/backend-template/test/unit/interface/CLI/entityModelManager.test.ts similarity index 100% rename from test/unit/interface/CLI/entityModelManager.test.ts rename to apps/backend-template/test/unit/interface/CLI/entityModelManager.test.ts diff --git a/test/unit/interface/CLI/index.test.ts b/apps/backend-template/test/unit/interface/CLI/index.test.ts similarity index 100% rename from test/unit/interface/CLI/index.test.ts rename to apps/backend-template/test/unit/interface/CLI/index.test.ts diff --git a/test/unit/interface/CLI/prompt.test.ts b/apps/backend-template/test/unit/interface/CLI/prompt.test.ts similarity index 100% rename from test/unit/interface/CLI/prompt.test.ts rename to apps/backend-template/test/unit/interface/CLI/prompt.test.ts diff --git a/test/unit/interface/HTTP/adapters/start-rest-api.test.ts b/apps/backend-template/test/unit/interface/HTTP/adapters/start-rest-api.test.ts similarity index 67% rename from test/unit/interface/HTTP/adapters/start-rest-api.test.ts rename to apps/backend-template/test/unit/interface/HTTP/adapters/start-rest-api.test.ts index d609b5c6..103e5e29 100644 --- a/test/unit/interface/HTTP/adapters/start-rest-api.test.ts +++ b/apps/backend-template/test/unit/interface/HTTP/adapters/start-rest-api.test.ts @@ -13,14 +13,26 @@ describe('start-rest-api adapter loader', () => { return {}; }); const { startRestApiAdapter } = await import('@src/interface/HTTP/adapters/start-rest-api'); - await startRestApiAdapter({ AAA_HTTP_FRAMEWORK: 'express' } as NodeJS.ProcessEnv); + await startRestApiAdapter({ AAA_HTTP_FRAMEWORK: 'express' } as unknown as NodeJS.ProcessEnv); expect(expressAdapterEvaluated).toHaveBeenCalledTimes(1); }); + it('loads fastify adapter when framework is fastify', async () => { + expect.assertions(1); + const fastifyAdapterEvaluated = jest.fn(); + jest.doMock('@src/interface/HTTP/adapters/fastify/fastify', () => { + fastifyAdapterEvaluated(); + return {}; + }); + const { startRestApiAdapter } = await import('@src/interface/HTTP/adapters/start-rest-api'); + await startRestApiAdapter({ AAA_HTTP_FRAMEWORK: 'fastify' } as unknown as NodeJS.ProcessEnv); + expect(fastifyAdapterEvaluated).toHaveBeenCalledTimes(1); + }); + it('throws for unsupported framework', async () => { expect.assertions(1); const { startRestApiAdapter } = await import('@src/interface/HTTP/adapters/start-rest-api'); - await expect(startRestApiAdapter({ AAA_HTTP_FRAMEWORK: 'fastify' } as NodeJS.ProcessEnv)) + await expect(startRestApiAdapter({ AAA_HTTP_FRAMEWORK: 'unknown-http' } as unknown as NodeJS.ProcessEnv)) .rejects .toThrow('Unsupported AAA_HTTP_FRAMEWORK'); }); diff --git a/test/unit/interface/HTTP/ports/BaseController.test.ts b/apps/backend-template/test/unit/interface/HTTP/ports/BaseController.test.ts similarity index 100% rename from test/unit/interface/HTTP/ports/BaseController.test.ts rename to apps/backend-template/test/unit/interface/HTTP/ports/BaseController.test.ts diff --git a/test/unit/interface/HTTP/validators/index.test.ts b/apps/backend-template/test/unit/interface/HTTP/validators/index.test.ts similarity index 100% rename from test/unit/interface/HTTP/validators/index.test.ts rename to apps/backend-template/test/unit/interface/HTTP/validators/index.test.ts diff --git a/test/unit/interface/WebSocket/WebSocketAPI.test.ts b/apps/backend-template/test/unit/interface/WebSocket/WebSocketAPI.test.ts similarity index 89% rename from test/unit/interface/WebSocket/WebSocketAPI.test.ts rename to apps/backend-template/test/unit/interface/WebSocket/WebSocketAPI.test.ts index 28a44835..fb4218b3 100644 --- a/test/unit/interface/WebSocket/WebSocketAPI.test.ts +++ b/apps/backend-template/test/unit/interface/WebSocket/WebSocketAPI.test.ts @@ -141,4 +141,22 @@ describe('websocket api', () => { onConnectionCall[1](socket); expect(bindSocketSpy).toHaveBeenCalledWith(socket); }); + + it('runs optional socket.io configure/cleanup hooks', async () => { + expect.hasAssertions(); + const configureSocketIo = jest.fn().mockResolvedValue(undefined); + const cleanupSocketIo = jest.fn().mockResolvedValue(undefined); + const api = new WebSocketAPI({ + databaseClient, + specDir: './spec/asyncapi', + configureSocketIo, + cleanupSocketIo + }); + + await api.start(); + await api.stop(); + + expect(configureSocketIo).toHaveBeenCalledTimes(1); + expect(cleanupSocketIo).toHaveBeenCalledTimes(1); + }); }); diff --git a/apps/backend-template/test/unit/interface/WebSocket/adapters/clusterAdapter.lifecycle.test.ts b/apps/backend-template/test/unit/interface/WebSocket/adapters/clusterAdapter.lifecycle.test.ts new file mode 100644 index 00000000..2fd90062 --- /dev/null +++ b/apps/backend-template/test/unit/interface/WebSocket/adapters/clusterAdapter.lifecycle.test.ts @@ -0,0 +1,41 @@ +/* eslint-disable jest/no-untyped-mock-factory */ +import cluster from 'cluster'; +import { Server } from 'socket.io'; + +const setupPrimaryMock = jest.fn(); +const createAdapterMock = jest.fn(); + +jest.mock('@socket.io/cluster-adapter', () => ({ + setupPrimary: (...args: any[]) => setupPrimaryMock(...args), + createAdapter: (...args: any[]) => createAdapterMock(...args) +})); + +describe('clusterAdapter lifecycle', () => { + beforeEach(() => { + setupPrimaryMock.mockReset(); + createAdapterMock.mockReset(); + }); + + it('sets up cluster primary process serialization and adapter bridge', async () => { + expect.hasAssertions(); + const setupPrimarySpy = jest.spyOn(cluster, 'setupPrimary').mockImplementation(() => undefined as any); + const fakeIo = { adapter: jest.fn() } as unknown as Server; + const fakeAdapter = jest.fn(); + createAdapterMock.mockReturnValue(fakeAdapter); + const { + setupSocketIoClusterPrimary, + createClusterSocketIoAdapter + } = await import('@src/interface/WebSocket/adapters/socket-io/clusterAdapter'); + + setupSocketIoClusterPrimary(); + const adapter = createClusterSocketIoAdapter(); + await adapter.configure(fakeIo); + await adapter.cleanup(); + + expect(setupPrimaryMock).toHaveBeenCalledTimes(1); + expect(setupPrimarySpy).toHaveBeenCalledWith({ serialization: 'advanced' }); + expect(createAdapterMock).toHaveBeenCalledTimes(1); + expect((fakeIo.adapter as any)).toHaveBeenCalledWith(fakeAdapter); + setupPrimarySpy.mockRestore(); + }); +}); diff --git a/apps/backend-template/test/unit/interface/WebSocket/adapters/redisStreamsAdapter.lifecycle.test.ts b/apps/backend-template/test/unit/interface/WebSocket/adapters/redisStreamsAdapter.lifecycle.test.ts new file mode 100644 index 00000000..f1a178b1 --- /dev/null +++ b/apps/backend-template/test/unit/interface/WebSocket/adapters/redisStreamsAdapter.lifecycle.test.ts @@ -0,0 +1,57 @@ +/* eslint-disable jest/no-untyped-mock-factory */ +import { Server } from 'socket.io'; + +const connectMock = jest.fn(); +const quitMock = jest.fn(); +const createClientMock = jest.fn(); +const createAdapterMock = jest.fn(); + +jest.mock('redis', () => ({ + createClient: (...args: any[]) => createClientMock(...args) +})); + +jest.mock('@socket.io/redis-streams-adapter', () => ({ + createAdapter: (...args: any[]) => createAdapterMock(...args) +})); + +describe('redisStreamsAdapter lifecycle', () => { + beforeEach(() => { + connectMock.mockReset(); + quitMock.mockReset(); + createClientMock.mockReset(); + createAdapterMock.mockReset(); + }); + + it('configures socket.io adapter and connects redis when client is closed', async () => { + expect.hasAssertions(); + const io = { adapter: jest.fn() } as unknown as Server; + const fakeRedisClient = { isOpen: false, connect: connectMock, quit: quitMock }; + const fakeAdapter = jest.fn(); + createClientMock.mockReturnValue(fakeRedisClient); + createAdapterMock.mockReturnValue(fakeAdapter); + const { createRedisStreamsSocketIoAdapter } = await import('@src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter'); + + const adapter = createRedisStreamsSocketIoAdapter(); + await adapter.configure(io); + + expect(connectMock).toHaveBeenCalledTimes(1); + expect(createAdapterMock).toHaveBeenCalledWith(fakeRedisClient); + expect((io.adapter as any)).toHaveBeenCalledWith(fakeAdapter); + }); + + it('skips connect when redis client is already open and quits on cleanup', async () => { + expect.hasAssertions(); + const io = { adapter: jest.fn() } as unknown as Server; + const fakeRedisClient = { isOpen: true, connect: connectMock, quit: quitMock }; + createClientMock.mockReturnValue(fakeRedisClient); + createAdapterMock.mockReturnValue(jest.fn()); + const { createRedisStreamsSocketIoAdapter } = await import('@src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter'); + + const adapter = createRedisStreamsSocketIoAdapter(); + await adapter.configure(io); + await adapter.cleanup(); + + expect(connectMock).toHaveBeenCalledTimes(0); + expect(quitMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/unit/interface/WebSocket/adapters/socket-io.bootstrap.test.ts b/apps/backend-template/test/unit/interface/WebSocket/adapters/socket-io.bootstrap.test.ts similarity index 59% rename from test/unit/interface/WebSocket/adapters/socket-io.bootstrap.test.ts rename to apps/backend-template/test/unit/interface/WebSocket/adapters/socket-io.bootstrap.test.ts index 4d46b189..607ad617 100644 --- a/test/unit/interface/WebSocket/adapters/socket-io.bootstrap.test.ts +++ b/apps/backend-template/test/unit/interface/WebSocket/adapters/socket-io.bootstrap.test.ts @@ -8,6 +8,16 @@ const compileDatabaseClientMock = jest.fn().mockReturnValue disconnect: jest.fn().mockResolvedValue(undefined), stores: {} as IDatabaseClient['stores'] }); +const isClusterSocketIoEnabledMock = jest.fn().mockReturnValue(false); +const isRedisStreamsSocketIoEnabledMock = jest.fn().mockReturnValue(false); +const createClusterSocketIoAdapterMock = jest.fn().mockReturnValue({ + configure: jest.fn().mockResolvedValue(undefined), + cleanup: jest.fn().mockResolvedValue(undefined) +}); +const createRedisStreamsSocketIoAdapterMock = jest.fn().mockReturnValue({ + configure: jest.fn().mockResolvedValue(undefined), + cleanup: jest.fn().mockResolvedValue(undefined) +}); jest.mock('@src/interface/WebSocket/WebSocketAPI', () => ({ WebSocketAPI: jest.fn().mockImplementation(() => ({ @@ -61,6 +71,20 @@ jest.mock('@src/infra/persistence/compileDatabaseClient', () => ({ compileDatabaseClient: () => compileDatabaseClientMock() })); +jest.mock('@src/interface/WebSocket/adapters/socket-io/clusterAdapter', () => ({ + isClusterSocketIoEnabled: (...args: any[]) => isClusterSocketIoEnabledMock(...args), + createClusterSocketIoAdapter: (...args: any[]) => createClusterSocketIoAdapterMock(...args), + setupSocketIoClusterPrimary: jest.fn(), + resolveWebSocketClusterWorkers: jest.fn().mockReturnValue(1) +})); + +jest.mock('@src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter', () => ({ + isRedisStreamsSocketIoEnabled: (...args: any[]) => isRedisStreamsSocketIoEnabledMock(...args), + createRedisStreamsSocketIoAdapter: (...args: any[]) => ( + createRedisStreamsSocketIoAdapterMock(...args) + ) +})); + describe('websocket socket-io adapter bootstrap', () => { const originalEnv = process.env; @@ -68,6 +92,12 @@ describe('websocket socket-io adapter bootstrap', () => { process.env = { ...originalEnv }; websocketAdapterStart.mockClear(); websocketFallbackRestStart.mockClear(); + isClusterSocketIoEnabledMock.mockReset(); + isRedisStreamsSocketIoEnabledMock.mockReset(); + createClusterSocketIoAdapterMock.mockClear(); + createRedisStreamsSocketIoAdapterMock.mockClear(); + isClusterSocketIoEnabledMock.mockReturnValue(false); + isRedisStreamsSocketIoEnabledMock.mockReturnValue(false); }); afterAll(() => { @@ -99,4 +129,23 @@ describe('websocket socket-io adapter bootstrap', () => { expect(websocketFallbackRestStart).toHaveBeenCalledTimes(1); expect(websocketAdapterStart).toHaveBeenCalledTimes(1); }); + + it('selects cluster socket.io adapter when cluster mode is enabled', async () => { + expect.assertions(2); + isClusterSocketIoEnabledMock.mockReturnValue(true); + const { startWebSocketAdapter } = await import('@src/interface/WebSocket/adapters/socket-io/socket-io'); + await startWebSocketAdapter(); + expect(createClusterSocketIoAdapterMock).toHaveBeenCalledTimes(1); + expect(createRedisStreamsSocketIoAdapterMock).toHaveBeenCalledTimes(0); + }); + + it('selects redis-streams socket.io adapter when enabled and cluster is disabled', async () => { + expect.assertions(2); + isClusterSocketIoEnabledMock.mockReturnValue(false); + isRedisStreamsSocketIoEnabledMock.mockReturnValue(true); + const { startWebSocketAdapter } = await import('@src/interface/WebSocket/adapters/socket-io/socket-io'); + await startWebSocketAdapter(); + expect(createRedisStreamsSocketIoAdapterMock).toHaveBeenCalledTimes(1); + expect(createClusterSocketIoAdapterMock).toHaveBeenCalledTimes(0); + }); }); diff --git a/apps/backend-template/test/unit/interface/WebSocket/adapters/start-websocket-api.test.ts b/apps/backend-template/test/unit/interface/WebSocket/adapters/start-websocket-api.test.ts new file mode 100644 index 00000000..6372e9f0 --- /dev/null +++ b/apps/backend-template/test/unit/interface/WebSocket/adapters/start-websocket-api.test.ts @@ -0,0 +1,112 @@ +/* eslint-disable jest/no-untyped-mock-factory */ + +const websocketLoaderAdapterStart = jest.fn().mockResolvedValue(undefined); +const clusterForkMock = jest.fn(); +const clusterOnMock = jest.fn(); +const isClusterSocketIoEnabledMock = jest.fn().mockReturnValue(false); +const resolveWebSocketClusterWorkersMock = jest.fn().mockReturnValue(2); +const setupSocketIoClusterPrimaryMock = jest.fn(); + +jest.mock('@src/interface/WebSocket/adapters/socket-io/socket-io', () => ({ + startWebSocketAdapter: websocketLoaderAdapterStart +})); + +jest.mock('cluster', () => ({ + __esModule: true, + default: { + isPrimary: true, + fork: (...args: any[]) => clusterForkMock(...args), + on: (...args: any[]) => clusterOnMock(...args), + setupPrimary: jest.fn() + } +})); + +jest.mock('@src/interface/WebSocket/adapters/socket-io/clusterAdapter', () => ({ + isClusterSocketIoEnabled: (...args: any[]) => isClusterSocketIoEnabledMock(...args), + resolveWebSocketClusterWorkers: (...args: any[]) => resolveWebSocketClusterWorkersMock(...args), + setupSocketIoClusterPrimary: (...args: any[]) => setupSocketIoClusterPrimaryMock(...args) +})); + +describe('start-websocket-api loader', () => { + beforeEach(() => { + websocketLoaderAdapterStart.mockClear(); + clusterForkMock.mockClear(); + clusterOnMock.mockClear(); + isClusterSocketIoEnabledMock.mockReset(); + isClusterSocketIoEnabledMock.mockReturnValue(false); + resolveWebSocketClusterWorkersMock.mockClear(); + setupSocketIoClusterPrimaryMock.mockClear(); + }); + + it('starts websocket runtime when realtime protocol is websocket', async () => { + expect.assertions(2); + const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); + const started = await startWebSocketApiAdapter({ + AAA_REALTIME_API: 'yes', + AAA_REALTIME_API_PROTOCOL: 'websocket' + } as unknown as NodeJS.ProcessEnv); + expect(started).toBe(true); + expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(1); + }); + + it('does not start websocket runtime when realtime protocol is grpc', async () => { + expect.assertions(2); + const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); + const started = await startWebSocketApiAdapter({ + AAA_REALTIME_API: 'yes', + AAA_REALTIME_API_PROTOCOL: 'grpc' + } as unknown as NodeJS.ProcessEnv); + expect(started).toBe(false); + expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(0); + }); + + it('uses process env when env argument is omitted', async () => { + expect.assertions(2); + process.env.AAA_REALTIME_API = 'yes'; + process.env.AAA_REALTIME_API_PROTOCOL = 'websocket'; + const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); + const started = await startWebSocketApiAdapter(); + expect(started).toBe(true); + expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(1); + }); + + it('starts in cluster primary mode and forks workers', async () => { + expect.hasAssertions(); + const clusterModule = await import('cluster'); + (clusterModule.default as any).isPrimary = true; + isClusterSocketIoEnabledMock.mockReturnValue(true); + resolveWebSocketClusterWorkersMock.mockReturnValue(3); + + const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); + await startWebSocketApiAdapter({ + AAA_REALTIME_API: 'yes', + AAA_REALTIME_API_PROTOCOL: 'websocket', + AAA_WEBSOCKET_SOCKETIO_ADAPTER: 'cluster' + } as unknown as NodeJS.ProcessEnv); + + expect(setupSocketIoClusterPrimaryMock).toHaveBeenCalledTimes(1); + expect(clusterForkMock).toHaveBeenCalledTimes(3); + expect(clusterOnMock).toHaveBeenCalledWith('exit', expect.any(Function)); + const onExitCallback = clusterOnMock.mock.calls.find((call) => call[0] === 'exit')?.[1] as (() => void); + onExitCallback(); + expect(clusterForkMock).toHaveBeenCalledTimes(4); + expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(0); + }); + + it('starts websocket adapter in cluster worker mode', async () => { + expect.assertions(2); + const clusterModule = await import('cluster'); + (clusterModule.default as any).isPrimary = false; + isClusterSocketIoEnabledMock.mockReturnValue(true); + + const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); + const started = await startWebSocketApiAdapter({ + AAA_REALTIME_API: 'yes', + AAA_REALTIME_API_PROTOCOL: 'websocket', + AAA_WEBSOCKET_SOCKETIO_ADAPTER: 'cluster' + } as unknown as NodeJS.ProcessEnv); + + expect(started).toBe(true); + expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/backend-template/test/unit/interface/WebSocket/clusterAdapter.test.ts b/apps/backend-template/test/unit/interface/WebSocket/clusterAdapter.test.ts new file mode 100644 index 00000000..cd2bdc5d --- /dev/null +++ b/apps/backend-template/test/unit/interface/WebSocket/clusterAdapter.test.ts @@ -0,0 +1,37 @@ +import os from 'os'; +import { + isClusterSocketIoEnabled, + resolveWebSocketClusterWorkers +} from '@src/interface/WebSocket/adapters/socket-io/clusterAdapter'; + +describe('clusterAdapter', () => { + it('should enable cluster adapter when configured', () => { + expect.hasAssertions(); + expect(isClusterSocketIoEnabled({ + AAA_WEBSOCKET_SOCKETIO_ADAPTER: 'cluster' + } as unknown as NodeJS.ProcessEnv)).toBe(true); + }); + + it('should disable cluster adapter for other values', () => { + expect.hasAssertions(); + expect(isClusterSocketIoEnabled({ + AAA_WEBSOCKET_SOCKETIO_ADAPTER: 'redis-streams' + } as unknown as NodeJS.ProcessEnv)).toBe(false); + }); + + it('should resolve worker count from env', () => { + expect.hasAssertions(); + expect(resolveWebSocketClusterWorkers({ + AAA_WEBSOCKET_CLUSTER_WORKERS: '8' + } as unknown as NodeJS.ProcessEnv)).toBe(8); + }); + + it('should fallback to cpu count when env is missing/invalid', () => { + expect.hasAssertions(); + const fallback = Math.max(1, os.cpus().length); + expect(resolveWebSocketClusterWorkers({} as unknown as NodeJS.ProcessEnv)).toBe(fallback); + expect(resolveWebSocketClusterWorkers({ + AAA_WEBSOCKET_CLUSTER_WORKERS: '0' + } as unknown as NodeJS.ProcessEnv)).toBe(fallback); + }); +}); diff --git a/apps/backend-template/test/unit/interface/WebSocket/redisStreamsAdapter.test.ts b/apps/backend-template/test/unit/interface/WebSocket/redisStreamsAdapter.test.ts new file mode 100644 index 00000000..7272c4a3 --- /dev/null +++ b/apps/backend-template/test/unit/interface/WebSocket/redisStreamsAdapter.test.ts @@ -0,0 +1,46 @@ +import { + buildRedisConnectionUrl, + isRedisStreamsSocketIoEnabled +} from '@src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter'; + +describe('redisStreamsAdapter', () => { + it('should enable redis streams adapter only when explicitly configured', () => { + expect.hasAssertions(); + expect(isRedisStreamsSocketIoEnabled({ + AAA_WEBSOCKET_SOCKETIO_ADAPTER: 'redis-streams' + } as unknown as NodeJS.ProcessEnv)).toBe(true); + + expect(isRedisStreamsSocketIoEnabled({ + AAA_WEBSOCKET_SOCKETIO_ADAPTER: 'none' + } as unknown as NodeJS.ProcessEnv)).toBe(false); + }); + + it('should prefer websocket specific redis url when provided', () => { + expect.hasAssertions(); + const url = buildRedisConnectionUrl({ + AAA_WEBSOCKET_REDIS_URL: 'redis://10.0.0.9:6379/3', + AAA_REDIS_URL: 'redis://10.0.0.1:6379/1' + } as unknown as NodeJS.ProcessEnv); + expect(url).toBe('redis://10.0.0.9:6379/3'); + }); + + it('should fallback to global redis url when websocket url is not set', () => { + expect.hasAssertions(); + const url = buildRedisConnectionUrl({ + AAA_REDIS_URL: 'redis://10.0.0.1:6379/1' + } as unknown as NodeJS.ProcessEnv); + expect(url).toBe('redis://10.0.0.1:6379/1'); + }); + + it('should compose redis url from host, port, database and password', () => { + expect.hasAssertions(); + const redisTestPassword = ['redis', 'test', 'password'].join('-'); + const url = buildRedisConnectionUrl({ + AAA_REDIS_HOST: '127.0.0.1', + AAA_REDIS_PORT: '6380', + AAA_REDIS_DATABASE: '5', + AAA_REDIS_PASSWORD: redisTestPassword + } as unknown as NodeJS.ProcessEnv); + expect(url).toBe(`redis://:${redisTestPassword}@127.0.0.1:6380/5`); + }); +}); diff --git a/test/unit/interface/gRPC/adapters/grpc.bootstrap.test.ts b/apps/backend-template/test/unit/interface/gRPC/adapters/grpc.bootstrap.test.ts similarity index 100% rename from test/unit/interface/gRPC/adapters/grpc.bootstrap.test.ts rename to apps/backend-template/test/unit/interface/gRPC/adapters/grpc.bootstrap.test.ts diff --git a/test/unit/interface/gRPC/adapters/start-grpc-api.test.ts b/apps/backend-template/test/unit/interface/gRPC/adapters/start-grpc-api.test.ts similarity index 95% rename from test/unit/interface/gRPC/adapters/start-grpc-api.test.ts rename to apps/backend-template/test/unit/interface/gRPC/adapters/start-grpc-api.test.ts index 9f8b6425..b4509d95 100644 --- a/test/unit/interface/gRPC/adapters/start-grpc-api.test.ts +++ b/apps/backend-template/test/unit/interface/gRPC/adapters/start-grpc-api.test.ts @@ -17,7 +17,7 @@ describe('start-grpc-api loader', () => { const started = await startGrpcApiAdapter({ AAA_REALTIME_API: 'yes', AAA_REALTIME_API_PROTOCOL: 'grpc' - } as NodeJS.ProcessEnv); + } as unknown as NodeJS.ProcessEnv); expect(started).toBe(true); expect(grpcLoaderAdapterStart).toHaveBeenCalledTimes(1); }); @@ -28,7 +28,7 @@ describe('start-grpc-api loader', () => { const started = await startGrpcApiAdapter({ AAA_REALTIME_API: 'yes', AAA_REALTIME_API_PROTOCOL: 'websocket' - } as NodeJS.ProcessEnv); + } as unknown as NodeJS.ProcessEnv); expect(started).toBe(false); expect(grpcLoaderAdapterStart).toHaveBeenCalledTimes(0); }); diff --git a/test/unit/interface/gRPC/gRPCAPI.test.ts b/apps/backend-template/test/unit/interface/gRPC/gRPCAPI.test.ts similarity index 83% rename from test/unit/interface/gRPC/gRPCAPI.test.ts rename to apps/backend-template/test/unit/interface/gRPC/gRPCAPI.test.ts index 4a9fcd47..18e356f3 100644 --- a/test/unit/interface/gRPC/gRPCAPI.test.ts +++ b/apps/backend-template/test/unit/interface/gRPC/gRPCAPI.test.ts @@ -11,6 +11,13 @@ const protoMockState: Record = {}; jest.mock('@grpc/grpc-js', () => ({ __esModule: true, + Server: function MockedGrpcServer(...args: any[]) { + return grpcMockState.Server(...args); + }, + loadPackageDefinition: (...args: any[]) => grpcMockState.loadPackageDefinition(...args), + ServerCredentials: { + createInsecure: (...args: any[]) => grpcMockState.createInsecure(...args) + }, default: { Server: function MockedGrpcServer(...args: any[]) { return grpcMockState.Server(...args); @@ -24,6 +31,7 @@ jest.mock('@grpc/grpc-js', () => ({ jest.mock('@grpc/proto-loader', () => ({ __esModule: true, + loadSync: (...args: any[]) => protoMockState.loadSync(...args), default: { loadSync: (...args: any[]) => protoMockState.loadSync(...args) } @@ -208,4 +216,31 @@ describe('grpc api', () => { }); await expect(api.start()).rejects.toThrow('bind failed'); }); + + it('supports module fallback when grpc/proto-loader default export is undefined', async () => { + expect.hasAssertions(); + const grpcModule: any = await import('@grpc/grpc-js'); + const protoLoaderModule: any = await import('@grpc/proto-loader'); + const previousGrpcDefault = grpcModule.default; + const previousProtoDefault = protoLoaderModule.default; + grpcModule.default = undefined; + protoLoaderModule.default = undefined; + + const api = new GrpcAPI({ + databaseClient, + specDir: './spec/asyncapi' + }); + + await api.start(); + await api.stop(); + + expect(protoMockState.loadSync).toHaveBeenCalledWith( + expect.any(String), + expect.any(Object) + ); + expect(grpcMockState.start).toHaveBeenCalledWith(); + + grpcModule.default = previousGrpcDefault; + protoLoaderModule.default = previousProtoDefault; + }); }); diff --git a/test/unit/interface/runtime/RuntimeEnvironment.test.ts b/apps/backend-template/test/unit/interface/runtime/RuntimeEnvironment.test.ts similarity index 66% rename from test/unit/interface/runtime/RuntimeEnvironment.test.ts rename to apps/backend-template/test/unit/interface/runtime/RuntimeEnvironment.test.ts index f4a77b8c..fb7573da 100644 --- a/test/unit/interface/runtime/RuntimeEnvironment.test.ts +++ b/apps/backend-template/test/unit/interface/runtime/RuntimeEnvironment.test.ts @@ -8,39 +8,39 @@ import { describe('runtime environment', () => { it('uses express as default framework', () => { expect.assertions(1); - expect(resolveHTTPFramework({} as NodeJS.ProcessEnv)).toBe('express'); + expect(resolveHTTPFramework({} as unknown as NodeJS.ProcessEnv)).toBe('express'); }); it('normalizes framework and protocol values from env strings', () => { expect.assertions(2); - expect(resolveHTTPFramework({ AAA_HTTP_FRAMEWORK: ' EXPRESS ' } as NodeJS.ProcessEnv)).toBe('express'); + expect(resolveHTTPFramework({ AAA_HTTP_FRAMEWORK: ' EXPRESS ' } as unknown as NodeJS.ProcessEnv)).toBe('express'); expect(resolveRealtimeApiProtocol({ AAA_REALTIME_API_PROTOCOL: ' GRPC ' - } as NodeJS.ProcessEnv)).toBe('grpc'); + } as unknown as NodeJS.ProcessEnv)).toBe('grpc'); }); - it('throws for unsupported framework', () => { + it('supports non-express frameworks and throws for unknown framework', () => { expect.assertions(1); - expect(() => resolveHTTPFramework({ AAA_HTTP_FRAMEWORK: 'fastify' } as NodeJS.ProcessEnv)) + expect(() => resolveHTTPFramework({ AAA_HTTP_FRAMEWORK: 'unknown-http' } as unknown as NodeJS.ProcessEnv)) .toThrow('Unsupported AAA_HTTP_FRAMEWORK'); }); it('resolves realtime enablement from env', () => { expect.assertions(3); - expect(isRealtimeApiEnabled({ AAA_REALTIME_API: 'yes' } as NodeJS.ProcessEnv)).toBe(true); - expect(isRealtimeApiEnabled({ AAA_REALTIME_API: 'no' } as NodeJS.ProcessEnv)).toBe(false); - expect(isRealtimeApiEnabled({} as NodeJS.ProcessEnv)).toBe(false); + expect(isRealtimeApiEnabled({ AAA_REALTIME_API: 'yes' } as unknown as NodeJS.ProcessEnv)).toBe(true); + expect(isRealtimeApiEnabled({ AAA_REALTIME_API: 'no' } as unknown as NodeJS.ProcessEnv)).toBe(false); + expect(isRealtimeApiEnabled({} as unknown as NodeJS.ProcessEnv)).toBe(false); }); it('resolves realtime protocol with websocket default', () => { expect.assertions(2); - expect(resolveRealtimeApiProtocol({} as NodeJS.ProcessEnv)).toBe('websocket'); - expect(resolveRealtimeApiProtocol({ AAA_REALTIME_API_PROTOCOL: 'grpc' } as NodeJS.ProcessEnv)).toBe('grpc'); + expect(resolveRealtimeApiProtocol({} as unknown as NodeJS.ProcessEnv)).toBe('websocket'); + expect(resolveRealtimeApiProtocol({ AAA_REALTIME_API_PROTOCOL: 'grpc' } as unknown as NodeJS.ProcessEnv)).toBe('grpc'); }); it('throws for unsupported realtime protocol', () => { expect.assertions(1); - expect(() => resolveRealtimeApiProtocol({ AAA_REALTIME_API_PROTOCOL: 'mqtt' } as NodeJS.ProcessEnv)) + expect(() => resolveRealtimeApiProtocol({ AAA_REALTIME_API_PROTOCOL: 'mqtt' } as unknown as NodeJS.ProcessEnv)) .toThrow('Unsupported AAA_REALTIME_API_PROTOCOL'); }); @@ -49,17 +49,17 @@ describe('runtime environment', () => { expect(shouldStartRealtimeApi('websocket', { AAA_REALTIME_API: 'yes', AAA_REALTIME_API_PROTOCOL: 'websocket' - } as NodeJS.ProcessEnv)).toBe(true); + } as unknown as NodeJS.ProcessEnv)).toBe(true); expect(shouldStartRealtimeApi('grpc', { AAA_REALTIME_API: 'yes', AAA_REALTIME_API_PROTOCOL: 'websocket' - } as NodeJS.ProcessEnv)).toBe(false); + } as unknown as NodeJS.ProcessEnv)).toBe(false); expect(shouldStartRealtimeApi('grpc', { AAA_REALTIME_API: 'no', AAA_REALTIME_API_PROTOCOL: 'grpc' - } as NodeJS.ProcessEnv)).toBe(false); + } as unknown as NodeJS.ProcessEnv)).toBe(false); }); it('uses process env defaults when env argument is omitted', () => { diff --git a/test/unit/modules/Users/application/OrganizationUseCases.test.ts b/apps/backend-template/test/unit/modules/Users/application/OrganizationUseCases.test.ts similarity index 100% rename from test/unit/modules/Users/application/OrganizationUseCases.test.ts rename to apps/backend-template/test/unit/modules/Users/application/OrganizationUseCases.test.ts diff --git a/test/unit/modules/Users/application/useCases.test.ts b/apps/backend-template/test/unit/modules/Users/application/useCases.test.ts similarity index 100% rename from test/unit/modules/Users/application/useCases.test.ts rename to apps/backend-template/test/unit/modules/Users/application/useCases.test.ts diff --git a/test/unit/modules/Users/composition/composeUsersAuthServices.test.ts b/apps/backend-template/test/unit/modules/Users/composition/composeUsersAuthServices.test.ts similarity index 100% rename from test/unit/modules/Users/composition/composeUsersAuthServices.test.ts rename to apps/backend-template/test/unit/modules/Users/composition/composeUsersAuthServices.test.ts diff --git a/test/unit/modules/Users/domain/Model/Organization.test.ts b/apps/backend-template/test/unit/modules/Users/domain/Model/Organization.test.ts similarity index 100% rename from test/unit/modules/Users/domain/Model/Organization.test.ts rename to apps/backend-template/test/unit/modules/Users/domain/Model/Organization.test.ts diff --git a/test/unit/modules/Users/domain/Model/User.test.ts b/apps/backend-template/test/unit/modules/Users/domain/Model/User.test.ts similarity index 100% rename from test/unit/modules/Users/domain/Model/User.test.ts rename to apps/backend-template/test/unit/modules/Users/domain/Model/User.test.ts diff --git a/test/unit/modules/Users/domain/security/Rbac.test.ts b/apps/backend-template/test/unit/modules/Users/domain/security/Rbac.test.ts similarity index 100% rename from test/unit/modules/Users/domain/security/Rbac.test.ts rename to apps/backend-template/test/unit/modules/Users/domain/security/Rbac.test.ts diff --git a/test/unit/modules/Users/events/events.test.ts b/apps/backend-template/test/unit/modules/Users/events/events.test.ts similarity index 100% rename from test/unit/modules/Users/events/events.test.ts rename to apps/backend-template/test/unit/modules/Users/events/events.test.ts diff --git a/test/unit/modules/Users/events/registerUserEventListeners.test.ts b/apps/backend-template/test/unit/modules/Users/events/registerUserEventListeners.test.ts similarity index 100% rename from test/unit/modules/Users/events/registerUserEventListeners.test.ts rename to apps/backend-template/test/unit/modules/Users/events/registerUserEventListeners.test.ts diff --git a/test/unit/modules/Users/events/registerUserMessageHandlers.test.ts b/apps/backend-template/test/unit/modules/Users/events/registerUserMessageHandlers.test.ts similarity index 100% rename from test/unit/modules/Users/events/registerUserMessageHandlers.test.ts rename to apps/backend-template/test/unit/modules/Users/events/registerUserMessageHandlers.test.ts diff --git a/test/unit/modules/Users/factories.test.ts b/apps/backend-template/test/unit/modules/Users/factories.test.ts similarity index 100% rename from test/unit/modules/Users/factories.test.ts rename to apps/backend-template/test/unit/modules/Users/factories.test.ts diff --git a/test/unit/modules/Users/features/index.test.ts b/apps/backend-template/test/unit/modules/Users/features/index.test.ts similarity index 100% rename from test/unit/modules/Users/features/index.test.ts rename to apps/backend-template/test/unit/modules/Users/features/index.test.ts diff --git a/test/unit/modules/Users/index.exports.test.ts b/apps/backend-template/test/unit/modules/Users/index.exports.test.ts similarity index 100% rename from test/unit/modules/Users/index.exports.test.ts rename to apps/backend-template/test/unit/modules/Users/index.exports.test.ts diff --git a/test/unit/modules/Users/interface/controller/controllers.test.ts b/apps/backend-template/test/unit/modules/Users/interface/controller/controllers.test.ts similarity index 100% rename from test/unit/modules/Users/interface/controller/controllers.test.ts rename to apps/backend-template/test/unit/modules/Users/interface/controller/controllers.test.ts diff --git a/test/unit/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createWebSocketOperationHandler.test.ts b/apps/backend-template/test/unit/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createWebSocketOperationHandler.test.ts similarity index 100% rename from test/unit/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createWebSocketOperationHandler.test.ts rename to apps/backend-template/test/unit/modules/Users/interface/websocketapi/frameworks/socket-io/handlers/createWebSocketOperationHandler.test.ts diff --git a/test/unit/modules/Users/service/AuthService.audit.test.ts b/apps/backend-template/test/unit/modules/Users/service/AuthService.audit.test.ts similarity index 100% rename from test/unit/modules/Users/service/AuthService.audit.test.ts rename to apps/backend-template/test/unit/modules/Users/service/AuthService.audit.test.ts diff --git a/test/unit/modules/Users/service/AuthService.branches.test.ts b/apps/backend-template/test/unit/modules/Users/service/AuthService.branches.test.ts similarity index 95% rename from test/unit/modules/Users/service/AuthService.branches.test.ts rename to apps/backend-template/test/unit/modules/Users/service/AuthService.branches.test.ts index d86984cf..f5395880 100644 --- a/test/unit/modules/Users/service/AuthService.branches.test.ts +++ b/apps/backend-template/test/unit/modules/Users/service/AuthService.branches.test.ts @@ -50,7 +50,7 @@ describe('auth service extra branches', () => { beforeEach(() => { process.env = { ...originalEnv, - NODE_ENV: 'dev', + NODE_ENV: 'development', AAA_ENABLE_BASIC_AUTH: 'yes' }; }); @@ -201,7 +201,7 @@ describe('auth service extra branches', () => { it('covers production credential masking and basic schema disablement', async () => { expect.hasAssertions(); - process.env.NODE_ENV = 'production'; + (process.env as any).NODE_ENV = 'production'; process.env.AAA_ENABLE_BASIC_AUTH = 'no'; const { service, passwordCryptoService } = setup(); @@ -309,7 +309,7 @@ describe('auth service extra branches', () => { process.env.AAA_AUTH_MAX_LOGIN_ATTEMPTS = ''; process.env.AAA_AUTH_LOGIN_WINDOW_SECONDS = ''; process.env.AAA_AUTH_LOCKOUT_SECONDS = ''; - process.env.NODE_ENV = 'prod'; + (process.env as any).NODE_ENV = 'prod'; const { service, passwordCryptoService } = setup(); passwordCryptoService.compare.mockResolvedValueOnce(false); const failed = await service.authenticate('john', 'invalid', EAuthSchemaType.Bearer); @@ -336,4 +336,19 @@ describe('auth service extra branches', () => { expect(response.result).toBe(true); expect(keyValueStorageClient.set).not.toHaveBeenCalled(); }); + + it('covers logout revocation branch when key-value storage is disabled', async () => { + expect.hasAssertions(); + const { service, jwtService } = setup(); + jwtService.decodeToken.mockReturnValueOnce({ + id: 'u1', + username: 'john', + jti: 'token-id', + exp: Math.floor(Date.now() / 1000) + 120 + }); + + const response = await service.logout('Bearer token'); + expect(response.result).toBe(true); + expect(response.error).toBeUndefined(); + }); }); diff --git a/test/unit/modules/Users/service/AuthService.rbac.test.ts b/apps/backend-template/test/unit/modules/Users/service/AuthService.rbac.test.ts similarity index 100% rename from test/unit/modules/Users/service/AuthService.rbac.test.ts rename to apps/backend-template/test/unit/modules/Users/service/AuthService.rbac.test.ts diff --git a/test/unit/modules/Users/service/OrganizationService.test.ts b/apps/backend-template/test/unit/modules/Users/service/OrganizationService.test.ts similarity index 79% rename from test/unit/modules/Users/service/OrganizationService.test.ts rename to apps/backend-template/test/unit/modules/Users/service/OrganizationService.test.ts index 455ea5f3..65e8f469 100644 --- a/test/unit/modules/Users/service/OrganizationService.test.ts +++ b/apps/backend-template/test/unit/modules/Users/service/OrganizationService.test.ts @@ -45,6 +45,55 @@ describe('organization service', () => { expect(dataRepository.create).toHaveBeenCalledWith({ name: 'Org' }); }); + it('uses cache for read operations and invalidates after mutations', async () => { + expect.hasAssertions(); + const dataRepository = { + create: jest.fn().mockResolvedValue({ id: 'o1', name: 'Org' }), + update: jest.fn().mockResolvedValue({ id: 'o1', name: 'Org Updated' }), + delete: jest.fn().mockResolvedValue(true), + getOneById: jest.fn().mockResolvedValue({ id: 'o1', name: 'Org' }), + getAll: jest.fn().mockResolvedValue({ + page: 1, + size: 10, + total: 1, + result: [{ id: 'o1', name: 'Org' }] + }) + }; + const cacheService = { + get: jest.fn().mockResolvedValueOnce(undefined).mockResolvedValueOnce({ id: 'o1', name: 'Org' }), + set: jest.fn().mockResolvedValue(undefined), + getVersion: jest.fn().mockResolvedValue(1), + bumpVersion: jest.fn().mockResolvedValue(2) + }; + const service = OrganizationService.compile({ + dataRepository, + services: { cacheService } + } as any); + + await service.getOneById('o1'); + expect(dataRepository.getOneById).toHaveBeenCalledTimes(1); + expect(cacheService.set).toHaveBeenCalledWith( + 'organizations:v1:getOneById:o1', + { id: 'o1', name: 'Org' } + ); + + await service.getOneById('o1'); + expect(dataRepository.getOneById).toHaveBeenCalledTimes(1); + + await service.getAll({ ids: ['o1', 'o2'] as any }, { page: 1, size: 10 }); + cacheService.get.mockResolvedValueOnce({ + page: 1, + size: 10, + total: 1, + result: [{ id: 'o1', name: 'Org' }] + }); + await service.getAll({}, { page: 1, size: 10 }); + expect(dataRepository.getAll).toHaveBeenCalledTimes(1); + + await service.update('o1', { name: 'Org Updated' } as any); + expect(cacheService.bumpVersion).toHaveBeenCalledWith('organizations'); + }); + it('returns service errors when repository fails', async () => { expect.hasAssertions(); const { service, dataRepository } = setup(); diff --git a/test/unit/modules/Users/service/UserProviderLocal.test.ts b/apps/backend-template/test/unit/modules/Users/service/UserProviderLocal.test.ts similarity index 100% rename from test/unit/modules/Users/service/UserProviderLocal.test.ts rename to apps/backend-template/test/unit/modules/Users/service/UserProviderLocal.test.ts diff --git a/test/unit/modules/Users/service/UserService.sanitization.test.ts b/apps/backend-template/test/unit/modules/Users/service/UserService.sanitization.test.ts similarity index 100% rename from test/unit/modules/Users/service/UserService.sanitization.test.ts rename to apps/backend-template/test/unit/modules/Users/service/UserService.sanitization.test.ts diff --git a/test/unit/modules/Users/service/UserService.test.ts b/apps/backend-template/test/unit/modules/Users/service/UserService.test.ts similarity index 85% rename from test/unit/modules/Users/service/UserService.test.ts rename to apps/backend-template/test/unit/modules/Users/service/UserService.test.ts index 18808f91..d80bba1c 100644 --- a/test/unit/modules/Users/service/UserService.test.ts +++ b/apps/backend-template/test/unit/modules/Users/service/UserService.test.ts @@ -81,6 +81,12 @@ const setup = () => { compare: jest.fn() }; const eventBus = { publish: jest.fn().mockResolvedValue(true) }; + const cacheService = { + get: jest.fn().mockResolvedValue(undefined), + set: jest.fn().mockResolvedValue(undefined), + getVersion: jest.fn().mockResolvedValue(1), + bumpVersion: jest.fn().mockResolvedValue(2) + }; const organizationDataRepository = { getOneById: jest.fn().mockResolvedValue({ id: 'org-1', @@ -102,7 +108,12 @@ const setup = () => { const service = new UserService({ dataRepository: dataRepository as any, organizationDataRepository: organizationDataRepository as any, - services: { mutexService, passwordCryptoService, eventBus } + services: { + mutexService, + passwordCryptoService, + eventBus, + cacheService + } } as any); return { @@ -110,6 +121,7 @@ const setup = () => { mutexService, passwordCryptoService, eventBus, + cacheService, organizationDataRepository, dataRepository }; @@ -160,6 +172,42 @@ describe('user service', () => { expect((all.result?.[0] as any)?.password).toBeUndefined(); }); + it('uses read-through cache for getOneById/getAll and invalidates on write', async () => { + expect.hasAssertions(); + const { service, cacheService } = setup(); + cacheService.get + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(baseUser); + + await service.getOneById(baseUser.id); + expect(cacheService.getVersion).toHaveBeenCalledWith('users'); + expect(cacheService.set).toHaveBeenCalledWith( + `users:v1:getOneById:${baseUser.id}`, + expect.objectContaining({ id: baseUser.id }) + ); + expect(mockedGetUserById).toHaveBeenCalledTimes(1); + + await service.getAll({ username: 'john' }, { page: 1, size: 10 }); + expect(cacheService.set).toHaveBeenCalledWith( + 'users:v1:getAll:{"filters":{"username":"john"},"paging":{"page":1,"size":10}}', + expect.any(Object) + ); + expect(mockedGetAllUsers).toHaveBeenCalledTimes(1); + + await service.getAll({ ids: ['u1', 'u2'] as any }, { page: 1, size: 10 }); + + await service.update(baseUser.id, { firstName: 'Mary' } as any); + expect(cacheService.bumpVersion).toHaveBeenCalledWith('users'); + + await service.getOneById(baseUser.id); + expect(mockedGetUserById).toHaveBeenCalledTimes(2); + + cacheService.get.mockResolvedValueOnce(baseUser); + await service.getOneById(baseUser.id); + expect(mockedGetUserById).toHaveBeenCalledTimes(2); + }); + it('covers update/delete and lock conflict behavior', async () => { expect.hasAssertions(); const { service, mutexService } = setup(); @@ -204,6 +252,10 @@ describe('user service', () => { const getAll = await service.getAll({}, { page: 1, size: 10 }); expect(getAll.error).toBeDefined(); + mockedGetUserById.mockRejectedValueOnce(new Error('not found')); + const getOne = await service.getOneById(baseUser.id); + expect(getOne.error).toBeDefined(); + const invalidCreate = await service.create({ firstName: 'John', username: 'john', @@ -367,4 +419,29 @@ describe('user service', () => { } as any); expect(service).toBeInstanceOf(UserService); }); + + it('covers sanitizer fallback branches for empty inputs', () => { + expect.hasAssertions(); + expect((UserService as any).sanitizeUser(undefined)).toBeUndefined(); + expect((UserService as any).sanitizeUsers(undefined)).toStrictEqual([]); + }); + + it('covers organization sync branch when organization users list is undefined', async () => { + expect.hasAssertions(); + const { service, organizationDataRepository } = setup(); + organizationDataRepository.getOneById.mockResolvedValueOnce({ + id: 'org-2', + name: 'Org 2', + address: [], + phone: [], + email: [] + }); + + await (service as any).syncOrganizationUsers(baseUser.id, '', 'org-2'); + + expect(organizationDataRepository.update).toHaveBeenCalledWith( + 'org-2', + expect.objectContaining({ users: [baseUser.id] }) + ); + }); }); diff --git a/test/unit/modules/ddd/valueObjects/AddressValueObject.test.ts b/apps/backend-template/test/unit/modules/ddd/valueObjects/AddressValueObject.test.ts similarity index 100% rename from test/unit/modules/ddd/valueObjects/AddressValueObject.test.ts rename to apps/backend-template/test/unit/modules/ddd/valueObjects/AddressValueObject.test.ts diff --git a/test/unit/modules/ddd/valueObjects/DocumentValueObject.test.ts b/apps/backend-template/test/unit/modules/ddd/valueObjects/DocumentValueObject.test.ts similarity index 100% rename from test/unit/modules/ddd/valueObjects/DocumentValueObject.test.ts rename to apps/backend-template/test/unit/modules/ddd/valueObjects/DocumentValueObject.test.ts diff --git a/test/unit/modules/port/BaseService.test.ts b/apps/backend-template/test/unit/modules/port/BaseService.test.ts similarity index 100% rename from test/unit/modules/port/BaseService.test.ts rename to apps/backend-template/test/unit/modules/port/BaseService.test.ts diff --git a/test/unit/modules/port/core.test.ts b/apps/backend-template/test/unit/modules/port/core.test.ts similarity index 100% rename from test/unit/modules/port/core.test.ts rename to apps/backend-template/test/unit/modules/port/core.test.ts diff --git a/test/unit/modules/port/index.exports.test.ts b/apps/backend-template/test/unit/modules/port/index.exports.test.ts similarity index 100% rename from test/unit/modules/port/index.exports.test.ts rename to apps/backend-template/test/unit/modules/port/index.exports.test.ts diff --git a/test/unit/modules/port/relations.test.ts b/apps/backend-template/test/unit/modules/port/relations.test.ts similarity index 100% rename from test/unit/modules/port/relations.test.ts rename to apps/backend-template/test/unit/modules/port/relations.test.ts diff --git a/apps/backend-template/test/unit/sdk-clients/grpc/GrpcApiClient.test.ts b/apps/backend-template/test/unit/sdk-clients/grpc/GrpcApiClient.test.ts new file mode 100644 index 00000000..5b818455 --- /dev/null +++ b/apps/backend-template/test/unit/sdk-clients/grpc/GrpcApiClient.test.ts @@ -0,0 +1,125 @@ +/* eslint-disable jest/no-untyped-mock-factory */ +const loadSpecsMock = jest.fn(); +const loadSyncMock = jest.fn(); +const loadPackageDefinitionMock = jest.fn(); +const createInsecureMock = jest.fn(); +const requestMock = jest.fn(); + +jest.mock('../../../../../../packages/sdk-grpc-client/src/spec/loadSpecs', () => ({ + loadSpecs: () => loadSpecsMock() +})); + +jest.mock('@grpc/proto-loader', () => ({ + __esModule: true, + loadSync: (...args: any[]) => loadSyncMock(...args) +})); + +jest.mock('@grpc/grpc-js', () => ({ + __esModule: true, + loadPackageDefinition: (...args: any[]) => loadPackageDefinitionMock(...args), + credentials: { + createInsecure: (...args: any[]) => createInsecureMock(...args) + }, + default: { + loadPackageDefinition: (...args: any[]) => loadPackageDefinitionMock(...args), + credentials: { + createInsecure: (...args: any[]) => createInsecureMock(...args) + } + } +})); + +describe('grpc api client sdk', () => { + beforeEach(() => { + loadSpecsMock.mockReset(); + loadSyncMock.mockReset(); + loadPackageDefinitionMock.mockReset(); + createInsecureMock.mockReset(); + requestMock.mockReset(); + loadSpecsMock.mockReturnValue({ + asyncApiGrpc: { + servers: { + local: { + host: 'localhost:3999' + } + } + } + }); + loadPackageDefinitionMock.mockReturnValue({ + realtime: { + AsyncApiGateway: jest.fn().mockImplementation(() => ({ + request: requestMock + })) + } + }); + createInsecureMock.mockReturnValue('insecure'); + }); + + it('builds grpc client from spec and returns parsed response', async () => { + expect.hasAssertions(); + const { GrpcApiClient } = await import('../../../../../../packages/sdk-grpc-client/src/GrpcApiClient'); + requestMock.mockImplementation( + (_payload: any, callback: (...args: any[]) => void) => callback(null, { + ok: true, + operationId: 'getAll', + version: '1.0.0', + resultJson: '{"items":[1,2]}' + }) + ); + const client = new GrpcApiClient(); + const response = await client.request({ operationId: 'getAll' }); + expect(loadSyncMock).toHaveBeenCalledWith(expect.any(String), expect.any(Object)); + expect(response.ok).toBe(true); + expect(response.result).toStrictEqual({ items: [1, 2] }); + }); + + it('rejects when grpc returns transport/domain error', async () => { + expect.hasAssertions(); + const { GrpcApiClient } = await import('../../../../../../packages/sdk-grpc-client/src/GrpcApiClient'); + requestMock.mockImplementation( + (_payload: any, callback: (...args: any[]) => void) => callback(null, { + ok: false, + operationId: 'create', + errorMessage: 'failed' + }) + ); + const client = new GrpcApiClient('localhost:5000'); + await expect(client.request({ operationId: 'create' })).rejects.toThrow('failed'); + }); + + it('rejects when grpc transport callback returns error', async () => { + expect.hasAssertions(); + const { GrpcApiClient } = await import('../../../../../../packages/sdk-grpc-client/src/GrpcApiClient'); + requestMock.mockImplementation( + (_payload: any, callback: (...args: any[]) => void) => callback(new Error('transport down')) + ); + const client = new GrpcApiClient('localhost:5000'); + await expect(client.request({ operationId: 'create' })).rejects.toThrow('transport down'); + }); + + it('supports module fallback when grpc/proto-loader default export is undefined', async () => { + expect.hasAssertions(); + const grpcModule: any = await import('@grpc/grpc-js'); + const protoLoaderModule: any = await import('@grpc/proto-loader'); + const previousGrpcDefault = grpcModule.default; + const previousProtoDefault = protoLoaderModule.default; + grpcModule.default = undefined; + protoLoaderModule.default = undefined; + + requestMock.mockImplementation( + (_payload: any, callback: (...args: any[]) => void) => callback(null, { + ok: true, + operationId: 'getAll', + resultJson: '{}' + }) + ); + const { GrpcApiClient } = await import('../../../../../../packages/sdk-grpc-client/src/GrpcApiClient'); + const client = new GrpcApiClient('localhost:5000'); + const response = await client.request({ operationId: 'getAll' }); + + expect(response.ok).toBe(true); + expect(loadSyncMock).toHaveBeenCalledWith(expect.any(String), expect.any(Object)); + + grpcModule.default = previousGrpcDefault; + protoLoaderModule.default = previousProtoDefault; + }); +}); diff --git a/apps/backend-template/test/unit/service-management/mvp.roadmap.features.test.ts b/apps/backend-template/test/unit/service-management/mvp.roadmap.features.test.ts new file mode 100644 index 00000000..20f800c6 --- /dev/null +++ b/apps/backend-template/test/unit/service-management/mvp.roadmap.features.test.ts @@ -0,0 +1,54 @@ +/* eslint-disable jest/prefer-expect-assertions, jest/max-expects */ +import fs from 'fs'; +import path from 'path'; + +describe('service management mvp roadmap features', () => { + const indexPath = path.resolve(process.cwd(), 'apps/service-management/index.html'); + const scriptPath = path.resolve(process.cwd(), 'apps/service-management/script.js'); + + it('exposes schema diff controls in UI', () => { + const html = fs.readFileSync(indexPath, 'utf-8'); + expect(html).toContain('id="save-baseline-btn"'); + expect(html).toContain('id="run-schema-diff-btn"'); + expect(html).toContain('id="schema-diff-list"'); + }); + + it('exposes RBAC and message contract controls in UI', () => { + const html = fs.readFileSync(indexPath, 'utf-8'); + expect(html).toContain('id="entity-rbac-action-select"'); + expect(html).toContain('id="entity-contract-name-input"'); + expect(html).toContain('id="entity-contract-list"'); + }); + + it('includes OAS composition, package IO and export quality gate logic in script', () => { + const script = fs.readFileSync(scriptPath, 'utf-8'); + expect(script).toContain('entity-oas-composition-mode-select'); + expect(script).toContain('saveSelectedEntityOasComposition'); + expect(script).toContain('canExportModel'); + expect(script).toContain('exportAsPackage'); + expect(script).toContain('importDomainPackage'); + }); + + it('includes request/response example generator and JSON Schema exporter hooks', () => { + const html = fs.readFileSync(indexPath, 'utf-8'); + const script = fs.readFileSync(scriptPath, 'utf-8'); + expect(html).toContain('id="generate-examples-btn"'); + expect(html).toContain('id="examples-preview-output"'); + expect(html).toContain('id="export-jsonschema-btn"'); + expect(script).toContain('generateExamplesPreview'); + expect(script).toContain('buildEntityRequestExample'); + expect(script).toContain('exportAsJsonSchema'); + }); + + it('includes advanced roadmap controls: relationship path, templates, OpenAPI advanced, mini-map', () => { + const html = fs.readFileSync(indexPath, 'utf-8'); + const script = fs.readFileSync(scriptPath, 'utf-8'); + expect(html).toContain('id="relationship-bend-x-input"'); + expect(html).toContain('id="entity-template-select"'); + expect(html).toContain('id="entity-oas-external-refs-input"'); + expect(html).toContain('id="mini-map"'); + expect(script).toContain('renderMiniMap'); + expect(script).toContain('exportAsAsyncApi'); + expect(script).toContain('exportBoilerplateBundle'); + }); +}); diff --git a/test/unit/shared/decorators/Authorize.test.ts b/apps/backend-template/test/unit/shared/decorators/Authorize.test.ts similarity index 100% rename from test/unit/shared/decorators/Authorize.test.ts rename to apps/backend-template/test/unit/shared/decorators/Authorize.test.ts diff --git a/test/unit/shared/openapi/OpenApi31DataEntity.test.ts b/apps/backend-template/test/unit/shared/openapi/OpenApi31DataEntity.test.ts similarity index 100% rename from test/unit/shared/openapi/OpenApi31DataEntity.test.ts rename to apps/backend-template/test/unit/shared/openapi/OpenApi31DataEntity.test.ts diff --git a/test/unit/shared/utils.errorExposure.test.ts b/apps/backend-template/test/unit/shared/utils.errorExposure.test.ts similarity index 83% rename from test/unit/shared/utils.errorExposure.test.ts rename to apps/backend-template/test/unit/shared/utils.errorExposure.test.ts index 27594872..da9568f6 100644 --- a/test/unit/shared/utils.errorExposure.test.ts +++ b/apps/backend-template/test/unit/shared/utils.errorExposure.test.ts @@ -27,8 +27,8 @@ describe('error exposure by environment', () => { it('exposes internal error details in dev/staging', () => { expect.assertions(2); - const devPayload = buildErrorResponsePayload(error, { NODE_ENV: 'dev' } as NodeJS.ProcessEnv); - const stagingPayload = buildErrorResponsePayload(error, { NODE_ENV: 'staging' } as NodeJS.ProcessEnv); + const devPayload = buildErrorResponsePayload(error, { NODE_ENV: 'dev' } as unknown as NodeJS.ProcessEnv); + const stagingPayload = buildErrorResponsePayload(error, { NODE_ENV: 'staging' } as unknown as NodeJS.ProcessEnv); expect(devPayload.error).toBe(error); expect(stagingPayload.error).toBe(error); @@ -36,8 +36,8 @@ describe('error exposure by environment', () => { it('masks internal error details in production/prod', () => { expect.assertions(2); - const productionPayload = buildErrorResponsePayload(error, { NODE_ENV: 'production' } as NodeJS.ProcessEnv); - const prodPayload = buildErrorResponsePayload(error, { NODE_ENV: 'prod' } as NodeJS.ProcessEnv); + const productionPayload = buildErrorResponsePayload(error, { NODE_ENV: 'production' } as unknown as NodeJS.ProcessEnv); + const prodPayload = buildErrorResponsePayload(error, { NODE_ENV: 'prod' } as unknown as NodeJS.ProcessEnv); expect(productionPayload.error).toBeUndefined(); expect(prodPayload.error).toBeUndefined(); @@ -47,19 +47,19 @@ describe('error exposure by environment', () => { expect.assertions(5); expect(replaceVars('/users/{id}/emails/{emailId}')).toBe('/users/:id/emails/:emailId'); expect(toHttpStatus('GENERIC.INVALID_INPUT' as any)).toBe(400); - expect(isProductionEnv({ NODE_ENV: 'prod' } as NodeJS.ProcessEnv)).toBe(true); - expect(isProductionEnv({ NODE_ENV: 'production' } as NodeJS.ProcessEnv)).toBe(true); - expect(shouldExposeInternalErrors({ NODE_ENV: 'dev' } as NodeJS.ProcessEnv)).toBe(true); + expect(isProductionEnv({ NODE_ENV: 'prod' } as unknown as NodeJS.ProcessEnv)).toBe(true); + expect(isProductionEnv({ NODE_ENV: 'production' } as unknown as NodeJS.ProcessEnv)).toBe(true); + expect(shouldExposeInternalErrors({ NODE_ENV: 'dev' } as unknown as NodeJS.ProcessEnv)).toBe(true); }); it('covers utility helpers for production env exposure gate', () => { expect.assertions(1); - expect(shouldExposeInternalErrors({ NODE_ENV: 'production' } as NodeJS.ProcessEnv)).toBe(false); + expect(shouldExposeInternalErrors({ NODE_ENV: 'production' } as unknown as NodeJS.ProcessEnv)).toBe(false); }); it('normalizes NODE_ENV before production check', () => { expect.assertions(1); - expect(isProductionEnv({ NODE_ENV: ' PRODUCTION ' } as NodeJS.ProcessEnv)).toBe(true); + expect(isProductionEnv({ NODE_ENV: ' PRODUCTION ' } as unknown as NodeJS.ProcessEnv)).toBe(true); }); it('covers bad-request and auth/lock message mapping branches', () => { @@ -96,7 +96,7 @@ describe('error exposure by environment', () => { Object.defineProperty(eventInvalidMessageError, 'name', { value: 'event_invalid_message' }); expect(formatErrorMessage(eventInvalidMessageError)).toBe('Bad Request - evt'); - process.env.NODE_ENV = 'production'; + (process.env as any).NODE_ENV = 'production'; expect(isProductionEnv()).toBe(true); expect(shouldExposeInternalErrors()).toBe(false); expect(buildErrorResponsePayload(new ValidationError('z')).error).toBeUndefined(); @@ -105,7 +105,7 @@ describe('error exposure by environment', () => { it('includes correlation id when available in context', () => { expect.assertions(1); Context.run(new Map([['correlationId', 'corr-123']]), () => { - const payload = buildErrorResponsePayload(new ValidationError('ctx'), { NODE_ENV: 'production' } as NodeJS.ProcessEnv); + const payload = buildErrorResponsePayload(new ValidationError('ctx'), { NODE_ENV: 'production' } as unknown as NodeJS.ProcessEnv); expect(payload.correlationId).toBe('corr-123'); }); }); diff --git a/apps/jumentix-website/.env.local.example b/apps/jumentix-website/.env.local.example new file mode 100644 index 00000000..eaa0c0ce --- /dev/null +++ b/apps/jumentix-website/.env.local.example @@ -0,0 +1,10 @@ +# Redis tokens retrieved here: https://console.upstash.com/ +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= + +# Vector database tokens retrieved here: https://console.upstash.com/vector +UPSTASH_VECTOR_REST_URL= +UPSTASH_VECTOR_REST_TOKEN= + +# OpenAI key retrieved here: https://platform.openai.com/api-keys +OPENAI_API_KEY= \ No newline at end of file diff --git a/apps/jumentix-website/.gitignore b/apps/jumentix-website/.gitignore new file mode 100644 index 00000000..2089a16a --- /dev/null +++ b/apps/jumentix-website/.gitignore @@ -0,0 +1,140 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +public/_pagefind/** +!public/_pagefind/ +!public/_pagefind/pagefind.js + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +.DS_Store +next-env.d.ts + +# Claude Code local settings +.claude/settings.local.json \ No newline at end of file diff --git a/apps/jumentix-website/.npmrc b/apps/jumentix-website/.npmrc new file mode 100644 index 00000000..e69de29b diff --git a/apps/jumentix-website/.nvmrc b/apps/jumentix-website/.nvmrc new file mode 100644 index 00000000..0a492611 --- /dev/null +++ b/apps/jumentix-website/.nvmrc @@ -0,0 +1 @@ +24.11.0 diff --git a/apps/jumentix-website/.oxfmtrc.json b/apps/jumentix-website/.oxfmtrc.json new file mode 100644 index 00000000..51353957 --- /dev/null +++ b/apps/jumentix-website/.oxfmtrc.json @@ -0,0 +1,33 @@ +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "es5", + "importOrder": [ + ".*styles.css$", + "", + "dayjs", + "^react$", + "^next$", + "^next/.*$", + "", + "", + "^@mantine/(.*)$", + "^@mantinex/(.*)$", + "^@mantine-tests/(.*)$", + "^@docs/(.*)$", + "^@/.*$", + "^../(?!.*\\.css$).*$", + "^./(?!.*\\.css$).*$", + "\\.module\\.css$", + "(? a.title.localeCompare(b.title, undefined, { numeric: true }), + }, + backgrounds: { disable: true }, +}; + +export const globalTypes = { + theme: { + name: 'Theme', + description: 'Mantine color scheme', + defaultValue: 'light', + toolbar: { + icon: 'mirror', + items: [ + { value: 'light', title: 'Light' }, + { value: 'dark', title: 'Dark' }, + ], + }, + }, +}; + +export const decorators = [ + (renderStory: any, context: any) => { + const scheme = (context.globals.theme || 'light') as 'light' | 'dark'; + return ( + + + {renderStory()} + + ); + }, +]; diff --git a/apps/jumentix-website/.stylelintignore b/apps/jumentix-website/.stylelintignore new file mode 100644 index 00000000..ee689e8b --- /dev/null +++ b/apps/jumentix-website/.stylelintignore @@ -0,0 +1,3 @@ +.next +out +public diff --git a/apps/jumentix-website/.stylelintrc.json b/apps/jumentix-website/.stylelintrc.json new file mode 100644 index 00000000..4ea6506d --- /dev/null +++ b/apps/jumentix-website/.stylelintrc.json @@ -0,0 +1,28 @@ +{ + "extends": ["stylelint-config-standard-scss"], + "rules": { + "custom-property-pattern": null, + "selector-class-pattern": null, + "scss/no-duplicate-mixins": null, + "declaration-empty-line-before": null, + "declaration-block-no-redundant-longhand-properties": null, + "alpha-value-notation": null, + "custom-property-empty-line-before": null, + "property-no-vendor-prefix": null, + "color-function-notation": null, + "length-zero-no-unit": null, + "selector-not-notation": null, + "no-descending-specificity": null, + "comment-empty-line-before": null, + "scss/at-mixin-pattern": null, + "scss/at-rule-no-unknown": null, + "value-keyword-case": null, + "media-feature-range-notation": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["global"] + } + ] + } +} diff --git a/apps/jumentix-website/.yarnrc.yml b/apps/jumentix-website/.yarnrc.yml new file mode 100644 index 00000000..5150051e --- /dev/null +++ b/apps/jumentix-website/.yarnrc.yml @@ -0,0 +1,5 @@ +nodeLinker: node-modules + +npmMinimalAgeGate: 0 + +yarnPath: .yarn/releases/yarn-4.16.0.cjs diff --git a/apps/jumentix-website/CLAUDE.md b/apps/jumentix-website/CLAUDE.md new file mode 100644 index 00000000..a043b586 --- /dev/null +++ b/apps/jumentix-website/CLAUDE.md @@ -0,0 +1,75 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a **Next.js 16 + Mantine 9 + Nextra 4** template used as the documentation site foundation for the Mantine Extensions ecosystem. It serves as a reusable starter for building docs sites with integrated Mantine components. + +## Commands + +| Command | Purpose | +|---------|---------| +| `yarn dev` | Start Next.js dev server | +| `yarn build` | Production build (Next.js + pagefind search index) | +| `yarn test` | Full suite: typegen, oxfmt, lint, typecheck, jest | +| `yarn jest` | Run Jest tests only | +| `yarn jest:watch` | Jest in watch mode | +| `yarn jest -- path/to/file` | Run a single test file | +| `yarn typecheck` | TypeScript type checking (`tsc --noEmit`) | +| `yarn lint` | oxlint + Stylelint | +| `yarn format:write` | Auto-format all TS/TSX/CSS files (oxfmt) | +| `yarn format:test` | Check formatting (oxfmt) | +| `yarn storybook` | Storybook dev server on port 6006 | +| `yarn analyze` | Bundle analysis with `@next/bundle-analyzer` | + +## Architecture + +### Routing & Content + +- **App Router** (`app/`): Next.js 16 app router with Nextra integration +- **Docs content** (`content/`): MDX files rendered via Nextra at `/docs/[[...mdxPath]]` +- Nextra is configured with `contentDirBasePath: '/docs'` — all MDX content is served under `/docs` +- `content/_meta.ts` controls sidebar navigation order and labels + +### Layout & Theme Integration + +- `app/layout.tsx` wraps the entire app in both `MantineProvider` and Nextra's `Layout` +- Dark mode sync between Mantine and Nextra is handled by `MantineNextraThemeObserver` +- Mantine theme overrides go in `theme.ts` (client-side `createTheme`) +- Global site configuration (metadata, GitHub API, search, Nextra layout) lives in `config/index.ts` + +### Key Components (`components/`) + +- `MantineNavBar` / `MantineFooter` — custom Nextra layout replacements +- `ColorSchemeControl` / `ColorSchemeToggle` — dark mode toggle +- `ReleaseNotes` — fetches GitHub releases via `/api/github-releases` +- `Logo`, `Welcome`, `Content` — branding and landing page components + +### API Routes (`app/api/`) + +- `version/` — returns current package version +- `github-releases/` — proxies GitHub releases API (configured in `config/index.ts`) +- `search/` — pagefind-based search endpoint + +### Search + +Search uses [pagefind](https://pagefind.app/). The index is built post-build (`yarn build:pagefind`) into `public/_pagefind/`. The search API route reads this index. + +### CSS Import Order + +In `app/layout.tsx`, CSS imports must follow this order: +1. `@mantine/core/styles.css` +2. Mantine extension styles (e.g., marquee, text-animate) +3. Global styles + +### Build Pipeline + +Next.js config (`next.config.mjs`) chains: `nextra()` → `bundleAnalyzer()`. Turbopack is configured with inline SVG loader for SVGs under ~4KB. + +## Tooling + +- **Formatter**: oxfmt (`.oxfmtrc.json`) +- **Linter**: oxlint + stylelint +- **TypeScript**: 6.x +- **Package Manager**: Yarn 4 (Berry). Do not use npm or pnpm. diff --git a/apps/jumentix-website/LICENCE b/apps/jumentix-website/LICENCE new file mode 100644 index 00000000..1a88111a --- /dev/null +++ b/apps/jumentix-website/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Vitaly Rtischev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/jumentix-website/README.md b/apps/jumentix-website/README.md new file mode 100644 index 00000000..ff319073 --- /dev/null +++ b/apps/jumentix-website/README.md @@ -0,0 +1,36 @@ +# Jumentix Website + +Commercial website application for selling Jumentix as an enterprise software factory. + +## Template Baseline + +This app uses the architecture baseline from the Vercel template: + +- [MantineUI + Nextra](https://vercel.com/templates/next.js/mantine-ui-nextra) +- Source template repository: `gfazioli/next-app-nextra-template` + +## Purpose + +- Present a conversion-oriented commercial narrative for enterprise buyers. +- Reuse markdown documentation as static content source. +- Publish statically on Vercel. + +## Run + +```bash +pnpm --filter @jumentix/website dev +``` + +## Build + +```bash +pnpm --filter @jumentix/website build +``` + +## Planning Artifacts + +- [Website IA and Conversion Plan](./documentation/WEBSITE-IA-AND-CONVERSION-PLAN.md) +- [Markdown Content Pipeline](./documentation/CONTENT-PIPELINE.md) +- [SEO and Performance Baseline](./documentation/SEO-AND-PERFORMANCE-BASELINE.md) +- [Vercel Deployment](./documentation/VERCEL-DEPLOYMENT.md) +- [NPM and Vercel Integration](./documentation/NPM-AND-VERCEL-INTEGRATION.md) diff --git a/apps/jumentix-website/app/_meta.tsx b/apps/jumentix-website/app/_meta.tsx new file mode 100644 index 00000000..7dc6d525 --- /dev/null +++ b/apps/jumentix-website/app/_meta.tsx @@ -0,0 +1,37 @@ +export default { + index: { + display: 'hidden', + }, + product: { + title: 'Product', + type: 'page', + }, + 'use-cases': { + title: 'Use Cases', + type: 'page', + }, + architecture: { + title: 'Architecture', + type: 'page', + }, + integrations: { + title: 'Integrations', + type: 'page', + }, + 'security-compliance': { + title: 'Security', + type: 'page', + }, + docs: { + type: 'page', + title: 'Documentation', + }, + 'pricing-or-engagement': { + title: 'Engagement', + type: 'page', + }, + contact: { + type: 'page', + title: 'Contact', + }, +}; diff --git a/apps/jumentix-website/app/api/github-releases/route.ts b/apps/jumentix-website/app/api/github-releases/route.ts new file mode 100644 index 00000000..03c0204f --- /dev/null +++ b/apps/jumentix-website/app/api/github-releases/route.ts @@ -0,0 +1,96 @@ +import config from '@/config'; + +export async function GET(request: Request) { + try { + const userAgent = request.headers.get('user-agent'); + + if (!userAgent) { + return Response.json({ error: 'User agent not found' }, { status: 400 }); + } + + const isBot = /bot|crawl|slurp|spider/i.test(userAgent); + + if (isBot) { + return Response.json({ error: 'Bots are not allowed' }, { status: 403 }); + } + + const url = `${config.gitHub.releasesUrl}?per_page=${config.releaseNotes.maxReleases}`; + const baseHeaders: Record = { + Accept: 'application/vnd.github+json', + // GitHub requires a User-Agent on unauthenticated requests. + 'User-Agent': config.gitHub.repo + }; + + // Try authenticated first if a token is configured (5000 req/hour vs 60/hour). + // If the token is invalid (401) or rejected by org policy (403 *not* due to + // rate limiting), retry unauthenticated — public releases are readable + // without auth. We do NOT fall back on a rate-limited 403, since that just + // consumes another quota slot and amplifies throttling. + const FETCH_TIMEOUT_MS = 10_000; + const fetchOptions = { + next: { revalidate: 3600 }, + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) + }; + let response: Response; + if (process.env.GITHUB_TOKEN) { + response = await fetch(url, { + ...fetchOptions, + headers: { ...baseHeaders, Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + }); + const rateRemaining = response.headers.get('x-ratelimit-remaining'); + const shouldFallback = response.status === 401 || (response.status === 403 && rateRemaining !== '0'); + if (shouldFallback) { + // Drain the body to release the undici connection before refetching. + await response.text(); + // eslint-disable-next-line no-console + console.warn( + `GITHUB_TOKEN returned ${response.status} — falling back to unauthenticated fetch` + ); + response = await fetch(url, { + ...fetchOptions, + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + headers: baseHeaders + }); + } + } else { + response = await fetch(url, { ...fetchOptions, headers: baseHeaders }); + } + + if (!response.ok) { + const bodyText = await response.text(); + const rateRemaining = response.headers.get('x-ratelimit-remaining'); + const rateReset = response.headers.get('x-ratelimit-reset'); + // eslint-disable-next-line no-console + console.error('[github-releases] non-OK response', { + status: response.status, + statusText: response.statusText, + rateRemaining, + rateReset, + body: bodyText.slice(0, 300) + }); + return Response.json( + { error: response.statusText || 'GitHub releases fetch failed' }, + { status: response.status } + ); + } + + const releases = await response.json(); + + return Response.json( + { releases, status: 'ok' }, + { + headers: { + 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', + Vary: 'User-Agent' + } + } + ); + } catch (error: any) { + // eslint-disable-next-line no-console + console.error('Error checking user agent:', error); + return Response.json( + { error: 'Internal server error', details: error.message }, + { status: 500 } + ); + } +} diff --git a/apps/jumentix-website/app/api/search/route.ts b/apps/jumentix-website/app/api/search/route.ts new file mode 100644 index 00000000..bda3c3ff --- /dev/null +++ b/apps/jumentix-website/app/api/search/route.ts @@ -0,0 +1,98 @@ +import config from '@/config'; +import * as pageFind from '../../../public/_pagefind/pagefind'; + +interface SearchResult { + title: string; + content: string; + items: Array<{ + title: string; + url: string; + excerpt: string; + }>; +} + +export async function GET(request: Request) { + const url = new URL(request.url); + const { + queryKeyword, + limitKeyword, + excerptLengthKeyword, + defaultMaxResults, + defaultExcerptLength, + defaultLanguage, + minQueryLength + } = config.search; + + if (url.searchParams.get(queryKeyword) === null) { + return Response.json({ error: 'No search query provided' }, { status: 200 }); + } + + const scriptSrc = `${url.origin}/docs`; + + // hard patch for pagefind + if (typeof global.window === 'undefined') { + global.window = { + location: { + href: scriptSrc, + protocol: url.protocol, + host: url.host, + pathname: url.pathname, + search: url.search, + hash: url.hash + } + } as any; + } + + if (typeof global.document === 'undefined') { + global.document = { + querySelector: () => ({ + getAttribute: () => defaultLanguage + }) + } as any; + } + + const query = url.searchParams.get(queryKeyword) || ''; + + if (query.length < minQueryLength) { + return Response.json({ error: 'Search query too short' }, { status: 200 }); + } + + const limit = url.searchParams.get(limitKeyword) || defaultMaxResults; + const excerptLength = url.searchParams.get(excerptLengthKeyword) || defaultExcerptLength; + + try { + await pageFind.options({ + basePath: `${url.origin}/_pagefind/`, + excerptLength: parseInt(excerptLength.toString(), 10) + }); + await pageFind.init(); + + const search = await pageFind.search(query); + const results = await Promise.all( + search.results.slice(0, limit).map((r: { data: () => unknown }) => r.data()) + ); + + // reset the global document, window and location + delete (global as any).document; + delete (global as any).window; + + // format results + const formattedResults: SearchResult[] = results.map((result: any) => { + return { + title: result.meta.title, + content: result.content, + items: result.sub_results.map((item: any) => ({ + title: item.title, + url: item.url, + excerpt: item.excerpt + })) + }; + }); + + return Response.json(formattedResults); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Search error:', error); + return Response.json({ error: 'Search failed' }, { status: 500 }); + } +} diff --git a/apps/jumentix-website/app/api/version/route.ts b/apps/jumentix-website/app/api/version/route.ts new file mode 100644 index 00000000..953012f2 --- /dev/null +++ b/apps/jumentix-website/app/api/version/route.ts @@ -0,0 +1,5 @@ +import pack from '../../../package.json'; + +export async function GET() { + return Response.json({ version: pack.version }); +} diff --git a/apps/jumentix-website/app/architecture/page.tsx b/apps/jumentix-website/app/architecture/page.tsx new file mode 100644 index 00000000..a3abe9ca --- /dev/null +++ b/apps/jumentix-website/app/architecture/page.tsx @@ -0,0 +1,23 @@ +import { Card, Container, List, Stack, Text, Title } from '@mantine/core'; + +export default function ArchitecturePage() { + return ( + + + Architecture + + Jumentix enforces DDD, Event-Driven Design, and Hexagonal Architecture as non-functional + requirements. + + + + Domain-first module boundaries + Ports and adapters isolation + Contract-driven interface exposure + Event and message flow consistency + + + + + ); +} diff --git a/apps/jumentix-website/app/contact/page.tsx b/apps/jumentix-website/app/contact/page.tsx new file mode 100644 index 00000000..6a143796 --- /dev/null +++ b/apps/jumentix-website/app/contact/page.tsx @@ -0,0 +1,27 @@ +import { Button, Card, Container, Stack, Text, TextInput, Textarea, Title } from '@mantine/core'; + +export default function ContactPage() { + return ( + + + Book an Enterprise Demo + + Share your product context and architecture goals. The team will respond with a suggested + pilot plan. + + + + + + + + +
+ + + + + + + +
    +
    +
    + + +
    + + +
    +
    + + +
    +
      +
      +
      + + + + + + +
      @@ -171,6 +264,11 @@

      Export

      + + + + +
      @@ -180,6 +278,16 @@

      Export

      +
      + + +
      +
      + + +
      +
      
      +            
      
                   
      @@ -202,6 +310,7 @@

      Legend

    • Ctrl/Cmd + wheel to zoom
    • Alt + L to auto layout
    • Select domain to add entity
    • +
    • Drag from entity edge dots to connect entities
    • @@ -210,8 +319,28 @@

      Model Check

      +
      + + + +
        + +
        +

        Schema Diff

        +

        Compare current model against saved baseline and preview migration hints.

        +
        + + + +
        +
          +
          @@ -228,6 +357,7 @@

          Model Check

          +
          @@ -237,6 +367,7 @@

          Model Check

          +
          diff --git a/apps/service-management/package.json b/apps/service-management/package.json new file mode 100644 index 00000000..53c463f8 --- /dev/null +++ b/apps/service-management/package.json @@ -0,0 +1,13 @@ +{ + "name": "@jumentix/service-management", + "version": "0.0.2", + "private": true, + "description": "JumentiX service-management workspace app", + "scripts": { + "build": "echo \"service-management runtime uses node server entrypoint\"", + "test": "npm run test:integration:service-management --prefix ../..", + "lint": "npm run lint --prefix ../..", + "typecheck": "echo \"service-management is plain JavaScript server\"", + "dev": "npm run dev:service-management --prefix ../.." + } +} diff --git a/servicemangement/script.js b/apps/service-management/script.js similarity index 59% rename from servicemangement/script.js rename to apps/service-management/script.js index 56453c11..d38c1ba9 100644 --- a/servicemangement/script.js +++ b/apps/service-management/script.js @@ -1,4 +1,5 @@ const STORAGE_KEY = 'service-management.v1'; +const DIFF_BASELINE_KEY = 'service-management.schema-baseline.v1'; const DOMAIN_COLORS = ['#60a5fa', '#34d399', '#f59e0b', '#f472b6', '#22d3ee', '#a78bfa', '#fb7185', '#84cc16']; const FIELD_TYPES = ['string', 'integer', 'number', 'boolean', 'array', 'object', 'date', 'datetime', 'uuid']; @@ -37,7 +38,10 @@ const state = { zoom: 1, compactEntities: false, snapToGrid: true, - edgeStyle: 'curved' + edgeStyle: 'curved', + modelCheckMinSeverity: 'info', + exportBlockCritical: true, + largeCanvasMode: false } }; @@ -51,6 +55,9 @@ const interaction = { panning: false, relationshipPickActive: false, relationshipPickFromEntityId: null, + relationshipAnchorDragActive: false, + relationshipAnchorFromEntityId: null, + relationshipAnchorFromSide: null, panStartX: 0, panStartY: 0, scrollStartLeft: 0, @@ -75,8 +82,10 @@ const dom = { zoomInBtn: document.getElementById('zoom-in-btn'), edgeStyleSelect: document.getElementById('edge-style-select'), autoLayoutBtn: document.getElementById('auto-layout-btn'), + toggleLargeCanvasBtn: document.getElementById('toggle-large-canvas-btn'), fitViewBtn: document.getElementById('fit-view-btn'), resetViewBtn: document.getElementById('reset-view-btn'), + miniMap: document.getElementById('mini-map'), zoomIndicator: document.getElementById('zoom-indicator'), toggleCompactViewBtn: document.getElementById('toggle-compact-view-btn'), toggleSnapBtn: document.getElementById('toggle-snap-btn'), @@ -86,6 +95,15 @@ const dom = { deleteDomainBtn: document.getElementById('delete-domain-btn'), domainColorInput: document.getElementById('domain-color-input'), setDomainColorBtn: document.getElementById('set-domain-color-btn'), + domainUbiquitousLanguageInput: document.getElementById('domain-ubiquitous-language-input'), + domainOwnerTeamInput: document.getElementById('domain-owner-team-input'), + domainUpstreamInput: document.getElementById('domain-upstream-input'), + domainDownstreamInput: document.getElementById('domain-downstream-input'), + domainIntegrationChannelInput: document.getElementById('domain-integration-channel-input'), + domainPackageDependenciesInput: document.getElementById('domain-package-dependencies-input'), + domainSharedValueObjectsInput: document.getElementById('domain-shared-value-objects-input'), + saveDomainContextBtn: document.getElementById('save-domain-context-btn'), + clearDomainContextBtn: document.getElementById('clear-domain-context-btn'), entityNameInput: document.getElementById('entity-name-input'), addEntityBtn: document.getElementById('add-entity-btn'), renameEntityBtn: document.getElementById('rename-entity-btn'), @@ -93,6 +111,8 @@ const dom = { duplicateEntityBtn: document.getElementById('duplicate-entity-btn'), entitySearchInput: document.getElementById('entity-search-input'), entitySearchBtn: document.getElementById('entity-search-btn'), + entityTemplateSelect: document.getElementById('entity-template-select'), + applyEntityTemplateBtn: document.getElementById('apply-entity-template-btn'), fromEntitySelect: document.getElementById('from-entity-select'), toEntitySelect: document.getElementById('to-entity-select'), fromCardSelect: document.getElementById('from-card-select'), @@ -109,11 +129,38 @@ const dom = { relationshipToCardSelect: document.getElementById('relationship-to-card-select'), saveRelationshipBtn: document.getElementById('save-relationship-btn'), reverseRelationshipBtn: document.getElementById('reverse-relationship-btn'), + relationshipLabelOffsetXInput: document.getElementById('relationship-label-offset-x-input'), + relationshipLabelOffsetYInput: document.getElementById('relationship-label-offset-y-input'), + resetRelationshipLabelOffsetBtn: document.getElementById('reset-relationship-label-offset-btn'), + relationshipBendXInput: document.getElementById('relationship-bend-x-input'), + relationshipBendYInput: document.getElementById('relationship-bend-y-input'), + relationshipAnchorBehaviorSelect: document.getElementById('relationship-anchor-behavior-select'), entityInspectorTitle: document.getElementById('entity-inspector-title'), entityRenameInput: document.getElementById('entity-rename-input'), saveEntityRenameBtn: document.getElementById('save-entity-rename-btn'), entityMoveDomainSelect: document.getElementById('entity-move-domain-select'), moveEntityBtn: document.getElementById('move-entity-btn'), + entityAggregateRootCheck: document.getElementById('entity-aggregate-root-check'), + entityInvariantsInput: document.getElementById('entity-invariants-input'), + saveEntityRulesBtn: document.getElementById('save-entity-rules-btn'), + entityRbacActionSelect: document.getElementById('entity-rbac-action-select'), + entityRbacSuperadminCheck: document.getElementById('entity-rbac-superadmin-check'), + entityRbacAdminCheck: document.getElementById('entity-rbac-admin-check'), + entityRbacUserCheck: document.getElementById('entity-rbac-user-check'), + entityRbacTenantCheck: document.getElementById('entity-rbac-tenant-check'), + saveEntityRbacBtn: document.getElementById('save-entity-rbac-btn'), + entityRbacList: document.getElementById('entity-rbac-list'), + entityContractNameInput: document.getElementById('entity-contract-name-input'), + entityContractTypeSelect: document.getElementById('entity-contract-type-select'), + entityContractChannelInput: document.getElementById('entity-contract-channel-input'), + entityContractVersionInput: document.getElementById('entity-contract-version-input'), + addEntityContractBtn: document.getElementById('add-entity-contract-btn'), + entityContractList: document.getElementById('entity-contract-list'), + entityOasCompositionModeSelect: document.getElementById('entity-oas-composition-mode-select'), + entityOasCompositionRefsInput: document.getElementById('entity-oas-composition-refs-input'), + entityOasExternalRefsInput: document.getElementById('entity-oas-external-refs-input'), + entityOasDiscriminatorInput: document.getElementById('entity-oas-discriminator-input'), + saveEntityOasCompositionBtn: document.getElementById('save-entity-oas-composition-btn'), fieldNameInput: document.getElementById('field-name-input'), fieldTemplateSelect: document.getElementById('field-template-select'), applyFieldTemplateBtn: document.getElementById('apply-field-template-btn'), @@ -130,16 +177,33 @@ const dom = { entityApiPreviewList: document.getElementById('entity-api-preview-list'), exportJsonBtn: document.getElementById('export-json-btn'), exportOasBtn: document.getElementById('export-oas-btn'), + exportMdBtn: document.getElementById('export-md-btn'), + exportJsonschemaBtn: document.getElementById('export-jsonschema-btn'), + exportAsyncapiBtn: document.getElementById('export-asyncapi-btn'), + exportBoilerplateBundleBtn: document.getElementById('export-boilerplate-bundle-btn'), + exportPackageBtn: document.getElementById('export-package-btn'), importJsonBtn: document.getElementById('import-json-btn'), importJsonInput: document.getElementById('import-json-input'), importOasBtn: document.getElementById('import-oas-btn'), importOasInput: document.getElementById('import-oas-input'), + importPackageBtn: document.getElementById('import-package-btn'), + importPackageInput: document.getElementById('import-package-input'), + generateCodePreviewBtn: document.getElementById('generate-code-preview-btn'), + generateExamplesBtn: document.getElementById('generate-examples-btn'), + codePreviewOutput: document.getElementById('code-preview-output'), + examplesPreviewOutput: document.getElementById('examples-preview-output'), resetCanvasBtn: document.getElementById('reset-canvas-btn'), clearStorageBtn: document.getElementById('clear-storage-btn'), undoBtn: document.getElementById('undo-btn'), redoBtn: document.getElementById('redo-btn'), runModelCheckBtn: document.getElementById('run-model-check-btn'), modelCheckList: document.getElementById('model-check-list'), + exportBlockCriticalCheck: document.getElementById('export-block-critical-check'), + modelCheckMinSeveritySelect: document.getElementById('model-check-min-severity-select'), + saveBaselineBtn: document.getElementById('save-baseline-btn'), + runSchemaDiffBtn: document.getElementById('run-schema-diff-btn'), + clearBaselineBtn: document.getElementById('clear-baseline-btn'), + schemaDiffList: document.getElementById('schema-diff-list'), interfaceTypeSelect: document.getElementById('interface-type-select'), interfaceFrameworkInput: document.getElementById('interface-framework-input'), interfaceEntrypointInput: document.getElementById('interface-entrypoint-input'), @@ -202,6 +266,18 @@ function parseEnumValues(raw) { .filter(Boolean); } +function parseCommaSeparated(raw) { + if (Array.isArray(raw)) return raw.map((item) => String(item).trim()).filter(Boolean); + return String(raw || '') + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +} + +function uniqueStrings(values) { + return Array.from(new Set((values || []).map((item) => String(item).trim()).filter(Boolean))); +} + function normalizeOptionalNumber(value) { if (value === null || value === undefined || value === '') return null; const numeric = Number(value); @@ -241,6 +317,20 @@ function normalizeField(field, fieldIndex) { }; } +function normalizeContractInput(contract, contractIndex = 0) { + const id = String(contract?.id || '').trim() || fallbackId('contract', contractIndex); + return { + id, + name: String(contract?.name || '').trim() || `Contract_${contractIndex + 1}`, + type: ['event', 'command', 'request', 'response'].includes(contract?.type) ? contract.type : 'event', + channel: String(contract?.channel || '').trim(), + version: String(contract?.version || '').trim() || '1.0.0', + payloadSchema: contract?.payloadSchema && typeof contract.payloadSchema === 'object' + ? contract.payloadSchema + : {} + }; +} + function saveState() { const payload = { domains: state.domains, @@ -337,11 +427,18 @@ function renderView() { state.view.zoom = zoom; if (typeof state.view.snapToGrid !== 'boolean') state.view.snapToGrid = true; if (!['curved', 'orthogonal'].includes(state.view.edgeStyle)) state.view.edgeStyle = 'curved'; + if (!['info', 'warn', 'error'].includes(state.view.modelCheckMinSeverity)) state.view.modelCheckMinSeverity = 'info'; + if (typeof state.view.exportBlockCritical !== 'boolean') state.view.exportBlockCritical = true; + if (typeof state.view.largeCanvasMode !== 'boolean') state.view.largeCanvasMode = false; dom.canvasInner.style.transform = `scale(${zoom})`; + dom.canvas.classList.toggle('large-canvas-mode', Boolean(state.view.largeCanvasMode)); dom.zoomIndicator.textContent = `${Math.round(zoom * 100)}%`; dom.toggleCompactViewBtn.textContent = state.view.compactEntities ? 'Full View' : 'Compact View'; dom.toggleSnapBtn.textContent = state.view.snapToGrid ? 'Snap: On' : 'Snap: Off'; + dom.toggleLargeCanvasBtn.textContent = state.view.largeCanvasMode ? 'Large Canvas: On' : 'Large Canvas: Off'; dom.edgeStyleSelect.value = state.view.edgeStyle; + dom.modelCheckMinSeveritySelect.value = state.view.modelCheckMinSeverity; + dom.exportBlockCriticalCheck.checked = state.view.exportBlockCritical; } function renderTabs() { @@ -719,6 +816,15 @@ function addDomain(name, options = {}) { color: options.color || DOMAIN_COLORS[state.domains.length % DOMAIN_COLORS.length], x: options.x ?? 120 + state.domains.length * 40, y: options.y ?? 90 + state.domains.length * 30, + context: { + ubiquitousLanguage: String(options?.context?.ubiquitousLanguage || '').trim(), + ownerTeam: String(options?.context?.ownerTeam || '').trim(), + upstreamDependencies: parseCommaSeparated(options?.context?.upstreamDependencies || []), + downstreamDependencies: parseCommaSeparated(options?.context?.downstreamDependencies || []), + integrationChannel: String(options?.context?.integrationChannel || '').trim(), + packageDependencies: parseCommaSeparated(options?.context?.packageDependencies || []), + sharedValueObjects: parseCommaSeparated(options?.context?.sharedValueObjects || []) + }, entities: [] }; state.domains.push(domain); @@ -739,7 +845,25 @@ function addEntity(domainId, name, options = {}) { name, x: options.x ?? 14 + (index % 2) * 206, y: options.y ?? 14 + Math.floor(index / 2) * 120, - fields: (options.fields || defaultFields()).map((field, fieldIndex) => normalizeField(field, fieldIndex)) + fields: (options.fields || defaultFields()).map((field, fieldIndex) => normalizeField(field, fieldIndex)), + meta: { + aggregateRoot: Boolean(options?.meta?.aggregateRoot), + invariants: Array.isArray(options?.meta?.invariants) + ? options.meta.invariants.map((item) => String(item).trim()).filter(Boolean) + : [], + rbac: options?.meta?.rbac || getDefaultRbacPolicy(), + contracts: Array.isArray(options?.meta?.contracts) + ? options.meta.contracts.map((contract, index) => normalizeContractInput(contract, index)) + : [], + oasComposition: { + mode: ['oneOf', 'allOf', 'anyOf'].includes(options?.meta?.oasComposition?.mode) + ? options.meta.oasComposition.mode + : '', + refs: parseCommaSeparated(options?.meta?.oasComposition?.refs || []), + externalRefs: parseCommaSeparated(options?.meta?.oasComposition?.externalRefs || []), + discriminator: String(options?.meta?.oasComposition?.discriminator || '').trim() + } + } }; domain.entities.push(entity); state.selectedEntityId = entity.id; @@ -911,7 +1035,7 @@ function removeField(entityId, fieldName) { }); } -function addRelationship(fromEntityId, toEntityId, fromCardinality, toCardinality) { +function addRelationship(fromEntityId, toEntityId, fromCardinality, toCardinality, options = {}) { if (!fromEntityId || !toEntityId || fromEntityId === toEntityId) { window.alert('Select two different entities to create a relationship.'); return; @@ -941,7 +1065,14 @@ function addRelationship(fromEntityId, toEntityId, fromCardinality, toCardinalit toEntityId: fromEntityId, name: `${junctionName} -> ${from.entity.name}`, fromCardinality: 'N', - toCardinality: '1' + toCardinality: '1', + fromAnchorSide: options.fromAnchorSide || null, + toAnchorSide: options.toAnchorSide || null, + anchorBehavior: 'auto', + bendX: null, + bendY: null, + labelOffsetX: 0, + labelOffsetY: 0 }; const relB = { id: nextId('rel'), @@ -949,7 +1080,14 @@ function addRelationship(fromEntityId, toEntityId, fromCardinality, toCardinalit toEntityId: toEntityId, name: `${junctionName} -> ${to.entity.name}`, fromCardinality: 'N', - toCardinality: '1' + toCardinality: '1', + fromAnchorSide: options.fromAnchorSide || null, + toAnchorSide: options.toAnchorSide || null, + anchorBehavior: 'auto', + bendX: null, + bendY: null, + labelOffsetX: 0, + labelOffsetY: 0 }; state.relationships.push(relA); state.relationships.push(relB); @@ -973,7 +1111,14 @@ function addRelationship(fromEntityId, toEntityId, fromCardinality, toCardinalit toEntityId, name: buildRelationshipName(fromEntityId, toEntityId, fromCardinality, toCardinality), fromCardinality, - toCardinality + toCardinality, + fromAnchorSide: options.fromAnchorSide || null, + toAnchorSide: options.toAnchorSide || null, + anchorBehavior: 'auto', + bendX: null, + bendY: null, + labelOffsetX: 0, + labelOffsetY: 0 }; state.relationships.push(relationship); if (dom.relationshipAutoFkCheck.checked) { @@ -1024,6 +1169,12 @@ function syncRelationshipInspector() { dom.relationshipToEntitySelect.disabled = disabled; dom.relationshipFromCardSelect.disabled = disabled; dom.relationshipToCardSelect.disabled = disabled; + dom.relationshipLabelOffsetXInput.disabled = disabled; + dom.relationshipLabelOffsetYInput.disabled = disabled; + dom.relationshipBendXInput.disabled = disabled; + dom.relationshipBendYInput.disabled = disabled; + dom.relationshipAnchorBehaviorSelect.disabled = disabled; + dom.resetRelationshipLabelOffsetBtn.disabled = disabled; dom.saveRelationshipBtn.disabled = disabled; dom.reverseRelationshipBtn.disabled = disabled; if (!relationship) { @@ -1032,6 +1183,11 @@ function syncRelationshipInspector() { dom.relationshipToEntitySelect.value = ''; dom.relationshipFromCardSelect.value = '1'; dom.relationshipToCardSelect.value = '1'; + dom.relationshipLabelOffsetXInput.value = ''; + dom.relationshipLabelOffsetYInput.value = ''; + dom.relationshipBendXInput.value = ''; + dom.relationshipBendYInput.value = ''; + dom.relationshipAnchorBehaviorSelect.value = 'auto'; dom.relationshipNameInput.placeholder = 'Select a relationship'; return; } @@ -1041,6 +1197,11 @@ function syncRelationshipInspector() { dom.relationshipToEntitySelect.value = relationship.toEntityId; dom.relationshipFromCardSelect.value = relationship.fromCardinality || '1'; dom.relationshipToCardSelect.value = relationship.toCardinality || '1'; + dom.relationshipLabelOffsetXInput.value = Number.isFinite(relationship.labelOffsetX) ? String(relationship.labelOffsetX) : '0'; + dom.relationshipLabelOffsetYInput.value = Number.isFinite(relationship.labelOffsetY) ? String(relationship.labelOffsetY) : '0'; + dom.relationshipBendXInput.value = Number.isFinite(relationship.bendX) ? String(relationship.bendX) : ''; + dom.relationshipBendYInput.value = Number.isFinite(relationship.bendY) ? String(relationship.bendY) : ''; + dom.relationshipAnchorBehaviorSelect.value = relationship.anchorBehavior === 'center' ? 'center' : 'auto'; } function saveSelectedRelationship() { @@ -1079,6 +1240,11 @@ function saveSelectedRelationship() { || buildRelationshipName(fromEntityId, toEntityId, fromCardinality, toCardinality); relationship.fromCardinality = fromCardinality; relationship.toCardinality = toCardinality; + relationship.labelOffsetX = normalizeOptionalNumber(dom.relationshipLabelOffsetXInput.value) ?? 0; + relationship.labelOffsetY = normalizeOptionalNumber(dom.relationshipLabelOffsetYInput.value) ?? 0; + relationship.bendX = normalizeOptionalNumber(dom.relationshipBendXInput.value); + relationship.bendY = normalizeOptionalNumber(dom.relationshipBendYInput.value); + relationship.anchorBehavior = dom.relationshipAnchorBehaviorSelect.value === 'center' ? 'center' : 'auto'; render(); }); } @@ -1207,6 +1373,24 @@ function renderStatus() { const selected = getSelectedDomain(); dom.status.textContent = selected ? `Selected domain: ${selected.name}` : 'No domain selected'; dom.domainColorInput.value = selected?.color || '#60a5fa'; + const context = selected?.context || {}; + dom.domainUbiquitousLanguageInput.value = context.ubiquitousLanguage || ''; + dom.domainOwnerTeamInput.value = context.ownerTeam || ''; + dom.domainUpstreamInput.value = Array.isArray(context.upstreamDependencies) ? context.upstreamDependencies.join(', ') : ''; + dom.domainDownstreamInput.value = Array.isArray(context.downstreamDependencies) ? context.downstreamDependencies.join(', ') : ''; + dom.domainIntegrationChannelInput.value = context.integrationChannel || ''; + dom.domainPackageDependenciesInput.value = Array.isArray(context.packageDependencies) ? context.packageDependencies.join(', ') : ''; + dom.domainSharedValueObjectsInput.value = Array.isArray(context.sharedValueObjects) ? context.sharedValueObjects.join(', ') : ''; + const disabled = !selected; + dom.domainUbiquitousLanguageInput.disabled = disabled; + dom.domainOwnerTeamInput.disabled = disabled; + dom.domainUpstreamInput.disabled = disabled; + dom.domainDownstreamInput.disabled = disabled; + dom.domainIntegrationChannelInput.disabled = disabled; + dom.domainPackageDependenciesInput.disabled = disabled; + dom.domainSharedValueObjectsInput.disabled = disabled; + dom.saveDomainContextBtn.disabled = disabled; + dom.clearDomainContextBtn.disabled = disabled; } function setSelectedDomainColor(color) { @@ -1226,6 +1410,26 @@ function setSelectedDomainColor(color) { }); } +function saveSelectedDomainContext() { + const selected = getSelectedDomain(); + if (!selected) { + window.alert('Select a domain first.'); + return; + } + withPersist(() => { + selected.context = { + ubiquitousLanguage: String(dom.domainUbiquitousLanguageInput.value || '').trim(), + ownerTeam: String(dom.domainOwnerTeamInput.value || '').trim(), + upstreamDependencies: parseCommaSeparated(dom.domainUpstreamInput.value), + downstreamDependencies: parseCommaSeparated(dom.domainDownstreamInput.value), + integrationChannel: String(dom.domainIntegrationChannelInput.value || '').trim(), + packageDependencies: uniqueStrings(parseCommaSeparated(dom.domainPackageDependenciesInput.value)), + sharedValueObjects: uniqueStrings(parseCommaSeparated(dom.domainSharedValueObjectsInput.value)) + }; + renderStatus(); + }); +} + function saveSelectedEntityName(name) { const found = findEntity(state.selectedEntityId); if (!found) { @@ -1247,6 +1451,179 @@ function saveSelectedEntityName(name) { }); } +function saveSelectedEntityRules() { + const found = findEntity(state.selectedEntityId); + if (!found) { + window.alert('Select an entity first.'); + return; + } + withPersist(() => { + const invariants = String(dom.entityInvariantsInput.value || '') + .split('\n') + .map((item) => item.trim()) + .filter(Boolean); + found.entity.meta = found.entity.meta || {}; + found.entity.meta.aggregateRoot = Boolean(dom.entityAggregateRootCheck.checked); + found.entity.meta.invariants = invariants; + render(); + }); +} + +function getDefaultRbacPolicy() { + return { + list: { roles: ['superadmin', 'admin'], tenantScoped: true }, + getById: { roles: ['superadmin', 'admin', 'user'], tenantScoped: true }, + create: { roles: ['superadmin', 'admin'], tenantScoped: true }, + update: { roles: ['superadmin', 'admin'], tenantScoped: true }, + delete: { roles: ['superadmin', 'admin'], tenantScoped: true } + }; +} + +function getEntityRbacPolicy(entity) { + if (!entity.meta) entity.meta = {}; + if (!entity.meta.rbac) entity.meta.rbac = getDefaultRbacPolicy(); + return entity.meta.rbac; +} + +function renderEntityRbacInspector(entity) { + const policy = getEntityRbacPolicy(entity); + const action = dom.entityRbacActionSelect.value || 'list'; + const rule = policy[action] || { roles: [], tenantScoped: true }; + const roles = Array.isArray(rule.roles) ? rule.roles : []; + dom.entityRbacSuperadminCheck.checked = roles.includes('superadmin'); + dom.entityRbacAdminCheck.checked = roles.includes('admin'); + dom.entityRbacUserCheck.checked = roles.includes('user'); + dom.entityRbacTenantCheck.checked = Boolean(rule.tenantScoped); + dom.entityRbacList.innerHTML = ''; + ['list', 'getById', 'create', 'update', 'delete'].forEach((key) => { + const item = document.createElement('li'); + const actionRule = policy[key] || { roles: [], tenantScoped: true }; + const label = `${key}: [${(actionRule.roles || []).join(', ')}] | tenantScoped=${Boolean(actionRule.tenantScoped)}`; + item.textContent = label; + dom.entityRbacList.appendChild(item); + }); +} + +function saveSelectedEntityRbacRule() { + const found = findEntity(state.selectedEntityId); + if (!found) { + window.alert('Select an entity first.'); + return; + } + const action = dom.entityRbacActionSelect.value || 'list'; + const roles = []; + if (dom.entityRbacSuperadminCheck.checked) roles.push('superadmin'); + if (dom.entityRbacAdminCheck.checked) roles.push('admin'); + if (dom.entityRbacUserCheck.checked) roles.push('user'); + withPersist(() => { + const policy = getEntityRbacPolicy(found.entity); + policy[action] = { + roles, + tenantScoped: Boolean(dom.entityRbacTenantCheck.checked) + }; + renderEntityRbacInspector(found.entity); + }); +} + +function renderEntityContractsInspector(entity) { + if (!entity.meta) entity.meta = {}; + if (!Array.isArray(entity.meta.contracts)) entity.meta.contracts = []; + dom.entityContractList.innerHTML = ''; + entity.meta.contracts.forEach((contract) => { + const item = document.createElement('li'); + item.className = 'relationship-item'; + const summary = document.createElement('div'); + summary.className = 'relationship-name'; + summary.textContent = `${contract.type}:${contract.name} | ${contract.channel || '-'} | v${contract.version}`; + item.appendChild(summary); + const payloadBtn = document.createElement('button'); + payloadBtn.type = 'button'; + payloadBtn.textContent = 'payload'; + payloadBtn.onclick = () => { + const raw = window.prompt( + `Payload schema JSON for ${contract.type}:${contract.name}`, + JSON.stringify(contract.payloadSchema || {}, null, 2) + ); + if (raw === null) return; + try { + const parsed = raw.trim() ? JSON.parse(raw) : {}; + withPersist(() => { + contract.payloadSchema = parsed; + renderEntityContractsInspector(entity); + }); + } catch (_) { + window.alert('Invalid JSON payload schema.'); + } + }; + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.textContent = 'x'; + removeBtn.onclick = () => { + withPersist(() => { + entity.meta.contracts = entity.meta.contracts.filter((candidate) => candidate.id !== contract.id); + renderEntityContractsInspector(entity); + }); + }; + item.appendChild(payloadBtn); + item.appendChild(removeBtn); + dom.entityContractList.appendChild(item); + }); +} + +function addSelectedEntityContract() { + const found = findEntity(state.selectedEntityId); + if (!found) { + window.alert('Select an entity first.'); + return; + } + const name = String(dom.entityContractNameInput.value || '').trim(); + const type = dom.entityContractTypeSelect.value || 'event'; + const channel = String(dom.entityContractChannelInput.value || '').trim(); + const version = String(dom.entityContractVersionInput.value || '').trim() || '1.0.0'; + if (!name) { + window.alert('Contract name is required.'); + return; + } + withPersist(() => { + if (!found.entity.meta) found.entity.meta = {}; + if (!Array.isArray(found.entity.meta.contracts)) found.entity.meta.contracts = []; + found.entity.meta.contracts.push(normalizeContractInput({ + id: nextId('contract'), + name, + type, + channel, + version, + payloadSchema: {} + }, found.entity.meta.contracts.length)); + dom.entityContractNameInput.value = ''; + dom.entityContractChannelInput.value = ''; + dom.entityContractVersionInput.value = '1.0.0'; + renderEntityContractsInspector(found.entity); + }); +} + +function saveSelectedEntityOasComposition() { + const found = findEntity(state.selectedEntityId); + if (!found) { + window.alert('Select an entity first.'); + return; + } + const mode = dom.entityOasCompositionModeSelect.value; + const refs = parseCommaSeparated(dom.entityOasCompositionRefsInput.value); + const externalRefs = parseCommaSeparated(dom.entityOasExternalRefsInput.value); + const discriminator = String(dom.entityOasDiscriminatorInput.value || '').trim(); + withPersist(() => { + if (!found.entity.meta) found.entity.meta = {}; + found.entity.meta.oasComposition = { + mode: ['oneOf', 'allOf', 'anyOf'].includes(mode) ? mode : '', + refs, + externalRefs, + discriminator + }; + renderEntityInspector(); + }); +} + function duplicateSelectedEntity() { const found = findEntity(state.selectedEntityId); if (!found) { @@ -1262,10 +1639,12 @@ function duplicateSelectedEntity() { } withPersist(() => { const clonedFields = JSON.parse(JSON.stringify(found.entity.fields || [])); + const clonedMeta = JSON.parse(JSON.stringify(found.entity.meta || { aggregateRoot: false, invariants: [] })); addEntity(found.domain.id, candidateName, { x: Math.min(320, found.entity.x + 22), y: Math.min(180, found.entity.y + 22), - fields: clonedFields + fields: clonedFields, + meta: clonedMeta }); render(); }); @@ -1419,18 +1798,24 @@ function buildRelationshipName(fromEntityId, toEntityId, fromCardinality, toCard return `${from} relates to ${to}`; } -function runModelChecks() { +function severityRank(severity) { + if (severity === 'error') return 3; + if (severity === 'warn') return 2; + return 1; +} + +function collectModelIssues() { const issues = []; - const pushIssue = (message, entityId = null) => issues.push({ message, entityId }); + const pushIssue = (message, entityId = null, severity = 'error') => issues.push({ message, entityId, severity }); const seenDomainNames = new Set(); state.domains.forEach((domain) => { const domainNameKey = normalizedName(domain.name); if (!domain.name || !domainNameKey) { - pushIssue('Domain with empty name found.'); + pushIssue('Domain with empty name found.', null, 'error'); } if (seenDomainNames.has(domainNameKey)) { - pushIssue(`Duplicate domain name: ${domain.name}`); + pushIssue(`Duplicate domain name: ${domain.name}`, null, 'error'); } seenDomainNames.add(domainNameKey); @@ -1438,10 +1823,10 @@ function runModelChecks() { domain.entities.forEach((entity) => { const entityKey = normalizedName(entity.name); if (!entity.name || !entityKey) { - pushIssue(`Entity with empty name in domain ${domain.name}`, entity.id); + pushIssue(`Entity with empty name in domain ${domain.name}`, entity.id, 'error'); } if (seenEntityNames.has(entityKey)) { - pushIssue(`Duplicate entity name in domain ${domain.name}: ${entity.name}`, entity.id); + pushIssue(`Duplicate entity name in domain ${domain.name}: ${entity.name}`, entity.id, 'error'); } seenEntityNames.add(entityKey); @@ -1450,25 +1835,56 @@ function runModelChecks() { entity.fields.forEach((field) => { const fieldKey = normalizedName(field.name); if (!field.name || !fieldKey) { - pushIssue(`Entity ${domain.name}/${entity.name} has an empty field name.`, entity.id); + pushIssue(`Entity ${domain.name}/${entity.name} has an empty field name.`, entity.id, 'error'); } if (seenFields.has(fieldKey)) { - pushIssue(`Entity ${domain.name}/${entity.name} has duplicated field: ${field.name}`, entity.id); + pushIssue(`Entity ${domain.name}/${entity.name} has duplicated field: ${field.name}`, entity.id, 'error'); } seenFields.add(fieldKey); if (field.type === 'array' && !field.itemsType) { - pushIssue(`Field ${domain.name}/${entity.name}.${field.name} is array but has no itemsType.`, entity.id); + pushIssue(`Field ${domain.name}/${entity.name}.${field.name} is array but has no itemsType.`, entity.id, 'error'); } if (field.minLength !== null && field.maxLength !== null && field.minLength > field.maxLength) { - pushIssue(`Field ${domain.name}/${entity.name}.${field.name} has minLength > maxLength.`, entity.id); + pushIssue(`Field ${domain.name}/${entity.name}.${field.name} has minLength > maxLength.`, entity.id, 'error'); } if (field.minimum !== null && field.maximum !== null && field.minimum > field.maximum) { - pushIssue(`Field ${domain.name}/${entity.name}.${field.name} has minimum > maximum.`, entity.id); + pushIssue(`Field ${domain.name}/${entity.name}.${field.name} has minimum > maximum.`, entity.id, 'error'); } if (field.pk) hasPrimaryKey = true; + if (field.required && field.nullable) { + pushIssue(`Field ${domain.name}/${entity.name}.${field.name} is required and nullable simultaneously.`, entity.id, 'warn'); + } }); if (!hasPrimaryKey) { - pushIssue(`Entity ${domain.name}/${entity.name} has no primary key field.`, entity.id); + pushIssue(`Entity ${domain.name}/${entity.name} has no primary key field.`, entity.id, 'error'); + } + if (!entity?.meta?.aggregateRoot && Array.isArray(entity?.meta?.invariants) && entity.meta.invariants.length > 0) { + pushIssue(`Entity ${domain.name}/${entity.name} has invariants but is not marked as aggregate root.`, entity.id, 'warn'); + } + const policy = getEntityRbacPolicy(entity); + ['list', 'getById', 'create', 'update', 'delete'].forEach((action) => { + const roles = Array.isArray(policy?.[action]?.roles) ? policy[action].roles : []; + if (!roles.length) { + pushIssue(`Entity ${domain.name}/${entity.name} has no RBAC roles for action "${action}".`, entity.id, 'warn'); + } + }); + const contracts = Array.isArray(entity?.meta?.contracts) ? entity.meta.contracts : []; + contracts.forEach((contract) => { + if (!contract?.name) { + pushIssue(`Entity ${domain.name}/${entity.name} has a contract without name.`, entity.id, 'error'); + } + if (!contract?.channel) { + pushIssue(`Contract ${domain.name}/${entity.name}.${contract?.name || 'unknown'} has empty channel/topic.`, entity.id, 'warn'); + } + }); + const composition = entity?.meta?.oasComposition || {}; + const mode = ['oneOf', 'allOf', 'anyOf'].includes(composition.mode) ? composition.mode : ''; + const refs = parseCommaSeparated(composition.refs || []); + if (mode && refs.length < 2) { + pushIssue(`Entity ${domain.name}/${entity.name} composition "${mode}" should reference at least 2 schemas.`, entity.id, 'warn'); + } + if (composition.discriminator && !mode) { + pushIssue(`Entity ${domain.name}/${entity.name} has discriminator without composition mode.`, entity.id, 'warn'); } }); }); @@ -1477,34 +1893,48 @@ function runModelChecks() { const from = findEntity(relationship.fromEntityId); const to = findEntity(relationship.toEntityId); if (!from || !to) { - pushIssue(`Relationship "${relationship.name || relationship.id}" references missing entities.`); + pushIssue(`Relationship "${relationship.name || relationship.id}" references missing entities.`, null, 'error'); } if (!['1', 'N'].includes(relationship.fromCardinality) || !['1', 'N'].includes(relationship.toCardinality)) { - pushIssue(`Relationship "${relationship.name || relationship.id}" has invalid cardinality.`); + pushIssue(`Relationship "${relationship.name || relationship.id}" has invalid cardinality.`, null, 'error'); + } + const bendX = normalizeOptionalNumber(relationship.bendX); + const bendY = normalizeOptionalNumber(relationship.bendY); + if ((bendX === null) !== (bendY === null)) { + pushIssue(`Relationship "${relationship.name || relationship.id}" should define both bendX and bendY or none.`, null, 'warn'); } }); + return issues; +} + +function runModelChecks() { + const issues = collectModelIssues(); renderModelCheckResults(issues); + return issues; } function renderModelCheckResults(issues) { dom.modelCheckList.innerHTML = ''; - if (!issues.length) { + const threshold = state.view.modelCheckMinSeverity || 'info'; + const filtered = issues.filter((issue) => severityRank(issue.severity || 'error') >= severityRank(threshold)); + if (!filtered.length) { const li = document.createElement('li'); li.textContent = 'No issues found.'; dom.modelCheckList.appendChild(li); return; } - issues.forEach((issue) => { + filtered.forEach((issue) => { const li = document.createElement('li'); + const prefix = `[${String(issue.severity || 'error').toUpperCase()}] `; if (issue.entityId) { const btn = document.createElement('button'); btn.type = 'button'; - btn.textContent = issue.message; + btn.textContent = `${prefix}${issue.message}`; btn.onclick = () => focusEntity(issue.entityId); li.appendChild(btn); } else { - li.textContent = issue.message; + li.textContent = `${prefix}${issue.message}`; } dom.modelCheckList.appendChild(li); }); @@ -1554,6 +1984,12 @@ function renderDomains() { const entityHeader = document.createElement('header'); entityHeader.className = 'entity-header'; entityHeader.textContent = entity.name; + if (entity?.meta?.aggregateRoot) { + const aggregateTag = document.createElement('span'); + aggregateTag.className = 'entity-aggregate-tag'; + aggregateTag.textContent = 'AR'; + entityHeader.appendChild(aggregateTag); + } attachDrag(entityHeader, (dx, dy) => { moveEntityInsideDomain(entity, entityEl, dx, dy); }); @@ -1568,6 +2004,35 @@ function renderDomains() { }); } + const anchorSides = ['top', 'right', 'bottom', 'left']; + anchorSides.forEach((side) => { + const anchorBtn = document.createElement('button'); + anchorBtn.type = 'button'; + anchorBtn.className = `entity-anchor entity-anchor-${side}`; + anchorBtn.title = `Drag from ${entity.name} (${side}) to create relationship`; + anchorBtn.addEventListener('pointerdown', (event) => { + event.stopPropagation(); + startAnchorDrag(entity.id, side, event); + }); + anchorBtn.addEventListener('pointerup', (event) => { + if (!interaction.relationshipAnchorDragActive) return; + event.stopPropagation(); + const fromEntityId = interaction.relationshipAnchorFromEntityId; + if (!fromEntityId || fromEntityId === entity.id) { + stopAnchorDrag(); + return; + } + const fromCardinality = dom.fromCardSelect.value || 'N'; + const toCardinality = dom.toCardSelect.value || '1'; + addRelationship(fromEntityId, entity.id, fromCardinality, toCardinality, { + fromAnchorSide: interaction.relationshipAnchorFromSide, + toAnchorSide: side + }); + stopAnchorDrag(); + }); + entityEl.appendChild(anchorBtn); + }); + entityEl.appendChild(entityHeader); entityEl.appendChild(fieldsEl); bodyEl.appendChild(entityEl); @@ -1599,20 +2064,89 @@ function entityCenterOnCanvas(entityId) { }; } +function entityAnchorOnCanvas(entityId, side) { + const found = findEntity(entityId); + if (!found) return null; + const originX = found.domain.x + found.entity.x; + const originY = found.domain.y + found.entity.y; + const width = 190; + const headerHeight = 32; + const bodyHeight = state.view.compactEntities ? 24 : 58; + const height = headerHeight + bodyHeight; + if (side === 'left') return { x: originX, y: originY + height / 2 }; + if (side === 'right') return { x: originX + width, y: originY + height / 2 }; + if (side === 'top') return { x: originX + width / 2, y: originY }; + if (side === 'bottom') return { x: originX + width / 2, y: originY + height }; + return entityCenterOnCanvas(entityId); +} + +function pointerToCanvasPoint(clientX, clientY) { + const zoom = state.view.zoom || 1; + const rect = dom.canvas.getBoundingClientRect(); + return { + x: (dom.canvas.scrollLeft + clientX - rect.left) / zoom, + y: (dom.canvas.scrollTop + clientY - rect.top) / zoom + }; +} + +function clearAnchorPreviewEdge() { + const preview = dom.edges.querySelector('.edge-preview'); + if (preview) preview.remove(); +} + +function renderAnchorPreviewEdge(fromPoint, toPoint) { + clearAnchorPreviewEdge(); + if (!fromPoint || !toPoint) return; + const controlX = (fromPoint.x + toPoint.x) / 2; + const isOrthogonal = state.view.edgeStyle === 'orthogonal'; + const pathD = isOrthogonal + ? `M ${fromPoint.x} ${fromPoint.y} L ${controlX} ${fromPoint.y} L ${controlX} ${toPoint.y} L ${toPoint.x} ${toPoint.y}` + : `M ${fromPoint.x} ${fromPoint.y} C ${controlX} ${fromPoint.y}, ${controlX} ${toPoint.y}, ${toPoint.x} ${toPoint.y}`; + const previewPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + previewPath.setAttribute('class', 'edge-preview'); + previewPath.setAttribute('d', pathD); + dom.edges.appendChild(previewPath); +} + +function stopAnchorDrag() { + interaction.relationshipAnchorDragActive = false; + interaction.relationshipAnchorFromEntityId = null; + interaction.relationshipAnchorFromSide = null; + clearAnchorPreviewEdge(); +} + +function startAnchorDrag(entityId, side, event) { + const fromPoint = entityAnchorOnCanvas(entityId, side); + if (!fromPoint) return; + interaction.relationshipAnchorDragActive = true; + interaction.relationshipAnchorFromEntityId = entityId; + interaction.relationshipAnchorFromSide = side; + const pointer = pointerToCanvasPoint(event.clientX, event.clientY); + renderAnchorPreviewEdge(fromPoint, pointer); +} + function renderEdges() { dom.edges.innerHTML = ''; state.relationships.forEach((relationship) => { - const from = entityCenterOnCanvas(relationship.fromEntityId); - const to = entityCenterOnCanvas(relationship.toEntityId); + const useCenter = relationship.anchorBehavior === 'center'; + const from = (!useCenter && relationship.fromAnchorSide) + ? entityAnchorOnCanvas(relationship.fromEntityId, relationship.fromAnchorSide) + : entityCenterOnCanvas(relationship.fromEntityId); + const to = (!useCenter && relationship.toAnchorSide) + ? entityAnchorOnCanvas(relationship.toEntityId, relationship.toAnchorSide) + : entityCenterOnCanvas(relationship.toEntityId); if (!from || !to) return; - const controlX = (from.x + to.x) / 2; + const controlX = Number.isFinite(relationship.bendX) ? relationship.bendX : (from.x + to.x) / 2; + const controlY = Number.isFinite(relationship.bendY) ? relationship.bendY : (from.y + to.y) / 2; const isOrthogonal = state.view.edgeStyle === 'orthogonal'; const edgePathD = isOrthogonal - ? `M ${from.x} ${from.y} L ${controlX} ${from.y} L ${controlX} ${to.y} L ${to.x} ${to.y}` + ? `M ${from.x} ${from.y} L ${controlX} ${from.y} L ${controlX} ${controlY} L ${controlX} ${to.y} L ${to.x} ${to.y}` : `M ${from.x} ${from.y} C ${controlX} ${from.y}, ${controlX} ${to.y}, ${to.x} ${to.y}`; const labelX = isOrthogonal ? controlX : controlX; - const labelY = isOrthogonal ? ((from.y + to.y) / 2) : ((from.y + to.y) / 2); + const labelY = isOrthogonal ? controlY : ((from.y + to.y) / 2); + const labelOffsetX = Number.isFinite(relationship.labelOffsetX) ? relationship.labelOffsetX : 0; + const labelOffsetY = Number.isFinite(relationship.labelOffsetY) ? relationship.labelOffsetY : 0; const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const activeClass = state.selectedRelationshipId === relationship.id ? ' active' : ''; path.setAttribute('class', `edge-line${activeClass}`); @@ -1629,26 +2163,47 @@ function renderEdges() { }); dom.edges.appendChild(hit); - const fromLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - fromLabel.setAttribute('class', 'edge-label'); - fromLabel.setAttribute('x', String(from.x + 6)); - fromLabel.setAttribute('y', String(from.y - 6)); - fromLabel.textContent = relationship.fromCardinality; - dom.edges.appendChild(fromLabel); - - const toLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - toLabel.setAttribute('class', 'edge-label'); - toLabel.setAttribute('x', String(to.x + 6)); - toLabel.setAttribute('y', String(to.y - 6)); - toLabel.textContent = relationship.toCardinality; - dom.edges.appendChild(toLabel); + if (!state.view.largeCanvasMode) { + const fromLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + fromLabel.setAttribute('class', 'edge-label'); + fromLabel.setAttribute('x', String(from.x + 6)); + fromLabel.setAttribute('y', String(from.y - 6)); + fromLabel.textContent = relationship.fromCardinality; + dom.edges.appendChild(fromLabel); + + const toLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + toLabel.setAttribute('class', 'edge-label'); + toLabel.setAttribute('x', String(to.x + 6)); + toLabel.setAttribute('y', String(to.y - 6)); + toLabel.textContent = relationship.toCardinality; + dom.edges.appendChild(toLabel); + + const nameLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + nameLabel.setAttribute('class', 'edge-label'); + nameLabel.setAttribute('x', String(labelX + 6 + labelOffsetX)); + nameLabel.setAttribute('y', String(labelY - 6 + labelOffsetY)); + nameLabel.textContent = relationship.name || ''; + dom.edges.appendChild(nameLabel); + } + }); +} - const nameLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - nameLabel.setAttribute('class', 'edge-label'); - nameLabel.setAttribute('x', String(labelX + 6)); - nameLabel.setAttribute('y', String(labelY - 6)); - nameLabel.textContent = relationship.name || ''; - dom.edges.appendChild(nameLabel); +function renderMiniMap() { + if (!dom.miniMap) return; + dom.miniMap.innerHTML = ''; + const scale = 0.055; + state.domains.forEach((domain) => { + const box = document.createElement('div'); + box.className = 'mini-map-domain'; + box.style.left = `${domain.x * scale}px`; + box.style.top = `${domain.y * scale}px`; + box.style.width = `${520 * scale}px`; + box.style.height = `${280 * scale}px`; + box.style.borderColor = domain.color || '#64748b'; + if (state.selectedDomainId === domain.id) box.classList.add('active'); + box.title = domain.name; + box.onclick = () => setSelectedDomain(domain.id); + dom.miniMap.appendChild(box); }); } @@ -1664,6 +2219,40 @@ function renderEntityInspector() { dom.duplicateEntityBtn.disabled = true; dom.entityMoveDomainSelect.disabled = true; dom.moveEntityBtn.disabled = true; + dom.entityAggregateRootCheck.checked = false; + dom.entityAggregateRootCheck.disabled = true; + dom.entityInvariantsInput.value = ''; + dom.entityInvariantsInput.disabled = true; + dom.saveEntityRulesBtn.disabled = true; + dom.entityRbacActionSelect.disabled = true; + dom.entityRbacSuperadminCheck.checked = false; + dom.entityRbacAdminCheck.checked = false; + dom.entityRbacUserCheck.checked = false; + dom.entityRbacTenantCheck.checked = false; + dom.entityRbacSuperadminCheck.disabled = true; + dom.entityRbacAdminCheck.disabled = true; + dom.entityRbacUserCheck.disabled = true; + dom.entityRbacTenantCheck.disabled = true; + dom.saveEntityRbacBtn.disabled = true; + dom.entityRbacList.innerHTML = ''; + dom.entityContractNameInput.value = ''; + dom.entityContractNameInput.disabled = true; + dom.entityContractTypeSelect.disabled = true; + dom.entityContractChannelInput.value = ''; + dom.entityContractChannelInput.disabled = true; + dom.entityContractVersionInput.value = '1.0.0'; + dom.entityContractVersionInput.disabled = true; + dom.addEntityContractBtn.disabled = true; + dom.entityContractList.innerHTML = ''; + dom.entityOasCompositionModeSelect.value = ''; + dom.entityOasCompositionModeSelect.disabled = true; + dom.entityOasCompositionRefsInput.value = ''; + dom.entityOasCompositionRefsInput.disabled = true; + dom.entityOasExternalRefsInput.value = ''; + dom.entityOasExternalRefsInput.disabled = true; + dom.entityOasDiscriminatorInput.value = ''; + dom.entityOasDiscriminatorInput.disabled = true; + dom.saveEntityOasCompositionBtn.disabled = true; dom.fieldTemplateSelect.disabled = true; dom.applyFieldTemplateBtn.disabled = true; return; @@ -1675,6 +2264,37 @@ function renderEntityInspector() { dom.duplicateEntityBtn.disabled = false; dom.entityMoveDomainSelect.disabled = false; dom.moveEntityBtn.disabled = false; + dom.entityAggregateRootCheck.disabled = false; + dom.entityAggregateRootCheck.checked = Boolean(found.entity?.meta?.aggregateRoot); + dom.entityInvariantsInput.disabled = false; + dom.entityInvariantsInput.value = Array.isArray(found.entity?.meta?.invariants) + ? found.entity.meta.invariants.join('\n') + : ''; + dom.saveEntityRulesBtn.disabled = false; + dom.entityRbacActionSelect.disabled = false; + dom.entityRbacSuperadminCheck.disabled = false; + dom.entityRbacAdminCheck.disabled = false; + dom.entityRbacUserCheck.disabled = false; + dom.entityRbacTenantCheck.disabled = false; + dom.saveEntityRbacBtn.disabled = false; + renderEntityRbacInspector(found.entity); + dom.entityContractNameInput.disabled = false; + dom.entityContractTypeSelect.disabled = false; + dom.entityContractChannelInput.disabled = false; + dom.entityContractVersionInput.disabled = false; + dom.addEntityContractBtn.disabled = false; + if (!dom.entityContractVersionInput.value) dom.entityContractVersionInput.value = '1.0.0'; + renderEntityContractsInspector(found.entity); + dom.entityOasCompositionModeSelect.disabled = false; + dom.entityOasCompositionRefsInput.disabled = false; + dom.entityOasExternalRefsInput.disabled = false; + dom.entityOasDiscriminatorInput.disabled = false; + dom.saveEntityOasCompositionBtn.disabled = false; + const composition = found.entity?.meta?.oasComposition || { mode: '', refs: [], externalRefs: [], discriminator: '' }; + dom.entityOasCompositionModeSelect.value = composition.mode || ''; + dom.entityOasCompositionRefsInput.value = Array.isArray(composition.refs) ? composition.refs.join(', ') : ''; + dom.entityOasExternalRefsInput.value = Array.isArray(composition.externalRefs) ? composition.externalRefs.join(', ') : ''; + dom.entityOasDiscriminatorInput.value = composition.discriminator || ''; dom.fieldTemplateSelect.disabled = false; dom.applyFieldTemplateBtn.disabled = false; dom.entityMoveDomainSelect.value = found.domain.id; @@ -1764,7 +2384,191 @@ function renderEntityInspector() { }); } +function buildModelSnapshot() { + const domains = state.domains.map((domain) => ({ + id: domain.id, + name: domain.name, + color: domain.color, + context: domain.context || {}, + entities: domain.entities.map((entity) => ({ + id: entity.id, + name: entity.name, + meta: entity.meta || { aggregateRoot: false, invariants: [] }, + contracts: Array.isArray(entity?.meta?.contracts) + ? entity.meta.contracts.map((contract, index) => normalizeContractInput(contract, index)) + : [], + fields: entity.fields.map((field) => ({ + name: field.name, + type: field.type, + required: Boolean(field.required), + pk: Boolean(field.pk), + fk: Boolean(field.fk), + unique: Boolean(field.unique), + nullable: Boolean(field.nullable), + format: field.format || '', + itemsType: field.itemsType || '', + enumValues: Array.isArray(field.enumValues) ? [...field.enumValues] : [] + })) + })) + })); + const relationships = state.relationships.map((relationship) => ({ + id: relationship.id, + fromEntityId: relationship.fromEntityId, + toEntityId: relationship.toEntityId, + fromCardinality: relationship.fromCardinality, + toCardinality: relationship.toCardinality + })); + return { domains, relationships }; +} + +function saveSchemaBaseline() { + const snapshot = buildModelSnapshot(); + localStorage.setItem(DIFF_BASELINE_KEY, JSON.stringify(snapshot)); + renderSchemaDiffResults([{ severity: 'info', message: 'Baseline saved.' }]); +} + +function loadSchemaBaseline() { + const raw = localStorage.getItem(DIFF_BASELINE_KEY); + if (!raw) return null; + try { + return JSON.parse(raw); + } catch (_) { + return null; + } +} + +function renderSchemaDiffResults(items) { + dom.schemaDiffList.innerHTML = ''; + if (!Array.isArray(items) || !items.length) { + const li = document.createElement('li'); + li.textContent = 'No schema diff available.'; + dom.schemaDiffList.appendChild(li); + return; + } + items.forEach((item) => { + const li = document.createElement('li'); + li.textContent = `[${String(item.severity || 'info').toUpperCase()}] ${item.message}`; + dom.schemaDiffList.appendChild(li); + }); +} + +function renderSchemaDiffStatus() { + if (dom.schemaDiffList.children.length > 0) return; + const hasBaseline = Boolean(loadSchemaBaseline()); + renderSchemaDiffResults([{ + severity: hasBaseline ? 'info' : 'warn', + message: hasBaseline ? 'Baseline loaded. Run diff to preview migration hints.' : 'No baseline saved yet.' + }]); +} + +function runSchemaDiff() { + const baseline = loadSchemaBaseline(); + if (!baseline) { + renderSchemaDiffResults([{ severity: 'warn', message: 'No baseline found. Save baseline first.' }]); + return; + } + const current = buildModelSnapshot(); + const changes = []; + const baseDomainsById = new Map((baseline.domains || []).map((domain) => [domain.id, domain])); + const currentDomainsById = new Map((current.domains || []).map((domain) => [domain.id, domain])); + + current.domains.forEach((domain) => { + if (!baseDomainsById.has(domain.id)) { + changes.push({ severity: 'info', message: `Create table/collection for domain "${domain.name}".` }); + } + }); + (baseline.domains || []).forEach((domain) => { + if (!currentDomainsById.has(domain.id)) { + changes.push({ severity: 'warn', message: `Drop domain "${domain.name}" scope and related entities.` }); + } + }); + + const baseEntities = new Map(); + (baseline.domains || []).forEach((domain) => { + (domain.entities || []).forEach((entity) => baseEntities.set(entity.id, { domain, entity })); + }); + const currentEntities = new Map(); + current.domains.forEach((domain) => { + (domain.entities || []).forEach((entity) => currentEntities.set(entity.id, { domain, entity })); + }); + + currentEntities.forEach(({ domain, entity }, entityId) => { + if (!baseEntities.has(entityId)) { + changes.push({ severity: 'info', message: `Create entity "${domain.name}/${entity.name}".` }); + return; + } + const baseEntity = baseEntities.get(entityId).entity; + const baseFieldsByName = new Map((baseEntity.fields || []).map((field) => [normalizedName(field.name), field])); + const currentFieldsByName = new Map((entity.fields || []).map((field) => [normalizedName(field.name), field])); + + entity.fields.forEach((field) => { + if (!baseFieldsByName.has(normalizedName(field.name))) { + changes.push({ severity: 'info', message: `Add field "${domain.name}/${entity.name}.${field.name}" (${field.type}).` }); + } + }); + (baseEntity.fields || []).forEach((field) => { + if (!currentFieldsByName.has(normalizedName(field.name))) { + changes.push({ severity: 'warn', message: `Remove field "${domain.name}/${entity.name}.${field.name}".` }); + } + }); + entity.fields.forEach((field) => { + const previous = baseFieldsByName.get(normalizedName(field.name)); + if (!previous) return; + if (previous.type !== field.type) { + changes.push({ severity: 'warn', message: `Alter type of "${domain.name}/${entity.name}.${field.name}" from ${previous.type} to ${field.type}.` }); + } + if (Boolean(previous.required) !== Boolean(field.required)) { + changes.push({ severity: 'warn', message: `Change required flag on "${domain.name}/${entity.name}.${field.name}" to ${field.required}.` }); + } + }); + + const baseContracts = new Set(((baseEntity.meta?.contracts || [])).map((contract) => `${contract.type}|${contract.name}|${contract.version}`)); + const currentContracts = new Set(((entity.meta?.contracts || [])).map((contract) => `${contract.type}|${contract.name}|${contract.version}`)); + currentContracts.forEach((key) => { + if (!baseContracts.has(key)) { + changes.push({ severity: 'info', message: `Add message contract "${domain.name}/${entity.name}" -> ${key}.` }); + } + }); + baseContracts.forEach((key) => { + if (!currentContracts.has(key)) { + changes.push({ severity: 'warn', message: `Remove message contract "${domain.name}/${entity.name}" -> ${key}.` }); + } + }); + }); + + baseEntities.forEach(({ domain, entity }, entityId) => { + if (!currentEntities.has(entityId)) { + changes.push({ severity: 'warn', message: `Drop entity "${domain.name}/${entity.name}".` }); + } + }); + + const baseRels = new Set((baseline.relationships || []).map((rel) => `${rel.fromEntityId}|${rel.toEntityId}|${rel.fromCardinality}|${rel.toCardinality}`)); + const currentRels = new Set((current.relationships || []).map((rel) => `${rel.fromEntityId}|${rel.toEntityId}|${rel.fromCardinality}|${rel.toCardinality}`)); + currentRels.forEach((key) => { + if (!baseRels.has(key)) changes.push({ severity: 'info', message: `Add relationship ${key}.` }); + }); + baseRels.forEach((key) => { + if (!currentRels.has(key)) changes.push({ severity: 'warn', message: `Remove relationship ${key}.` }); + }); + + if (!changes.length) { + changes.push({ severity: 'info', message: 'No schema changes detected versus baseline.' }); + } + renderSchemaDiffResults(changes); +} + +function canExportModel() { + if (!state.view.exportBlockCritical) return true; + const issues = collectModelIssues(); + const criticalCount = issues.filter((issue) => issue.severity === 'error').length; + if (criticalCount === 0) return true; + renderModelCheckResults(issues); + window.alert(`Export blocked: ${criticalCount} critical model issue(s). Run "Validate Model" and fix errors before exporting.`); + return false; +} + function exportAsJson() { + if (!canExportModel()) return; const payload = { domains: state.domains, relationships: state.relationships, view: state.view }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -1775,6 +2579,299 @@ function exportAsJson() { URL.revokeObjectURL(url); } +function exportAsMarkdown() { + if (!canExportModel()) return; + const lines = []; + lines.push('# Domain Designer Model'); + lines.push(''); + state.domains.forEach((domain) => { + lines.push(`## Domain: ${domain.name}`); + lines.push(''); + if (domain?.context) { + lines.push(`- Ubiquitous Language: ${domain.context.ubiquitousLanguage || '-'}`); + lines.push(`- Owner Team: ${domain.context.ownerTeam || '-'}`); + lines.push(`- Upstream: ${(domain.context.upstreamDependencies || []).join(', ') || '-'}`); + lines.push(`- Downstream: ${(domain.context.downstreamDependencies || []).join(', ') || '-'}`); + lines.push(`- Integration Channel: ${domain.context.integrationChannel || '-'}`); + lines.push(`- Package Dependencies: ${(domain.context.packageDependencies || []).join(', ') || '-'}`); + lines.push(`- Shared Value Objects: ${(domain.context.sharedValueObjects || []).join(', ') || '-'}`); + lines.push(''); + } + domain.entities.forEach((entity) => { + lines.push(`### Entity: ${entity.name}`); + lines.push(''); + lines.push(`- Aggregate Root: ${Boolean(entity?.meta?.aggregateRoot)}`); + lines.push(`- Invariants: ${(entity?.meta?.invariants || []).join('; ') || '-'}`); + lines.push(''); + lines.push('| Field | Type | Required | PK | FK | Unique | Nullable |'); + lines.push('|---|---|---:|---:|---:|---:|---:|'); + entity.fields.forEach((field) => { + lines.push(`| ${field.name} | ${field.type}${field.format ? `(${field.format})` : ''} | ${Boolean(field.required)} | ${Boolean(field.pk)} | ${Boolean(field.fk)} | ${Boolean(field.unique)} | ${Boolean(field.nullable)} |`); + }); + lines.push(''); + lines.push('RBAC:'); + const policy = getEntityRbacPolicy(entity); + ['list', 'getById', 'create', 'update', 'delete'].forEach((action) => { + const rule = policy[action] || { roles: [], tenantScoped: true }; + lines.push(`- ${action}: [${(rule.roles || []).join(', ')}], tenantScoped=${Boolean(rule.tenantScoped)}`); + }); + lines.push(''); + lines.push('Message Contracts:'); + const contracts = Array.isArray(entity?.meta?.contracts) ? entity.meta.contracts : []; + if (!contracts.length) { + lines.push('- none'); + } else { + contracts.forEach((contract) => { + lines.push(`- ${contract.type}:${contract.name} | channel=${contract.channel || '-'} | version=${contract.version}`); + }); + } + lines.push(''); + }); + }); + if (state.relationships.length) { + lines.push('## Relationships'); + lines.push(''); + state.relationships.forEach((relationship) => { + lines.push(`- ${relationship.name || relationship.id}: ${entityLabel(relationship.fromEntityId)} (${relationship.fromCardinality}) -> (${relationship.toCardinality}) ${entityLabel(relationship.toEntityId)}`); + }); + } + const blob = new Blob([lines.join('\n')], { type: 'text/markdown' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'domain-designer-model.md'; + a.click(); + URL.revokeObjectURL(url); +} + +function buildExampleValueForField(field) { + if (field.enumValues?.length) return field.enumValues[0]; + if (field.type === 'uuid') return '00000000-0000-4000-8000-000000000001'; + if (field.type === 'datetime') return '2026-01-01T00:00:00.000Z'; + if (field.type === 'date') return '2026-01-01'; + if (field.type === 'integer') return 1; + if (field.type === 'number') return 10.5; + if (field.type === 'boolean') return true; + if (field.type === 'array') return []; + if (field.type === 'object') return {}; + if (field.format === 'email') return 'user@example.com'; + if (field.format === 'uri') return 'https://example.com/resource'; + return `${field.name || 'value'}_example`; +} + +function buildEntityRequestExample(entity, mode = 'create') { + const payload = {}; + (entity.fields || []).forEach((field) => { + if (mode === 'create' && field.pk) return; + if (mode === 'update' && field.pk) return; + if (mode === 'update' && !field.required && !field.fk && !field.unique) return; + payload[field.name] = buildExampleValueForField(field); + }); + return payload; +} + +function buildEntityResponseExample(entity) { + const payload = {}; + (entity.fields || []).forEach((field) => { + payload[field.name] = buildExampleValueForField(field); + }); + return payload; +} + +function generateExamplesPreview() { + const selected = findEntity(state.selectedEntityId); + const targets = selected + ? [{ domain: selected.domain, entity: selected.entity }] + : state.domains.flatMap((domain) => domain.entities.map((entity) => ({ domain, entity }))); + if (!targets.length) { + dom.examplesPreviewOutput.textContent = '// No entities found for example generation.'; + return; + } + const chunks = targets.map(({ domain, entity }) => { + const schemaName = toSchemaName(domain.name, entity.name); + const requestCreate = buildEntityRequestExample(entity, 'create'); + const requestUpdate = buildEntityRequestExample(entity, 'update'); + const response = buildEntityResponseExample(entity); + return [ + `// ${domain.name}/${entity.name}`, + `// create${schemaName} request`, + JSON.stringify(requestCreate, null, 2), + `// update${schemaName} request`, + JSON.stringify(requestUpdate, null, 2), + `// ${schemaName} response`, + JSON.stringify(response, null, 2) + ].join('\n'); + }); + dom.examplesPreviewOutput.textContent = chunks.join('\n\n/* ---------------------------------------- */\n\n'); +} + +function exportAsJsonSchema() { + if (!canExportModel()) return; + const definitions = {}; + state.domains.forEach((domain) => { + domain.entities.forEach((entity) => { + const schemaName = toSchemaName(domain.name, entity.name); + const properties = {}; + const required = []; + (entity.fields || []).forEach((field) => { + properties[field.name] = toOasFieldSchema(field); + if (field.required) required.push(field.name); + }); + definitions[schemaName] = { + $id: schemaName, + type: 'object', + properties, + required, + additionalProperties: false + }; + }); + }); + const payload = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Domain Designer JSON Schemas', + type: 'object', + definitions + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'domain-designer-json-schema.json'; + a.click(); + URL.revokeObjectURL(url); +} + +function exportAsAsyncApi() { + if (!canExportModel()) return; + const channels = {}; + state.domains.forEach((domain) => { + domain.entities.forEach((entity) => { + const contracts = Array.isArray(entity?.meta?.contracts) ? entity.meta.contracts : []; + contracts.forEach((contract) => { + const channelName = contract.channel || `${toPathToken(domain.name)}/${toPathToken(entity.name)}/${contract.type}`; + if (!channels[channelName]) channels[channelName] = {}; + const operationKey = contract.type === 'response' ? 'subscribe' : 'publish'; + channels[channelName][operationKey] = { + operationId: `${contract.type}_${toSchemaName(domain.name, entity.name)}_${contract.name}`, + message: { + name: contract.name, + payload: contract.payloadSchema || {} + } + }; + }); + }); + }); + const payload = { + asyncapi: '3.0.0', + info: { + title: 'Domain Designer AsyncAPI Export', + version: '1.0.0' + }, + channels + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'domain-designer-asyncapi.json'; + a.click(); + URL.revokeObjectURL(url); +} + +function exportBoilerplateBundle() { + if (!canExportModel()) return; + const modules = state.domains.flatMap((domain) => domain.entities.map((entity) => ({ + module: `${domain.name}/${entity.name}`, + files: { + model: `src/modules/${domain.name}/domain/Model/${entity.name}.ts`, + repository: `src/modules/${domain.name}/application/ports/${entity.name}Repository.ts`, + useCase: `src/modules/${domain.name}/application/useCases/Create${entity.name}.ts`, + controller: `src/modules/${domain.name}/interface/controller/${entity.name}Controller.ts`, + handler: `src/modules/${domain.name}/interface/restapi/frameworks/express/handlers/create${entity.name}.ts` + } + }))); + const payload = { + kind: 'boilerplate-bundle', + version: '1.0.0', + generatedAt: new Date().toISOString(), + modules + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'domain-designer-boilerplate-bundle.json'; + a.click(); + URL.revokeObjectURL(url); +} + +function exportAsPackage() { + if (!canExportModel()) return; + const selected = getSelectedDomain(); + if (!selected) { + window.alert('Select a domain to export package.'); + return; + } + const payload = { + kind: 'domain-package', + version: '1.0.0', + exportedAt: new Date().toISOString(), + domain: selected + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${toPathToken(selected.name) || 'domain'}-package.json`; + a.click(); + URL.revokeObjectURL(url); +} + +function importDomainPackage(file) { + const reader = new FileReader(); + reader.onload = () => { + try { + const parsed = JSON.parse(String(reader.result || '{}')); + const sourceDomain = parsed?.domain; + if (!sourceDomain || !Array.isArray(sourceDomain.entities)) { + window.alert('Invalid package format.'); + return; + } + withPersist(() => { + const nextDomain = normalizeDomainInput(sourceDomain, state.domains.length); + nextDomain.context = nextDomain.context || {}; + nextDomain.context.packageDependencies = uniqueStrings(nextDomain.context.packageDependencies || []); + nextDomain.context.sharedValueObjects = uniqueStrings(nextDomain.context.sharedValueObjects || []); + const existingDeps = new Set( + state.domains.flatMap((domain) => domain?.context?.packageDependencies || []) + ); + const incomingNewDeps = nextDomain.context.packageDependencies.filter((dep) => !existingDeps.has(dep)); + if (incomingNewDeps.length) { + nextDomain.context.packageDependencies = uniqueStrings([ + ...nextDomain.context.packageDependencies, + ...incomingNewDeps + ]); + } + let domainName = nextDomain.name; + let suffix = 2; + while (isDomainNameTaken(domainName)) { + domainName = `${nextDomain.name}_${suffix}`; + suffix += 1; + } + nextDomain.name = domainName; + state.domains.push(nextDomain); + state.selectedDomainId = nextDomain.id; + state.selectedEntityId = nextDomain.entities[0]?.id || null; + recomputeIdCounter(); + render(); + }); + } catch (_) { + window.alert('Could not parse package JSON.'); + } + }; + reader.readAsText(file); +} + function toOasType(fieldType) { if (fieldType === 'integer') return { type: 'integer' }; if (fieldType === 'number') return { type: 'number' }; @@ -1838,7 +2935,64 @@ function toPathToken(value) { .replace(/^-+|-+$/g, ''); } +function buildCodePreviewForEntity(domain, entity) { + const className = toSchemaName('', entity.name).replace(/^_+/, ''); + const repositoryName = `${className}RepositoryPort`; + const useCaseName = `Create${className}UseCase`; + const controllerName = `${className}Controller`; + const handlerName = `create${className}Handler`; + const fields = (entity.fields || []).map((field) => ` ${field.name}: ${field.type};`).join('\n'); + return [ + `// ${domain.name} / ${entity.name}`, + `export interface ${className}Model {`, + fields || ' id: uuid;', + '}', + '', + `export interface ${repositoryName} {`, + ` create(input: ${className}Model): Promise<${className}Model>;`, + ` getById(id: string): Promise<${className}Model | null>;`, + '}', + '', + `export class ${useCaseName} {`, + ` constructor(private readonly repo: ${repositoryName}) {}`, + ` async execute(input: ${className}Model): Promise<${className}Model> {`, + ' return this.repo.create(input);', + ' }', + '}', + '', + `export class ${controllerName} {`, + ` constructor(private readonly createUseCase: ${useCaseName}) {}`, + ` async create(input: ${className}Model) {`, + ' return this.createUseCase.execute(input);', + ' }', + '}', + '', + `export async function ${handlerName}(requestBody: unknown) {`, + ` // validate requestBody against OAS schema for ${className}`, + ` // map to controller.${'create'} and return framework-specific response`, + '}' + ].join('\n'); +} + +function generateCodePreview() { + const found = findEntity(state.selectedEntityId); + if (found) { + dom.codePreviewOutput.textContent = buildCodePreviewForEntity(found.domain, found.entity); + return; + } + const chunks = []; + state.domains.forEach((domain) => { + domain.entities.forEach((entity) => { + chunks.push(buildCodePreviewForEntity(domain, entity)); + }); + }); + dom.codePreviewOutput.textContent = chunks.length + ? chunks.join('\n\n/* ---------------------------------------- */\n\n') + : '// Select an entity or create domains/entities to preview generated skeletons.'; +} + function exportAsOas() { + if (!canExportModel()) return; const schemas = {}; const paths = {}; const entitySchemaIndex = {}; @@ -1857,8 +3011,32 @@ function exportAsOas() { properties, required, 'x-domain': domain.name, - 'x-entity': entity.name + 'x-entity': entity.name, + 'x-message-contracts': Array.isArray(entity?.meta?.contracts) + ? entity.meta.contracts.map((contract, index) => normalizeContractInput(contract, index)) + : [] }; + const composition = entity?.meta?.oasComposition || {}; + const mode = ['oneOf', 'allOf', 'anyOf'].includes(composition.mode) ? composition.mode : ''; + const refs = parseCommaSeparated(composition.refs || []); + if (mode && refs.length) { + schemas[schemaName][mode] = refs.map((ref) => ({ $ref: `#/components/schemas/${ref}` })); + } + const externalRefs = parseCommaSeparated(composition.externalRefs || []); + if (externalRefs.length) { + schemas[schemaName]['x-external-refs'] = externalRefs; + } + const discriminator = String(composition.discriminator || '').trim(); + if (discriminator) { + const mapping = {}; + refs.forEach((refName) => { + mapping[refName] = `#/components/schemas/${refName}`; + }); + schemas[schemaName].discriminator = { + propertyName: discriminator, + mapping + }; + } const domainPath = toPathToken(domain.name); const entityPath = toPathToken(entity.name); @@ -1967,6 +3145,15 @@ function exportAsOas() { info: { title: 'Domain Designer Export', version: '1.0.0' }, paths, components: { schemas }, + 'x-message-contracts': state.domains.flatMap((domain) => ( + domain.entities.flatMap((entity) => ( + (Array.isArray(entity?.meta?.contracts) ? entity.meta.contracts : []).map((contract, index) => ({ + ...normalizeContractInput(contract, index), + domain: domain.name, + entity: entity.name + })) + )) + )), 'x-relations': state.relationships.map((relationship) => ({ name: relationship.name, fromEntityId: relationship.fromEntityId, @@ -1994,7 +3181,15 @@ function seed() { state.selectedEntityId = null; state.selectedRelationshipId = null; state.idCounter = 1; - state.view = { zoom: 1, compactEntities: false, snapToGrid: true, edgeStyle: 'curved' }; + state.view = { + zoom: 1, + compactEntities: false, + snapToGrid: true, + edgeStyle: 'curved', + modelCheckMinSeverity: 'info', + exportBlockCritical: true, + largeCanvasMode: false + }; const users = addDomain('Users', { x: 80, y: 80, color: '#93c5fd' }); const billing = addDomain('Billing', { x: 700, y: 200, color: '#86efac' }); @@ -2041,7 +3236,14 @@ function normalizeRelationship(relationship) { ...relationship, name: relationship.name || `${relationship.fromEntityId} -> ${relationship.toEntityId}`, fromCardinality: relationship.fromCardinality || 'N', - toCardinality: relationship.toCardinality || '1' + toCardinality: relationship.toCardinality || '1', + fromAnchorSide: ['top', 'right', 'bottom', 'left'].includes(relationship.fromAnchorSide) ? relationship.fromAnchorSide : null, + toAnchorSide: ['top', 'right', 'bottom', 'left'].includes(relationship.toAnchorSide) ? relationship.toAnchorSide : null, + anchorBehavior: relationship.anchorBehavior === 'center' ? 'center' : 'auto', + bendX: normalizeOptionalNumber(relationship.bendX), + bendY: normalizeOptionalNumber(relationship.bendY), + labelOffsetX: normalizeOptionalNumber(relationship.labelOffsetX) ?? 0, + labelOffsetY: normalizeOptionalNumber(relationship.labelOffsetY) ?? 0 }; } @@ -2053,12 +3255,46 @@ function normalizeEntityInput(entity, entityIndex) { const fieldsInput = Array.isArray(entity?.fields) ? entity.fields : defaultFields(); const fields = fieldsInput.map((field, fieldIndex) => normalizeField(field, fieldIndex)); const entityName = String(entity?.name || '').trim() || `Entity_${entityIndex + 1}`; + const invariants = Array.isArray(entity?.meta?.invariants) + ? entity.meta.invariants.map((item) => String(item).trim()).filter(Boolean) + : parseCommaSeparated(String(entity?.meta?.invariants || '').replace(/\n/g, ',')); + const rbac = getDefaultRbacPolicy(); + const sourceRbac = entity?.meta?.rbac || {}; + ['list', 'getById', 'create', 'update', 'delete'].forEach((action) => { + const rule = sourceRbac[action] || rbac[action] || {}; + rbac[action] = { + roles: Array.isArray(rule.roles) + ? rule.roles.map((role) => String(role).trim()).filter(Boolean) + : Array.isArray(rbac[action]?.roles) + ? rbac[action].roles + : [], + tenantScoped: typeof rule.tenantScoped === 'boolean' ? rule.tenantScoped : Boolean(rbac[action]?.tenantScoped) + }; + }); + const contracts = Array.isArray(entity?.meta?.contracts) + ? entity.meta.contracts.map((contract, index) => normalizeContractInput(contract, index)) + : []; + const oasComposition = { + mode: ['oneOf', 'allOf', 'anyOf'].includes(entity?.meta?.oasComposition?.mode) + ? entity.meta.oasComposition.mode + : '', + refs: parseCommaSeparated(entity?.meta?.oasComposition?.refs || []), + externalRefs: parseCommaSeparated(entity?.meta?.oasComposition?.externalRefs || []), + discriminator: String(entity?.meta?.oasComposition?.discriminator || '').trim() + }; return { id: entity?.id || fallbackId('entity', entityIndex), name: entityName, x: Number.isFinite(entity?.x) ? entity.x : 14 + (entityIndex % 2) * 206, y: Number.isFinite(entity?.y) ? entity.y : 14 + Math.floor(entityIndex / 2) * 120, - fields + fields, + meta: { + aggregateRoot: Boolean(entity?.meta?.aggregateRoot), + invariants, + rbac, + contracts, + oasComposition + } }; } @@ -2071,6 +3307,15 @@ function normalizeDomainInput(domain, domainIndex) { color: /^#[0-9a-f]{6}$/i.test(domain?.color || '') ? domain.color : DOMAIN_COLORS[domainIndex % DOMAIN_COLORS.length], x: Number.isFinite(domain?.x) ? domain.x : 120 + domainIndex * 40, y: Number.isFinite(domain?.y) ? domain.y : 90 + domainIndex * 30, + context: { + ubiquitousLanguage: String(domain?.context?.ubiquitousLanguage || '').trim(), + ownerTeam: String(domain?.context?.ownerTeam || '').trim(), + upstreamDependencies: parseCommaSeparated(domain?.context?.upstreamDependencies || []), + downstreamDependencies: parseCommaSeparated(domain?.context?.downstreamDependencies || []), + integrationChannel: String(domain?.context?.integrationChannel || '').trim(), + packageDependencies: parseCommaSeparated(domain?.context?.packageDependencies || []), + sharedValueObjects: parseCommaSeparated(domain?.context?.sharedValueObjects || []) + }, entities }; } @@ -2087,7 +3332,12 @@ function normalizeStatePayload(parsed) { zoom: clampZoom(parsed?.view?.zoom || 1), compactEntities: Boolean(parsed?.view?.compactEntities), snapToGrid: parsed?.view?.snapToGrid !== false, - edgeStyle: ['curved', 'orthogonal'].includes(parsed?.view?.edgeStyle) ? parsed.view.edgeStyle : 'curved' + edgeStyle: ['curved', 'orthogonal'].includes(parsed?.view?.edgeStyle) ? parsed.view.edgeStyle : 'curved', + modelCheckMinSeverity: ['info', 'warn', 'error'].includes(parsed?.view?.modelCheckMinSeverity) + ? parsed.view.modelCheckMinSeverity + : 'info', + exportBlockCritical: parsed?.view?.exportBlockCritical !== false, + largeCanvasMode: Boolean(parsed?.view?.largeCanvasMode) }; return { domains, @@ -2140,7 +3390,15 @@ function loadState() { } catch (error) { seed(); saveState(); - state.view = { zoom: 1, compactEntities: false, snapToGrid: true, edgeStyle: 'curved' }; + state.view = { + zoom: 1, + compactEntities: false, + snapToGrid: true, + edgeStyle: 'curved', + modelCheckMinSeverity: 'info', + exportBlockCritical: true, + largeCanvasMode: false + }; history.past = []; history.future = []; } @@ -2256,7 +3514,15 @@ function importStateFromOasFile(file) { state.selectedDomainId = nextDomains[0]?.id || null; state.selectedEntityId = null; state.selectedRelationshipId = null; - state.view = { zoom: 1, compactEntities: false, snapToGrid: true, edgeStyle: 'curved' }; + state.view = { + zoom: 1, + compactEntities: false, + snapToGrid: true, + edgeStyle: 'curved', + modelCheckMinSeverity: 'info', + exportBlockCritical: true, + largeCanvasMode: false + }; recomputeIdCounter(); render(); }, { recordHistory: false }); @@ -2285,6 +3551,10 @@ function render() { renderPickStatus(); renderEntityInspector(); renderEdges(); + renderMiniMap(); + renderSchemaDiffStatus(); + generateCodePreview(); + generateExamplesPreview(); dom.undoBtn.disabled = history.past.length === 0; dom.redoBtn.disabled = history.future.length === 0; } @@ -2407,6 +3677,18 @@ function wireEvents() { dom.addDomainBtn.click(); }; dom.setDomainColorBtn.onclick = () => setSelectedDomainColor(dom.domainColorInput.value); + dom.saveDomainContextBtn.onclick = saveSelectedDomainContext; + dom.clearDomainContextBtn.onclick = () => { + if (!getSelectedDomain()) return; + dom.domainUbiquitousLanguageInput.value = ''; + dom.domainOwnerTeamInput.value = ''; + dom.domainUpstreamInput.value = ''; + dom.domainDownstreamInput.value = ''; + dom.domainIntegrationChannelInput.value = ''; + dom.domainPackageDependenciesInput.value = ''; + dom.domainSharedValueObjectsInput.value = ''; + saveSelectedDomainContext(); + }; dom.renameDomainBtn.onclick = () => { const selected = getSelectedDomain(); @@ -2448,6 +3730,33 @@ function wireEvents() { render(); }); }; + dom.applyEntityTemplateBtn.onclick = () => { + const found = findEntity(state.selectedEntityId); + if (!found) { + window.alert('Select an entity first.'); + return; + } + const template = dom.entityTemplateSelect.value; + if (!template) return; + withPersist(() => { + if (template === 'crudAggregate') { + ensureForeignKeyField(found.entity, 'owner'); + } + if (template === 'eventSourced') { + const hasVersion = found.entity.fields.some((field) => normalizedName(field.name) === 'version'); + if (!hasVersion) found.entity.fields.push(normalizeField({ name: 'version', type: 'integer', required: true }, found.entity.fields.length)); + found.entity.meta.aggregateRoot = true; + } + if (template === 'referenceData') { + const hasCode = found.entity.fields.some((field) => normalizedName(field.name) === 'code'); + if (!hasCode) found.entity.fields.push(normalizeField({ name: 'code', type: 'string', required: true, unique: true }, found.entity.fields.length)); + } + if (template === 'tenantOwned') { + ensureForeignKeyField(found.entity, 'tenant'); + } + render(); + }); + }; dom.entityNameInput.onkeydown = (event) => { if (event.key !== 'Enter') return; event.preventDefault(); @@ -2484,6 +3793,15 @@ function wireEvents() { dom.entitySearchBtn.click(); }; dom.saveEntityRenameBtn.onclick = () => saveSelectedEntityName(dom.entityRenameInput.value); + dom.saveEntityRulesBtn.onclick = saveSelectedEntityRules; + dom.saveEntityRbacBtn.onclick = saveSelectedEntityRbacRule; + dom.entityRbacActionSelect.onchange = () => { + const found = findEntity(state.selectedEntityId); + if (!found) return; + renderEntityRbacInspector(found.entity); + }; + dom.addEntityContractBtn.onclick = addSelectedEntityContract; + dom.saveEntityOasCompositionBtn.onclick = saveSelectedEntityOasComposition; dom.entityRenameInput.onkeydown = (event) => { if (event.key !== 'Enter') return; event.preventDefault(); @@ -2504,6 +3822,15 @@ function wireEvents() { }; dom.saveRelationshipBtn.onclick = saveSelectedRelationship; dom.reverseRelationshipBtn.onclick = reverseSelectedRelationship; + dom.resetRelationshipLabelOffsetBtn.onclick = () => { + if (!state.selectedRelationshipId) return; + dom.relationshipLabelOffsetXInput.value = '0'; + dom.relationshipLabelOffsetYInput.value = '0'; + dom.relationshipBendXInput.value = ''; + dom.relationshipBendYInput.value = ''; + dom.relationshipAnchorBehaviorSelect.value = 'auto'; + saveSelectedRelationship(); + }; dom.undoBtn.onclick = undo; dom.redoBtn.onclick = redo; dom.zoomInBtn.onclick = () => zoomBy(0.1); @@ -2523,7 +3850,32 @@ function wireEvents() { renderView(); }, { recordHistory: false }); }; + dom.toggleLargeCanvasBtn.onclick = () => { + withPersist(() => { + state.view.largeCanvasMode = !state.view.largeCanvasMode; + if (state.view.largeCanvasMode) state.view.compactEntities = true; + render(); + }, { recordHistory: false }); + }; dom.runModelCheckBtn.onclick = runModelChecks; + dom.modelCheckMinSeveritySelect.onchange = () => { + withPersist(() => { + state.view.modelCheckMinSeverity = dom.modelCheckMinSeveritySelect.value; + renderModelCheckResults(collectModelIssues()); + }, { recordHistory: false }); + }; + dom.exportBlockCriticalCheck.onchange = () => { + withPersist(() => { + state.view.exportBlockCritical = Boolean(dom.exportBlockCriticalCheck.checked); + renderView(); + }, { recordHistory: false }); + }; + dom.saveBaselineBtn.onclick = saveSchemaBaseline; + dom.runSchemaDiffBtn.onclick = runSchemaDiff; + dom.clearBaselineBtn.onclick = () => { + localStorage.removeItem(DIFF_BASELINE_KEY); + renderSchemaDiffResults([{ severity: 'info', message: 'Baseline cleared.' }]); + }; dom.autoLayoutBtn.onclick = autoLayout; dom.fitViewBtn.onclick = fitView; dom.resetViewBtn.onclick = resetView; @@ -2537,6 +3889,13 @@ function wireEvents() { }; dom.exportJsonBtn.onclick = exportAsJson; dom.exportOasBtn.onclick = exportAsOas; + dom.exportMdBtn.onclick = exportAsMarkdown; + dom.exportJsonschemaBtn.onclick = exportAsJsonSchema; + dom.exportAsyncapiBtn.onclick = exportAsAsyncApi; + dom.exportBoilerplateBundleBtn.onclick = exportBoilerplateBundle; + dom.exportPackageBtn.onclick = exportAsPackage; + dom.generateCodePreviewBtn.onclick = generateCodePreview; + dom.generateExamplesBtn.onclick = generateExamplesPreview; dom.importJsonBtn.onclick = () => dom.importJsonInput.click(); dom.importJsonInput.onchange = (event) => { const file = event.target.files?.[0]; @@ -2551,6 +3910,13 @@ function wireEvents() { importStateFromOasFile(file); dom.importOasInput.value = ''; }; + dom.importPackageBtn.onclick = () => dom.importPackageInput.click(); + dom.importPackageInput.onchange = (event) => { + const file = event.target.files?.[0]; + if (!file) return; + importDomainPackage(file); + dom.importPackageInput.value = ''; + }; dom.resetCanvasBtn.onclick = () => { if (!window.confirm('Reset canvas to default template?')) return; @@ -2605,6 +3971,25 @@ function wireEvents() { dom.canvas.addEventListener('pointercancel', stopPan); dom.canvas.addEventListener('pointerleave', stopPan); + window.addEventListener('pointermove', (event) => { + if (!interaction.relationshipAnchorDragActive) return; + const fromPoint = entityAnchorOnCanvas( + interaction.relationshipAnchorFromEntityId, + interaction.relationshipAnchorFromSide + ); + if (!fromPoint) { + stopAnchorDrag(); + return; + } + const pointer = pointerToCanvasPoint(event.clientX, event.clientY); + renderAnchorPreviewEdge(fromPoint, pointer); + }); + + window.addEventListener('pointerup', () => { + if (!interaction.relationshipAnchorDragActive) return; + stopAnchorDrag(); + }); + window.addEventListener('keydown', (event) => { const key = event.key.toLowerCase(); const targetTag = String(event.target?.tagName || '').toLowerCase(); @@ -2618,6 +4003,7 @@ function wireEvents() { } if (key === 'escape') { setRelationshipPickMode(false); + stopAnchorDrag(); state.selectedRelationshipId = null; render(); return; diff --git a/servicemangement/server.js b/apps/service-management/server.js similarity index 100% rename from servicemangement/server.js rename to apps/service-management/server.js diff --git a/servicemangement/styles.css b/apps/service-management/styles.css similarity index 80% rename from servicemangement/styles.css rename to apps/service-management/styles.css index 5b16aae3..e6452549 100644 --- a/servicemangement/styles.css +++ b/apps/service-management/styles.css @@ -243,6 +243,7 @@ button:focus { } .workspace { + position: relative; display: grid; grid-template-rows: 38px 1fr; height: 100%; @@ -311,6 +312,10 @@ button:focus { background-size: 24px 24px; } +.canvas.large-canvas-mode .entity-fields { + display: none; +} + .canvas.space-mode { cursor: grab; } @@ -335,6 +340,31 @@ button:focus { pointer-events: auto; } +.mini-map { + position: absolute; + right: 12px; + bottom: 12px; + width: 180px; + height: 130px; + border: 1px solid #cbd5e1; + background: rgba(255, 255, 255, 0.92); + border-radius: 6px; + z-index: 10; + overflow: hidden; +} + +.mini-map-domain { + position: absolute; + border: 1px solid #64748b; + background: rgba(148, 163, 184, 0.12); + cursor: pointer; +} + +.mini-map-domain.active { + border-width: 2px; + border-color: #2563eb; +} + .domain { position: absolute; width: 520px; @@ -384,6 +414,9 @@ button:focus { } .entity-header { + display: flex; + align-items: center; + justify-content: space-between; padding: 7px 8px; border-bottom: 1px solid #e2e8f0; font-size: 12px; @@ -391,6 +424,20 @@ button:focus { cursor: move; } +.entity-aggregate-tag { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 18px; + border-radius: 4px; + border: 1px solid #0ea5e9; + color: #0369a1; + font-size: 10px; + font-weight: 700; + background: #e0f2fe; +} + .entity-fields { margin: 0; padding: 6px 8px 8px 18px; @@ -404,6 +451,37 @@ button:focus { box-shadow: 0 0 0 2px color-mix(in srgb, var(--focus) 24%, transparent); } +.entity-anchor { + position: absolute; + width: 10px; + height: 10px; + border: 1px solid #2563eb; + border-radius: 999px; + background: #fff; + cursor: crosshair; + z-index: 3; +} + +.entity-anchor-top { + top: -5px; + left: calc(50% - 5px); +} + +.entity-anchor-right { + right: -5px; + top: calc(50% - 5px); +} + +.entity-anchor-bottom { + bottom: -5px; + left: calc(50% - 5px); +} + +.entity-anchor-left { + left: -5px; + top: calc(50% - 5px); +} + .edge-line { stroke: #0f172a; stroke-width: 1.7; @@ -411,6 +489,14 @@ button:focus { pointer-events: none; } +.edge-preview { + stroke: #2563eb; + stroke-width: 1.7; + stroke-dasharray: 6 4; + fill: none; + pointer-events: none; +} + .edge-line.active { stroke: #2563eb; stroke-width: 2.4; @@ -429,3 +515,15 @@ button:focus { font-weight: 600; pointer-events: none; } + +.code-preview-output { + margin: 8px 0 0; + padding: 8px; + max-height: 240px; + overflow: auto; + background: #0f172a; + color: #e2e8f0; + border-radius: 6px; + font-size: 11px; + white-space: pre; +} diff --git a/bin/aaa-bootstrap.js b/bin/aaa-bootstrap.js index 530a3cce..4b188841 100755 --- a/bin/aaa-bootstrap.js +++ b/bin/aaa-bootstrap.js @@ -1,136 +1,6 @@ #!/usr/bin/env node /* eslint-disable no-console */ -const fs = require('fs'); -const path = require('path'); -const readline = require('readline'); -const { spawnSync } = require('child_process'); - -const BOILERPLATE_REPOSITORY = 'https://github.com/web2solutions/aaa-typescript-boilerplate.git'; -const SERVICE_TYPES = [ - { - id: 'rest', - label: 'HTTP/REST server (OpenAPI/Swagger + static assets)', - profile: { interface: 'http-rest', staticAssets: true, functions: false } - }, - { - id: 'websocket', - label: 'WebSocket server (+ static assets)', - profile: { interface: 'websocket', staticAssets: true, functions: false } - }, - { - id: 'grpc', - label: 'gRPC server (+ static assets)', - profile: { interface: 'grpc', staticAssets: true, functions: false } - }, - { - id: 'graphql', - label: 'GraphQL server (+ static assets)', - profile: { interface: 'graphql', staticAssets: true, functions: false } - }, - { - id: 'functions', - label: 'Function services bundle (AWS/Google/Azure/Vercel/Cloudflare)', - profile: { interface: 'functions', staticAssets: false, functions: true } - } -]; - -function createPrompt() { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - const ask = (question) => new Promise((resolve) => { - rl.question(question, (answer) => resolve(String(answer || '').trim())); - }); - - return { - ask, - close: () => rl.close() - }; -} - -function runCommand(command, args, cwd) { - const result = spawnSync(command, args, { - cwd, - stdio: 'inherit' - }); - if (result.status !== 0) { - throw new Error(`${command} ${args.join(' ')} failed with exit code ${String(result.status)}`); - } -} - -function toAbsolute(targetPath) { - if (path.isAbsolute(targetPath)) return targetPath; - return path.resolve(process.cwd(), targetPath); -} - -function ensureTargetFolderIsEmpty(targetPath) { - if (!fs.existsSync(targetPath)) return; - const files = fs.readdirSync(targetPath); - if (files.length > 0) { - throw new Error(`Target folder "${targetPath}" already exists and is not empty.`); - } -} - -async function chooseServiceType(ask) { - console.log('\nSelect service type:'); - SERVICE_TYPES.forEach((type, index) => { - console.log(` ${index + 1}. ${type.label}`); - }); - const selected = await ask('Type number: '); - const index = Number(selected) - 1; - if (!Number.isInteger(index) || index < 0 || index >= SERVICE_TYPES.length) { - throw new Error('Invalid service type selection.'); - } - return SERVICE_TYPES[index]; -} - -function writeBootstrapProfile(targetPath, payload) { - const configDir = path.join(targetPath, '.aaa'); - fs.mkdirSync(configDir, { recursive: true }); - const outputPath = path.join(configDir, 'service-profile.json'); - fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); -} - -async function run() { - const prompt = createPrompt(); - - try { - console.log('\nAAA Boilerplate Bootstrap CLI'); - const serviceType = await chooseServiceType(prompt.ask); - const projectName = await prompt.ask('Project folder name (e.g. my-service): '); - if (!projectName) throw new Error('Project folder name is required.'); - const targetPath = toAbsolute(projectName); - ensureTargetFolderIsEmpty(targetPath); - - const gitBranch = (await prompt.ask('Git branch to clone (default: main): ')) || 'main'; - const installDeps = ((await prompt.ask('Run npm install after scaffold? (Y/n): ')) || 'y').toLowerCase() !== 'n'; - - console.log('\nCloning boilerplate repository...'); - runCommand('git', ['clone', '--branch', gitBranch, BOILERPLATE_REPOSITORY, targetPath], process.cwd()); - - writeBootstrapProfile(targetPath, { - generatedAt: new Date().toISOString(), - template: 'aaa-typescript-boilerplate', - repository: BOILERPLATE_REPOSITORY, - branch: gitBranch, - serviceType: serviceType.id, - profile: serviceType.profile - }); - - if (installDeps) { - console.log('\nInstalling dependencies...'); - runCommand('npm', ['install'], targetPath); - } - - console.log('\nScaffold completed successfully.'); - console.log(`Project path: ${targetPath}`); - console.log(`Profile: ${path.join(targetPath, '.aaa', 'service-profile.json')}`); - } finally { - prompt.close(); - } -} +const { run } = require('../packages/cli-init/src/bootstrap'); run().catch((error) => { console.error(`\nBootstrap failed: ${error.message}`); diff --git a/ci-cd/check-affected-workspaces.js b/ci-cd/check-affected-workspaces.js new file mode 100644 index 00000000..d0196c85 --- /dev/null +++ b/ci-cd/check-affected-workspaces.js @@ -0,0 +1,87 @@ +/* eslint-disable no-console */ +const { execSync } = require('child_process'); + +const ROOT_MARKERS = [ + 'package.json', + 'pnpm-workspace.yaml', + '.circleci/config.yml', + '.github/workflows', + 'ci-cd', + 'tsconfig', + 'jest.config', + '.npmrc' +]; + +function normalizePath(value) { + return String(value || '').replace(/\\/g, '/').trim(); +} + +function isDocsOnlyPath(filePath) { + if (!filePath) return false; + if (filePath.startsWith('documentation/')) return true; + if (filePath.startsWith('.agents/')) return true; + if (filePath.endsWith('.md')) return true; + if (filePath.endsWith('.mdx')) return true; + return false; +} + +function computeAffectedWorkspaces(files) { + const normalized = files.map(normalizePath).filter(Boolean); + const affected = { + root: false, + apps: [], + packages: [], + docsOnly: normalized.length > 0 && normalized.every(isDocsOnlyPath), + files: normalized + }; + + const appsSet = new Set(); + const packagesSet = new Set(); + + for (const file of normalized) { + if (ROOT_MARKERS.some((marker) => file === marker || file.startsWith(`${marker}/`))) { + affected.root = true; + } + + if (file.startsWith('apps/')) { + const appSegments = file.split('/'); + const appName = appSegments[1]; + if (appName && appSegments.length >= 3) appsSet.add(appName); + continue; + } + + if (file.startsWith('packages/')) { + const packageSegments = file.split('/'); + const packageName = packageSegments[1]; + if (packageName && packageSegments.length >= 3) packagesSet.add(packageName); + } + } + + affected.apps = Array.from(appsSet).sort(); + affected.packages = Array.from(packagesSet).sort(); + return affected; +} + +function readChangedFiles(baseRef = 'origin/dev') { + const output = execSync(`git diff --name-only ${baseRef}...HEAD`, { encoding: 'utf8' }); + return output.split('\n').map((line) => line.trim()).filter(Boolean); +} + +function run() { + const baseRef = process.env.AAA_CI_BASE_REF || 'origin/dev'; + const files = process.argv.slice(2); + const changedFiles = files.length > 0 ? files : readChangedFiles(baseRef); + const result = computeAffectedWorkspaces(changedFiles); + console.log(JSON.stringify(result, null, 2)); +} + +if (require.main === module) { + run(); +} + +module.exports = { + computeAffectedWorkspaces, + isDocsOnlyPath, + normalizePath, + readChangedFiles +}; diff --git a/ci-cd/check-core-import-cycles.js b/ci-cd/check-core-import-cycles.js index c6bfecaa..7e71dc7d 100644 --- a/ci-cd/check-core-import-cycles.js +++ b/ci-cd/check-core-import-cycles.js @@ -5,12 +5,12 @@ const path = require('path'); const ROOT = process.cwd(); const EXTENSIONS = ['.ts', '.tsx', '.js', '.mjs', '.cjs']; const CORE_DIRS = [ - 'src/modules/Users/domain', - 'src/modules/Users/features', - 'src/modules/Users/service', - 'src/modules/Users/infra/repository', - 'src/modules/Users/composition', - 'src/modules/port' + 'apps/backend-template/src/modules/Users/domain', + 'apps/backend-template/src/modules/Users/features', + 'apps/backend-template/src/modules/Users/service', + 'apps/backend-template/src/modules/Users/infra/repository', + 'apps/backend-template/src/modules/Users/composition', + 'apps/backend-template/src/modules/port' ].map((dir) => path.resolve(ROOT, dir)); const importRegex = /from\s+['"]([^'"]+)['"]|require\(['"]([^'"]+)['"]\)/g; @@ -38,7 +38,7 @@ const walk = (dirPath, files = []) => { const resolveImport = (sourceFile, specifier) => { let candidateBase; if (specifier.startsWith('@src/')) { - candidateBase = path.resolve(ROOT, 'src', specifier.slice('@src/'.length)); + candidateBase = path.resolve(ROOT, 'apps/backend-template/src', specifier.slice('@src/'.length)); } else if (specifier.startsWith('.')) { candidateBase = path.resolve(path.dirname(sourceFile), specifier); } else { diff --git a/ci-cd/check-hexagonal-boundaries.js b/ci-cd/check-hexagonal-boundaries.js index cd5753b4..074e5c95 100644 --- a/ci-cd/check-hexagonal-boundaries.js +++ b/ci-cd/check-hexagonal-boundaries.js @@ -3,8 +3,12 @@ const fs = require('fs'); const path = require('path'); const ROOT = process.cwd(); -const CONTROLLERS_ROOT = path.resolve(ROOT, 'src/modules'); +const CONTROLLERS_ROOT = path.resolve(ROOT, 'apps/backend-template/src/modules'); const ALLOWED_SERVICE_IMPORT = /\/service\/ports\//; +const CONTROLLER_PATH_MARKERS = [ + `${path.sep}interface${path.sep}controller${path.sep}`, + `${path.sep}adapters${path.sep}in${path.sep}http${path.sep}controllers${path.sep}` +]; const walk = (dirPath, files = []) => { if (!fs.existsSync(dirPath)) return files; @@ -15,10 +19,8 @@ const walk = (dirPath, files = []) => { walk(absPath, files); continue; } - if ( - entry.name.endsWith('.ts') - && absPath.includes(`${path.sep}interface${path.sep}controller${path.sep}`) - ) { + const isControllerFile = CONTROLLER_PATH_MARKERS.some((marker) => absPath.includes(marker)); + if (entry.name.endsWith('.ts') && isControllerFile) { files.push(absPath); } } @@ -36,49 +38,77 @@ const readImports = (content) => { return imports; }; -const controllerFiles = walk(CONTROLLERS_ROOT); -const violations = []; +const hasUseCaseImport = (imports) => imports.some( + (importPath) => importPath.includes('/application/ports/') || importPath.includes('/application/use-cases/') +); -for (const controllerFile of controllerFiles) { - const source = fs.readFileSync(controllerFile, 'utf8'); - const imports = readImports(source); - const relative = path.relative(ROOT, controllerFile); +function validateControllerFile(source, imports, relativePath) { + const violations = []; for (const importPath of imports) { if (importPath.includes('/infra/repository/')) { - violations.push(`${relative}: forbidden controller import "${importPath}"`); + violations.push(`${relativePath}: forbidden controller import "${importPath}"`); } if (importPath.includes('/service/') && !ALLOWED_SERVICE_IMPORT.test(importPath)) { - violations.push(`${relative}: controller must not import service implementation "${importPath}"`); + violations.push(`${relativePath}: controller must not import service implementation "${importPath}"`); } } + const isHttpController = relativePath.includes('/adapters/in/http/controllers/'); + const isHttpControllerIndex = isHttpController && /\/index\.ts$/.test(relativePath); + if (isHttpController && !isHttpControllerIndex && !hasUseCaseImport(imports)) { + violations.push(`${relativePath}: HTTP controller must import application use-case contract`); + } + const hasServiceInstantiation = /new\s+[A-Za-z0-9_]*Service\s*\(/.test(source); if (hasServiceInstantiation) { - violations.push(`${relative}: controller must not instantiate Service directly`); + violations.push(`${relativePath}: controller must not instantiate Service directly`); } const hasRepositoryInstantiation = /new\s+[A-Za-z0-9_]*Repositor(y|ies)\s*\(/.test(source); if (hasRepositoryInstantiation) { - violations.push(`${relative}: controller must not instantiate Repository directly`); + violations.push(`${relativePath}: controller must not instantiate Repository directly`); } const hasServiceCompile = /[A-Za-z0-9_]*Service\.compile\s*\(/.test(source); if (hasServiceCompile) { - violations.push(`${relative}: controller must not call Service.compile directly`); + violations.push(`${relativePath}: controller must not call Service.compile directly`); } const hasRepositoryCompile = /[A-Za-z0-9_]*Repositor(y|ies)\.compile\s*\(/.test(source); if (hasRepositoryCompile) { - violations.push(`${relative}: controller must not call Repository.compile directly`); + violations.push(`${relativePath}: controller must not call Repository.compile directly`); } + + return violations; } -if (violations.length > 0) { - console.error('Hexagonal boundary violations found:'); - violations.forEach((violation) => console.error(`- ${violation}`)); - process.exit(1); +function run() { + const controllerFiles = walk(CONTROLLERS_ROOT); + const violations = []; + + for (const controllerFile of controllerFiles) { + const source = fs.readFileSync(controllerFile, 'utf8'); + const imports = readImports(source); + const relative = path.relative(ROOT, controllerFile); + violations.push(...validateControllerFile(source, imports, relative)); + } + + if (violations.length > 0) { + console.error('Hexagonal boundary violations found:'); + violations.forEach((violation) => console.error(`- ${violation}`)); + process.exit(1); + } + + console.log('Hexagonal boundary check passed.'); } -console.log('Hexagonal boundary check passed.'); +if (require.main === module) { + run(); +} + +module.exports = { + validateControllerFile, + readImports +}; diff --git a/ci-cd/check-npm-org-integration.js b/ci-cd/check-npm-org-integration.js new file mode 100644 index 00000000..b2c69ec6 --- /dev/null +++ b/ci-cd/check-npm-org-integration.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +const { execSync } = require('node:child_process'); + +function run(cmd) { + return execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim(); +} + +function fail(message) { + console.error(message); + process.exit(1); +} + +try { + const whoami = run('npm whoami'); + if (!whoami) { + fail('[npm-org-check] Unable to resolve current npm user.'); + } + console.log(`[npm-org-check] Authenticated as: ${whoami}`); +} catch (error) { + fail('[npm-org-check] npm authentication is not configured. Run npm login for the target account.'); +} + +try { + const orgUsersRaw = run('npm org ls xpertminds --json'); + let orgUsers; + try { + orgUsers = JSON.parse(orgUsersRaw); + } catch { + orgUsers = null; + } + if (!orgUsers || typeof orgUsers !== 'object') { + fail('[npm-org-check] Could not parse xpertminds org members from npm CLI.'); + } + console.log('[npm-org-check] xpertminds org membership data is accessible.'); +} catch (error) { + fail('[npm-org-check] Unable to access xpertminds org membership. Ensure account has org access.'); +} + +console.log('[npm-org-check] npm integration check passed.'); diff --git a/ci-cd/check-oas-route-resolution.js b/ci-cd/check-oas-route-resolution.js index 16260b3a..63e6157c 100644 --- a/ci-cd/check-oas-route-resolution.js +++ b/ci-cd/check-oas-route-resolution.js @@ -164,6 +164,8 @@ function validateRouteResolution() { const controllerCandidates = [ path.join( root, + 'apps', + 'backend-template', 'src', 'modules', moduleName, @@ -175,6 +177,8 @@ function validateRouteResolution() { ), path.join( root, + 'apps', + 'backend-template', 'src', 'modules', moduleName, @@ -206,6 +210,8 @@ function validateRouteResolution() { FRAMEWORKS.forEach((framework) => { const handlerFile = path.join( root, + 'apps', + 'backend-template', 'src', 'modules', moduleName, diff --git a/ci-cd/check-release-governance.js b/ci-cd/check-release-governance.js new file mode 100644 index 00000000..b2b8dce4 --- /dev/null +++ b/ci-cd/check-release-governance.js @@ -0,0 +1,150 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); + +const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; + +function readJson(filePath) { + if (!fs.existsSync(filePath)) return null; + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function getWorkspacePackageDirs(rootDir) { + const packagesDir = path.join(rootDir, 'packages'); + if (!fs.existsSync(packagesDir)) return []; + return fs.readdirSync(packagesDir) + .map((name) => path.join(packagesDir, name)) + .filter((fullPath) => fs.statSync(fullPath).isDirectory()); +} + +function getWorkspaceAppDirs(rootDir) { + const appsDir = path.join(rootDir, 'apps'); + if (!fs.existsSync(appsDir)) return []; + return fs.readdirSync(appsDir) + .map((name) => path.join(appsDir, name)) + .filter((fullPath) => fs.statSync(fullPath).isDirectory()); +} + +function validateReleasePolicy(releasePolicy, rootPackageJson) { + const failures = []; + if (!releasePolicy || typeof releasePolicy !== 'object') { + failures.push('[release-policy] missing release-policy.json'); + return failures; + } + + if (releasePolicy.packageVersioning !== 'independent') { + failures.push('[release-policy] packageVersioning must be "independent"'); + } + + if (releasePolicy.appVersioning !== 'locked') { + failures.push('[release-policy] appVersioning must be "locked"'); + } + + if (!SEMVER_RE.test(String(releasePolicy.appLockedVersion || ''))) { + failures.push(`[release-policy] appLockedVersion must be a valid semver, got: ${String(releasePolicy.appLockedVersion || '')}`); + } + + if (releasePolicy.appLockedVersion !== rootPackageJson.version) { + failures.push(`[release-policy] appLockedVersion must match root package version (${rootPackageJson.version})`); + } + + return failures; +} + +function validateRootReleaseScripts(rootPackageJson) { + const scripts = (rootPackageJson && rootPackageJson.scripts) || {}; + const requiredScripts = [ + 'changelog:update', + 'changelog:check', + 'release:dry-run', + 'release:dry-run:packages', + 'release:dry-run:apps' + ]; + const failures = []; + + for (const scriptName of requiredScripts) { + const value = scripts[scriptName]; + if (typeof value !== 'string' || value.trim().length === 0) { + failures.push(`[root] missing required release script: ${scriptName}`); + } + } + + return failures; +} + +function validatePackageReleaseMetadata(pkg, packageDir) { + const failures = []; + const packageName = pkg.name || packageDir; + + if (!SEMVER_RE.test(String(pkg.version || ''))) { + failures.push(`[${packageName}] invalid semver version: ${String(pkg.version || '')}`); + } + + if (pkg.private !== true) { + const filesField = pkg.files; + if (!Array.isArray(filesField) || filesField.length === 0) { + failures.push(`[${packageName}] publishable package must define non-empty files field`); + } + } + + return failures; +} + +function validateAppReleaseMetadata(pkg, appDir, releasePolicy) { + const failures = []; + const appName = pkg.name || appDir; + + if (pkg.private !== true) { + failures.push(`[${appName}] app workspace must be private`); + } + + if (String(pkg.version || '') !== String(releasePolicy.appLockedVersion || '')) { + failures.push(`[${appName}] app version must match release-policy appLockedVersion (${releasePolicy.appLockedVersion})`); + } + + return failures; +} + +function run() { + const rootDir = process.cwd(); + const rootPackageJson = readJson(path.join(rootDir, 'package.json')); + const releasePolicy = readJson(path.join(rootDir, 'release-policy.json')); + const packageDirs = getWorkspacePackageDirs(rootDir); + const appDirs = getWorkspaceAppDirs(rootDir); + const failures = []; + + failures.push(...validateRootReleaseScripts(rootPackageJson)); + failures.push(...validateReleasePolicy(releasePolicy, rootPackageJson)); + + for (const packageDir of packageDirs) { + const pkg = readJson(path.join(packageDir, 'package.json')); + if (!pkg) continue; + failures.push(...validatePackageReleaseMetadata(pkg, packageDir)); + } + + for (const appDir of appDirs) { + const pkg = readJson(path.join(appDir, 'package.json')); + if (!pkg) continue; + failures.push(...validateAppReleaseMetadata(pkg, appDir, releasePolicy || {})); + } + + if (failures.length > 0) { + for (const failure of failures) { + console.error(failure); + } + process.exit(1); + } + + console.log('Release governance check passed.'); +} + +if (require.main === module) { + run(); +} + +module.exports = { + validateReleasePolicy, + validateRootReleaseScripts, + validatePackageReleaseMetadata, + validateAppReleaseMetadata +}; diff --git a/ci-cd/check-serverless-handler-paths.js b/ci-cd/check-serverless-handler-paths.js new file mode 100644 index 00000000..24263df1 --- /dev/null +++ b/ci-cd/check-serverless-handler-paths.js @@ -0,0 +1,68 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); + +function collectHandlerPaths(serverlessSource) { + const handlerRegex = /handler:\s*'([^']+)'/g; + const handlers = []; + let match = handlerRegex.exec(serverlessSource); + while (match) { + handlers.push(match[1]); + match = handlerRegex.exec(serverlessSource); + } + return handlers; +} + +function buildCandidatePaths(handlerPathWithoutExtension) { + const candidates = new Set([`${handlerPathWithoutExtension}.ts`]); + + const withoutAppPrefix = handlerPathWithoutExtension.startsWith('apps/backend-template/') + ? handlerPathWithoutExtension.replace('apps/backend-template/', '') + : handlerPathWithoutExtension; + candidates.add(`${withoutAppPrefix}.ts`); + + const legacyApiPath = withoutAppPrefix.replace('/interface/restapi/', '/interface/api/'); + candidates.add(`${legacyApiPath}.ts`); + + const legacyApiWithAppPrefix = handlerPathWithoutExtension.replace('/interface/restapi/', '/interface/api/'); + candidates.add(`${legacyApiWithAppPrefix}.ts`); + + return Array.from(candidates); +} + +function validateServerlessHandlers() { + const root = process.cwd(); + const serverlessFile = path.join(root, 'serverless.ts'); + + if (!fs.existsSync(serverlessFile)) { + console.error(`Missing serverless file: ${serverlessFile}`); + process.exit(1); + } + + const source = fs.readFileSync(serverlessFile, 'utf8'); + const handlers = collectHandlerPaths(source); + + if (handlers.length === 0) { + console.error('No handlers were found in serverless.ts'); + process.exit(1); + } + + const missing = handlers + .map((handlerPath) => handlerPath.split('.').slice(0, -1).join('.')) + .map((handlerPathWithoutExtension) => { + const candidates = buildCandidatePaths(handlerPathWithoutExtension); + const hasAnyCandidate = candidates.some((candidate) => fs.existsSync(path.join(root, candidate))); + return hasAnyCandidate ? null : `${handlerPathWithoutExtension}.ts`; + }) + .filter(Boolean); + + if (missing.length > 0) { + console.error('Serverless handler path validation failed. Missing files:'); + missing.forEach((missingPath) => console.error(`- ${missingPath}`)); + process.exit(1); + } + + console.log(`Serverless handler path validation passed (${handlers.length} handlers).`); +} + +validateServerlessHandlers(); diff --git a/ci-cd/check-users-legacy-imports.js b/ci-cd/check-users-legacy-imports.js index cf699d85..99199226 100644 --- a/ci-cd/check-users-legacy-imports.js +++ b/ci-cd/check-users-legacy-imports.js @@ -12,8 +12,8 @@ const LEGACY_PATHS = [ ]; const ALLOWLIST = new Set([ - path.resolve(ROOT, 'src/modules/Users/adapters/in/http/controllers/UserController.ts'), - path.resolve(ROOT, 'src/modules/Users/adapters/in/http/controllers/AuthController.ts') + path.resolve(ROOT, 'apps/backend-template/src/modules/Users/adapters/in/http/controllers/UserController.ts'), + path.resolve(ROOT, 'apps/backend-template/src/modules/Users/adapters/in/http/controllers/AuthController.ts') ]); const walk = (dirPath, files = []) => { @@ -33,8 +33,8 @@ const walk = (dirPath, files = []) => { }; const files = [ - ...walk(path.resolve(ROOT, 'src')), - ...walk(path.resolve(ROOT, 'test')) + ...walk(path.resolve(ROOT, 'apps/backend-template/src')), + ...walk(path.resolve(ROOT, 'apps/backend-template/test')) ]; const violations = []; diff --git a/ci-cd/check-workspace-boundaries.js b/ci-cd/check-workspace-boundaries.js new file mode 100644 index 00000000..036b1a01 --- /dev/null +++ b/ci-cd/check-workspace-boundaries.js @@ -0,0 +1,133 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); + +const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']; +const IGNORE_DIRS = new Set(['node_modules', '.git', '.build', 'coverage', '.tmp']); +const IMPORT_REGEX = /from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; +const SRC_ALIAS_BRIDGE_ALLOWLIST = new Set([ + path.join('packages', 'external-store-proxy', 'src', 'ExternalStoreProxy.ts') +]); + +function collectSourceFiles(rootDir, sourceDir, files = []) { + const dirPath = path.join(rootDir, sourceDir); + if (!fs.existsSync(dirPath)) return files; + + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + if (IGNORE_DIRS.has(entry.name)) continue; + const absPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + collectSourceFiles(rootDir, path.join(sourceDir, entry.name), files); + continue; + } + if (SUPPORTED_EXTENSIONS.includes(path.extname(entry.name))) { + files.push(absPath); + } + } + return files; +} + +function readImports(source) { + const imports = []; + let match = IMPORT_REGEX.exec(source); + while (match) { + imports.push(match[1] || match[2]); + match = IMPORT_REGEX.exec(source); + } + return imports; +} + +function classifyZone(relativeFilePath) { + if (relativeFilePath.startsWith(`apps${path.sep}backend-template${path.sep}`)) return 'backend'; + if (relativeFilePath.startsWith(`apps${path.sep}service-management${path.sep}`)) return 'service-management'; + if (relativeFilePath.startsWith(`packages${path.sep}`)) return 'package'; + if (relativeFilePath.startsWith(`sdk-clients${path.sep}`)) return 'legacy-sdk'; + return 'other'; +} + +function normalizeImportTarget(currentFile, importPath) { + if (!importPath.startsWith('.')) return null; + return path.resolve(path.dirname(currentFile), importPath); +} + +function validateImport({ + rootDir, + currentFile, + relativeFilePath, + importPath +}) { + const zone = classifyZone(relativeFilePath); + const violations = []; + const normalized = importPath.replace(/\\/g, '/'); + + const srcAliasAllowed = SRC_ALIAS_BRIDGE_ALLOWLIST.has(relativeFilePath); + if (zone !== 'backend' && normalized.startsWith('@src/') && !srcAliasAllowed) { + violations.push(`${relativeFilePath}: @src alias is only allowed inside apps/backend-template`); + } + + if (zone !== 'legacy-sdk' && (normalized.startsWith('sdk-clients/') || normalized.includes('/sdk-clients/'))) { + violations.push(`${relativeFilePath}: legacy sdk-clients import is forbidden ("${importPath}")`); + } + + const resolvedRelative = normalizeImportTarget(currentFile, importPath); + if (!resolvedRelative) return violations; + + const resolved = path.relative(rootDir, resolvedRelative); + if (resolved.startsWith('..')) return violations; + + if (zone === 'package' && resolved.startsWith(`apps${path.sep}`)) { + violations.push(`${relativeFilePath}: packages must not import from apps ("${importPath}")`); + } + + if (zone === 'service-management' + && resolved.startsWith(`apps${path.sep}backend-template${path.sep}`)) { + violations.push(`${relativeFilePath}: service-management must not import backend-template internals ("${importPath}")`); + } + + return violations; +} + +function run() { + const rootDir = process.cwd(); + const targets = [ + path.join('apps', 'backend-template'), + path.join('apps', 'service-management'), + 'packages' + ]; + const files = targets.flatMap((target) => collectSourceFiles(rootDir, target)); + const violations = []; + + for (const filePath of files) { + const source = fs.readFileSync(filePath, 'utf8'); + const imports = readImports(source); + const relativeFilePath = path.relative(rootDir, filePath); + + for (const importPath of imports) { + violations.push(...validateImport({ + rootDir, + currentFile: filePath, + relativeFilePath, + importPath + })); + } + } + + if (violations.length > 0) { + console.error('Workspace boundary violations found:'); + violations.forEach((violation) => console.error(`- ${violation}`)); + process.exit(1); + } + + console.log('Workspace boundary check passed.'); +} + +if (require.main === module) { + run(); +} + +module.exports = { + classifyZone, + readImports, + validateImport +}; diff --git a/ci-cd/check-workspace-coverage-policy.js b/ci-cd/check-workspace-coverage-policy.js new file mode 100644 index 00000000..fbbc8f3b --- /dev/null +++ b/ci-cd/check-workspace-coverage-policy.js @@ -0,0 +1,110 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); + +const MINIMUM_GLOBAL_THRESHOLDS = { + statements: 99, + lines: 99, + functions: 99, + branches: 90 +}; + +const TEST_PLACEHOLDER_PATTERN = /echo\s+["'][^"']*(no tests yet|placeholder|pending)[^"']*["']/i; +const PACKAGE_TEST_PLACEHOLDER_ALLOWLIST = new Set([ + '@jumentix/config-eslint', + '@jumentix/config-jest', + '@jumentix/config-ts', + '@jumentix/shared-contracts' +]); + +function readRootJestConfig(rootDir) { + const jestPath = path.join(rootDir, 'jest.config.js'); + if (!fs.existsSync(jestPath)) return null; + // eslint-disable-next-line import/no-dynamic-require, global-require + return require(jestPath); +} + +function collectWorkspacePackageJson(rootDir) { + const packageRoots = ['packages', 'apps']; + const files = []; + for (const baseDir of packageRoots) { + const absBase = path.join(rootDir, baseDir); + if (!fs.existsSync(absBase)) continue; + for (const dirName of fs.readdirSync(absBase)) { + const pkgPath = path.join(absBase, dirName, 'package.json'); + if (fs.existsSync(pkgPath)) { + files.push(pkgPath); + } + } + } + return files; +} + +function validateGlobalCoverageThreshold(jestConfig) { + const failures = []; + const globalThreshold = jestConfig?.coverageThreshold?.global || {}; + for (const [metric, minimum] of Object.entries(MINIMUM_GLOBAL_THRESHOLDS)) { + const current = Number(globalThreshold?.[metric]); + if (!Number.isFinite(current) || current < minimum) { + failures.push( + `Root coverageThreshold.global.${metric} must be >= ${minimum} (current: ${ + Number.isFinite(current) ? current : 'missing' + })` + ); + } + } + return failures; +} + +function validatePackageCoveragePolicy(pkg) { + const failures = []; + const scripts = pkg.scripts || {}; + const testScript = String(scripts.test || '').trim(); + + if (testScript.length === 0) { + failures.push(`[${pkg.name}] missing test script`); + return failures; + } + + const allowlisted = PACKAGE_TEST_PLACEHOLDER_ALLOWLIST.has(pkg.name); + if (!allowlisted && TEST_PLACEHOLDER_PATTERN.test(testScript)) { + failures.push(`[${pkg.name}] test script must not be placeholder output`); + } + return failures; +} + +function run() { + const rootDir = process.cwd(); + const failures = []; + + const jestConfig = readRootJestConfig(rootDir); + if (!jestConfig) { + failures.push('Root jest.config.js not found'); + } else { + failures.push(...validateGlobalCoverageThreshold(jestConfig)); + } + + const packageJsonFiles = collectWorkspacePackageJson(rootDir); + for (const pkgFile of packageJsonFiles) { + const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8')); + failures.push(...validatePackageCoveragePolicy(pkg)); + } + + if (failures.length > 0) { + console.error('Workspace coverage policy violations found:'); + failures.forEach((failure) => console.error(`- ${failure}`)); + process.exit(1); + } + + console.log('Workspace coverage policy check passed.'); +} + +if (require.main === module) { + run(); +} + +module.exports = { + MINIMUM_GLOBAL_THRESHOLDS, + validateGlobalCoverageThreshold, + validatePackageCoveragePolicy +}; diff --git a/ci-cd/check-workspace-quality.js b/ci-cd/check-workspace-quality.js new file mode 100644 index 00000000..d104df31 --- /dev/null +++ b/ci-cd/check-workspace-quality.js @@ -0,0 +1,67 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); + +function readPackageJson(targetDir) { + const packageJsonPath = path.join(targetDir, 'package.json'); + if (!fs.existsSync(packageJsonPath)) return null; + return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +} + +function getWorkspacePackageDirs(rootDir) { + const packagesDir = path.join(rootDir, 'packages'); + if (!fs.existsSync(packagesDir)) return []; + return fs.readdirSync(packagesDir) + .map((name) => path.join(packagesDir, name)) + .filter((fullPath) => fs.statSync(fullPath).isDirectory()); +} + +function validatePackageScripts(pkg, packageDir) { + const scripts = pkg.scripts || {}; + const required = ['build', 'test', 'typecheck']; + const failures = []; + + for (const scriptName of required) { + const scriptValue = scripts[scriptName]; + if (typeof scriptValue !== 'string' || scriptValue.trim().length === 0) { + failures.push(`[${pkg.name || packageDir}] missing required script: ${scriptName}`); + } + } + + const testScript = String(scripts.test || '').toLowerCase(); + if (testScript.includes('smoke ok')) { + failures.push(`[${pkg.name || packageDir}] test script uses placeholder smoke output`); + } + + return failures; +} + +function run() { + const rootDir = process.cwd(); + const packageDirs = getWorkspacePackageDirs(rootDir); + const failures = []; + + for (const packageDir of packageDirs) { + const pkg = readPackageJson(packageDir); + if (!pkg) continue; + failures.push(...validatePackageScripts(pkg, packageDir)); + } + + if (failures.length > 0) { + for (const failure of failures) { + console.error(failure); + } + process.exit(1); + } + + console.log('Workspace package quality check passed.'); +} + +if (require.main === module) { + run(); +} + +module.exports = { + getWorkspacePackageDirs, + validatePackageScripts +}; diff --git a/ci-cd/loadEnvironment.js b/ci-cd/loadEnvironment.js index f160f6cb..cf155a5a 100644 --- a/ci-cd/loadEnvironment.js +++ b/ci-cd/loadEnvironment.js @@ -3,10 +3,10 @@ const path = require('path'); const NODE_ENV = process.env.NODE_ENV || 'dev'; const envFilesByEnv = { - dev: ['src/config/.env.dev', 'src/config/.env.dev.example', 'src/config/.env.ci'], - ci: ['src/config/.env.ci', 'src/config/.env.dev.example'], - prod: ['src/config/.env.prod'], - staging: ['src/config/.env.staging'], + dev: ['apps/backend-template/src/config/.env.dev', 'apps/backend-template/src/config/.env.dev.example', 'apps/backend-template/src/config/.env.ci'], + ci: ['apps/backend-template/src/config/.env.ci', 'apps/backend-template/src/config/.env.dev.example'], + prod: ['apps/backend-template/src/config/.env.prod'], + staging: ['apps/backend-template/src/config/.env.staging'], }; const candidateFiles = envFilesByEnv[NODE_ENV] || envFilesByEnv.dev; diff --git a/ci-cd/npm-publish-dry-run.js b/ci-cd/npm-publish-dry-run.js new file mode 100644 index 00000000..3fe99e8e --- /dev/null +++ b/ci-cd/npm-publish-dry-run.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +const fs = require('node:fs'); +const path = require('node:path'); +const { execSync } = require('node:child_process'); + +const root = process.cwd(); +const packagesDir = path.join(root, 'packages'); + +if (!fs.existsSync(packagesDir)) { + console.log('[npm-dry-run] No packages directory found.'); + process.exit(0); +} + +const packageDirs = fs + .readdirSync(packagesDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(packagesDir, entry.name)); + +for (const dir of packageDirs) { + const packageJsonPath = path.join(dir, 'package.json'); + if (!fs.existsSync(packageJsonPath)) continue; + + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (pkg.private) { + console.log(`[npm-dry-run] skip private package: ${pkg.name}`); + continue; + } + + const name = String(pkg.name || ''); + if (!name.startsWith('@xpertminds/')) { + console.log( + `[npm-dry-run][warning] package is not in @xpertminds scope yet: ${name} (prepare rename before publish)` + ); + } + + try { + execSync('npm publish --dry-run --access public', { cwd: dir, stdio: 'inherit' }); + } catch (error) { + console.error(`[npm-dry-run][error] dry-run failed for ${name}`); + process.exit(1); + } +} + +console.log('[npm-dry-run] completed.'); diff --git a/ci-cd/release-dry-run.js b/ci-cd/release-dry-run.js new file mode 100644 index 00000000..ff88fe7c --- /dev/null +++ b/ci-cd/release-dry-run.js @@ -0,0 +1,80 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +function runCommand(command, args, cwd = process.cwd()) { + const result = spawnSync(command, args, { cwd, stdio: 'inherit' }); + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${String(result.status)}`); + } +} + +function readWorkspaceFolders(baseDir) { + if (!fs.existsSync(baseDir)) return []; + return fs.readdirSync(baseDir) + .map((name) => path.join(baseDir, name)) + .filter((fullPath) => fs.statSync(fullPath).isDirectory()); +} + +function readPackageJson(targetDir) { + const packageJsonPath = path.join(targetDir, 'package.json'); + if (!fs.existsSync(packageJsonPath)) return null; + return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +} + +function dryRunPackages(packagesDir) { + const packageDirs = readWorkspaceFolders(packagesDir); + for (const packageDir of packageDirs) { + const pkg = readPackageJson(packageDir); + if (!pkg || pkg.private === true) continue; + console.log(`\n[dry-run][package] ${pkg.name || packageDir}`); + runCommand('npm', ['pack', '--dry-run', packageDir]); + } +} + +function dryRunApps(appsDir) { + const appDirs = readWorkspaceFolders(appsDir); + for (const appDir of appDirs) { + const pkg = readPackageJson(appDir); + if (!pkg) continue; + const scripts = pkg.scripts || {}; + const hasBuild = typeof scripts.build === 'string' && scripts.build.trim().length > 0; + const hasTest = typeof scripts.test === 'string' && scripts.test.trim().length > 0; + if (!hasBuild || !hasTest) { + throw new Error(`App workspace ${pkg.name || appDir} is missing required build/test scripts for release readiness.`); + } + console.log(`[dry-run][app] ${pkg.name || appDir} -> build/test scripts present`); + } +} + +function run() { + const mode = (process.argv[2] || 'all').toLowerCase(); + const root = process.cwd(); + const packagesDir = path.join(root, 'packages'); + const appsDir = path.join(root, 'apps'); + + if (!['all', 'packages', 'apps'].includes(mode)) { + throw new Error(`Invalid mode "${mode}". Use one of: all, packages, apps.`); + } + + if (mode === 'all' || mode === 'packages') { + dryRunPackages(packagesDir); + } + + if (mode === 'all' || mode === 'apps') { + dryRunApps(appsDir); + } + + console.log('\n[dry-run] release dry-run completed successfully.'); +} + +if (require.main === module) { + run(); +} + +module.exports = { + dryRunApps, + dryRunPackages, + readPackageJson +}; diff --git a/ci-cd/run-api-smoke.js b/ci-cd/run-api-smoke.js new file mode 100644 index 00000000..611da178 --- /dev/null +++ b/ci-cd/run-api-smoke.js @@ -0,0 +1,33 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const CANDIDATE_SMOKE_TESTS = [ + 'apps/backend-template/test/integration/Express/get.localhost.test.ts', + 'test/integration/Express/get.localhost.test.ts' +]; + +function run() { + const root = process.cwd(); + const testFile = CANDIDATE_SMOKE_TESTS.find((target) => fs.existsSync(path.join(root, target))); + + if (!testFile) { + console.error('[ci] api smoke: missing localhost smoke test file.'); + console.error(`[ci] expected one of: ${CANDIDATE_SMOKE_TESTS.join(', ')}`); + process.exit(1); + } + + console.log(`[ci] api smoke target: ${testFile}`); + + const result = spawnSync('jest', [testFile, '--runInBand', '--coverage=false'], { + stdio: 'inherit', + env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'ci' } + }); + + if (result.status !== 0) { + process.exit(result.status || 1); + } +} + +run(); diff --git a/ci-cd/run-monorepo-ci.js b/ci-cd/run-monorepo-ci.js new file mode 100644 index 00000000..bd16a706 --- /dev/null +++ b/ci-cd/run-monorepo-ci.js @@ -0,0 +1,72 @@ +/* eslint-disable no-console */ +const { spawnSync } = require('child_process'); +const { computeAffectedWorkspaces, readChangedFiles } = require('./check-affected-workspaces'); + +function runCommand(command, args, cwd = process.cwd()) { + const result = spawnSync(command, args, { cwd, stdio: 'inherit' }); + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${String(result.status)}`); + } +} + +function resolveCiPlan(affected) { + const commands = []; + + if (affected.docsOnly) { + commands.push(['npm', ['run', 'lint']]); + commands.push(['npm', ['run', 'changelog:check']]); + return commands; + } + + commands.push(['npm', ['run', 'ci:gate:strict']]); + + for (const app of affected.apps) { + commands.push(['npm', ['run', 'build', '--prefix', `apps/${app}`]]); + commands.push(['npm', ['run', 'test', '--prefix', `apps/${app}`]]); + } + + for (const pkg of affected.packages) { + commands.push(['npm', ['run', 'build', '--prefix', `packages/${pkg}`]]); + commands.push(['npm', ['run', 'test', '--prefix', `packages/${pkg}`]]); + } + + return commands; +} + +function resolveInputFiles(argvFiles = [], options = {}) { + const files = argvFiles.map((file) => String(file || '').trim()).filter(Boolean); + if (files.length > 0) { + return files; + } + + const baseRef = String(options.baseRef || process.env.AAA_CI_BASE_REF || 'origin/main'); + const readChanged = typeof options.readChangedFiles === 'function' + ? options.readChangedFiles + : readChangedFiles; + return readChanged(baseRef); +} + +function run() { + const files = resolveInputFiles(process.argv.slice(2)); + const affected = computeAffectedWorkspaces(files); + const commands = resolveCiPlan(affected); + + console.log('[ci-monorepo] affected scope'); + console.log(JSON.stringify(affected, null, 2)); + + for (const [command, args] of commands) { + console.log(`\n[ci-monorepo] running: ${command} ${args.join(' ')}`); + runCommand(command, args); + } + + console.log('\n[ci-monorepo] completed successfully.'); +} + +if (require.main === module) { + run(); +} + +module.exports = { + resolveCiPlan, + resolveInputFiles +}; diff --git a/ci-cd/run-security-smoke.js b/ci-cd/run-security-smoke.js new file mode 100644 index 00000000..ebe512fa --- /dev/null +++ b/ci-cd/run-security-smoke.js @@ -0,0 +1,51 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const CANDIDATE_SECURITY_ROOTS = ['apps/backend-template/test/unit', 'test/unit']; +const SECURITY_TEST_SUFFIXES = [ + 'config/security.test.ts', + 'shared/utils.errorExposure.test.ts' +]; + +function run() { + const root = process.cwd(); + const existingTests = CANDIDATE_SECURITY_ROOTS.reduce((found, candidateRoot) => { + if (found.length > 0) { + return found; + } + + if (!fs.existsSync(path.join(root, candidateRoot))) { + return found; + } + + const testsForRoot = SECURITY_TEST_SUFFIXES + .map((suffix) => path.join(candidateRoot, suffix)) + .filter((target) => fs.existsSync(path.join(root, target))); + + return testsForRoot; + }, []); + + if (existingTests.length === 0) { + console.error('[ci] security smoke: no matching tests were found.'); + const expected = CANDIDATE_SECURITY_ROOTS + .flatMap((candidateRoot) => SECURITY_TEST_SUFFIXES.map((suffix) => `${candidateRoot}/${suffix}`)) + .join(', '); + console.error(`[ci] expected one of: ${expected}`); + process.exit(1); + } + + console.log(`[ci] security smoke targets: ${existingTests.join(', ')}`); + + const result = spawnSync('jest', [...existingTests, '--runInBand', '--coverage=false'], { + stdio: 'inherit', + env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'ci' } + }); + + if (result.status !== 0) { + process.exit(result.status || 1); + } +} + +run(); diff --git a/ci-cd/run-service-management-integration.js b/ci-cd/run-service-management-integration.js new file mode 100644 index 00000000..40eb985b --- /dev/null +++ b/ci-cd/run-service-management-integration.js @@ -0,0 +1,33 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const CANDIDATE_TEST_DIRS = [ + 'apps/backend-template/test/integration/ServiceManagement', + 'test/integration/ServiceManagement' +]; + +function run() { + const root = process.cwd(); + const testDir = CANDIDATE_TEST_DIRS.find((target) => fs.existsSync(path.join(root, target))); + + if (!testDir) { + console.error('[ci] service-management integration: no test directories found.'); + console.error(`[ci] expected one of: ${CANDIDATE_TEST_DIRS.join(', ')}`); + process.exit(1); + } + + console.log(`[ci] service-management integration target: ${testDir}`); + + const result = spawnSync('jest', [testDir, '--runInBand'], { + stdio: 'inherit', + env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'dev' } + }); + + if (result.status !== 0) { + process.exit(result.status || 1); + } +} + +run(); diff --git a/ci-cd/run-unit-tests.js b/ci-cd/run-unit-tests.js new file mode 100644 index 00000000..3f5aecf3 --- /dev/null +++ b/ci-cd/run-unit-tests.js @@ -0,0 +1,30 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const CANDIDATE_UNIT_DIRS = ['apps/backend-template/test/unit', 'test/unit']; + +function run() { + const root = process.cwd(); + const testTarget = CANDIDATE_UNIT_DIRS.find((target) => fs.existsSync(path.join(root, target))); + + if (!testTarget) { + console.error('[ci] unit tests: no test directories found.'); + console.error(`[ci] expected one of: ${CANDIDATE_UNIT_DIRS.join(', ')}`); + process.exit(1); + } + + console.log(`[ci] unit tests target: ${testTarget}`); + + const result = spawnSync('jest', [testTarget, '--runInBand'], { + stdio: 'inherit', + env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'dev' } + }); + + if (result.status !== 0) { + process.exit(result.status || 1); + } +} + +run(); diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 00000000..52d45402 --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,24 @@ +# Jumentix Technical Documentation Index + +## Monorepo and Governance + +- [Project Overview](./md/PROJECT-OVERVIEW.md) +- [Architecture and Structure](./md/ARCHITECTURE-AND-STRUCTURE.md) +- [Jumentix Monorepo Execution Plan](./md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md) +- [Jumentix Project Governance](./md/JUMENTIX-PROJECT-GOVERNANCE.md) +- [Testing, CI and Quality](./md/TESTING-CI-AND-QUALITY.md) + +## Component Hubs + +- [Backend Template Documentation](../apps/backend-template/documentation/README.md) +- [Service Management Documentation](../apps/service-management/documentation/README.md) +- [Jumentix Website](../apps/jumentix-website/README.md) +- [Workspace Packages](../packages/README.md) +- [Tooling](../tooling/README.md) + +## Architecture and Contracts + +- [Events and Messages Map](./md/EVENTS-AND-MESSAGES-MAP.md) +- [Error Contracts and Responses](./md/ERROR-CONTRACTS-AND-RESPONSES.md) +- [OpenAPI Spec](../spec/1.0.0.yml) +- [Runtime Environment Contracts](./md/RUNTIME-ENVIRONMENT-CONTRACTS.md) diff --git a/documentation/md/ARCHITECTURE-AND-STRUCTURE.md b/documentation/md/ARCHITECTURE-AND-STRUCTURE.md index f38fc94b..290d1447 100644 --- a/documentation/md/ARCHITECTURE-AND-STRUCTURE.md +++ b/documentation/md/ARCHITECTURE-AND-STRUCTURE.md @@ -3,7 +3,7 @@ ## Project Structure (Current) ```txt -src/ +apps/backend-template/src/ config/ infra/ # cross-cutting infrastructure adapters interface/ # HTTP runtime adapters and transport plumbing @@ -80,6 +80,4 @@ Current highlights already implemented: ## Classes Diagram -![Diagram](../OASdoc/miro.png "Diagram") - - +Architecture diagram: Miro board diff --git a/documentation/md/BOOTSTRAP-CLI-SCAFFOLDING.md b/documentation/md/BOOTSTRAP-CLI-SCAFFOLDING.md index 526d299f..280f13d0 100644 --- a/documentation/md/BOOTSTRAP-CLI-SCAFFOLDING.md +++ b/documentation/md/BOOTSTRAP-CLI-SCAFFOLDING.md @@ -1,17 +1,24 @@ # Bootstrap CLI Scaffolding -This boilerplate now exposes an npm-installable bootstrap CLI command: +This boilerplate now exposes npm-installable bootstrap CLI commands: - `aaa-bootstrap` +- `jumentix-init` The command clones `aaa-typescript-boilerplate` into a target folder and writes initial service profile metadata. +Workspace ownership: + +- `packages/cli-init` contains the canonical bootstrap implementation. +- root `bin/aaa-bootstrap.js` delegates to `packages/cli-init` to keep behavior consistent during monorepo migration. + ## Usage Install globally (or run with `npx` from package registry): ```bash -npm install -g aaa-typescript-boilerplate +npm install -g @jumentix/cli-init +jumentix-init aaa-bootstrap ``` @@ -21,6 +28,26 @@ Local repository usage: npm run cli:bootstrap ``` +Non-interactive usage: + +```bash +jumentix-init --service-type=rest --project-name=my-service --git-branch=main --install-deps=false +``` + +CLI help: + +```bash +jumentix-init --help +``` + +Supported flags: + +- `--service-type` (`rest|websocket|grpc|graphql|functions`) +- `--project-name` +- `--git-branch` +- `--install-deps` (`y|n|true|false`) +- `--repo` (override template repository URL) + ## Supported Scaffold Profiles 1. HTTP/REST server (OpenAPI/Swagger + static assets) diff --git a/documentation/md/CACHE-SERVICE-AND-ENDPOINT-CACHING.md b/documentation/md/CACHE-SERVICE-AND-ENDPOINT-CACHING.md new file mode 100644 index 00000000..c3453a57 --- /dev/null +++ b/documentation/md/CACHE-SERVICE-AND-ENDPOINT-CACHING.md @@ -0,0 +1,70 @@ +# Cache Service and Read Caching + +## Purpose + +Provide a contract-based cache layer that can be reused by services without coupling domain logic to a specific storage engine. + +## Implementation + +- Cache contract and implementation: + - `apps/backend-template/src/infra/cache/ICacheService.ts` + - `apps/backend-template/src/infra/cache/CacheService.ts` +- Storage adapter dependency: + - `apps/backend-template/src/infra/persistence/KeyValueStorage/IKeyValueStorageClient.ts` + +The cache uses key-value envelopes: + +```ts +{ + value: any; + expiresAt?: number; +} +``` + +`expiresAt` is optional and is evaluated on reads. + +## Invalidation model + +The implementation uses namespace versioning: + +- `cache:version:users` +- `cache:version:organizations` + +Read keys include the current namespace version (`vN`). +Write operations bump namespace version, invalidating previous read keys without wildcard deletes. + +## Endpoint read caching behavior + +Current read-through integration: + +- `UserService` + - `getOneById` + - `getAll` +- `OrganizationService` + - `getOneById` + - `getAll` + +Current write-trigger invalidation: + +- User aggregate mutations: + - create/update/delete + - updatePassword + - create/update/delete document + - create/update/delete phone + - create/update/delete email +- Organization aggregate mutations: + - create/update/delete + - create/update/delete address + - create/update/delete phone + - create/update/delete email + +## Composition wiring + +`composeUsersAuthServices` now compiles and injects `CacheService` when `keyValueStorageClient` is available. + +## Tests + +- `apps/backend-template/test/unit/infra/cache/CacheService.test.ts` +- `apps/backend-template/test/unit/modules/Users/service/UserService.test.ts` +- `apps/backend-template/test/unit/modules/Users/service/OrganizationService.test.ts` + diff --git a/documentation/md/CI-TROUBLESHOOTING.md b/documentation/md/CI-TROUBLESHOOTING.md index a653c3c2..6b306956 100644 --- a/documentation/md/CI-TROUBLESHOOTING.md +++ b/documentation/md/CI-TROUBLESHOOTING.md @@ -36,7 +36,7 @@ Symptoms: - ENOENT while loading env file during Jest startup Cause: -- current `NODE_ENV` does not map to an existing file in `src/config`. +- current `NODE_ENV` does not map to an existing file in `apps/backend-template/src/config`. Fix: @@ -45,8 +45,8 @@ NODE_ENV=ci npm run test:unit ``` Ensure at least one valid file exists for CI fallback: -- `src/config/.env.ci` -- `src/config/.env.dev.example` +- `apps/backend-template/src/config/.env.ci` +- `apps/backend-template/src/config/.env.dev.example` How this works: - `jest.config.js` loads `ci-cd/loadEnvironment.js` @@ -67,7 +67,7 @@ npm run oas:check-routes Checklist: - each operation in `spec/*.yml` has `operationId` - each runtime has handler at: - - `src/modules//interface/api/frameworks//handlers/.ts` + - `apps/backend-template/src/modules//interface/restapi/frameworks//handlers/.ts` - controller implements methods invoked in those handlers Related file: diff --git a/documentation/md/CONTRIBUTING-AND-TOOLING.md b/documentation/md/CONTRIBUTING-AND-TOOLING.md index a402df02..3e677ee8 100644 --- a/documentation/md/CONTRIBUTING-AND-TOOLING.md +++ b/documentation/md/CONTRIBUTING-AND-TOOLING.md @@ -3,14 +3,21 @@ ## Contributing 1. Create a branch. +2. Ensure there is a related GitHub Issue and it is added to the **Jumentix** project: + +```text +https://github.com/users/web2solutions/projects/1 +``` + +3. Set/update project fields for the issue (`Status`, `Priority`, `Size`, `Estimate`, `Start date`, `End date`). 2. Run TDD mode: ```bash npm run tdd ``` -3. Make your changes. -4. Commit using: +4. Make your changes. +5. Commit using: ```bash npm run commit @@ -18,6 +25,8 @@ npm run commit This command runs lint/tests and then opens commitizen flow. +PRs must include linked issue and project context (Jumentix project item). + ## Tooling Lint: diff --git a/documentation/md/DATABASE-DRIVERS-SMOKE-TESTS.md b/documentation/md/DATABASE-DRIVERS-SMOKE-TESTS.md index 5089a755..970cd456 100644 --- a/documentation/md/DATABASE-DRIVERS-SMOKE-TESTS.md +++ b/documentation/md/DATABASE-DRIVERS-SMOKE-TESTS.md @@ -19,18 +19,18 @@ This boilerplate now supports runtime selection of multiple database drivers thr ## Runtime Client Compilation -- Source: `src/infra/persistence/compileDatabaseClient.ts` -- External stores now use fail-fast proxies (`src/infra/persistence/external/ExternalStoreProxy.ts`) instead of silently reusing in-memory stores. -- Oracle has dedicated connector support (`src/infra/persistence/external/OracleRepository.ts`). +- Source: `apps/backend-template/src/infra/persistence/compileDatabaseClient.ts` +- External stores now use fail-fast proxies (`apps/backend-template/src/infra/persistence/external/ExternalStoreProxy.ts`) instead of silently reusing in-memory stores. +- Oracle has dedicated connector support (`apps/backend-template/src/infra/persistence/external/OracleRepository.ts`). - Bootstrap adapters that inject `IDatabaseClient` from `AAA_DATABASE_DRIVER`: - - `src/interface/HTTP/adapters/*` - - `src/interface/WebSocket/adapters/socket-io/socket-io.ts` - - `src/interface/gRPC/adapters/grpc/grpc.ts` - - `src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/runtime.ts` + - `apps/backend-template/src/interface/HTTP/adapters/*` + - `apps/backend-template/src/interface/WebSocket/adapters/socket-io/socket-io.ts` + - `apps/backend-template/src/interface/gRPC/adapters/grpc/grpc.ts` + - `apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/runtime.ts` ## Smoke Test Suite -- Test file: `test/smoke/database/DatabaseDrivers.smoke.test.ts` +- Test file: `apps/backend-template/test/smoke/database/DatabaseDrivers.smoke.test.ts` - Run all configured smoke drivers: ```bash @@ -66,16 +66,16 @@ AAA_DB_SMOKE_DRIVERS=PostgreSQL,MySQL npm run test:smoke:db ## Docker Compose Files Per Database -- `docker-compose-postgresql.yml` -- `docker-compose-mysql.yml` -- `docker-compose-mssql.yml` -- `docker-compose-oracle.yml` -- `docker-compose-mongodb.yml` -- `docker-compose-cassandra.yml` -- `docker-compose-dynamodb.yml` -- `docker-compose-firebase.yml` -- `docker-compose-aurora.yml` -- `docker-compose-rds.yml` +- `apps/backend-template/docker-compose-postgresql.yml` +- `apps/backend-template/docker-compose-mysql.yml` +- `apps/backend-template/docker-compose-mssql.yml` +- `apps/backend-template/docker-compose-oracle.yml` +- `apps/backend-template/docker-compose-mongodb.yml` +- `apps/backend-template/docker-compose-cassandra.yml` +- `apps/backend-template/docker-compose-dynamodb.yml` +- `apps/backend-template/docker-compose-firebase.yml` +- `apps/backend-template/docker-compose-aurora.yml` +- `apps/backend-template/docker-compose-rds.yml` ## One-command DB smoke workflows @@ -96,7 +96,7 @@ npm run smoke:db:rds ## Environment Variables -Add/update these keys in `src/config/.env.*` for driver-specific runtime: +Add/update these keys in `apps/backend-template/src/config/.env.*` for driver-specific runtime: - `AAA_DATABASE_DRIVER` - `AAA_DATABASE_CONNECTION_URL` diff --git a/documentation/md/DEVELOPER-AUTOMATION-CLI.md b/documentation/md/DEVELOPER-AUTOMATION-CLI.md index 76a39526..154f7d70 100644 --- a/documentation/md/DEVELOPER-AUTOMATION-CLI.md +++ b/documentation/md/DEVELOPER-AUTOMATION-CLI.md @@ -25,7 +25,7 @@ npm run start:cli All commands start the same CLI entrypoint: -- `src/interface/CLI/index.ts` +- `apps/backend-template/src/interface/CLI/index.ts` Bootstrap/scaffold command: @@ -122,17 +122,17 @@ Field definition includes: Central validation helpers and registries: -- `src/shared/openapi/OpenApi31DataEntity.ts` +- `apps/backend-template/src/shared/openapi/OpenApi31DataEntity.ts` Domain model enforcement: -- `src/modules/port/BaseModel.ts` +- `apps/backend-template/src/modules/port/BaseModel.ts` - `throwIfFieldSchemaIsNotOpenApi31Compliant(...)` - `throwIfDataEntitySchemaIsNotOpenApi31Compliant(...)` Implemented domain schema example: -- `src/modules/Users/domain/Model/User.ts` +- `apps/backend-template/src/modules/Users/domain/Model/User.ts` - static `dataEntitySchema` ## Persistence @@ -143,7 +143,7 @@ CLI data is persisted in: The file is managed by: -- `src/interface/CLI/core/catalogStorage.ts` +- `apps/backend-template/src/interface/CLI/core/catalogStorage.ts` ## Architectural Notes @@ -155,7 +155,7 @@ The file is managed by: CLI behavior is covered by unit tests under: -- `test/unit/interface/CLI/` +- `apps/backend-template/test/unit/interface/CLI/` This ensures new CLI features and flows remain stable and coverage-compliant. @@ -163,7 +163,7 @@ This ensures new CLI features and flows remain stable and coverage-compliant. The visual design surface moved to: -- `servicemangement/` +- `apps/service-management/` See: diff --git a/documentation/md/DOMAIN-DATA-ENTITIES.md b/documentation/md/DOMAIN-DATA-ENTITIES.md index 1f6f5d29..1ac88a6f 100644 --- a/documentation/md/DOMAIN-DATA-ENTITIES.md +++ b/documentation/md/DOMAIN-DATA-ENTITIES.md @@ -21,8 +21,8 @@ Detailed per-domain files: Code references: -- `src/modules/Users/domain/Model/User.ts` -- `src/modules/Users/domain/Entity/IUser.ts` +- `apps/backend-template/src/modules/Users/domain/Model/User.ts` +- `apps/backend-template/src/modules/Users/domain/Entity/IUser.ts` - `spec/1.0.0.yml` (`components.schemas.User`) | Field | Type (TS) | Format / Allowed values | Required | Validation expected | @@ -48,7 +48,7 @@ Code references: Code references: -- `src/modules/ddd/valueObjects/EmailValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/EmailValueObject.ts` - `spec/1.0.0.yml` (`components.schemas.Email`, `RequestCreateEmail`, `RequestUpdateEmail`) | Field | Type (TS) | Format / Allowed values | Required | Validation expected | @@ -62,8 +62,8 @@ Code references: Code references: -- `src/modules/ddd/valueObjects/DocumentValueObject.ts` -- `src/modules/ddd/valueObjects/EDocumentType.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/DocumentValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/EDocumentType.ts` - `spec/1.0.0.yml` (`components.schemas.Document`, `RequestCreateDocument`, `RequestUpdateDocument`) | Field | Type (TS) | Format / Allowed values | Required | Validation expected | @@ -77,7 +77,7 @@ Code references: Code references: -- `src/modules/ddd/valueObjects/PhoneValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/PhoneValueObject.ts` - `spec/1.0.0.yml` (`components.schemas.Phone`, `RequestCreatePhone`, `RequestUpdatePhone`) | Field | Type (TS) | Format / Allowed values | Required | Validation expected | @@ -92,8 +92,8 @@ Code references: Code references: -- `src/modules/ddd/valueObjects/AddressValueObject.ts` -- `src/modules/ddd/valueObjects/EAddressType.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/AddressValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/EAddressType.ts` - `spec/1.0.0.yml` (`components.schemas.Address`) | Field | Type (TS) | Format / Allowed values | Required | Validation expected | @@ -107,8 +107,8 @@ Code references: Code references: -- `src/modules/Users/domain/Model/Organization.ts` -- `src/modules/Users/domain/Entity/IOrganization.ts` +- `apps/backend-template/src/modules/Users/domain/Model/Organization.ts` +- `apps/backend-template/src/modules/Users/domain/Entity/IOrganization.ts` - `spec/1.0.0.yml` (`components.schemas.Organization`) | Field | Type (TS) | Format / Allowed values | Required | Validation expected | @@ -127,7 +127,7 @@ Code references: ## Domain Validation Rules (Cross-entity) -Shared validators in `src/shared/validators/index.ts` enforce rules used across entities/use cases: +Shared validators in `apps/backend-template/src/shared/validators/index.ts` enforce rules used across entities/use cases: - `canNotBeEmpty` - `throwIfReadOnly` diff --git a/documentation/md/DOMAIN-DESIGNER-FEATURES-AND-USAGE.md b/documentation/md/DOMAIN-DESIGNER-FEATURES-AND-USAGE.md new file mode 100644 index 00000000..aee9b171 --- /dev/null +++ b/documentation/md/DOMAIN-DESIGNER-FEATURES-AND-USAGE.md @@ -0,0 +1,244 @@ +# Domain Designer Features and Usage + +This document is the technical guide for all Domain Designer MVP capabilities inside: + +- `apps/service-management/` + +It covers what each feature does and how to use it in practice. + +## 1) Canvas and Navigation + +Features: + +- Domain rectangles (color-coded) +- Entity cards inside domains +- Pan, zoom, fit, reset +- Snap-to-grid +- Compact/full view +- Large-canvas performance mode +- Mini-map navigation +- Undo/redo + +How to use: + +1. Create domains and entities from the left panel. +2. Drag domain headers and entity headers to reposition. +3. Use: + - `Ctrl/Cmd + mouse wheel` for zoom + - `Space + drag` for panning + - `Fit` and `Reset View` for quick framing +4. Turn on `Large Canvas` for high-density diagrams. +5. Use the mini-map to focus a domain quickly. + +## 2) Relationship Design + +Features: + +- Form-based relationship creation (`from`, `to`, cardinality) +- Pick-on-canvas mode +- Anchor-to-anchor drag connectors +- Relationship reverse +- Auto FK generation +- Label offset controls (`x`, `y`) +- Bend path controls (`bendX`, `bendY`) +- Anchor behavior (`auto`, `center`) +- Routing style (`curved`, `orthogonal`) + +How to use: + +1. Select source and target entities and click `Connect`. +2. Or click `Pick On Canvas` and select entities directly. +3. For anchor-aware connections, drag from edge anchors (`top/right/bottom/left`) between entities. +4. Select a relationship in the list and tune: + - label offset + - bend points + - anchor behavior +5. Click `Save Relationship`. + +## 3) Domain Context Metadata + +Per-domain metadata: + +- Ubiquitous language +- Owner team +- Upstream dependencies +- Downstream dependencies +- Integration channel +- Package dependencies +- Shared value objects + +How to use: + +1. Select a domain. +2. Fill values in `Bounded Context`. +3. Click `Save Context`. + +These fields are persisted in the designer state and exported through JSON/package flows. + +## 4) Entity Editing and Templates + +Entity-level features: + +- Rename, move, duplicate, delete +- Aggregate root flag +- Invariants editor +- Field CRUD with OpenAPI-aligned metadata +- Field templates (`tenantRef`, `auditTrail`, `softDelete`, `contactPack`) +- Entity templates: + - `crudAggregate` + - `eventSourced` + - `referenceData` + - `tenantOwned` + +How to use: + +1. Select an entity. +2. Use Entity Inspector for rename/move/rules. +3. Add fields manually or apply field templates. +4. Apply entity templates from `Entities` panel. + +## 5) RBAC Policy Mapping + +Per-entity action policy: + +- Actions: + - `list` + - `getById` + - `create` + - `update` + - `delete` +- Role toggles: + - `superadmin` + - `admin` + - `user` +- Tenant scope flag + +How to use: + +1. Select entity and action. +2. Mark allowed roles and tenant scope. +3. Click `Save RBAC Rule`. +4. Review generated matrix in the RBAC list. + +## 6) Message Contract Designer + +Supported contract types: + +- `event` +- `command` +- `request` +- `response` + +Contract fields: + +- name +- type +- channel/topic +- version +- payload schema (JSON editor) + +How to use: + +1. Select entity. +2. Fill contract name/type/channel/version. +3. Click `Add Contract`. +4. Use `payload` button to edit schema JSON. + +Exports include these contracts in: + +- OpenAPI extension (`x-message-contracts`) +- AsyncAPI export + +## 7) OpenAPI Advanced Composition + +Per-entity controls: + +- `oneOf`, `allOf`, `anyOf` +- schema refs list +- external `$ref` list +- discriminator property + +How to use: + +1. Select entity. +2. Choose composition mode. +3. Add schema refs and optional external refs. +4. Define discriminator if needed. +5. Save composition. + +## 8) Validation, Quality Gate, and Diff + +Features: + +- Model checks with severity: + - `error` + - `warn` + - `info` +- Configurable minimum severity filter +- Export block on critical issues +- Schema baseline save/clear +- Schema diff and migration hints + +How to use: + +1. Click `Validate Model`. +2. Adjust severity filter if needed. +3. Enable `block export on critical issues` to enforce quality gate. +4. Save a baseline, then run diff to detect changes. + +## 9) Example and Code Generation + +Generated outputs: + +- Request/response payload examples +- Code skeleton preview: + - model + - repository port + - use case + - controller + - handler + +How to use: + +1. Select an entity for focused output, or keep none selected for full-canvas output. +2. Click: + - `Generate Examples` + - `Code Preview` + +## 10) Export and Import Targets + +Export: + +- JSON model +- OpenAPI 3.1 +- Markdown +- JSON Schema +- AsyncAPI +- Boilerplate bundle +- Domain package + +Import: + +- JSON model +- OpenAPI 3.1 +- Domain package + +How to use: + +1. Use export buttons in `Export` panel. +2. Use import buttons for JSON/OAS/package. +3. For package export, selected domain is used as source package. + +## 11) Smoke Coverage + +Service Management smoke tests: + +- `apps/backend-template/test/integration/ServiceManagement/domainDesigner.smoke.test.ts` +- `apps/backend-template/test/unit/service-management/mvp.roadmap.features.test.ts` + +Run: + +```bash +npm run test:integration:service-management +NODE_ENV=dev npx jest apps/backend-template/test/unit/service-management/mvp.roadmap.features.test.ts --runInBand +``` diff --git a/documentation/md/DOMAIN-DESIGNER-MVP-ROADMAP.md b/documentation/md/DOMAIN-DESIGNER-MVP-ROADMAP.md index 0b1100ff..c9bb1c4e 100644 --- a/documentation/md/DOMAIN-DESIGNER-MVP-ROADMAP.md +++ b/documentation/md/DOMAIN-DESIGNER-MVP-ROADMAP.md @@ -28,24 +28,36 @@ Canonical backlog source: - undo/redo - Model checks with actionable issue focus - CRUD endpoint preview in entity inspector - -## Next MVP priorities - - Anchor-based drag-and-drop relationship connectors -- Relationship label positioning and routing styles - Bounded-context metadata editor per domain +- Relationship label positioning controls (offset editor + reset) +- Advanced relationship path controls (bend points + anchor behavior) - Aggregate root and invariant editor - Schema diff and migration preview +- Validation severity levels and export quality gate +- RBAC mapping editor per entity/action - Event/message contract designer tied to entities -- RBAC mapping UI per entity/action -- Advanced OpenAPI composition builder (`oneOf`, `allOf`, `anyOf`) -- Code generation preview and pluggable exporters -- E2E smoke coverage for Domain Designer workflows +- Code generation preview pane +- Entity templates and scaffolding packs (`crudAggregate`, `eventSourced`, `referenceData`, `tenantOwned`) +- Pluggable exporters (JSON, OAS 3.1, Markdown, JSON Schema, AsyncAPI, Boilerplate bundle) +- Advanced OpenAPI composition builder (`oneOf`, `allOf`, `anyOf`, external `$ref`, discriminator) +- Collaborative model package support (domain package import/export, dependencies, shared value objects) +- Request/response example generator from entity schema +- Visual mini-map and large-canvas performance mode +- Starter e2e/smoke coverage for Domain Designer workflows + +## Next MVP priorities + +- MVP roadmap completed. + +## Post-MVP enhancements + +- Collaborative package versioning and semantic dependency conflict resolution ## Documentation and governance rules - Every Domain Designer feature must update: - - `servicemangement/README.md` + - `apps/service-management/README.md` - this roadmap (when scope or priority changes) - relevant README/document index links - Every newly identified MVP item must be added to backlog tracking before or along with implementation. diff --git a/documentation/md/ENGINEERING-BOOTSTRAP-GUIDE.md b/documentation/md/ENGINEERING-BOOTSTRAP-GUIDE.md index 99c76767..accb8df6 100644 --- a/documentation/md/ENGINEERING-BOOTSTRAP-GUIDE.md +++ b/documentation/md/ENGINEERING-BOOTSTRAP-GUIDE.md @@ -8,6 +8,24 @@ This guide explains how to use this boilerplate to create a new backend service The goal is to help software engineers move from idea to working service with the existing architecture and conventions. +## Monorepo workspace map + +Current workspace layout: + +- `apps/backend-template`: runtime app ownership for backend bootstrap profiles and PM2 ecosystems. +- `apps/service-management`: service management web app and runtime server. +- `packages/*`: reusable internal packages (message mediator, SDK clients, infra adapters). +- root `apps/backend-template/src/`: incremental migration bridge while app/package ownership is finalized. + +Core orchestration commands: + +```bash +npm run mono:build +npm run mono:test +npm run mono:lint +npm run mono:typecheck +``` + ## 1. Before you start ### Runtime and tooling requirements @@ -139,6 +157,16 @@ npm run dev:hyper-express npm run dev:serverless ``` +### PM2 multi-app (monorepo) + +```bash +npm run pm2:start:dev:restapi +npm run pm2:start:dev:websocket-rest +npm run pm2:start:dev:grpc-rest +``` + +These profiles start service-management alongside backend adapters using `pm2/*`. + ### Quality and CI parity ```bash @@ -148,6 +176,7 @@ npm run oas:check-routes npm run build:dev npm run ci:smoke npm run ci:gate +npm run ci:monorepo ``` ## 9. Definition of done for new features diff --git a/documentation/md/ERROR-CONTRACTS-AND-RESPONSES.md b/documentation/md/ERROR-CONTRACTS-AND-RESPONSES.md index e8ef4955..961ce9f0 100644 --- a/documentation/md/ERROR-CONTRACTS-AND-RESPONSES.md +++ b/documentation/md/ERROR-CONTRACTS-AND-RESPONSES.md @@ -4,7 +4,7 @@ This document defines how errors are represented in code and serialized through ## 1) Base Error Contract -All domain/infra errors extend `BaseError` (`src/infra/exceptions/BaseError.ts`) and expose: +All domain/infra errors extend `BaseError` (`apps/backend-template/src/infra/exceptions/BaseError.ts`) and expose: ```ts { @@ -19,7 +19,7 @@ All domain/infra errors extend `BaseError` (`src/infra/exceptions/BaseError.ts`) ## 2) Canonical Error Codes -Defined in `src/infra/exceptions/error.codes.ts`: +Defined in `apps/backend-template/src/infra/exceptions/error.codes.ts`: | String Code | HTTP Status | |---|---| @@ -32,7 +32,7 @@ Defined in `src/infra/exceptions/error.codes.ts`: | `GENERIC.NOT_IMPLEMENTED` | `501` | | `GENERIC.INTERNAL_SERVER_ERROR` | `500` | -Status mapping is resolved by `toHttpStatus(...)` in `src/shared/utils.ts`. +Status mapping is resolved by `toHttpStatus(...)` in `apps/backend-template/src/shared/utils.ts`. ## 3) Error Class to Code Mapping @@ -70,7 +70,7 @@ Current adapters serialize to the same payload shape: Sources: - Express/Fastify/Restify/Hyper-Express: `sendErrorResponse(...)` -- Lambda adapters: `src/interface/HTTP/adapters/aws/lambda/responses/sendErrorResponse.ts` +- Lambda adapters: `apps/backend-template/src/interface/HTTP/adapters/aws/lambda/responses/sendErrorResponse.ts` ## 5) MessageMediator Request/Response Error Contract @@ -102,3 +102,33 @@ When creating a new custom error: 3. Ensure `code` is covered in `EErrorStringCodes`/`EErrorNumberCodes`. 4. Add `formatErrorMessage` branch if custom human-readable text is required. 5. Keep HTTP and message-level error envelopes backward-compatible. + +## 8) Realtime Error Envelopes + +WebSocket (`ApiResponse`): + +```json +{ + "ok": false, + "operationId": "createOrganization", + "error": { + "name": "validation_error", + "message": "Invalid input data" + } +} +``` + +gRPC (`AsyncApiResponse`): + +```json +{ + "ok": false, + "operationId": "createOrganization", + "errorName": "validation_error", + "errorMessage": "Invalid input data" +} +``` + +See transport-specific contract references: +- `documentation/md/contracts/WEBSOCKET-REALTIME-CONTRACTS.md` +- `documentation/md/contracts/GRPC-REALTIME-CONTRACTS.md` diff --git a/documentation/md/EVENTS-AND-MESSAGES-MAP.md b/documentation/md/EVENTS-AND-MESSAGES-MAP.md index 991e4290..a40ff124 100644 --- a/documentation/md/EVENTS-AND-MESSAGES-MAP.md +++ b/documentation/md/EVENTS-AND-MESSAGES-MAP.md @@ -48,11 +48,11 @@ interface IMessageResponse { - `rabbitmq`: queue-based request/reply + topic exchange publish. - `bullmq`: Redis queue-based request/reply + event queues. -Selection is made by `AAA_MESSAGE_MEDIATOR_ADAPTER` in `src/infra/messages/compileMessageMediator.ts`. +Selection is made by `AAA_MESSAGE_MEDIATOR_ADAPTER` in `apps/backend-template/src/infra/messages/compileMessageMediator.ts`. ## 4) Current Domain Event Map (Users) -Producer: `UserService` (`src/modules/Users/service/UserService.ts`) +Producer: `UserService` (`apps/backend-template/src/modules/Users/service/UserService.ts`) | Event Name | Published By | Trigger | |---|---|---| @@ -61,7 +61,7 @@ Producer: `UserService` (`src/modules/Users/service/UserService.ts`) | `users.user.deleted` | `UserService.delete` | user deleted | | `users.user.credentialsUpdated` | `UserService.updatePassword` | password/credentials updated | -Listeners are registered in `registerUserEventListeners` (`src/modules/Users/events/listeners/registerUserEventListeners.ts`) through: +Listeners are registered in `registerUserEventListeners` (`apps/backend-template/src/modules/Users/events/listeners/registerUserEventListeners.ts`) through: - `onUserCreated` - `onUserUpdated` - `onUserDeleted` @@ -69,7 +69,7 @@ Listeners are registered in `registerUserEventListeners` (`src/modules/Users/eve ## 5) Current Request/Response Contract Map (Users/Auth) -Registered in `registerUserMessageHandlers` (`src/modules/Users/events/listeners/registerUserMessageHandlers.ts`): +Registered in `registerUserMessageHandlers` (`apps/backend-template/src/modules/Users/events/listeners/registerUserMessageHandlers.ts`): | Contract | Handler Owner | Input Payload | Result / Error | |---|---|---|---| @@ -78,7 +78,7 @@ Registered in `registerUserMessageHandlers` (`src/modules/Users/events/listeners ## 6) Where Contracts Are Consumed -- `Authorize` decorator (`src/shared/decorators/guard/Authorize.ts`) calls: +- `Authorize` decorator (`apps/backend-template/src/shared/decorators/guard/Authorize.ts`) calls: - `users.auth.ensure-access` - Composition root (`composeUsersAuthServices`) registers domain contracts when a mediator exists. @@ -101,3 +101,12 @@ When adding a new domain: 4. Register handlers in domain composition. 5. Keep payloads versionable (`version` + metadata). 6. Document new contracts in this file and in domain docs. + +## 9) Realtime Interface Contract References + +- WebSocket transport contracts: + - `documentation/md/contracts/WEBSOCKET-REALTIME-CONTRACTS.md` +- gRPC transport contracts: + - `documentation/md/contracts/GRPC-REALTIME-CONTRACTS.md` + +These transport documents define the external request/response envelopes and channel/service semantics used by realtime clients. diff --git a/documentation/md/EXTERNAL-DATA-ADAPTER-FOUNDATIONS.md b/documentation/md/EXTERNAL-DATA-ADAPTER-FOUNDATIONS.md index edb2cb5d..c6ae4aac 100644 --- a/documentation/md/EXTERNAL-DATA-ADAPTER-FOUNDATIONS.md +++ b/documentation/md/EXTERNAL-DATA-ADAPTER-FOUNDATIONS.md @@ -4,7 +4,7 @@ To support heterogeneous deployment topologies, the project includes initial ada ## Location -- `src/infra/persistence/external/` +- `apps/backend-template/src/infra/persistence/external/` ## Foundations Included @@ -30,7 +30,7 @@ For end-to-end smoke validation with Docker per database, see: For asynchronous request-response patterns over queue-like transports: -- `src/infra/messages/repositories/QueueRequestResponseRepository.ts` +- `apps/backend-template/src/infra/messages/repositories/QueueRequestResponseRepository.ts` This class is contract-based and works through the message mediator abstraction, which can be backed by: diff --git a/documentation/md/HEXAGONAL-FEATURE-DRIVEN-MIGRATION.md b/documentation/md/HEXAGONAL-FEATURE-DRIVEN-MIGRATION.md index fee1e2cd..aacd0991 100644 --- a/documentation/md/HEXAGONAL-FEATURE-DRIVEN-MIGRATION.md +++ b/documentation/md/HEXAGONAL-FEATURE-DRIVEN-MIGRATION.md @@ -6,7 +6,7 @@ Move the codebase to a feature-driven, DDD-aligned hexagonal architecture with c ## Approved Target Structure ```txt -src/ +apps/backend-template/src/ app/ shared/ modules/ @@ -23,14 +23,14 @@ src/ ## Current-to-Target Mapping (Users module) -- Current `src/modules/Users/domain/*` -> target `src/modules/users/domain/*` -- Current `src/modules/Users/service/*` -> split: +- Current `apps/backend-template/src/modules/Users/domain/*` -> target `apps/backend-template/src/modules/users/domain/*` +- Current `apps/backend-template/src/modules/Users/service/*` -> split: - application orchestration -> `application/use-cases/*` - domain logic -> `domain/*` (where applicable) -- Current `src/modules/Users/interface/controller/*` -> `adapters/in/http/controllers/*` -- Current `src/modules/Users/interface/api/frameworks/*` -> `adapters/in/http/handlers//*` -- Current `src/modules/Users/infra/repository/*` -> `adapters/out/persistence/*` -- Current `src/modules/Users/composition/*` -> `composition/*` +- Current `apps/backend-template/src/modules/Users/interface/controller/*` -> `adapters/in/http/controllers/*` +- Current `apps/backend-template/src/modules/Users/interface/restapi/frameworks/*` -> `adapters/in/http/handlers//*` +- Current `apps/backend-template/src/modules/Users/infra/repository/*` -> `adapters/out/persistence/*` +- Current `apps/backend-template/src/modules/Users/composition/*` -> `composition/*` ## Migration Principles diff --git a/documentation/md/JUMENTIX-BUNDLER-RUNTIME-TEMPLATES.md b/documentation/md/JUMENTIX-BUNDLER-RUNTIME-TEMPLATES.md new file mode 100644 index 00000000..29de95a5 --- /dev/null +++ b/documentation/md/JUMENTIX-BUNDLER-RUNTIME-TEMPLATES.md @@ -0,0 +1,59 @@ +# JumentiX Bundler and Runtime Templates + +## Objective + +Define baseline templates by artifact type so the bootstrap CLI and workspace packages produce predictable build/runtime behavior. + +## Template Matrix + +| Artifact Type | Build Tooling Baseline | Runtime Baseline | Output Contract | +|---|---|---|---| +| Backend Service (Node) | TypeScript compiler (`tsc`) with workspace paths | Node 22 + PM2 profile + env-based adapter loader | JS build output + source maps + env contract | +| Frontend SPA | Modern bundler profile (Vite-equivalent) + TS | Browser runtime with static hosting | Static assets bundle + optional PWA manifest | +| Frontend SSR | SSR-capable bundler profile + TS | Node runtime (server entry) + static assets | server bundle + client bundle + env contract | +| Backend npm Library | `tsc` + declaration output | Consumer-managed Node runtime | package `main` + `types` + `files` whitelist | +| Frontend npm Library | bundler library mode + TS declarations | Browser/SSR consumer runtime | ESM/CJS bundle + types + style/assets contract | + +## Runtime Template Rules + +- Node runtime is fixed to major version `22`. +- Backend startup is selected by environment contracts: + - `AAA_HTTP_FRAMEWORK` + - `AAA_REALTIME_API` + - `AAA_REALTIME_API_PROTOCOL` + - `AAA_DATABASE_DRIVER` + - `AAA_KEYVALUESTORAGE_DRIVER` +- REST fallback remains active for realtime service profiles. + +## Build/Release Rules + +- All workspace packages must expose `build`, `test`, and `typecheck` scripts. +- Publishable packages must have valid semver versions and non-empty `files` metadata. +- Root release automation must keep: + - `changelog:update` + - `changelog:check` + - `release:dry-run` + - `release:dry-run:packages` + - `release:dry-run:apps` +- CI gate includes release governance validation through `npm run release:governance:check`. + +## CLI Scaffold Mapping + +Bootstrap service types should map to templates: + +| CLI Service Type | Template | +|---|---| +| `restapi` | Backend Service (Node) | +| `websocket+restapi` | Backend Service (Node) + Realtime profile | +| `grpc+restapi` | Backend Service (Node) + Realtime profile | +| `functions` | Provider function bundle template | +| `frontend-spa` | Frontend SPA | +| `frontend-ssr` | Frontend SSR | +| `lib-backend` | Backend npm Library | +| `lib-frontend` | Frontend npm Library | + +## Acceptance Criteria + +- Template definitions are documented and linked from README index. +- CI and release checks enforce required metadata and script contracts. +- Scaffold logic can map each service type to one explicit template. diff --git a/documentation/md/JUMENTIX-DEPLOY-TARGET-AND-PACKAGING-MATRIX.md b/documentation/md/JUMENTIX-DEPLOY-TARGET-AND-PACKAGING-MATRIX.md new file mode 100644 index 00000000..420fdff1 --- /dev/null +++ b/documentation/md/JUMENTIX-DEPLOY-TARGET-AND-PACKAGING-MATRIX.md @@ -0,0 +1,43 @@ +# JumentiX Deploy Target and Packaging Matrix + +## Objective + +Define deploy targets and artifact packaging contracts for backend and frontend services managed by JumentiX. + +## Deploy Target Matrix + +| Deploy Target | Service Types | Runtime Manager | Packaging Contract | Delivery Path | +|---|---|---|---|---| +| Dedicated Server (SSH) | REST, WebSocket+REST, gRPC+REST, frontend | PM2 | Build output + PM2 ecosystem profile | SSH deploy + PM2 reload | +| VM Cloud Instance (EC2/GCE/Azure VM) | REST, WebSocket+REST, gRPC+REST, frontend | PM2 | Build output + PM2 ecosystem profile + env contract | IaC/SSH deploy + PM2 orchestration | +| AWS Lambda | Function APIs | Serverless framework | Function bundle + serverless config + env contract | `serverless deploy` | +| Vercel Functions | Function APIs | Vercel runtime | Vercel function entrypoints + config | Vercel deploy workflow | +| Cloudflare Workers | Function/event APIs | Worker runtime | Worker module bundle + worker config | Wrangler/Worker deployment | + +## Packaging Contracts + +| Artifact Type | Mandatory Files | Validation Gate | +|---|---|---| +| Backend VM Service | `package.json`, build output, PM2 ecosystem, env template | `ci:gate`, build, smoke tests | +| Realtime Service | AsyncAPI contract, runtime adapter entrypoint, REST fallback wiring | unit + integration realtime tests | +| Function Bundle | function handlers, deployment manifest (`serverless` or platform config), env template | function smoke + contract checks | +| Frontend SPA/PWA | app build output, runtime env mapping, offline storage config | frontend build/test/lint gates | +| Shared Package (npm) | `package.json` semver, `files` whitelist, type/build outputs | release governance + dry-run publish | + +## Service Management Metadata Contract + +Service Management should track, per service: + +- `serviceType`: `restapi`, `websocket+restapi`, `grpc+restapi`, `functions` +- `deployTarget`: `dedicated-server`, `vm`, `ec2`, `lambda`, `vercel-functions`, `cloudflare-workers` +- `runtimeProtocol`: `http`, `websocket`, `grpc` +- `databaseDriver`: selected `AAA_DATABASE_DRIVER` +- `keyValueDriver`: selected `AAA_KEYVALUESTORAGE_DRIVER` +- `pm2Profile`: `dev`, `staging`, `production` (for VM-based deployments) + +## Acceptance Criteria + +- Every service type has at least one valid deploy target and packaging contract. +- VM-based deployments are PM2-first and keep processes separated by interface. +- Function targets keep provider-native packaging contracts. +- Release governance blocks publishing artifacts with invalid metadata. diff --git a/documentation/md/JUMENTIX-MIGRATION-INVENTORY-AND-ROLLBACK.md b/documentation/md/JUMENTIX-MIGRATION-INVENTORY-AND-ROLLBACK.md new file mode 100644 index 00000000..d3c75926 --- /dev/null +++ b/documentation/md/JUMENTIX-MIGRATION-INVENTORY-AND-ROLLBACK.md @@ -0,0 +1,84 @@ +# JumentiX Migration Inventory and Rollback + +This document defines concrete migration guardrails for the JumentiX monorepo transformation. + +## Scope and Wave Criteria + +### Wave 1 (must-have) + +- Keep current runtime behavior and public API contracts stable. +- Establish pnpm workspaces and package boundaries already scaffolded under `apps/` and `packages/`. +- Keep root compatibility scripts operational while app/package ownership is progressively moved. +- Update docs and `.agents` requirements in the same PR as code changes. + +### Wave 2+ (enhancements) + +- Full runtime cutover to app-owned paths (`apps/backend-template`, `apps/service-management`). +- Workspace-native CI matrix (`pnpm -r`) as default. +- Independent package release automation for reusable adapters and SDKs. + +## Naming Decisions + +- Product name: `JumentiX`. +- CLI package name: `@jumentix/cli-init`. +- Bootstrap command: + - current compatibility command: `aaa-bootstrap` + - official global install target: `npm install -g @jumentix/cli-init` + - runtime command: `jumentix-init` + +## Branch, Tag, and Rollback Strategy + +## Branch conventions + +- Mainline integration branch: `dev`. +- Migration branches by wave: + - `codex/jumentix-wave0-guardrails` + - `codex/jumentix-wave1-topology` + - `codex/jumentix-wave2-foundation` + - `codex/jumentix-wave3-packages` + - `codex/jumentix-wave4-cli` + - `codex/jumentix-wave5-app-rehoming` + +## Release checkpoints (tags) + +- Pre-wave checkpoint tag: + - `jumentix-pre-waveX-` +- Post-wave checkpoint tag: + - `jumentix-post-waveX-` + +## Rollback procedure per wave + +1. Identify failing wave branch and latest stable pre-wave tag. +2. Revert wave PR(s) from `dev` in reverse order if partially merged. +3. Re-run mandatory gates: + - `npm run ci:gate` + - `npm run coverage:patch` +4. If rollback is required in production-like branches, fast-forward only from pre-wave tag and re-apply safe commits. + +## Inventory Mapping (Current -> Target) + +## Apps + +- `apps/backend-template/src/` + runtime bootstraps -> `apps/backend-template/src/` (staged migration with compatibility scripts). +- `apps/service-management/` -> `apps/service-management/`. + +## Packages + +- `bin/aaa-bootstrap.js` + bootstrap logic -> `packages/cli-init/`. +- `apps/backend-template/src/infra/messages/*` + `apps/backend-template/src/modules/port/IMessage*` -> `packages/message-mediator/`. +- `apps/backend-template/src/infra/persistence/KeyValueStorage/*` -> `packages/key-value-storage/`. +- `apps/backend-template/src/infra/mutex/*` -> `packages/mutex-service/`. +- `apps/backend-template/src/infra/ports/persistence/IStore.ts` + `apps/backend-template/src/infra/persistence/port/IDatabaseClient.ts` -> `packages/persistence-contracts/`. +- `apps/backend-template/src/infra/persistence/external/BaseExternalDataRepository.ts` -> `packages/external-persistence-core/`. +- `apps/backend-template/src/infra/persistence/external/ExternalStoreProxy.ts` -> `packages/external-store-proxy/`. +- `apps/backend-template/src/infra/persistence/external/*.ts` concrete db adapters -> `packages/external-db-repositories/`. +- `apps/backend-template/src/infra/persistence/compileDatabaseClient.ts` -> `packages/database-client-factory/`. +- `apps/backend-template/src/interface/runtime/RuntimeEnvironment.ts` + runtime infra compiler usage -> `packages/runtime-infra/`. +- adapter bootstrap composition helpers -> `packages/adapter-runtime-bootstrap/`. +- `sdk-clients/rest|websocket|grpc` canonical sources -> `packages/sdk-*` (legacy `sdk-clients` remains compatibility bridge). + +## Blockers and staged constraints + +- Root path assumptions in PM2 configs and package scripts still require compatibility layer during cutover. +- CI still executes npm-root flows as baseline; pnpm recursive gate will become primary after full lockfile stabilization. +- Import aliases in `tsconfig.json` must remain dual-mapped until full source relocation is complete. diff --git a/documentation/md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md b/documentation/md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md new file mode 100644 index 00000000..1b2d9dff --- /dev/null +++ b/documentation/md/JUMENTIX-MONOREPO-EXECUTION-PLAN.md @@ -0,0 +1,206 @@ +# JumentiX Monorepo Execution Plan + +## Objective + +Convert the current repository into a pnpm monorepo product named `JumentiX`, minimizing uncertain implementation paths and preserving delivery predictability. + +## Guiding Principles + +1. Functional parity before advancing each migration wave. +2. Small, reviewable waves with explicit rollback points. +3. Hard quality gates (`lint`, `test`, `build`, `coverage`, security checks) in every wave. +4. Documentation and requirement registry updates in the same wave as code changes. +5. Generic reusable adapters must be packaged as distributable npm libraries. + +## Scope Baseline + +Target monorepo components: + +- `apps/backend-template` +- `apps/service-management` +- `packages/cli-init` +- `packages/message-mediator` +- `packages/sdk-rest-client` +- `packages/sdk-websocket-client` +- `packages/sdk-grpc-client` +- shared config/tooling packages (ts/eslint/jest/contracts) as needed + +Planning guardrails and migration inventory reference: + +- `documentation/md/JUMENTIX-MIGRATION-INVENTORY-AND-ROLLBACK.md` + +## Current Implementation Snapshot + +Implemented in repository: + +- `pnpm-workspace.yaml` added. +- root `package.json` includes `packageManager` and `mono:*` recursive scripts. +- root `.npmrc` includes workspace linking and shared lockfile settings for monorepo consistency. +- initial workspace scaffolding created: + - `apps/backend-template` + - `apps/service-management` + - `packages/cli-init` + - `packages/message-mediator` + - `packages/sdk-rest-client` + - `packages/sdk-websocket-client` + - `packages/sdk-grpc-client` +- Wave 3 startup in progress: + - `packages/sdk-rest-client`, `packages/sdk-websocket-client`, and `packages/sdk-grpc-client` now have package-level source files and TypeScript build/typecheck scripts. + - SDK packages now include package-level READMEs with usage examples. +- Wave 4 startup in progress: + - `packages/cli-init` now exposes executable bin entrypoints and owns the bootstrap implementation used by root CLI wrapper. + - CLI package now includes package-level README with command contract. +- Wave 6 startup in progress: + - added affected-workspace detector (`npm run ci:affected`) to classify file deltas by `root`, `apps/*`, `packages/*`, and docs-only scope as a base primitive for selective monorepo CI execution. + - added release dry-run scripts (`npm run release:dry-run`, `release:dry-run:packages`, `release:dry-run:apps`) to verify package artifact readiness and app workspace build/test script contracts. + - added monorepo CI runner (`npm run ci:monorepo`) that executes lightweight docs-only validation or strict gate + affected app/package commands depending on changed scope. + - CI pipelines aligned to monorepo flow: GitHub Actions now installs with pnpm and runs scope-aware `ci:monorepo`; CircleCI now installs pnpm and executes `ci:monorepo`. +- reusable package extraction in progress: + - `packages/message-mediator` (with local bridge exports in backend code) + - `packages/key-value-storage` (with local bridge exports in backend code) + - `packages/persistence-contracts` (shared `IStore` and generic `IDatabaseClient`) + - `packages/mutex-service` (with local bridge exports in backend code) + - `packages/external-persistence-core` (base class + connection options for external DB adapters) + - `packages/external-store-proxy` (external DB `IStore` proxy + factory with local bridge export) + - `packages/external-db-repositories` (concrete external DB connectors with local bridge exports) + - `packages/database-client-factory` (driver-based DB client compiler with local bridge export) + - `packages/runtime-infra` (shared env-based runtime infra compilation for adapter bootstraps) + - `packages/adapter-runtime-bootstrap` (shared auth/runtime composition bootstrap for adapters) + - adapter bootstrap migration expanded to include new HTTP frameworks and serverless HTTP adapters + - `start-rest-api` loader expanded to support framework matrix via `AAA_HTTP_FRAMEWORK` and script coupling reduced + +Pending validation note: + +- Full pnpm recursive execution is currently blocked in this environment due registry network resolution (`ENOTFOUND`) during `pnpm install` bootstrap. +- Local npm validation in this environment currently shows package-manager instability (`npm ci` exit-handler crash and cache permission drift), so final recursive verification must run in CI/clean machine with Node `22.23.1`. + +## Migration Milestones + +### Milestone 1 - Workspace Foundation + +Deliverables: + +- `pnpm-workspace.yaml` +- root `package.json` workspace scripts +- Node 22 enforced at workspace root and CI +- base shared configs (ts/eslint/jest) published internally in workspace + +Exit Criteria: + +- `pnpm -r lint`, `pnpm -r test`, and `pnpm -r build` pass. +- CI runs workspace commands successfully. + +### Milestone 2 - Message Mediator Extraction + +Deliverables: + +- `packages/message-mediator` containing contracts/ports/adapters +- backend app imports mediator package via workspace dependency + +Exit Criteria: + +- mediator package unit tests pass +- backend tests pass after import rewrites + +Current status: + +- Extracted and bridged (`apps/backend-template/src/modules/port/*` + `apps/backend-template/src/infra/messages/*` now re-export package contracts/adapters). +- Runtime compile helper migration completed with compatibility bridge. + +### Milestone 3 - SDK Split + +Deliverables: + +- protocol SDKs as independent packages +- package-level docs and examples + +Exit Criteria: + +- all SDK packages build and test independently +- contract behavior validated by automated tests + +### Milestone 4 - CLI Productization + +Deliverables: + +- CLI moved to `packages/cli-init` +- bootstrap orchestration for backend/frontend/hybrid project generation + +Exit Criteria: + +- bootstrap smoke test passes end-to-end +- installation and usage docs are complete + +### Milestone 5 - App Re-homing + +Deliverables: + +- backend moved to `apps/backend-template` +- service management moved to `apps/service-management` + +Exit Criteria: + +- both apps run under workspace scripts +- PM2 profiles remain functional + +Current status: + +- `apps/service-management` is already re-homed. +- `apps/backend-template` now owns operational workspace scripts (build/test/integration/CI/PM2) mapped to root runtime as migration bridge. +- Pending: physical move of backend runtime directories/files into `apps/backend-template`. + +Execution guide: + +- `documentation/md/JUMENTIX-WAVE5-APP-REHOMING-CUTOVER.md` + +### Milestone 6 - CI/CD and Release Hardening + +Deliverables: + +- workspace-aware CI matrix +- release strategy (independent/locked versions) implemented +- changelog flow defined for product and packages + +Exit Criteria: + +- PR checks are green in monorepo mode +- release dry-run validated + +## Delivery Rhythm (Suggested) + +- Sprint P0: Milestone 1 + pre-work for Milestone 2 +- Sprint P1: Milestones 2 and 3 +- Sprint P2: Milestones 4 and 5 +- Sprint P3: Milestone 6 + stabilization + +## Risk Controls + +1. Import-break risk: + - Use codemod-assisted import rewrites and compile checks in each PR. +2. CI instability: + - Enable workspace matrix incrementally and enforce pass before next wave. +3. Oversized PR risk: + - Limit PR scope to one wave or one package extraction unit. +4. Naming/packaging ambiguity: + - Confirm npm package naming and distribution model before CLI publication. + +## Phase 0/1 readiness status + +- Scope split defined for Wave 1 (must-have) vs Wave 2+ (enhancements). +- Naming decisions locked (`JumentiX`, `@jumentix/cli-init` as official install target, `jumentix-init` as runtime command). +- Branch/tag/rollback procedure documented with pre/post-wave checkpoints. +- Current -> target inventory mapping documented with blocker notes. + +## Definition of Ready (per wave) + +- Wave scope documented. +- Required files list defined. +- Test and rollback strategy documented. +- Acceptance criteria agreed. + +## Definition of Done (per wave) + +- Code merged with all required checks green. +- Documentation updated. +- Agents requirement and project todo synced. +- No unresolved blocker for next wave. diff --git a/documentation/md/JUMENTIX-PROJECT-GOVERNANCE.md b/documentation/md/JUMENTIX-PROJECT-GOVERNANCE.md new file mode 100644 index 00000000..0590512d --- /dev/null +++ b/documentation/md/JUMENTIX-PROJECT-GOVERNANCE.md @@ -0,0 +1,57 @@ +# Jumentix Project Governance + +This project uses GitHub Project **Jumentix** (`https://github.com/users/web2solutions/projects/1`) as the single source of truth for execution tracking. + +## Single Source of Truth Rules + +1. Every bug, feature, refactor, and technical task must exist as a GitHub Issue. +2. Every tracked issue must be added to the `Jumentix` project. +3. Project fields are mandatory for active items: + - `Status` + - `Priority` + - `Size` + - `Estimate` + - `Start date` + - `End date` +4. No work starts without a linked issue and project item. +5. Task progress updates must happen in the project item status, not only in local notes. + +## PR Governance + +Each PR must include: + +- Related issue link(s) +- Related project item context (Project: `Jumentix`) +- Acceptance criteria and validation evidence +- Coverage and quality-gate evidence + +If a PR is not linked to project work items, it is out of process. + +### Priority-based PR grouping (mandatory) + +PRs must be created by priority group: + +1. `P0` tasks in dedicated PR(s) containing only `P0` items. +2. `P1` tasks in dedicated PR(s) containing only `P1` items. +3. `P2` tasks in dedicated PR(s) containing only `P2` items. + +Mixing `P0`, `P1`, and `P2` work in the same PR is not allowed. + +## Backlog and Delivery Flow + +1. Create/triage issue. +2. Add issue to `Jumentix` project. +3. Set field values and cycle dates. +4. Implement with PR linked to issue/project. +5. Move project status (`Backlog` -> `Ready` -> `In progress` -> `In review` -> `Done`). + +## Cycle and Estimation Policy + +1. Cycle window standard: 14 days (`Start date` / `End date` in project fields). +2. Every active task must have: + - `Priority` + - `Estimate` (story points) + - cycle dates +3. Maximum story points per task item: **8**. +4. Any item above 8 points must be split into subtasks. +5. Subtasks inherit parent priority and cycle. diff --git a/documentation/md/JUMENTIX-RELEASE-AND-VERSIONING-STRATEGY.md b/documentation/md/JUMENTIX-RELEASE-AND-VERSIONING-STRATEGY.md new file mode 100644 index 00000000..a082a835 --- /dev/null +++ b/documentation/md/JUMENTIX-RELEASE-AND-VERSIONING-STRATEGY.md @@ -0,0 +1,48 @@ +# JumentiX Release and Versioning Strategy + +## Policy + +JumentiX uses a hybrid strategy: + +- `packages/*`: **independent versioning** +- `apps/*`: **locked versioning** tied to root project version + +Canonical policy source: + +- `release-policy.json` + +Current values: + +- `packageVersioning`: `independent` +- `appVersioning`: `locked` +- `appLockedVersion`: must match root `package.json` version + +## Governance Enforcement + +The CI gate enforces release policy through: + +- `npm run release:governance:check` +- included in `npm run ci:gate` + +Validation includes: + +- required release scripts exist in root (`changelog:*`, `release:dry-run*`) +- `release-policy.json` exists and is valid +- publishable packages have valid semver and `files` metadata +- app workspaces are private and their version equals `appLockedVersion` + +## Operational Workflow + +1. Update root version intentionally. +2. Update `release-policy.json` `appLockedVersion` to same value. +3. Keep app workspace versions synchronized with locked value. +4. Run: + - `npm run release:governance:check` + - `npm run release:dry-run` +5. Open PR with traceability to related Jumentix project issue(s). + +## Future Evolution + +If release orchestration moves to Changesets, this document and +`release-policy.json` remain the policy source; automation can be swapped without +changing governance intent. diff --git a/documentation/md/JUMENTIX-SERVICE-FACTORY-CAPABILITIES-MATRIX.md b/documentation/md/JUMENTIX-SERVICE-FACTORY-CAPABILITIES-MATRIX.md new file mode 100644 index 00000000..82b09de6 --- /dev/null +++ b/documentation/md/JUMENTIX-SERVICE-FACTORY-CAPABILITIES-MATRIX.md @@ -0,0 +1,37 @@ +# JumentiX Service Factory Capabilities Matrix + +## Objective + +Define the supported software factory modes for JumentiX so engineering and product can choose a delivery shape with predictable architecture and operations. + +## Capability Matrix + +| Factory Mode | Primary Output | Supported Interfaces | Communication Pattern | Persistence Strategy | Typical Use | +|---|---|---|---|---|---| +| Modular Monolith (Backend) | Single backend service with multiple domains | REST, WebSocket + REST fallback, gRPC + REST fallback, Functions | In-process MessageMediator request/response + pub/sub | In-memory, SQL, NoSQL via env driver selection | Early-stage products and teams optimizing speed with future decoupling path | +| Multi-service Backend Group | Multiple backend services in one workspace | REST, WebSocket, gRPC, Functions | Contract-based mediator and event contracts per service boundary | Per-service database adapter selection | Domain isolation and independent scaling by bounded context | +| Hybrid Backend + Frontend | Backend services plus SPA/PWA/SSR apps | REST + realtime contracts consumed by SDK clients | API contracts (OpenAPI/AsyncAPI) and event-first integration | Backend adapter plus frontend local/offline storage strategy | End-to-end product delivery from one monorepo | +| Frontend-only SPA/PWA Offline | Frontend application package with API contract compatibility | Local app + optional remote API consumption | Contract-first client SDK integration | IndexedDB/local storage for offline-first flows | Offline-capable field and operations applications | + +## Non-functional Guarantees + +- DDD + Hexagonal boundaries are mandatory for backend modules. +- Event-driven integration is prioritized to avoid tight coupling and circular dependencies. +- Every generated service must keep CI, coverage, contract checks, and architecture checks enabled by default. +- Runtime startup must be environment-driven and PM2-orchestrated for VM contexts. + +## Required Contracts per Factory Mode + +| Contract | Modular Monolith | Multi-service Backend | Hybrid | Frontend-only | +|---|---:|---:|---:|---:| +| OpenAPI 3.1 endpoint contracts | required | required | required (backend side) | optional (consumer side) | +| AsyncAPI realtime contracts | required when realtime enabled | required when realtime enabled | required when realtime enabled | optional | +| Message/event map documentation | required | required | required | optional | +| Error contract map | required | required | required | recommended | +| Data entity/model docs | required | required | required | optional | + +## Acceptance Criteria + +- Service profile selection maps to one of the factory modes above. +- Generated scaffolds include required contract files and default quality scripts. +- Documentation index references this matrix as the canonical product capability source. diff --git a/documentation/md/JUMENTIX-WAVE5-APP-REHOMING-CUTOVER.md b/documentation/md/JUMENTIX-WAVE5-APP-REHOMING-CUTOVER.md new file mode 100644 index 00000000..fc393f77 --- /dev/null +++ b/documentation/md/JUMENTIX-WAVE5-APP-REHOMING-CUTOVER.md @@ -0,0 +1,130 @@ +# JumentiX Wave 5 App Re-homing Cutover + +This document is the executable cutover guide for moving runtime apps into workspace app boundaries. + +## Scope + +- Move backend runtime from repository root to `apps/backend-template`. +- Move Service Management app from `/service-management` to `apps/service-management`. +- Preserve current behavior, PM2 profiles, CI gates, and coverage policy. + +## Preconditions + +1. Wave 3 and Wave 4 code is merged (SDK/CLI ownership stabilized). +2. `@jumentix/*` shared packages compile and are imported by compatibility bridges. +3. Node runtime is `22.23.1` in CI and local validation environment. + +## Cutover sequence + +Path rewrite companion: + +- `documentation/md/JUMENTIX-WAVE5-PATH-DELTA-MAP.md` + +### Step 1 - Backend app file move + +Move these root directories into `apps/backend-template`: + +- `apps/backend-template/src/` +- `spec/` +- `apps/backend-template/test/` +- `pm2/` +- `seed/` +- `serverless.ts` +- `docker-compose*.yml` + +Status: + +- Completed on July 2, 2026. +- Physical move completed for runtime directories: + - `apps/backend-template/src/` -> `apps/backend-template/src/` + - `apps/backend-template/test/` -> `apps/backend-template/test/` + - `docker/` + `docker-compose*.yml` -> `apps/backend-template/*` + - `OASdoc/` + `AsyncAPIdoc/` -> `apps/backend-template/*` +- PM2 ecosystem ownership now lives at root `pm2/*`. +- Wave closeout evidence: + - PR #112: `https://github.com/web2solutions/aaa-typescript-boilerplate/pull/112` + - Normalization issue: `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/113` + - CI stabilization issue: `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/114` + - Serverless path issue: `https://github.com/web2solutions/aaa-typescript-boilerplate/issues/115` +- Residual tasks moved to dedicated follow-up: + - Governance closure and evidence mapping (`#116`) + - Wave 6 CI/release hardening subtasks (`#94`, `#98`, `#99`) + +Keep shared root assets in root: + +- `.agents/` +- `documentation/` +- `packages/` +- `tooling/` +- workspace-level config files (`pnpm-workspace.yaml`, root `package.json`, root lockfile). + +### Step 2 - Service Management move + +Move: + +- `/service-management/*` (legacy) -> `apps/service-management/*` + +Update all runtime references: + +- package scripts +- PM2 app paths +- docs links + +### Step 3 - Import path and config rewrite + +1. Rewrite absolute aliases and tsconfig paths where paths still assume root runtime. +2. Ensure test rootDir/moduleNameMapper paths resolve from app workspace. +3. Keep backward compatibility wrappers where necessary for one wave. + +### Step 4 - PM2 and runtime validation + +Validate all profiles after path migration: + +- dev +- staging +- production + +Required process checks: + +- RESTAPI +- websocketAPI + RESTAPI +- grpcAPI + RESTAPI +- service-management app process + +### Step 5 - CI gate validation + +Minimum required green checks: + +- lint +- unit tests +- selected integration smoke +- OAS route resolution +- build +- coverage threshold gate +- security smoke + +### Step 6 - Bridge cleanup decision + +After successful cutover: + +1. keep compatibility wrappers for one stabilization wave, or +2. remove wrappers in dedicated cleanup PR. + +## Rollback plan + +If any step fails: + +1. Revert only the failing wave PR scope. +2. Restore original PM2 paths. +3. Re-run `ci:gate` and coverage checks. +4. Resume with narrower move batches (backend then service-management separately). + +## Acceptance criteria + +1. `apps/backend-template` runs API runtime with unchanged external behavior. +2. `apps/service-management` runs unchanged UI/server behavior. +3. PM2 profiles run successfully with new app paths. +4. CI gates and coverage thresholds remain green. +5. README/docs/agents reflect final app locations. + +Status: satisfied for Wave 5 baseline; residual items tracked in follow-up issues. diff --git a/documentation/md/JUMENTIX-WAVE5-GOVERNANCE-EVIDENCE-2026-07-02.md b/documentation/md/JUMENTIX-WAVE5-GOVERNANCE-EVIDENCE-2026-07-02.md new file mode 100644 index 00000000..9bac5996 --- /dev/null +++ b/documentation/md/JUMENTIX-WAVE5-GOVERNANCE-EVIDENCE-2026-07-02.md @@ -0,0 +1,57 @@ +# Jumentix Wave 5 Governance Evidence (2026-07-02) + +This snapshot records governance evidence for monorepo Wave 5 closeout tasks tracked in GitHub Project **Jumentix**. + +## Tracked Issues + +- #113 — Normalize remaining legacy paths after app re-homing +- #114 — Stabilize CI gate and test entrypoints after migration +- #115 — Fix Serverless/Lambda handler path mapping to `restapi` +- #116 — Close Wave 5 governance status and evidence mapping + +## Execution Evidence + +### CI and test gate stabilization (#114) + +Validated locally: + +```bash +npm run ci:gate +``` + +Result: **pass** (lint + architecture checks + unit + OAS route resolution + build + smoke). + +### Serverless handler mapping validation (#115) + +Added explicit validation command: + +```bash +npm run serverless:check-handlers +``` + +Result: **pass** (`34` handlers resolved to existing files). + +`ci:gate` now runs `serverless:check-handlers` before build. + +### Path normalization updates (#113) + +Updated key runtime docs/examples to post-migration app-owned paths: + +- `README.md` +- `documentation/md/CI-TROUBLESHOOTING.md` +- `documentation/md/SETUP-RUNTIME-AND-API.md` +- `documentation/md/DATABASE-DRIVERS-SMOKE-TESTS.md` +- `documentation/md/RUNTIME-ENVIRONMENT-CONTRACTS.md` +- `documentation/md/EVENTS-AND-MESSAGES-MAP.md` +- `documentation/md/HEXAGONAL-FEATURE-DRIVEN-MIGRATION.md` + +## GitHub Project Status Snapshot + +- #114: **Done** +- #115: **Done** +- #113: **In progress** +- #116: **In progress** + +## Constraints / Follow-up + +- `.agents/*` is read-only in the current execution environment, so agent-file alignment for this wave must be completed in a writable follow-up pass. diff --git a/documentation/md/JUMENTIX-WAVE5-PATH-DELTA-MAP.md b/documentation/md/JUMENTIX-WAVE5-PATH-DELTA-MAP.md new file mode 100644 index 00000000..4bcfe4e9 --- /dev/null +++ b/documentation/md/JUMENTIX-WAVE5-PATH-DELTA-MAP.md @@ -0,0 +1,66 @@ +# JumentiX Wave 5 Path Delta Map + +This map tracks runtime path rewrites for the backend-template and service-management re-homing wave. + +## Post-cutover mapping table + +| Legacy path | Current path | +| --- | --- | +| `src/` | `apps/backend-template/src/` | +| `test/` | `apps/backend-template/test/` | +| `seed/` | `apps/backend-template/seed/` | +| `OASdoc/` | `apps/backend-template/OASdoc/` | +| `AsyncAPIdoc/` | `apps/backend-template/AsyncAPIdoc/` | +| `docker/` | `apps/backend-template/docker/` | +| `docker-compose-*.yml` | `apps/backend-template/docker-compose-*.yml` | +| `service-management/` | `apps/service-management/` | +| `apps/backend-template/pm2/` | `pm2/` (root ownership) | + +## Legacy path anchors (pre-cutover) + +### Backend runtime anchors + +- `./src/interface/HTTP/adapters/start-rest-api.ts` +- `./src/interface/WebSocket/adapters/start-websocket-api.ts` +- `./src/interface/gRPC/adapters/start-grpc-api.ts` +- production compiled equivalents under `./.build/interface/...` +- seed data under `./seed/*` + +### Service Management anchor + +- `./service-management/server.js` + +## Current anchors (post-cutover) + +### Backend template app + +- `./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` +- `./apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` +- `./apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts` +- production compiled equivalents under `./.build/apps/backend-template/src/interface/...` +- seed data under `./apps/backend-template/seed/*` + +### Service Management app + +- `./apps/service-management/server.js` + +## Rewritten ownership files + +1. `pm2/ecosystem.dev.cjs` +2. `pm2/ecosystem.staging.cjs` +3. `pm2/ecosystem.production.cjs` +4. root `package.json` scripts containing: + - runtime starters + - build/test commands pointing to `apps/backend-template/*` + - docker compose references under `apps/backend-template/` +5. docs and onboarding references: + - `README.md` + - `documentation/md/SETUP-RUNTIME-AND-API.md` + - `documentation/md/SERVICE-MANAGEMENT-APPLICATION.md` + +## Validation checklist + +- `npm run oas:check-routes` green with `apps/backend-template/src` resolution. +- `npm run test:unit` green using `apps/backend-template/test/unit`. +- PM2 dev/staging/prod startup scripts reference `pm2/*` ecosystems. +- Service Management app still starts from `apps/service-management/server.js`. diff --git a/documentation/md/JUMENTIX-WORKSPACE-PACKAGES.md b/documentation/md/JUMENTIX-WORKSPACE-PACKAGES.md new file mode 100644 index 00000000..7ba71d92 --- /dev/null +++ b/documentation/md/JUMENTIX-WORKSPACE-PACKAGES.md @@ -0,0 +1,36 @@ +# JumentiX Workspace Packages + +This document tracks the current package map for monorepo migration waves. + +## Runtime and Architecture Packages + +- `@jumentix/message-mediator` +- `@jumentix/key-value-storage` +- `@jumentix/persistence-contracts` +- `@jumentix/mutex-service` +- `@jumentix/external-persistence-core` +- `@jumentix/external-store-proxy` +- `@jumentix/external-db-repositories` +- `@jumentix/database-client-factory` +- `@jumentix/runtime-infra` +- `@jumentix/adapter-runtime-bootstrap` + +## Developer/Product Packages + +- `@jumentix/cli-init` +- `@jumentix/sdk-rest-client` +- `@jumentix/sdk-websocket-client` +- `@jumentix/sdk-grpc-client` + +## App Workspaces + +- `@jumentix/backend-template` (placeholder during migration) +- `@jumentix/service-management` (placeholder during migration) + +## Notes + +- SDK packages now have package-level source and build scripts. +- Legacy `sdk-clients/` remains as backward-compatible bridge that re-exports from `packages/sdk-*`. +- Compatibility bridge contract is documented in `documentation/md/SDK-COMPATIBILITY-BRIDGE.md`. +- CLI package now owns canonical bootstrap implementation. +- Root CLI entrypoint remains as compatibility wrapper. diff --git a/documentation/md/PCI-REMEDIATION-PLAN-AND-EVIDENCE.md b/documentation/md/PCI-REMEDIATION-PLAN-AND-EVIDENCE.md index cf5ef833..fc893570 100644 --- a/documentation/md/PCI-REMEDIATION-PLAN-AND-EVIDENCE.md +++ b/documentation/md/PCI-REMEDIATION-PLAN-AND-EVIDENCE.md @@ -10,30 +10,30 @@ This document defines the remediation plan in sprint order and maps technical ev - Enforce organization scope for tenant-bound operations. - Enforce cross-organization deny paths. - Evidence: - - `src/modules/Users/adapters/in/http/controllers/UserController.ts` - - `test/unit/modules/Users/interface/controller/controllers.test.ts` + - `apps/backend-template/src/modules/Users/adapters/in/http/controllers/UserController.ts` + - `apps/backend-template/test/unit/modules/Users/interface/controller/controllers.test.ts` 2. Authentication risk controls - Login lockout windows and failed-attempt tracking. - Token revocation behavior on logout. - Evidence: - - `src/modules/Users/service/AuthService.ts` - - `test/unit/modules/Users/service/AuthService.branches.test.ts` + - `apps/backend-template/src/modules/Users/service/AuthService.ts` + - `apps/backend-template/test/unit/modules/Users/service/AuthService.branches.test.ts` 3. Internal error exposure policy by environment - `dev/staging`: include internal details for debugging. - `production`: mask internals. - Evidence: - - `src/shared/utils.ts` - - `src/interface/HTTP/adapters/*/responses/sendErrorResponse.ts` + - `apps/backend-template/src/shared/utils.ts` + - `apps/backend-template/src/interface/HTTP/adapters/*/responses/sendErrorResponse.ts` 4. Transport security baseline - CORS allowlist strategy via environment. - Security headers middleware enabled where adapter supports it. - Evidence: - - `src/config/security.ts` - - `src/interface/HTTP/adapters/express/ExpressServer.ts` - - `src/interface/HTTP/adapters/fastify/FastifyServer.ts` + - `apps/backend-template/src/config/security.ts` + - `apps/backend-template/src/interface/HTTP/adapters/express/ExpressServer.ts` + - `apps/backend-template/src/interface/HTTP/adapters/fastify/FastifyServer.ts` ### P1 - Stability and compatibility hardening @@ -41,15 +41,15 @@ This document defines the remediation plan in sprint order and maps technical ev - Controlled Basic auth enablement. - Bearer-first behavior and production credential masking. - Evidence: - - `src/config/.env.dev` - - `src/config/.env.staging` - - `src/config/.env.ci` + - `apps/backend-template/src/config/.env.dev` + - `apps/backend-template/src/config/.env.staging` + - `apps/backend-template/src/config/.env.ci` - `ci-cd/loadEnvironment.js` 2. JWT contract hardening - Issuer/audience support and token-id usage for revocation path. - Evidence: - - `src/infra/jwt/JwtService.ts` + - `apps/backend-template/src/infra/jwt/JwtService.ts` - auth service unit coverage. 3. Quality-gate proof @@ -63,11 +63,11 @@ This document defines the remediation plan in sprint order and maps technical ev - Persist audit events to dedicated adapter with retention policy. - Status: implemented with in-memory official adapter and auth/authorization wiring. - Evidence: - - `src/infra/audit/InMemorySecurityAuditRepository.ts` - - `src/infra/audit/ISecurityAuditRepository.ts` - - `src/modules/Users/service/AuthService.ts` - - `test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts` - - `test/unit/modules/Users/service/AuthService.audit.test.ts` + - `apps/backend-template/src/infra/audit/InMemorySecurityAuditRepository.ts` + - `apps/backend-template/src/infra/audit/ISecurityAuditRepository.ts` + - `apps/backend-template/src/modules/Users/service/AuthService.ts` + - `apps/backend-template/test/unit/infra/audit/InMemorySecurityAuditRepository.test.ts` + - `apps/backend-template/test/unit/modules/Users/service/AuthService.audit.test.ts` 2. Production security runbooks - Key rotation, incident response, and audit export procedure. - Status: documented. diff --git a/documentation/md/PROJECT-MANAGEMENT.md b/documentation/md/PROJECT-MANAGEMENT.md index a7f2458e..87e51efb 100644 --- a/documentation/md/PROJECT-MANAGEMENT.md +++ b/documentation/md/PROJECT-MANAGEMENT.md @@ -10,5 +10,10 @@ - `.agents/README.md` - Project todos and delivery status: - `.agents/project-todos.md` +- GitHub issue tracking for migrated TODOs: + - +- Official project management source of truth: + - `documentation/md/JUMENTIX-PROJECT-GOVERNANCE.md` + - `https://github.com/users/web2solutions/projects/1` - Domain Designer MVP roadmap: - `documentation/md/DOMAIN-DESIGNER-MVP-ROADMAP.md` diff --git a/documentation/md/REALTIME-API-TESTING.md b/documentation/md/REALTIME-API-TESTING.md new file mode 100644 index 00000000..9ea7673b --- /dev/null +++ b/documentation/md/REALTIME-API-TESTING.md @@ -0,0 +1,96 @@ +# Realtime API Testing Guide + +This document defines the official test matrix for realtime interfaces (`WebSocketAPI` and `gRPCAPI`). + +## Scope + +Covered interfaces: + +- `apps/backend-template/src/interface/WebSocket/WebSocketAPI.ts` +- `apps/backend-template/src/interface/gRPC/gRPCAPI.ts` +- Socket.IO horizontal adapters: + - `cluster` + - `redis-streams` + +## Test Matrix + +## 1) Unit Tests + +WebSocket: + +- `apps/backend-template/test/unit/interface/WebSocket/WebSocketAPI.test.ts` +- `apps/backend-template/test/unit/interface/WebSocket/adapters/start-websocket-api.test.ts` +- `apps/backend-template/test/unit/interface/WebSocket/adapters/socket-io.bootstrap.test.ts` +- `apps/backend-template/test/unit/interface/WebSocket/clusterAdapter.test.ts` +- `apps/backend-template/test/unit/interface/WebSocket/redisStreamsAdapter.test.ts` + +gRPC: + +- `apps/backend-template/test/unit/interface/gRPC/gRPCAPI.test.ts` +- `apps/backend-template/test/unit/interface/gRPC/adapters/start-grpc-api.test.ts` +- `apps/backend-template/test/unit/interface/gRPC/adapters/grpc.bootstrap.test.ts` + +Shared realtime core: + +- `apps/backend-template/test/unit/interface/Async/RealtimeAPIBase.test.ts` + +Run: + +```bash +npm run test:unit +``` + +## 2) Integration Tests + +Protocol-level integration: + +- `apps/backend-template/test/integration/realtime/websocket.basic.integration.test.ts` +- `apps/backend-template/test/integration/realtime/grpc.basic.integration.test.ts` + +Redis multi-instance integration: + +- `apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts` + +Run basic realtime integrations: + +```bash +npm run test:integration:realtime +``` + +Run Redis multi-instance integration (requires Redis): + +```bash +npm run test:integration:realtime:redis-streams +``` + +## 3) Smoke Tests + +Local realtime smoke: + +- `apps/backend-template/test/smoke/realtime/RealtimeApis.smoke.test.ts` + +Run: + +```bash +npm run test:smoke:realtime +``` + +End-to-end Redis smoke (Docker + multi-instance + cleanup): + +```bash +npm run smoke:realtime:redis-streams +``` + +## CI Behavior + +- Realtime basic integration and smoke tests are runnable without external Redis. +- Redis-backed multi-instance test is gated by `RUN_REDIS_INTEGRATION=1`. +- Jest ignore policy excludes only `socketio.redis-streams.multi-instance.test.ts` when Redis integration is disabled. + +## Operational Notes + +1. Keep realtime tests `--coverage=false` for smoke/integration scripts. +2. Maintain deterministic fixed ports for isolated test files. +3. Always close socket clients and stop server instances in `afterAll`. +4. Update this file and `.agents` requirements whenever realtime transport behavior changes. + diff --git a/documentation/md/RUNTIME-ENVIRONMENT-CONTRACTS.md b/documentation/md/RUNTIME-ENVIRONMENT-CONTRACTS.md index 5d17a5cf..f3da32d9 100644 --- a/documentation/md/RUNTIME-ENVIRONMENT-CONTRACTS.md +++ b/documentation/md/RUNTIME-ENVIRONMENT-CONTRACTS.md @@ -14,26 +14,38 @@ All runtime startup entrypoints must follow this contract. ## Runtime Keys -The following keys are mandatory across env files in `src/config/`: +The following keys are mandatory across env files in `apps/backend-template/src/config/`: - `AAA_HTTP_FRAMEWORK` - default: `express` - - current supported values: `express` - - used by: `src/interface/HTTP/adapters/start-rest-api.ts` + - current supported values: + - `express` + - `fastify` + - `restify` + - `hyper-express` + - `cloudflare-workers` + - `vercel-functions` + - `loopback` + - `sails-js` + - `feathers` + - `derby-js` + - `adonis-js` + - `total-js` + - used by: `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` - `AAA_REALTIME_API` - default: `no` - supported values: `yes`, `no` - used by: - - `src/interface/WebSocket/adapters/start-websocket-api.ts` - - `src/interface/gRPC/adapters/start-grpc-api.ts` + - `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` + - `apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts` - `AAA_REALTIME_API_PROTOCOL` - default: `websocket` - supported values: `websocket`, `grpc` - used by: - - `src/interface/WebSocket/adapters/start-websocket-api.ts` - - `src/interface/gRPC/adapters/start-grpc-api.ts` + - `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` + - `apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts` - `AAA_REALTIME_API_DATABASE_DRIVER` - default: `Mongo` @@ -47,16 +59,37 @@ The following keys are mandatory across env files in `src/config/`: - `Cassandra` - used by: runtime profile metadata and Service Management configuration workflows. +- `AAA_WEBSOCKET_SOCKETIO_ADAPTER` + - default: empty (in-memory Socket.IO adapter) + - supported values: `cluster`, `redis-streams` + - used by: `apps/backend-template/src/interface/WebSocket/adapters/socket-io/socket-io.ts` + +- `AAA_WEBSOCKET_CLUSTER_WORKERS` + - optional worker count for Socket.IO cluster mode. + - default: CPU core count. + - used by: `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` + +- `AAA_WEBSOCKET_REDIS_URL` + - optional dedicated Redis connection URL for Socket.IO scaling adapter. + - example: `redis://127.0.0.1:6379/1` + - used by: `apps/backend-template/src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter.ts` + +- `AAA_REDIS_URL` + - optional global Redis URL fallback used by WebSocket scaling adapter when + `AAA_WEBSOCKET_REDIS_URL` is not set. + - used by: `apps/backend-template/src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter.ts` + ## Startup Entrypoints - REST: - - `src/interface/HTTP/adapters/start-rest-api.ts` + - `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` - WebSocket: - - `src/interface/WebSocket/adapters/start-websocket-api.ts` + - `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` - gRPC: - - `src/interface/gRPC/adapters/start-grpc-api.ts` + - `apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts` These files are the official process entrypoints used by PM2 ecosystem profiles. +Single-protocol helper scripts also route through these entrypoints (`dev:http`, `prod:http`, `dev:websocket`, `dev:grpc`). ## PM2 Process Model @@ -85,6 +118,7 @@ The env editor mutates only approved keys from this contract, preserving guardra ## Guardrails - Unsupported `AAA_HTTP_FRAMEWORK` must fail fast. +- REST bootstrap must always resolve framework via `start-rest-api` loader. - Realtime boot must not start when: - `AAA_REALTIME_API` is not `yes`, or - `AAA_REALTIME_API_PROTOCOL` does not match the adapter entrypoint. @@ -105,6 +139,18 @@ WebSocket + REST: AAA_HTTP_FRAMEWORK=express AAA_REALTIME_API=yes AAA_REALTIME_API_PROTOCOL=websocket +AAA_WEBSOCKET_SOCKETIO_ADAPTER=cluster +AAA_WEBSOCKET_CLUSTER_WORKERS=4 +``` + +WebSocket + REST (multi-host via Redis Streams): + +```bash +AAA_HTTP_FRAMEWORK=express +AAA_REALTIME_API=yes +AAA_REALTIME_API_PROTOCOL=websocket +AAA_WEBSOCKET_SOCKETIO_ADAPTER=redis-streams +AAA_WEBSOCKET_REDIS_URL=redis://127.0.0.1:6379/1 ``` gRPC + REST: diff --git a/documentation/md/SDK-COMPATIBILITY-BRIDGE.md b/documentation/md/SDK-COMPATIBILITY-BRIDGE.md new file mode 100644 index 00000000..5376a651 --- /dev/null +++ b/documentation/md/SDK-COMPATIBILITY-BRIDGE.md @@ -0,0 +1,30 @@ +# SDK Compatibility Bridge + +This project currently exposes two SDK access paths during monorepo migration: + +1. New canonical workspace packages: + - `@jumentix/sdk-rest-client` + - `@jumentix/sdk-websocket-client` + - `@jumentix/sdk-grpc-client` +2. Legacy compatibility path: + - `sdk-clients/*` + +## Why this bridge exists + +- Avoid breaking existing imports while migration is in progress. +- Keep Wave 3 (SDK package split) incremental and low-risk. +- Enable parallel adoption without a forced big-bang refactor. + +## Bridge contract + +- Legacy files under `sdk-clients/` must only re-export package implementations. +- New SDK features should be implemented only in `packages/sdk-*`. +- Documentation and examples should prioritize `@jumentix/sdk-*` imports. + +## Decommission criteria + +The compatibility bridge can be removed when: + +1. all internal imports are migrated to `@jumentix/sdk-*`, +2. external consumers confirm no dependency on `sdk-clients/*`, +3. migration wave acceptance checklist marks SDK split as complete. diff --git a/documentation/md/SERVICE-MANAGEMENT-APPLICATION.md b/documentation/md/SERVICE-MANAGEMENT-APPLICATION.md index d19b5ff0..7c895df0 100644 --- a/documentation/md/SERVICE-MANAGEMENT-APPLICATION.md +++ b/documentation/md/SERVICE-MANAGEMENT-APPLICATION.md @@ -2,14 +2,33 @@ The previous `domaindesigner` static app was consolidated into: -- `servicemangement/` +- `apps/service-management/` It is now a tabbed suite for service lifecycle design. +Core implementation files: + +- `apps/service-management/index.html` +- `apps/service-management/script.js` +- `apps/service-management/styles.css` +- `apps/service-management/server.js` + ## Tabs 1. **Domain Designer** - - Existing ER modeling MVP (domains/entities/relationships, OpenAPI export/import, model checks). + - Full ER modeling MVP including: + - domain/entity lifecycle and inspector + - relationship anchors, bend/path controls, routing style + - bounded-context metadata + - aggregate + invariants + - RBAC per entity/action + - message contracts (`event/command/request/response`) + - OpenAPI composition (`oneOf/allOf/anyOf`, external refs, discriminator) + - schema diff + migration hints + - request/response examples + - code skeleton preview + - export/import flows (JSON, OAS, Markdown, JSON Schema, AsyncAPI, package, boilerplate bundle) + - mini-map and large-canvas mode 2. **Communication Interface Designer** - Registers inbound interface adapters and controller mappings: - HTTP/REST @@ -30,27 +49,39 @@ It is now a tabbed suite for service lifecycle design. - `AAA_REALTIME_API_PROTOCOL` - `AAA_REALTIME_API_DATABASE_DRIVER` - Runtime env editor targets the selected environment file: - - `dev` -> `src/config/.env.dev` - - `staging` -> `src/config/.env.staging` - - `ci` -> `src/config/.env.ci` + - `dev` -> `apps/backend-template/src/config/.env.dev` + - `staging` -> `apps/backend-template/src/config/.env.staging` + - `ci` -> `apps/backend-template/src/config/.env.ci` 4. **Deploy Management** - Tracks deploy targets and runtime deployment metadata. +Detailed usage guide: + +- [Domain Designer Features and Usage](./DOMAIN-DESIGNER-FEATURES-AND-USAGE.md) + ## Run PM2-served: - `npm run dev:service-management` -- default dev profile (`npm run dev`) also starts `servicemangement` through PM2. +- default dev profile (`npm run dev`) also starts `service-management` through PM2. The app state persists with browser `localStorage`. +Recommended dev path: + +1. `npm run dev:service-management` +2. Open the local Service Management URL +3. Model domains/entities +4. Run exports (OAS/AsyncAPI/JSON Schema/package) +5. Use generated artifacts as contracts for API implementation + ## Runtime Env API (built-in) - `GET /api/runtime/env?environment=dev|staging|ci` - `POST /api/runtime/env` -The server persists approved runtime keys to files under `src/config/`. +The server persists approved runtime keys to files under `apps/backend-template/src/config/`. ## Runtime Edit Flow @@ -69,8 +100,22 @@ The server persists approved runtime keys to files under `src/config/`. Service-level container templates are provided in: -- `docker/services/` +- `apps/backend-template/docker/services/` Orchestrated profiles: -- `docker-compose-service-templates.yml` +- `apps/backend-template/docker-compose-service-templates.yml` + +## Tests + +Integration smoke: + +```bash +npm run test:integration:service-management +``` + +Unit smoke for roadmap feature presence: + +```bash +NODE_ENV=dev npx jest apps/backend-template/test/unit/service-management/mvp.roadmap.features.test.ts --runInBand +``` diff --git a/documentation/md/SETUP-RUNTIME-AND-API.md b/documentation/md/SETUP-RUNTIME-AND-API.md index 685a8e55..3e6deeae 100644 --- a/documentation/md/SETUP-RUNTIME-AND-API.md +++ b/documentation/md/SETUP-RUNTIME-AND-API.md @@ -93,9 +93,9 @@ Detailed runtime contract: Startup entrypoints used by PM2: -- `src/interface/HTTP/adapters/start-rest-api.ts` -- `src/interface/WebSocket/adapters/start-websocket-api.ts` -- `src/interface/gRPC/adapters/start-grpc-api.ts` +- `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` +- `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` +- `apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts` Behavior summary: @@ -103,7 +103,7 @@ Behavior summary: - `start-websocket-api.ts` starts only when `AAA_REALTIME_API=yes` and `AAA_REALTIME_API_PROTOCOL=websocket`. - `start-grpc-api.ts` starts only when `AAA_REALTIME_API=yes` and `AAA_REALTIME_API_PROTOCOL=grpc`. -Dev (auto-starts `servicemangement`): +Dev (auto-starts `service-management`): ```bash npm run pm2:start:dev:restapi @@ -172,7 +172,7 @@ Response payload example: ## Single Adapter Boot Commands (via PM2) -HTTP adapters: +HTTP adapters (all routed through `start-rest-api` loader with `AAA_HTTP_FRAMEWORK`): ```bash npm run dev:express @@ -189,11 +189,27 @@ npm run dev:adonis-js npm run dev:total-js ``` +Generic REST loader commands: + +```bash +# uses AAA_HTTP_FRAMEWORK from env file (default express) +npm run dev:http +npm run prod:http +``` + +Equivalent direct loader style: + +```bash +AAA_HTTP_FRAMEWORK=fastify pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-fastify --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env +AAA_HTTP_FRAMEWORK=cloudflare-workers pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-cloudflare-workers --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env +``` + Combined service profiles: ```bash npm run dev:websocket npm run dev:grpc +npm run test:integration:service-management ``` Serverless dev mode: @@ -218,7 +234,7 @@ npm run dev:service-management ## Production Commands -All `prod:*` adapter commands now boot through PM2. +All `prod:*` adapter commands now boot through PM2 and route through `start-rest-api` loader. Use `pm2:start:prod:*` profiles for separated REST/WebSocket/gRPC orchestration. ## Lambda Handlers Coverage diff --git a/documentation/md/TESTING-CI-AND-QUALITY.md b/documentation/md/TESTING-CI-AND-QUALITY.md index 0a43b19f..74fe44ca 100644 --- a/documentation/md/TESTING-CI-AND-QUALITY.md +++ b/documentation/md/TESTING-CI-AND-QUALITY.md @@ -20,12 +20,31 @@ Run integration tests: npm run test:integration ``` +Run realtime integration tests: + +```bash +npm run test:integration:realtime +``` + +Run Redis-backed multi-instance realtime integration: + +```bash +npm run test:integration:realtime:redis-streams +``` + Run database driver smoke tests: ```bash npm run test:smoke:db:all ``` +Run realtime smoke tests: + +```bash +npm run test:smoke:realtime +npm run smoke:realtime:redis-streams +``` + `test:smoke:db:all` orchestrates each driver-specific smoke command, including automatic `docker compose up/down` for container-backed databases. @@ -68,6 +87,8 @@ Included checks: - core import cycle check - hexagonal boundary check - users legacy import check +- workspace package quality check (required script contracts and no placeholder test scripts) +- release governance check (required release scripts, semver validity, publish metadata for non-private packages) - unit tests - OpenAPI route resolution check - build @@ -76,8 +97,9 @@ Included checks: Local enforcement: -- `.husky/pre-commit` runs `npm run lint && npm run test:unit` -- `.husky/pre-push` runs `npm run ci:gate` +- `.husky/pre-commit` runs `npm run changelog:update && git add CHANGELOG.md && npm run lint && npm run test:unit` +- `.husky/pre-push` runs `npm run ci:gate:strict` +- `post-commit` is mutation-free (no auto-amend, no bypass flags) - `.husky/commit-msg` runs commitlint (`@commitlint/config-conventional`) - `.husky/post-commit` updates `CHANGELOG.md` from Git history and auto-amends the commit when needed @@ -92,14 +114,15 @@ SonarQube Cloud coverage import: | Integration | Purpose | Where it is configured | What to run / requirements | |------------|---------|-------------------------|-----------------------------| -| CircleCI | Main pipeline for lint + tests + architecture checks + smoke + upload coverage | `.circleci/config.yml` | Runs automatically; local equivalent is `npm run ci:gate` | -| GitHub Actions (tests) | Secondary CI validation on push/PR | `.github/workflows/test.yml` | Uses Node `22.x`, starts Redis, runs `npm run ci:gate` | +| CircleCI | Main pipeline for lint + tests + architecture checks + smoke + upload coverage | `.circleci/config.yml` | Installs with `pnpm`, runs `npm run ci:monorepo` | +| GitHub Actions (tests) | Secondary CI validation on push/PR | `.github/workflows/test.yml` | Uses Node `22.x`, installs with `pnpm`, runs scope-aware `npm run ci:monorepo -- ` | | GitHub Actions (SonarQube Cloud) | Static analysis + quality gate + coverage import | `.github/workflows/sonarqube-cloud.yml`, `sonar-project.properties` | Requires `SONAR_TOKEN`; runs `npm run test:unit` first | | Codecov | Coverage status checks for project and patch | `codecov.yml` | Target is `95%` for project and patch | | Jest coverage gate | Local hard gate to prevent low-coverage merges | `jest.config.js` | Global thresholds: `lines/statements >= 95%`, `branches/functions >= 80%` | | Husky | Local Git hooks for quality checks | `.husky/*` | Installed by `npm run prepare` | | Commitlint + Commitizen | Conventional commits and guided commit flow | `commitlint.config.js`, `package.json` | `npm run commit` | | Changelog sync automation | Keeps `CHANGELOG.md` aligned with Git history | `ci-cd/update-changelog.js`, `.husky/post-commit` | `npm run changelog:update`, `npm run changelog:check` | +| Release governance check | Enforces release script contracts and package publish metadata | `ci-cd/check-release-governance.js` | `npm run release:governance:check` | | OpenAPI route resolution check | Ensures each operationId maps to handlers and controller methods | `ci-cd/check-oas-route-resolution.js` | `npm run oas:check-routes` | | Hexagonal boundary check | Blocks controller-layer violations | `ci-cd/check-hexagonal-boundaries.js` | `npm run arch:check-boundaries` | | Core import cycle check | Prevents cyclic dependencies in core namespaces | `ci-cd/check-core-import-cycles.js` | `npm run deps:check-cycles` | @@ -110,8 +133,8 @@ SonarQube Cloud coverage import: #### CircleCI - Pipeline file: `.circleci/config.yml` -- Uses `cimg/node:22.9` plus `redis:latest` -- Installs dependencies, waits for Redis, executes `npm run ci:gate`, uploads coverage with Codecov orb +- Uses `cimg/node:22.23` plus `redis:latest` +- Installs `pnpm@9.15.3`, runs `pnpm install --no-frozen-lockfile`, waits for Redis, executes `npm run ci:monorepo`, uploads coverage with Codecov orb - This is the primary all-in-one gate #### GitHub Actions - Test Workflow @@ -120,8 +143,8 @@ SonarQube Cloud coverage import: - Triggers on: - `push` to `main` and `dev` - `pull_request` to `main` -- Sets up Redis (with password), installs dependencies, runs `npm run ci:gate` -- Mirrors the same quality gate used locally and in CircleCI +- Sets up Redis (with password), installs `pnpm`, computes changed files against `main`, runs `npm run ci:monorepo -- ` +- Uses docs-only lightweight checks or strict gate + affected app/package flow depending on delta scope #### GitHub Actions - SonarQube Cloud Workflow @@ -132,16 +155,16 @@ SonarQube Cloud coverage import: - Installs dependencies, runs unit tests with coverage, then executes SonarQube scan action - Scanner reads `./coverage/lcov.info` as configured in `sonar-project.properties` -### Coverage Policy (95% Standard) +### Coverage Policy (Strict Standard) - Codecov enforces: - project coverage target: `95%` - patch coverage target: `95%` - Jest enforces local gate before merge: - - `lines >= 95%` - - `statements >= 95%` - - `branches >= 80%` - - `functions >= 80%` + - `lines >= 99%` + - `statements >= 99%` + - `branches >= 90%` + - `functions >= 99%` - Commits and PRs are expected to respect these thresholds before approval. ### Node 22 Runtime Enforcement @@ -194,6 +217,10 @@ Run targeted checks: npm run deps:check-cycles npm run arch:check-boundaries npm run arch:check-users-legacy-imports +npm run arch:check-workspace-boundaries +npm run workspace:check-quality +npm run workspace:check-coverage-policy +npm run release:governance:check npm run oas:check-routes npm run test:unit npm run ci:smoke @@ -204,3 +231,4 @@ npm run ci:smoke For CI incidents and failing checks, see: - [CI / SonarQube / Codecov Troubleshooting](./CI-TROUBLESHOOTING.md) +- [Realtime API Testing Guide](./REALTIME-API-TESTING.md) diff --git a/documentation/md/adapters/databases/AURORA.md b/documentation/md/adapters/databases/AURORA.md new file mode 100644 index 00000000..9f8d797f --- /dev/null +++ b/documentation/md/adapters/databases/AURORA.md @@ -0,0 +1,22 @@ +# Aurora Adapter + +## Technology + +Aurora integration path (SQL-compatible profile in external adapter layer). + +## Build Services with Aurora + +1. Set env: + +```bash +AAA_DATABASE_DRIVER=Aurora +AAA_DATABASE_CONNECTION_URL=postgres://user:pass@aurora-host:5432/aaa +``` + +2. Start service adapter. +3. Validate with smoke command: + +```bash +npm run smoke:db:aurora +``` + diff --git a/documentation/md/adapters/databases/CASSANDRA.md b/documentation/md/adapters/databases/CASSANDRA.md new file mode 100644 index 00000000..b50dc940 --- /dev/null +++ b/documentation/md/adapters/databases/CASSANDRA.md @@ -0,0 +1,25 @@ +# Cassandra Adapter + +## Technology + +Cassandra driver profile. + +## Build Services with Cassandra + +1. Start container: + +```bash +npm run docker:up:cassandra +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=Cassandra +AAA_CASSANDRA_CONTACT_POINTS=127.0.0.1 +AAA_CASSANDRA_KEYSPACE=aaa +AAA_CASSANDRA_PORT=9042 +``` + +3. Start service adapter. + diff --git a/documentation/md/adapters/databases/DYNAMODB.md b/documentation/md/adapters/databases/DYNAMODB.md new file mode 100644 index 00000000..2a775a54 --- /dev/null +++ b/documentation/md/adapters/databases/DYNAMODB.md @@ -0,0 +1,24 @@ +# DynamoDB Adapter + +## Technology + +AWS SDK DynamoDB client profile. + +## Build Services with DynamoDB + +1. Start local container: + +```bash +npm run docker:up:dynamodb +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=DynamoDB +AAA_DYNAMODB_ENDPOINT=http://127.0.0.1:8000 +AAA_AWS_REGION=us-east-1 +``` + +3. Start service adapter. + diff --git a/documentation/md/adapters/databases/FIREBASE.md b/documentation/md/adapters/databases/FIREBASE.md new file mode 100644 index 00000000..bc947df7 --- /dev/null +++ b/documentation/md/adapters/databases/FIREBASE.md @@ -0,0 +1,24 @@ +# Firebase Adapter + +## Technology + +Firebase Admin profile. + +## Build Services with Firebase + +1. Start local emulator container (if configured): + +```bash +npm run docker:up:firebase +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=Firebase +AAA_FIREBASE_PROJECT_ID=aaa-dev +AAA_FIREBASE_CREDENTIALS_JSON=./path/to/service-account.json +``` + +3. Start service adapter. + diff --git a/documentation/md/adapters/databases/INMEMORY.md b/documentation/md/adapters/databases/INMEMORY.md new file mode 100644 index 00000000..800bb70d --- /dev/null +++ b/documentation/md/adapters/databases/INMEMORY.md @@ -0,0 +1,22 @@ +# InMemory Database Adapter + +## Purpose + +Default official adapter for local development and deterministic tests. + +## Entrypoints + +- `apps/backend-template/src/infra/persistence/InMemoryDatabase/InMemoryDbClient.ts` +- `apps/backend-template/src/infra/persistence/compileDatabaseClient.ts` + +## Build Services with InMemory + +1. Set env: + +```bash +AAA_DATABASE_DRIVER=InMemory +``` + +2. Start API adapter. +3. Repositories will use `dbClient.stores` contract. + diff --git a/documentation/md/adapters/databases/MONGODB.md b/documentation/md/adapters/databases/MONGODB.md new file mode 100644 index 00000000..f3a484d3 --- /dev/null +++ b/documentation/md/adapters/databases/MONGODB.md @@ -0,0 +1,23 @@ +# MongoDB Adapter + +## Technology + +Mongoose profile. + +## Build Services with MongoDB + +1. Start container: + +```bash +npm run docker:up:mongodb +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=Mongo +AAA_DATABASE_CONNECTION_URL=mongodb://127.0.0.1:27027/aaa +``` + +3. Start service adapter. + diff --git a/documentation/md/adapters/databases/MSSQL.md b/documentation/md/adapters/databases/MSSQL.md new file mode 100644 index 00000000..f0ac8765 --- /dev/null +++ b/documentation/md/adapters/databases/MSSQL.md @@ -0,0 +1,27 @@ +# SQL Server Adapter + +## Technology + +Sequelize + tedious profile. + +## Build Services with SQL Server + +1. Start container: + +```bash +npm run docker:up:mssql +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=MSSQL +AAA_DB_HOST=127.0.0.1 +AAA_DB_PORT=1433 +AAA_DB_NAME=aaa +AAA_DB_USERNAME=sa +AAA_DB_PASSWORD=YourStrong!Passw0rd +``` + +3. Start service adapter. + diff --git a/documentation/md/adapters/databases/MYSQL.md b/documentation/md/adapters/databases/MYSQL.md new file mode 100644 index 00000000..a60de9e4 --- /dev/null +++ b/documentation/md/adapters/databases/MYSQL.md @@ -0,0 +1,27 @@ +# MySQL Adapter + +## Technology + +Sequelize + mysql2 profile. + +## Build Services with MySQL + +1. Start container: + +```bash +npm run docker:up:mysql +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=MySQL +AAA_DB_HOST=127.0.0.1 +AAA_DB_PORT=3306 +AAA_DB_NAME=aaa +AAA_DB_USERNAME=root +AAA_DB_PASSWORD=root +``` + +3. Start your API adapter. + diff --git a/documentation/md/adapters/databases/ORACLE.md b/documentation/md/adapters/databases/ORACLE.md new file mode 100644 index 00000000..ef654eda --- /dev/null +++ b/documentation/md/adapters/databases/ORACLE.md @@ -0,0 +1,27 @@ +# Oracle Adapter + +## Technology + +Sequelize + Oracle profile. + +## Build Services with Oracle + +1. Start container: + +```bash +npm run docker:up:oracle +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=Oracle +AAA_DB_HOST=127.0.0.1 +AAA_DB_PORT=1521 +AAA_DB_NAME=XE +AAA_DB_USERNAME=system +AAA_DB_PASSWORD=oracle +``` + +3. Start service adapter. + diff --git a/documentation/md/adapters/databases/POSTGRESQL.md b/documentation/md/adapters/databases/POSTGRESQL.md new file mode 100644 index 00000000..8fe4db6b --- /dev/null +++ b/documentation/md/adapters/databases/POSTGRESQL.md @@ -0,0 +1,27 @@ +# PostgreSQL Adapter + +## Technology + +Sequelize-based SQL client profile. + +## Build Services with PostgreSQL + +1. Start container: + +```bash +npm run docker:up:postgresql +``` + +2. Set env: + +```bash +AAA_DATABASE_DRIVER=PostgreSQL +AAA_DB_HOST=127.0.0.1 +AAA_DB_PORT=5432 +AAA_DB_NAME=aaa +AAA_DB_USERNAME=postgres +AAA_DB_PASSWORD=postgres +``` + +3. Start service adapter (`dev:*` command). + diff --git a/documentation/md/adapters/databases/RDS.md b/documentation/md/adapters/databases/RDS.md new file mode 100644 index 00000000..634977bb --- /dev/null +++ b/documentation/md/adapters/databases/RDS.md @@ -0,0 +1,22 @@ +# RDS Adapter + +## Technology + +Amazon RDS integration path (SQL profile in external adapter layer). + +## Build Services with RDS + +1. Set env: + +```bash +AAA_DATABASE_DRIVER=RDS +AAA_DATABASE_CONNECTION_URL=postgres://user:pass@rds-host:5432/aaa +``` + +2. Start service adapter. +3. Validate with smoke command: + +```bash +npm run smoke:db:rds +``` + diff --git a/documentation/md/adapters/databases/README.md b/documentation/md/adapters/databases/README.md new file mode 100644 index 00000000..2906f572 --- /dev/null +++ b/documentation/md/adapters/databases/README.md @@ -0,0 +1,19 @@ +# Database Adapters Documentation + +Use this index to choose the database technology for your service. + +## Database Pages + +- [InMemory](./INMEMORY.md) +- [PostgreSQL](./POSTGRESQL.md) +- [MySQL](./MYSQL.md) +- [SQL Server](./MSSQL.md) +- [Oracle](./ORACLE.md) +- [SQLite](./SQLITE.md) +- [MongoDB](./MONGODB.md) +- [DynamoDB](./DYNAMODB.md) +- [Cassandra](./CASSANDRA.md) +- [Firebase](./FIREBASE.md) +- [Aurora](./AURORA.md) +- [RDS](./RDS.md) + diff --git a/documentation/md/adapters/databases/SQLITE.md b/documentation/md/adapters/databases/SQLITE.md new file mode 100644 index 00000000..638562c9 --- /dev/null +++ b/documentation/md/adapters/databases/SQLITE.md @@ -0,0 +1,17 @@ +# SQLite Adapter + +## Technology + +Sequelize + SQLite profile for lightweight local persistence. + +## Build Services with SQLite + +1. Set env: + +```bash +AAA_DATABASE_DRIVER=SQLite +AAA_DB_FILE=.tmp/aaa.sqlite +``` + +2. Start API adapter. + diff --git a/documentation/md/adapters/http/ADONIS-JS.md b/documentation/md/adapters/http/ADONIS-JS.md new file mode 100644 index 00000000..653eb280 --- /dev/null +++ b/documentation/md/adapters/http/ADONIS-JS.md @@ -0,0 +1,20 @@ +# Adonis.js Adapter + +## Purpose + +Expose API operations through Adonis.js runtime bridge. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/adonis-js/adonis-js.ts` + +## Build a Service with Adonis.js + +1. Keep modules framework-agnostic. +2. Bridge Adonis request handling to controller operations. +3. Run: + +```bash +npm run dev:adonis-js +``` + diff --git a/documentation/md/adapters/http/AWS-LAMBDA.md b/documentation/md/adapters/http/AWS-LAMBDA.md new file mode 100644 index 00000000..63f2cd30 --- /dev/null +++ b/documentation/md/adapters/http/AWS-LAMBDA.md @@ -0,0 +1,26 @@ +# AWS Lambda Adapter + +## Purpose + +Deploy handlers as function-based services with Serverless. + +## Entrypoints + +- `apps/backend-template/src/interface/aws/lambda/handlers/` +- `apps/backend-template/src/interface/HTTP/adapters/serverless/*` + +## Build a Service with Lambda + +1. Implement controller/use case methods. +2. Create Lambda handlers mapping request to controller operation. +3. Configure deployment in `serverless` files. +4. Run local mode: + +```bash +npm run dev:serverless +``` + +## Notes + +- Keep RESTAPI fallback available for documentation/operational fallback. + diff --git a/documentation/md/adapters/http/CLOUDFLARE-WORKERS.md b/documentation/md/adapters/http/CLOUDFLARE-WORKERS.md new file mode 100644 index 00000000..604bac7c --- /dev/null +++ b/documentation/md/adapters/http/CLOUDFLARE-WORKERS.md @@ -0,0 +1,21 @@ +# Cloudflare Workers Adapter + +## Purpose + +Run HTTP APIs in Cloudflare Workers style (`fetch` contract), without Express runtime. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts` + +## Build a Service with Cloudflare Workers + +1. Implement operation handlers for this framework. +2. Keep domain and use case layers shared with other adapters. +3. Use worker fetch dispatcher. +4. Run: + +```bash +npm run dev:cloudflare-workers +``` + diff --git a/documentation/md/adapters/http/DERBY-JS.md b/documentation/md/adapters/http/DERBY-JS.md new file mode 100644 index 00000000..bd754313 --- /dev/null +++ b/documentation/md/adapters/http/DERBY-JS.md @@ -0,0 +1,20 @@ +# Derby.js Adapter + +## Purpose + +Run HTTP interface via Derby.js adapter. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/derby-js/derby-js.ts` + +## Build a Service with Derby.js + +1. Implement domain/use-case/controller operations. +2. Implement Derby-specific handlers. +3. Run: + +```bash +npm run dev:derby-js +``` + diff --git a/documentation/md/adapters/http/EXPRESS.md b/documentation/md/adapters/http/EXPRESS.md new file mode 100644 index 00000000..a2d82fec --- /dev/null +++ b/documentation/md/adapters/http/EXPRESS.md @@ -0,0 +1,27 @@ +# Express Adapter + +## Purpose + +Use Express as your REST inbound adapter. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/express/express.ts` +- `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` (environment-driven bootstrap) + +## Build a Service with Express + +1. Implement domain/use cases/controllers in modules. +2. Add framework handlers for operations in module interface layer. +3. Start with: + +```bash +npm run dev:express +``` + +## Production + +```bash +npm run prod:express +``` + diff --git a/documentation/md/adapters/http/FASTIFY.md b/documentation/md/adapters/http/FASTIFY.md new file mode 100644 index 00000000..93dfc3e3 --- /dev/null +++ b/documentation/md/adapters/http/FASTIFY.md @@ -0,0 +1,27 @@ +# Fastify Adapter + +## Purpose + +Use Fastify as your REST inbound adapter with Fastify-native server features. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/fastify/fastify.ts` +- `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` + +## Build a Service with Fastify + +1. Keep business logic in domain/use cases. +2. Implement Fastify handlers in module interface framework folder. +3. Run: + +```bash +npm run dev:fastify +``` + +## Production + +```bash +npm run prod:fastify +``` + diff --git a/documentation/md/adapters/http/FEATHERS.md b/documentation/md/adapters/http/FEATHERS.md new file mode 100644 index 00000000..bfc2cbb7 --- /dev/null +++ b/documentation/md/adapters/http/FEATHERS.md @@ -0,0 +1,20 @@ +# Feathers Adapter + +## Purpose + +Expose API operations through Feathers runtime adapter. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/feathers/feathers.ts` + +## Build a Service with Feathers + +1. Keep service behavior in module use cases. +2. Map Feathers handlers/services to controller operations. +3. Run: + +```bash +npm run dev:feathers +``` + diff --git a/documentation/md/adapters/http/HYPER-EXPRESS.md b/documentation/md/adapters/http/HYPER-EXPRESS.md new file mode 100644 index 00000000..dc2a12d6 --- /dev/null +++ b/documentation/md/adapters/http/HYPER-EXPRESS.md @@ -0,0 +1,27 @@ +# Hyper-Express Adapter + +## Purpose + +Use Hyper-Express as high-performance REST adapter. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/hyper-express/hyper-express.ts` +- `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` + +## Build a Service with Hyper-Express + +1. Keep module contracts/use cases unchanged. +2. Add Hyper-Express-specific handlers. +3. Run: + +```bash +npm run dev:hyper-express +``` + +## Production + +```bash +npm run prod:hyper-express +``` + diff --git a/documentation/md/adapters/http/LOOPBACK.md b/documentation/md/adapters/http/LOOPBACK.md new file mode 100644 index 00000000..12f95b49 --- /dev/null +++ b/documentation/md/adapters/http/LOOPBACK.md @@ -0,0 +1,20 @@ +# LoopBack Adapter + +## Purpose + +Run API operations with LoopBack runtime integration. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/loopback/loopback.ts` + +## Build a Service with LoopBack + +1. Keep business/use-case contracts in module layer. +2. Use LoopBack adapter bootstrap to expose routes/handlers. +3. Run: + +```bash +npm run dev:loopback +``` + diff --git a/documentation/md/adapters/http/README.md b/documentation/md/adapters/http/README.md new file mode 100644 index 00000000..0a622ff1 --- /dev/null +++ b/documentation/md/adapters/http/README.md @@ -0,0 +1,20 @@ +# HTTP Interface Adapters Documentation + +Use this index to choose the HTTP/runtime adapter you want to use as the inbound interface of your service. + +## Adapter Pages + +- [Express Adapter](./EXPRESS.md) +- [Fastify Adapter](./FASTIFY.md) +- [Restify Adapter](./RESTIFY.md) +- [Hyper-Express Adapter](./HYPER-EXPRESS.md) +- [AWS Lambda Adapter](./AWS-LAMBDA.md) +- [Cloudflare Workers Adapter](./CLOUDFLARE-WORKERS.md) +- [Vercel Functions Adapter](./VERCEL-FUNCTIONS.md) +- [LoopBack Adapter](./LOOPBACK.md) +- [Sails.js Adapter](./SAILS-JS.md) +- [Feathers Adapter](./FEATHERS.md) +- [Derby.js Adapter](./DERBY-JS.md) +- [Adonis.js Adapter](./ADONIS-JS.md) +- [Total.js Adapter](./TOTAL-JS.md) + diff --git a/documentation/md/adapters/http/RESTIFY.md b/documentation/md/adapters/http/RESTIFY.md new file mode 100644 index 00000000..66f15101 --- /dev/null +++ b/documentation/md/adapters/http/RESTIFY.md @@ -0,0 +1,27 @@ +# Restify Adapter + +## Purpose + +Use Restify as REST adapter where Restify middleware/runtime behavior is required. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/restify/restify.ts` +- `apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts` + +## Build a Service with Restify + +1. Implement operation controllers and use cases. +2. Implement Restify handler mapping in module interface framework folder. +3. Run: + +```bash +npm run dev:restify +``` + +## Production + +```bash +npm run prod:restify +``` + diff --git a/documentation/md/adapters/http/SAILS-JS.md b/documentation/md/adapters/http/SAILS-JS.md new file mode 100644 index 00000000..59f5ab74 --- /dev/null +++ b/documentation/md/adapters/http/SAILS-JS.md @@ -0,0 +1,20 @@ +# Sails.js Adapter + +## Purpose + +Use Sails.js runtime as HTTP inbound interface. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/sails-js/sails-js.ts` + +## Build a Service with Sails.js + +1. Keep domain + use cases framework-agnostic. +2. Implement Sails.js framework handlers. +3. Run: + +```bash +npm run dev:sails-js +``` + diff --git a/documentation/md/adapters/http/TOTAL-JS.md b/documentation/md/adapters/http/TOTAL-JS.md new file mode 100644 index 00000000..e7110c71 --- /dev/null +++ b/documentation/md/adapters/http/TOTAL-JS.md @@ -0,0 +1,20 @@ +# Total.js Adapter + +## Purpose + +Use Total.js runtime bridge as HTTP inbound adapter. + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/total-js/total-js.ts` + +## Build a Service with Total.js + +1. Implement module operations in controllers/use cases. +2. Bind Total.js routes/handlers to those operations. +3. Run: + +```bash +npm run dev:total-js +``` + diff --git a/documentation/md/adapters/http/VERCEL-FUNCTIONS.md b/documentation/md/adapters/http/VERCEL-FUNCTIONS.md new file mode 100644 index 00000000..1d8a6b58 --- /dev/null +++ b/documentation/md/adapters/http/VERCEL-FUNCTIONS.md @@ -0,0 +1,20 @@ +# Vercel Functions Adapter + +## Purpose + +Expose API operations through Vercel-style function handlers (`req`/`res`). + +## Entrypoints + +- `apps/backend-template/src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts` + +## Build a Service with Vercel Functions + +1. Implement controller/use case logic in modules. +2. Implement framework-specific handler wrappers. +3. Run: + +```bash +npm run dev:vercel-functions +``` + diff --git a/documentation/md/adapters/realtime/GRPC-API.md b/documentation/md/adapters/realtime/GRPC-API.md new file mode 100644 index 00000000..9ac7c0b7 --- /dev/null +++ b/documentation/md/adapters/realtime/GRPC-API.md @@ -0,0 +1,129 @@ +# gRPC Realtime API + +This guide is exclusively for the gRPC realtime interface. + +## Scope + +- Transport: gRPC +- Server implementation: `apps/backend-template/src/interface/gRPC/gRPCAPI.ts` +- Proto contract: `apps/backend-template/src/interface/gRPC/proto/async-api.proto` +- SDK client: `sdk-clients/grpc/GrpcApiClient.ts` +- AsyncAPI source: `spec/asyncapi/1.0.0.grpc.yml` + +## Contract References + +- [gRPC Realtime Contracts](../../contracts/GRPC-REALTIME-CONTRACTS.md) +- [Error Contracts and Responses](../../ERROR-CONTRACTS-AND-RESPONSES.md) +- [Events and Messages Map](../../EVENTS-AND-MESSAGES-MAP.md) + +## Service Contract + +- Service name: `realtime.AsyncApiGateway` +- RPC methods: + - `Request(AsyncApiRequest) returns (AsyncApiResponse)` (unary) + - `Exchange(stream AsyncApiRequest) returns (stream AsyncApiResponse)` (bi-directional stream) + +## Runtime Flow + +```mermaid +sequenceDiagram + participant C as gRPC Client + participant G as AsyncApiGateway Server + participant R as RealtimeAPIBase + participant U as Users Controller/UseCase + + C->>G: Request(AsyncApiRequest) + G->>R: executeOperation(request) + R->>U: invoke(operationId) + U-->>R: result or domain error + R-->>G: normalized async response + G-->>C: AsyncApiResponse +``` + +## Deep Example: Unary Request + +```ts +import { GrpcApiClient } from '../sdk-clients/grpc/GrpcApiClient'; + +const client = new GrpcApiClient('localhost:3002'); + +const response = await client.request({ + version: '1.0.0', + operationId: 'createOrganization', + authorization: 'Bearer ', + input: { + name: 'Acme Group', + address: [], + phone: [], + email: [] + }, + metadata: { + requestId: 'req-grpc-001', + correlationId: 'corr-tenant-42' + } +}); + +if (!response.ok) { + console.error(response.error); +} else { + console.log(response.result); +} +``` + +## Deep Example: Native gRPC Bi-directional Stream + +```ts +import path from 'path'; +import grpc from '@grpc/grpc-js'; +import protoLoader from '@grpc/proto-loader'; + +const protoPath = path.resolve(process.cwd(), 'src/interface/gRPC/proto/async-api.proto'); +const packageDefinition = protoLoader.loadSync(protoPath, { + longs: String, + enums: String, + defaults: true, + oneofs: true +}); +const grpcObject = grpc.loadPackageDefinition(packageDefinition) as any; +const client = new grpcObject.realtime.AsyncApiGateway( + 'localhost:3002', + grpc.credentials.createInsecure() +); + +const stream = client.exchange(); + +stream.on('data', (msg: any) => { + const result = msg.resultJson ? JSON.parse(msg.resultJson) : null; + console.log('stream response', msg.operationId, result, msg.errorMessage); +}); + +stream.on('error', (err: Error) => { + console.error('stream error', err); +}); + +stream.write({ + version: '1.0.0', + operationId: 'getAllOrganizations', + authorization: 'Bearer ', + inputJson: JSON.stringify({}), + paramsJson: JSON.stringify({}), + queryStringJson: JSON.stringify({ page: 1, size: 20 }), + metadataJson: JSON.stringify({ requestId: 'stream-1' }) +}); + +stream.end(); +``` + +## Payload Serialization Rules + +1. Transport fields `inputJson`, `paramsJson`, `queryStringJson`, `metadataJson` are JSON strings. +2. Server maps transport payload into the internal async request envelope. +3. `resultJson` in response must be parsed by client consumers. +4. `errorName` and `errorMessage` provide normalized failure metadata. + +## Operational Guidance + +1. Keep one client per service host/port for connection reuse. +2. Prefer unary RPC for independent operations. +3. Use stream exchange for high-frequency operation batches. +4. Enforce per-request correlation with `metadataJson.requestId`. diff --git a/documentation/md/adapters/realtime/WEBSOCKET-API.md b/documentation/md/adapters/realtime/WEBSOCKET-API.md new file mode 100644 index 00000000..a8fcd868 --- /dev/null +++ b/documentation/md/adapters/realtime/WEBSOCKET-API.md @@ -0,0 +1,206 @@ +# WebSocket Realtime API + +This guide is exclusively for the Socket.IO realtime interface. + +## Scope + +- Transport: WebSocket (Socket.IO protocol) +- Server implementation: `apps/backend-template/src/interface/WebSocket/WebSocketAPI.ts` +- SDK client: `sdk-clients/websocket/WebSocketApiClient.ts` +- AsyncAPI source: `spec/asyncapi/1.0.0.websocket.yml` + +## Contract References + +- [WebSocket Realtime Contracts](../../contracts/WEBSOCKET-REALTIME-CONTRACTS.md) +- [Error Contracts and Responses](../../ERROR-CONTRACTS-AND-RESPONSES.md) +- [Events and Messages Map](../../EVENTS-AND-MESSAGES-MAP.md) + +## Endpoint and Channels + +- Base URL: `ws://localhost:3001` +- Socket.IO path: `/ws` +- Generic request channel: `api:request` +- Generic response channel: `api:response` +- Per-operation request: `api:{operationId}:request` +- Per-operation response: `api:{operationId}:response` + +## Horizontal Scaling with Redis Streams Adapter + +To run multiple Socket.IO servers with consistent cross-node delivery, enable Redis Streams adapter: + +```bash +AAA_WEBSOCKET_SOCKETIO_ADAPTER=redis-streams +AAA_WEBSOCKET_REDIS_URL=redis://127.0.0.1:6379/1 +``` + +Fallback resolution order for Redis connection: + +1. `AAA_WEBSOCKET_REDIS_URL` +2. `AAA_REDIS_URL` +3. `AAA_REDIS_HOST` + `AAA_REDIS_PORT` + `AAA_REDIS_DATABASE` (+ `AAA_REDIS_PASSWORD`) + +Implementation files: + +- `apps/backend-template/src/interface/WebSocket/adapters/socket-io/redisStreamsAdapter.ts` +- `apps/backend-template/src/interface/WebSocket/adapters/socket-io/socket-io.ts` + +## Multi-thread Resilience with Socket.IO Cluster Adapter + +To scale across CPU workers (multiple Node.js threads/processes in the same host), enable: + +```bash +AAA_WEBSOCKET_SOCKETIO_ADAPTER=cluster +AAA_WEBSOCKET_CLUSTER_WORKERS=4 +``` + +Implementation files: + +- `apps/backend-template/src/interface/WebSocket/adapters/socket-io/clusterAdapter.ts` +- `apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts` +- `apps/backend-template/src/interface/WebSocket/adapters/socket-io/socket-io.ts` + +Notes: + +1. Primary process forks workers and restarts dead workers automatically. +2. Worker processes host Socket.IO and share events via `@socket.io/cluster-adapter`. +3. For multi-host deployments, prefer Redis Streams adapter. + +## Multi-instance Validation Test + +A dedicated integration test validates resilience with 2 Socket.IO servers + Redis: + +- `apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts` + +Run it with Docker: + +```bash +npm run smoke:realtime:redis-streams +``` + +Or run only the test (requires Redis running): + +```bash +npm run test:integration:realtime:redis-streams +``` + +## Runtime Flow + +```mermaid +sequenceDiagram + participant C as WebSocket Client + participant WS as Socket.IO Server + participant R as RealtimeAPIBase + participant U as Users Controller/UseCase + + C->>WS: emit api:request {operationId,input,metadata} + WS->>R: executeOperation(request) + R->>U: invoke(operationId) + U-->>R: result or domain error + R-->>WS: normalized async response + WS-->>C: emit api:response + WS-->>C: emit api:{operationId}:response +``` + +## Deep Example: Generic Request + ACK correlation + +```ts +import { io } from 'socket.io-client'; +import { randomUUID } from 'crypto'; + +const socket = io('ws://localhost:3001', { + path: '/ws', + transports: ['websocket'] +}); + +await new Promise((resolve) => socket.on('connect', () => resolve())); + +const requestId = randomUUID(); +const channel = 'api:createOrganization:response'; + +socket.on(channel, (payload) => { + if (payload?.metadata?.requestId !== requestId) return; + console.log('Operation channel response:', payload); +}); + +socket.timeout(30000).emit( + 'api:request', + { + version: '1.0.0', + operationId: 'createOrganization', + authorization: 'Bearer ', + input: { + name: 'Acme Group', + address: [], + phone: [], + email: [] + }, + metadata: { requestId } + }, + (ackPayload) => { + console.log('ACK response:', ackPayload); + } +); +``` + +## Deep Example: Per-operation Channel Request + +```ts +import { io } from 'socket.io-client'; + +const socket = io('ws://localhost:3001', { + path: '/ws', + transports: ['websocket'] +}); + +await new Promise((resolve) => socket.on('connect', () => resolve())); + +socket.emit( + 'api:getAllOrganizations:request', + { + version: '1.0.0', + authorization: 'Bearer ', + queryString: { page: 1, size: 20 }, + metadata: { requestId: 'req-001' } + }, + (response) => { + if (!response.ok) { + console.error(response.error); + return; + } + console.log(response.result); + } +); +``` + +## SDK Example + +```ts +import { WebSocketApiClient } from '../sdk-clients/websocket/WebSocketApiClient'; + +const client = new WebSocketApiClient('ws://localhost:3001'); +client.connect(); + +const response = await client.request({ + version: '1.0.0', + operationId: 'getAllOrganizations', + authorization: 'Bearer ', + queryString: { page: 1, size: 10 }, + metadata: { requestId: 'req-ws-01' } +}); + +console.log(response.result); +client.disconnect(); +``` + +## Response/Error Handling Rules + +1. `ok=true` means `result` is the response payload for the `operationId`. +2. `ok=false` means `error` contains normalized error data. +3. `metadata.requestId` is the correlation key to match client requests. +4. `metadata.channel` contains the response channel used by the server. + +## Operational Guidance + +1. Always send `metadata.requestId` from the client. +2. Subscribe to both `api:response` and `api:{operationId}:response` when building generic clients. +3. Keep a client-side timeout and retry strategy for transient network failures. diff --git a/documentation/md/contracts/GRPC-REALTIME-CONTRACTS.md b/documentation/md/contracts/GRPC-REALTIME-CONTRACTS.md new file mode 100644 index 00000000..8bd04d53 --- /dev/null +++ b/documentation/md/contracts/GRPC-REALTIME-CONTRACTS.md @@ -0,0 +1,84 @@ +# gRPC Realtime Contracts + +Canonical contracts for gRPC realtime transport. + +## Source of Truth + +- `apps/backend-template/src/interface/gRPC/proto/async-api.proto` +- `spec/asyncapi/1.0.0.grpc.yml` +- `apps/backend-template/src/interface/gRPC/gRPCAPI.ts` + +## Service Definition + +```proto +service AsyncApiGateway { + rpc Request (AsyncApiRequest) returns (AsyncApiResponse); + rpc Exchange (stream AsyncApiRequest) returns (stream AsyncApiResponse); +} +``` + +## Message Contracts + +```proto +message AsyncApiRequest { + string version = 1; + string operationId = 2; + string authorization = 3; + string inputJson = 4; + string paramsJson = 5; + string queryStringJson = 6; + string metadataJson = 7; + string requestId = 8; + string clientId = 9; +} + +message AsyncApiResponse { + bool ok = 1; + string version = 2; + string operationId = 3; + string resultJson = 4; + string errorName = 5; + string errorMessage = 6; + string requestId = 7; + string clientId = 8; + string metadataJson = 9; +} +``` + +## Serialization Rules + +1. `inputJson`, `paramsJson`, `queryStringJson`, and `metadataJson` are JSON-serialized strings. +2. `resultJson` is JSON-serialized output payload. +3. `requestId/clientId` can be provided directly and/or inside `metadataJson`. +4. Server normalizes and returns `requestId/clientId` in the response envelope. + +## Error Contract + +When `ok=false`, response carries: + +```json +{ + "ok": false, + "operationId": "updateOrganization", + "errorName": "domain_validation_error", + "errorMessage": "organization name is required" +} +``` + +Error semantics must follow: +- [Error Contracts and Responses](../ERROR-CONTRACTS-AND-RESPONSES.md) + +## Operation Registry + +`operationId` must be from: +- `spec/asyncapi/1.0.0.grpc.yml` (`components.schemas.GrpcRequest.properties.operationId.enum`) + +## Stream Semantics + +`Exchange` is a bidirectional stream where each request message receives one response message with matching `operationId` and correlation data. + +## Contract Change Policy + +1. Update `.proto` and AsyncAPI together. +2. Regenerate or validate SDK/client assumptions after contract changes. +3. Document any field additions/removals in this file and changelog. diff --git a/documentation/md/contracts/WEBSOCKET-REALTIME-CONTRACTS.md b/documentation/md/contracts/WEBSOCKET-REALTIME-CONTRACTS.md new file mode 100644 index 00000000..68dbf3a4 --- /dev/null +++ b/documentation/md/contracts/WEBSOCKET-REALTIME-CONTRACTS.md @@ -0,0 +1,93 @@ +# WebSocket Realtime Contracts + +Canonical contracts for Socket.IO realtime transport. + +## Source of Truth + +- `spec/asyncapi/1.0.0.websocket.yml` +- `apps/backend-template/src/interface/WebSocket/WebSocketAPI.ts` + +## Envelope: Request + +```ts +interface ApiRequest { + version?: string; + operationId: string; + authorization?: string; + input?: Record; + params?: Record; + queryString?: Record; + metadata?: { + requestId?: string; + correlationId?: string; + causationId?: string; + [key: string]: any; + }; +} +``` + +## Envelope: Response + +```ts +interface ApiResponse { + ok: boolean; + version?: string; + operationId: string; + result?: Record; + error?: { + name?: string; + message?: string; + }; + metadata?: { + requestId?: string; + clientId?: string; + channel?: string; + [key: string]: any; + }; +} +``` + +## Channel Contracts + +```txt +api:request -> generic request +api:response -> generic response +api:{operationId}:request -> operation-specific request +api:{operationId}:response -> operation-specific response +``` + +## Correlation Rules + +1. Client SHOULD send `metadata.requestId`. +2. Server reuses provided `requestId` or generates one. +3. Server includes `metadata.requestId` in response. +4. Server includes `metadata.clientId` (socket id) and `metadata.channel`. + +## Error Contract + +When `ok=false`, response includes: + +```json +{ + "ok": false, + "operationId": "createOrganization", + "error": { + "name": "validation_error", + "message": "Invalid input data" + } +} +``` + +Error semantics must follow: +- [Error Contracts and Responses](../ERROR-CONTRACTS-AND-RESPONSES.md) + +## Operation Registry + +`operationId` must match the allowed operation list in: +- `spec/asyncapi/1.0.0.websocket.yml` (`components.schemas.ApiRequest.properties.operationId.enum`) + +## Contract Change Policy + +1. Update AsyncAPI file first. +2. Keep backward compatibility for envelope shape when possible. +3. Document new operation ids and response shapes in this file. diff --git a/documentation/md/domains/users/ORGANIZATION-MODEL.md b/documentation/md/domains/users/ORGANIZATION-MODEL.md index 3a653d60..379444de 100644 --- a/documentation/md/domains/users/ORGANIZATION-MODEL.md +++ b/documentation/md/domains/users/ORGANIZATION-MODEL.md @@ -4,9 +4,9 @@ `Organization` is the tenant aggregate in the Users domain. It models organization identity, communication channels, and membership references to users. ## Source of truth -- `src/modules/Users/domain/Model/Organization.ts` -- `src/modules/Users/domain/Entity/IOrganization.ts` -- `src/modules/Users/service/OrganizationService.ts` +- `apps/backend-template/src/modules/Users/domain/Model/Organization.ts` +- `apps/backend-template/src/modules/Users/domain/Entity/IOrganization.ts` +- `apps/backend-template/src/modules/Users/service/OrganizationService.ts` ## Construction Constructor accepts `RequestCreateOrganization` + optional metadata. diff --git a/documentation/md/domains/users/USER-ENTITY-CONTRACT.md b/documentation/md/domains/users/USER-ENTITY-CONTRACT.md index e5859151..a560ac0d 100644 --- a/documentation/md/domains/users/USER-ENTITY-CONTRACT.md +++ b/documentation/md/domains/users/USER-ENTITY-CONTRACT.md @@ -4,7 +4,7 @@ `IUser` defines the domain entity shape used across repository/service/use-case boundaries for the Users domain. ## Source of truth -- `src/modules/Users/domain/Entity/IUser.ts` +- `apps/backend-template/src/modules/Users/domain/Entity/IUser.ts` ## Contract fields diff --git a/documentation/md/domains/users/USER-MODEL.md b/documentation/md/domains/users/USER-MODEL.md index 270462a8..723bd958 100644 --- a/documentation/md/domains/users/USER-MODEL.md +++ b/documentation/md/domains/users/USER-MODEL.md @@ -4,9 +4,9 @@ `User` is the aggregate root of the Users domain. It encapsulates identity, profile data, credentials, roles, tenant organization binding, and child value objects (`emails`, `documents`, `phones`), and is responsible for enforcing mutation rules. ## Source of truth -- `src/modules/Users/domain/Model/User.ts` -- `src/modules/Users/domain/Entity/IUser.ts` -- `src/modules/port/BaseModel.ts` +- `apps/backend-template/src/modules/Users/domain/Model/User.ts` +- `apps/backend-template/src/modules/Users/domain/Entity/IUser.ts` +- `apps/backend-template/src/modules/port/BaseModel.ts` ## Construction Constructor accepts `UserFactory` (internal type extending `RequestCreateUser`) and applies defaults: diff --git a/documentation/md/domains/users/USER-VALUE-OBJECTS.md b/documentation/md/domains/users/USER-VALUE-OBJECTS.md index 182bfd3b..25efd8f8 100644 --- a/documentation/md/domains/users/USER-VALUE-OBJECTS.md +++ b/documentation/md/domains/users/USER-VALUE-OBJECTS.md @@ -8,13 +8,13 @@ Value objects used by the Users domain: - `AddressValueObject` (used by `Organization`) ## Source of truth -- `src/modules/ddd/valueObjects/EmailValueObject.ts` -- `src/modules/ddd/valueObjects/DocumentValueObject.ts` -- `src/modules/ddd/valueObjects/PhoneValueObject.ts` -- `src/modules/ddd/valueObjects/AddressValueObject.ts` -- `src/modules/ddd/valueObjects/EEmailType.ts` -- `src/modules/ddd/valueObjects/EDocumentType.ts` -- `src/modules/ddd/valueObjects/EAddressType.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/EmailValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/DocumentValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/PhoneValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/AddressValueObject.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/EEmailType.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/EDocumentType.ts` +- `apps/backend-template/src/modules/ddd/valueObjects/EAddressType.ts` --- diff --git a/documentation/md/guides/CREATING-SAAS-MICROSERVICES-WITH-JUMENTIX.md b/documentation/md/guides/CREATING-SAAS-MICROSERVICES-WITH-JUMENTIX.md new file mode 100644 index 00000000..7c4e9aff --- /dev/null +++ b/documentation/md/guides/CREATING-SAAS-MICROSERVICES-WITH-JUMENTIX.md @@ -0,0 +1,35 @@ +# Creating SaaS Microservices with Jumentix + +Use this path when domain scale, team autonomy, and traffic profiles demand service-level decomposition. + +## Recommended Strategy + +1. Start modular and contract-first from day one. +2. Identify extraction candidates by domain ownership and operational profile. +3. Move generic adapters/contracts to reusable workspace packages. +4. Keep communication contract-based (message mediator/events/requests-responses). + +## Evolution Path from Monolith + +1. Stabilize domain boundaries in the modular monolith. +2. Extract one bounded context at a time. +3. Keep API contracts backward compatible during migration. +4. Gradually isolate persistence and deployment pipelines per service. + +## Operational Pattern + +- REST + realtime interfaces based on service responsibilities. +- Independent CI gates and test suites per service package/app. +- PM2/containers/functions according to runtime needs. + +## Product and Engineering Benefits + +- Independent release cadence by domain. +- Horizontal scaling on hotspot services. +- Better ownership and clearer team boundaries. + +## Related Docs + +- [Message and Event Contracts](../EVENTS-AND-MESSAGES-MAP.md) +- [Jumentix Workspace Packages](../JUMENTIX-WORKSPACE-PACKAGES.md) +- [Jumentix Monorepo Execution Plan](../JUMENTIX-MONOREPO-EXECUTION-PLAN.md) diff --git a/documentation/md/guides/CREATING-SAAS-MONOLITH-WITH-JUMENTIX.md b/documentation/md/guides/CREATING-SAAS-MONOLITH-WITH-JUMENTIX.md new file mode 100644 index 00000000..14615ab0 --- /dev/null +++ b/documentation/md/guides/CREATING-SAAS-MONOLITH-WITH-JUMENTIX.md @@ -0,0 +1,35 @@ +# Creating SaaS Monolith with Jumentix + +Use this path when you want fast delivery with strong modular boundaries and a clean future migration path. + +## Recommended Strategy + +1. Start with bounded contexts in Domain Designer. +2. Keep each domain isolated through ports/adapters and explicit contracts. +3. Expose interfaces via REST and optionally realtime channels. +4. Keep infrastructure pluggable through workspace packages (`@jumentix/*`). + +## Why This Works + +- Faster first production release. +- Lower operational complexity than early microservices. +- Clear extraction path when scale or team boundaries require service split. + +## Delivery Blueprint + +1. Design domain model and contracts (OpenAPI/AsyncAPI). +2. Implement domain/use-case/controller flows. +3. Run in one deployment unit with PM2-managed processes. +4. Add observability and compliance controls. +5. Validate quality gates and coverage thresholds before each push. + +## Target Audience + +- Product teams validating new SaaS propositions quickly. +- Engineering teams that need maintainability without over-architecting too early. + +## Related Docs + +- [Architecture and Structure](../ARCHITECTURE-AND-STRUCTURE.md) +- [Backend Template Hub](../../../apps/backend-template/documentation/README.md) +- [Deploy Target and Packaging Matrix](../JUMENTIX-DEPLOY-TARGET-AND-PACKAGING-MATRIX.md) diff --git a/jest.config.js b/jest.config.js index cee70eb1..2a18fe13 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,14 +1,17 @@ const nodeMajor = Number(process.versions.node.split('.')[0]); const unsupportedRuntimeIgnorePatterns = nodeMajor > 22 ? [ - '/test/integration/Hyper-Express/', - '/test/integration/Restify/', - '/test/integration/mutex/redis.restify.test.ts' + '/apps/backend-template/test/integration/Hyper-Express/', + '/apps/backend-template/test/integration/Restify/', + '/apps/backend-template/test/integration/mutex/redis.restify.test.ts' ] : []; const redisIntegrationIgnorePatterns = process.env.RUN_REDIS_INTEGRATION ? [] - : ['/test/integration/mutex/']; + : [ + '/apps/backend-template/test/integration/mutex/', + '/apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts' + ]; module.exports = { preset: 'ts-jest', @@ -18,15 +21,22 @@ module.exports = { coverageDirectory: 'coverage', testEnvironment: 'node', moduleNameMapper: { - '@src/(.*)$': '/src/$1', - '@seed/(.*)$': '/seed/$1', - '@test/(.*)$': '/test/$1', + '@src/(.*)$': '/apps/backend-template/src/$1', + '@seed/(.*)$': '/apps/backend-template/seed/$1', + '@test/(.*)$': '/apps/backend-template/test/$1', + '@jumentix/(.*)$': '/packages/$1/src', }, testPathIgnorePatterns: [ ...unsupportedRuntimeIgnorePatterns, ...redisIntegrationIgnorePatterns ], modulePathIgnorePatterns: ['dist', '.build', '.serverless', '.resources'], + coveragePathIgnorePatterns: [ + '/packages/', + '/ci-cd/', + '/apps/backend-template/src/modules/Users/adapters/out/persistence/UserDataRepository.ts', + '/apps/backend-template/src/modules/Users/adapters/out/persistence/OrganizationDataRepository.ts' + ], coverageThreshold: { global: { branches: 90, diff --git a/package-lock.json b/package-lock.json index 822b51b4..e6f86fb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "aaa-typescript-boilerplate", + "name": "jumentix", "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "aaa-typescript-boilerplate", + "name": "jumentix", "version": "0.0.2", "hasInstallScript": true, "license": "MIT", @@ -17,6 +17,8 @@ "@fastify/static": "^9.1.3", "@grpc/grpc-js": "^1.14.1", "@grpc/proto-loader": "^0.8.0", + "@socket.io/cluster-adapter": "^0.3.0", + "@socket.io/redis-streams-adapter": "^0.3.1", "amqplib": "^2.0.1", "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", @@ -42,6 +44,7 @@ "postgres": "^3.4.7", "redis": "^5.9.0", "reflect-metadata": "^0.2.2", + "restify": "^11.1.0", "semver": "^7.6.3", "sequelize": "^6.37.7", "serverless": "^4.4.4", @@ -2914,6 +2917,15 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", + "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", @@ -2992,6 +3004,26 @@ "win32" ] }, + "node_modules/@netflix/nerror": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@netflix/nerror/-/nerror-1.1.3.tgz", + "integrity": "sha512-b+MGNyP9/LXkapreJzNUzcvuzZslj/RGgdVVJ16P2wSlYatfLycPObImqVJSmNAdyeShvNeM/pl3sVZsObFueg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "extsprintf": "^1.4.0", + "lodash": "^4.17.15" + } + }, + "node_modules/@netflix/nerror/node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -3455,12 +3487,60 @@ "node": ">=14.0.0" } }, + "node_modules/@socket.io/cluster-adapter": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@socket.io/cluster-adapter/-/cluster-adapter-0.3.0.tgz", + "integrity": "sha512-pHtPlQrKCWb1FE49nSaQSqB0xWYZ/OU8ONxWLWU79u6OA92/IHeHolVTcJfrsiK2Zvrh1yvMo3tVFKKjlKkKRQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "socket.io-adapter": "~2.5.5" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@socket.io/redis-streams-adapter": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@socket.io/redis-streams-adapter/-/redis-streams-adapter-0.3.1.tgz", + "integrity": "sha512-J+kcx5w4TUYSSmvimMC+UZKdxdVAmppiVoscoOPkeJeLDgsf154Sth/NRVNNn8PUg5/bEYV5Q9MxDm+ztTx1LQ==", + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "~2.8.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "socket.io-adapter": "^2.5.4" + } + }, + "node_modules/@socket.io/redis-streams-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -4582,6 +4662,24 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -4908,6 +5006,15 @@ "node": ">=10.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -5110,6 +5217,30 @@ "node": ">=16.20.1" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -5856,6 +5987,12 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -5979,6 +6116,39 @@ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", "license": "MIT" }, + "node_modules/csv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.6.1.tgz", + "integrity": "sha512-QKTUoSvOaJQGL23LxlGqQiy1n0g2Eg/T72tzsKqtX7APrchpwBpf0SJMX2fzrrXj87wkDAPQBgvoSAVKqKAYHA==", + "license": "MIT", + "dependencies": { + "csv-generate": "^4.6.1", + "csv-parse": "^7.0.1", + "csv-stringify": "^6.8.1", + "stream-transform": "^3.5.1" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.6.1.tgz", + "integrity": "sha512-eELl9K716LSSeP2/YcCjch525JztnnERe3jEARWw2v1FN9ukUYfZTNYZ4Rq2Jj/MFKMauffOy9VCaqQTpFThDQ==", + "license": "MIT" + }, + "node_modules/csv-parse": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.1.tgz", + "integrity": "sha512-+2z7Ar0APQ7Uu6fX4cn+pitRmxjZ1WPBcGmZFKmA74FCyi7Et/XZx8cjNQ5CjbZ4HCOxXCOpRBYvYH08Qa003A==", + "license": "MIT" + }, + "node_modules/csv-stringify": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.8.1.tgz", + "integrity": "sha512-tZ6X6TKQyQgCo5OptXcyAbfN1pwmoxEqELPQ7KFazNErx7kiVsDK8o+VYRXhfMl4N9vvOOLXuioquR2MeP847A==", + "license": "MIT" + }, "node_modules/culvert": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz", @@ -6094,6 +6264,18 @@ "node": ">=8" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -6430,6 +6612,12 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -6554,6 +6742,16 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -6866,6 +7064,11 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-regexp-component": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-regexp-component/-/escape-regexp-component-1.0.2.tgz", + "integrity": "sha512-B0yxafj1D1ZTNEHkFoQxz4iboZSfaZHhaNhIug7GcUCL4ZUrVSJZTmWUAkPOFaYDfi3RNT9XM082TuGE6jpmiQ==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7529,6 +7732,24 @@ "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/ewma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ewma/-/ewma-2.0.1.tgz", + "integrity": "sha512-MYYK17A76cuuyvkR7MnqLW4iFYPEi5Isl2qb8rXiWpLiwFS9dxW/rncuNnjjgSENuVqZQkIuR4+DChVL4g1lnw==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7668,6 +7889,15 @@ "follow-redirects": "^1.14.0" } }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/farmhash-modern": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", @@ -7772,6 +8002,15 @@ "fast-decode-uri-component": "^1.0.1" } }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -8254,6 +8493,16 @@ "node": ">=12.20.0" } }, + "node_modules/formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8545,6 +8794,15 @@ "node": ">= 14" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/git-node-fs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz", @@ -9137,6 +9395,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -9295,6 +9559,48 @@ "dev": true, "license": "ISC" }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -9319,6 +9625,12 @@ "dev": true, "license": "MIT" }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -9367,6 +9679,20 @@ "node": ">= 14" } }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -9467,10 +9793,23 @@ } }, "node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "BSD-3-Clause" }, "node_modules/ignore": { @@ -10151,6 +10490,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -11017,6 +11362,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -11053,6 +11404,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -11167,6 +11524,21 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", @@ -11771,6 +12143,12 @@ "node": ">=4" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -12605,6 +12983,12 @@ "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==", "license": "MIT" }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -12630,7 +13014,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -13075,6 +13458,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -13425,6 +13820,12 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", @@ -13780,7 +14181,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -13994,6 +14394,186 @@ "node": ">=10" } }, + "node_modules/restify": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/restify/-/restify-11.1.0.tgz", + "integrity": "sha512-ng7uBlj4wpIpshhAjNNSd6JG5Eg32+zgync2gG8OlF4e2xzIflZo54GJ/qLs765OtQaVU+uJPcNOL5Atm2F/dg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "csv": "^6.2.2", + "escape-regexp-component": "^1.0.2", + "ewma": "^2.0.1", + "find-my-way": "^7.2.0", + "formidable": "^1.2.1", + "http-signature": "^1.3.6", + "lodash": "^4.17.11", + "lru-cache": "^7.14.1", + "mime": "^3.0.0", + "negotiator": "^0.6.2", + "once": "^1.4.0", + "pidusage": "^3.0.2", + "pino": "^8.7.0", + "qs": "^6.7.0", + "restify-errors": "^8.0.2", + "semver": "^7.3.8", + "send": "^0.18.0", + "spdy": "^4.0.0", + "uuid": "^9.0.0", + "vasync": "^2.2.0" + }, + "bin": { + "report-latency": "bin/report-latency" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "dtrace-provider": "~0.8" + } + }, + "node_modules/restify-errors": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-8.1.0.tgz", + "integrity": "sha512-9ZYZTL9qli2hYykL3newPLf8RLgpSz+4RpZ7KKVSAj4CgCA8PwlfATI4onV13IZ3ldzV3M/2bJom6GQEAwS9aQ==", + "license": "MIT", + "dependencies": { + "@netflix/nerror": "^1.1.3", + "assert-plus": "^1.0.0", + "lodash": "^4.17.21" + }, + "optionalDependencies": { + "safe-json-stringify": "^1.2.0" + } + }, + "node_modules/restify/node_modules/find-my-way": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", + "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/restify/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/restify/node_modules/pino": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/restify/node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/restify/node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "license": "MIT" + }, + "node_modules/restify/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/restify/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/restify/node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restify/node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "license": "MIT", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/restify/node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/restify/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/restify/node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -14333,6 +14913,12 @@ ], "license": "BSD-3-Clause" }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -14974,6 +15560,36 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, "node_modules/split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -14999,6 +15615,31 @@ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -15054,6 +15695,12 @@ "license": "MIT", "optional": true }, + "node_modules/stream-transform": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.5.1.tgz", + "integrity": "sha512-TTDX+qKFr7GGRXATn66rprmlFPx08W0UBIccE/rMPgW2sT7GovduZYP4xcdJ7Nu2YigF17U+CNFxYY11+W1oPw==", + "license": "MIT" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -15411,39 +16058,6 @@ "readable-stream": "^4.2.0" } }, - "node_modules/tedious/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/tedious/node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/tedious/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -15456,26 +16070,6 @@ "node": ">=0.10.0" } }, - "node_modules/tedious/node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/tedious/node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -15957,6 +16551,12 @@ "node": ">= 0.8.0" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/tx2": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", @@ -16219,7 +16819,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -16300,6 +16899,32 @@ "node": ">= 0.8" } }, + "node_modules/vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "verror": "1.10.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vizion": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", @@ -16334,6 +16959,15 @@ "makeerror": "1.0.12" } }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -16504,7 +17138,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index f9d6fbc3..b88f5037 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { - "name": "aaa-typescript-boilerplate", + "name": "jumentix", "version": "0.0.2", - "description": "This is a boilerplate to build REST APIs, Monolithic Modular and Microservice applications with Typescript.", - "main": "src/interface/HTTP/adapters/fastify.ts", + "description": "Build services from the ground up with AAA (Authentication, Authorization, Accounting) principles in mind. This is a boilerplate complete services network with a service management system, REST API, gRPC API, WebSocket API, and more.", + "packageManager": "pnpm@9.15.3", + "main": "apps/backend-template/src/interface/HTTP/adapters/fastify.ts", "bin": { "aaa-bootstrap": "./bin/aaa-bootstrap.js" }, @@ -12,7 +13,11 @@ "engineStrict": true, "scripts": { "check-node-version": "node ci-cd/check-node-version.js", - "preinstall": "npm run check-node-version", + "preinstall": "node ci-cd/check-node-version.js", + "mono:build": "pnpm -r --workspace-concurrency=1 --if-present build", + "mono:test": "pnpm -r --workspace-concurrency=1 --if-present test", + "mono:lint": "pnpm -r --workspace-concurrency=1 --if-present lint", + "mono:typecheck": "pnpm -r --workspace-concurrency=1 --if-present typecheck", "prepare": "husky install", "changelog:update": "node ci-cd/update-changelog.js", "changelog:check": "node ci-cd/update-changelog.js --check", @@ -20,48 +25,75 @@ "deps:check-cycles": "node ci-cd/check-core-import-cycles.js", "arch:check-boundaries": "node ci-cd/check-hexagonal-boundaries.js", "arch:check-users-legacy-imports": "node ci-cd/check-users-legacy-imports.js", - "ci:smoke": "AAA_JWT_TOKEN_SECRET_KEY=${AAA_JWT_TOKEN_SECRET_KEY:-ci_jwt_secret_key} NODE_ENV=ci jest ./test/integration/Express/get.localhost.test.ts --runInBand --coverage=false", - "ci:security-smoke": "NODE_ENV=ci jest ./test/unit/config/security.test.ts ./test/unit/shared/utils.errorExposure.test.ts --runInBand --coverage=false", - "ci:gate": "npm run lint && npm run deps:check-cycles && npm run arch:check-boundaries && npm run arch:check-users-legacy-imports && npm run test:unit && npm run ci:security-smoke && npm run oas:check-routes && npm run build:dev && npm run ci:smoke", + "arch:check-workspace-boundaries": "node ci-cd/check-workspace-boundaries.js", + "workspace:check-coverage-policy": "node ci-cd/check-workspace-coverage-policy.js", + "workspace:check-quality": "node ci-cd/check-workspace-quality.js", + "ci:affected": "node ci-cd/check-affected-workspaces.js", + "ci:monorepo": "node ci-cd/run-monorepo-ci.js", + "release:dry-run": "node ci-cd/release-dry-run.js all", + "release:dry-run:packages": "node ci-cd/release-dry-run.js packages", + "release:dry-run:apps": "node ci-cd/release-dry-run.js apps", + "release:governance:check": "node ci-cd/check-release-governance.js", + "serverless:check-handlers": "node ci-cd/check-serverless-handler-paths.js", + "ci:smoke": "AAA_JWT_TOKEN_SECRET_KEY=${AAA_JWT_TOKEN_SECRET_KEY:-ci_jwt_secret_key} NODE_ENV=ci node ci-cd/run-api-smoke.js", + "ci:security-smoke": "NODE_ENV=ci node ci-cd/run-security-smoke.js", + "ci:gate": "npm run lint && npm run deps:check-cycles && npm run arch:check-boundaries && npm run arch:check-users-legacy-imports && npm run arch:check-workspace-boundaries && npm run workspace:check-quality && npm run workspace:check-coverage-policy && npm run release:governance:check && npm run test:unit && npm run ci:security-smoke && npm run oas:check-routes && npm run serverless:check-handlers && npm run build:dev && npm run ci:smoke", "ci:gate:strict": "npm run ci:gate && npm run coverage:patch", "dev": "npm run pm2:start:dev:restapi", + "website:dev": "pnpm --filter @jumentix/website dev", + "website:build": "pnpm --filter @jumentix/website build", + "website:start": "pnpm --filter @jumentix/website start", + "website:vercel:link": "npx vercel link --cwd apps/jumentix-website --scope web2solutions --project jumentix-website --yes", + "website:vercel:pull:preview": "npx vercel pull --cwd apps/jumentix-website --environment=preview --scope web2solutions --yes", + "website:vercel:pull:prod": "npx vercel pull --cwd apps/jumentix-website --environment=production --scope web2solutions --yes", + "website:deploy:vercel": "npx vercel --cwd apps/jumentix-website --scope web2solutions --prod", + "website:deploy:vercel:preview": "npx vercel --cwd apps/jumentix-website --scope web2solutions", + "npm:whoami": "npm whoami", + "npm:org:check:xpertminds": "node ci-cd/check-npm-org-integration.js", + "npm:publish:dry-run:packages": "node ci-cd/npm-publish-dry-run.js", "pm2:list": "pm2 ls", "pm2:logs": "pm2 logs", "pm2:stop:all": "pm2 stop all", "pm2:delete:all": "pm2 delete all", - "pm2:start:dev:restapi": "pm2 start ./pm2/ecosystem.dev.cjs --only aaa-dev-servicemangement,aaa-dev-restapi --update-env", - "pm2:start:dev:websocket-rest": "pm2 start ./pm2/ecosystem.dev.cjs --only aaa-dev-servicemangement,aaa-dev-restapi,aaa-dev-websocketapi --update-env", - "pm2:start:dev:grpc-rest": "pm2 start ./pm2/ecosystem.dev.cjs --only aaa-dev-servicemangement,aaa-dev-restapi,aaa-dev-grpcapi --update-env", - "pm2:start:staging:restapi": "pm2 start ./pm2/ecosystem.staging.cjs --only aaa-staging-servicemangement,aaa-staging-restapi --update-env", - "pm2:start:staging:websocket-rest": "pm2 start ./pm2/ecosystem.staging.cjs --only aaa-staging-servicemangement,aaa-staging-restapi,aaa-staging-websocketapi --update-env", - "pm2:start:staging:grpc-rest": "pm2 start ./pm2/ecosystem.staging.cjs --only aaa-staging-servicemangement,aaa-staging-restapi,aaa-staging-grpcapi --update-env", - "pm2:start:prod:restapi": "pm2 start ./pm2/ecosystem.production.cjs --only aaa-prod-servicemangement,aaa-prod-restapi --update-env", - "pm2:start:prod:websocket-rest": "pm2 start ./pm2/ecosystem.production.cjs --only aaa-prod-servicemangement,aaa-prod-restapi,aaa-prod-websocketapi --update-env", - "pm2:start:prod:grpc-rest": "pm2 start ./pm2/ecosystem.production.cjs --only aaa-prod-servicemangement,aaa-prod-restapi,aaa-prod-grpcapi --update-env", + "pm2:start:dev:restapi": "pm2 start ./pm2/ecosystem.dev.cjs --only aaa-dev-service-management,aaa-dev-restapi --update-env", + "pm2:start:dev:websocket-rest": "pm2 start ./pm2/ecosystem.dev.cjs --only aaa-dev-service-management,aaa-dev-restapi,aaa-dev-websocketapi --update-env", + "pm2:start:dev:grpc-rest": "pm2 start ./pm2/ecosystem.dev.cjs --only aaa-dev-service-management,aaa-dev-restapi,aaa-dev-grpcapi --update-env", + "pm2:start:staging:restapi": "pm2 start ./pm2/ecosystem.staging.cjs --only aaa-staging-service-management,aaa-staging-restapi --update-env", + "pm2:start:staging:websocket-rest": "pm2 start ./pm2/ecosystem.staging.cjs --only aaa-staging-service-management,aaa-staging-restapi,aaa-staging-websocketapi --update-env", + "pm2:start:staging:grpc-rest": "pm2 start ./pm2/ecosystem.staging.cjs --only aaa-staging-service-management,aaa-staging-restapi,aaa-staging-grpcapi --update-env", + "pm2:start:prod:restapi": "pm2 start ./pm2/ecosystem.production.cjs --only aaa-prod-service-management,aaa-prod-restapi --update-env", + "pm2:start:prod:websocket-rest": "pm2 start ./pm2/ecosystem.production.cjs --only aaa-prod-service-management,aaa-prod-restapi,aaa-prod-websocketapi --update-env", + "pm2:start:prod:grpc-rest": "pm2 start ./pm2/ecosystem.production.cjs --only aaa-prod-service-management,aaa-prod-restapi,aaa-prod-grpcapi --update-env", "commit": "npm run lint && npm run test && node -r ts-node/register ci-cd/bumpPackage.ts && git add . && git-cz", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", - "build:dev": "NODE_ENV=dev tsc", - "build:prod": "NODE_ENV=prod tsc", + "build:dev": "NODE_ENV=dev tsc -p tsconfig.build.json", + "build:prod": "NODE_ENV=prod tsc -p tsconfig.build.json", "tdd": "NODE_ENV=dev npx jest --watchAll", - "npm": "NODE_ENV=ci npx jest ./test", - "test": "NODE_ENV=dev npx jest ./test", - "test:unit": "NODE_ENV=dev jest ./test/unit --runInBand", + "npm": "NODE_ENV=ci npx jest ./apps/backend-template/test", + "test": "NODE_ENV=dev npx jest ./apps/backend-template/test", + "test:unit": "NODE_ENV=dev node ci-cd/run-unit-tests.js", "coverage:patch": "node ci-cd/check-patch-coverage.js", - "test:integration": "npm run test:integration:express && npm run test:integration:fastify && npm run test:integration:restify && npm run test:integration:hyper-express && npm run test:integration:cloudflare-workers && npm run test:integration:vercel-functions && npm run test:integration:loopback && npm run test:integration:sails-js && npm run test:integration:feathers && npm run test:integration:derby-js && npm run test:integration:adonis-js && npm run test:integration:total-js", - "test:integration:express": "NODE_ENV=dev jest ./test/integration/Express", - "test:integration:fastify": "NODE_ENV=dev jest ./test/integration/Fastify", - "test:integration:restify": "NODE_ENV=dev jest ./test/integration/Restify", - "test:integration:lambda": "NODE_ENV=dev jest ./test/integration/Lambda", - "test:integration:hyper-express": "NODE_ENV=dev jest ./test/integration/Hyper-Express", - "test:integration:cloudflare-workers": "NODE_ENV=dev jest ./test/integration/Cloudflare-Workers", - "test:integration:vercel-functions": "NODE_ENV=dev jest ./test/integration/Vercel-Functions", - "test:integration:loopback": "NODE_ENV=dev jest ./test/integration/LoopBack --passWithNoTests", - "test:integration:sails-js": "NODE_ENV=dev jest ./test/integration/Sails-JS --passWithNoTests", - "test:integration:feathers": "NODE_ENV=dev jest ./test/integration/Feathers --passWithNoTests", - "test:integration:derby-js": "NODE_ENV=dev jest ./test/integration/Derby-JS", - "test:integration:adonis-js": "NODE_ENV=dev jest ./test/integration/Adonis-JS", - "test:integration:total-js": "NODE_ENV=dev jest ./test/integration/Total-JS", + "test:integration": "npm run test:integration:express && npm run test:integration:fastify && npm run test:integration:restify && npm run test:integration:hyper-express && npm run test:integration:cloudflare-workers && npm run test:integration:vercel-functions && npm run test:integration:loopback && npm run test:integration:sails-js && npm run test:integration:feathers && npm run test:integration:derby-js && npm run test:integration:adonis-js && npm run test:integration:total-js && npm run test:integration:realtime && npm run test:integration:service-management", + "test:integration:express": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Express", + "test:integration:fastify": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Fastify", + "test:integration:restify": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Restify", + "test:integration:lambda": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Lambda", + "test:integration:hyper-express": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Hyper-Express", + "test:integration:cloudflare-workers": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Cloudflare-Workers", + "test:integration:vercel-functions": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Vercel-Functions", + "test:integration:loopback": "NODE_ENV=dev jest ./apps/backend-template/test/integration/LoopBack", + "test:integration:sails-js": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Sails-JS", + "test:integration:feathers": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Feathers", + "test:integration:derby-js": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Derby-JS", + "test:integration:adonis-js": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Adonis-JS", + "test:integration:total-js": "NODE_ENV=dev jest ./apps/backend-template/test/integration/Total-JS", + "test:integration:service-management": "NODE_ENV=dev node ci-cd/run-service-management-integration.js", + "test:integration:service-mangement": "npm run test:integration:service-management", + "test:integration:realtime:websocket": "NODE_ENV=dev jest ./apps/backend-template/test/integration/realtime/websocket.basic.integration.test.ts --runInBand --coverage=false", + "test:integration:realtime:grpc": "NODE_ENV=dev jest ./apps/backend-template/test/integration/realtime/grpc.basic.integration.test.ts --runInBand --coverage=false", + "test:integration:realtime": "npm run test:integration:realtime:websocket && npm run test:integration:realtime:grpc", + "test:integration:realtime:redis-streams": "RUN_REDIS_INTEGRATION=1 NODE_ENV=dev jest ./apps/backend-template/test/integration/realtime/socketio.redis-streams.multi-instance.test.ts --runInBand --coverage=false", "test:integration:db": "npm run test:smoke:db:all", "test:integration:db:inmemory": "npm run test:smoke:db:inmemory", "test:integration:db:sqlite": "npm run test:smoke:db:sqlite", @@ -78,7 +110,8 @@ "test:smoke": "npm run ci:smoke", "test:smoke:security": "npm run ci:security-smoke", "test:smoke:api": "npm run ci:smoke", - "test:smoke:db": "RUN_DB_SMOKE=1 NODE_ENV=dev jest ./test/smoke/database/DatabaseDrivers.smoke.test.ts --runInBand --coverage=false", + "test:smoke:db": "RUN_DB_SMOKE=1 NODE_ENV=dev jest ./apps/backend-template/test/smoke/database/DatabaseDrivers.smoke.test.ts --runInBand --coverage=false", + "test:smoke:realtime": "NODE_ENV=dev jest ./apps/backend-template/test/smoke/realtime/RealtimeApis.smoke.test.ts --runInBand --coverage=false", "test:smoke:db:inmemory": "AAA_DB_SMOKE_DRIVERS=InMemory npm run test:smoke:db", "test:smoke:db:postgresql": "AAA_DB_SMOKE_DRIVERS=PostgreSQL npm run test:smoke:db", "test:smoke:db:mysql": "AAA_DB_SMOKE_DRIVERS=MySQL npm run test:smoke:db", @@ -92,7 +125,7 @@ "test:smoke:db:aurora": "AAA_DB_SMOKE_DRIVERS=Aurora npm run test:smoke:db", "test:smoke:db:rds": "AAA_DB_SMOKE_DRIVERS=RDS npm run test:smoke:db", "test:smoke:db:all": "npm run smoke:db:inmemory && npm run smoke:db:sqlite && npm run smoke:db:postgresql && npm run smoke:db:mongodb && npm run smoke:db:mysql && npm run smoke:db:mssql && npm run smoke:db:dynamodb && npm run smoke:db:cassandra && npm run smoke:db:oracle && npm run smoke:db:firebase && npm run smoke:db:aurora && npm run smoke:db:rds", - "test:smoke:all": "npm run test:smoke:security && npm run test:smoke:api && npm run test:smoke:db:all", + "test:smoke:all": "npm run test:smoke:security && npm run test:smoke:api && npm run test:smoke:realtime && npm run test:smoke:db:all", "smoke:db:postgresql": "npm run docker:up:postgresql && npm run test:smoke:db:postgresql && npm run docker:down:postgresql", "smoke:db:mysql": "npm run docker:up:mysql && npm run test:smoke:db:mysql && npm run docker:down:mysql", "smoke:db:mssql": "npm run docker:up:mssql && npm run test:smoke:db:mssql && npm run docker:down:mssql", @@ -105,74 +138,77 @@ "smoke:db:rds": "npm run docker:up:rds && npm run test:smoke:db:rds && npm run docker:down:rds", "smoke:db:sqlite": "npm run test:smoke:db:sqlite", "smoke:db:inmemory": "npm run test:smoke:db:inmemory", - "dev:express": "pm2 start ./src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-express --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:fastify": "pm2 start ./src/interface/HTTP/adapters/fastify/fastify.ts --name aaa-dev-fastify --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:restify": "pm2 start ./src/interface/HTTP/adapters/restify/restify.ts --name aaa-dev-restify --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:hyper-express": "pm2 start ./src/interface/HTTP/adapters/hyper-express/hyper-express.ts --name aaa-dev-hyper-express --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:cloudflare-workers": "pm2 start ./src/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.ts --name aaa-dev-cloudflare-workers --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:vercel-functions": "pm2 start ./src/interface/HTTP/adapters/vercel-functions/vercel-functions.ts --name aaa-dev-vercel-functions --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:loopback": "pm2 start ./src/interface/HTTP/adapters/loopback/loopback.ts --name aaa-dev-loopback --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:sails-js": "pm2 start ./src/interface/HTTP/adapters/sails-js/sails-js.ts --name aaa-dev-sails-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:feathers": "pm2 start ./src/interface/HTTP/adapters/feathers/feathers.ts --name aaa-dev-feathers --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:derby-js": "pm2 start ./src/interface/HTTP/adapters/derby-js/derby-js.ts --name aaa-dev-derby-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:adonis-js": "pm2 start ./src/interface/HTTP/adapters/adonis-js/adonis-js.ts --name aaa-dev-adonis-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", - "dev:total-js": "pm2 start ./src/interface/HTTP/adapters/total-js/total-js.ts --name aaa-dev-total-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev' --update-env", + "dev:http": "pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-http --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:express": "AAA_HTTP_FRAMEWORK=express pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-express --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:fastify": "AAA_HTTP_FRAMEWORK=fastify pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-fastify --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:restify": "AAA_HTTP_FRAMEWORK=restify pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-restify --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:hyper-express": "AAA_HTTP_FRAMEWORK=hyper-express pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-hyper-express --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:cloudflare-workers": "AAA_HTTP_FRAMEWORK=cloudflare-workers pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-cloudflare-workers --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:vercel-functions": "AAA_HTTP_FRAMEWORK=vercel-functions pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-vercel-functions --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:loopback": "AAA_HTTP_FRAMEWORK=loopback pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-loopback --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:sails-js": "AAA_HTTP_FRAMEWORK=sails-js pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-sails-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:feathers": "AAA_HTTP_FRAMEWORK=feathers pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-feathers --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:derby-js": "AAA_HTTP_FRAMEWORK=derby-js pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-derby-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:adonis-js": "AAA_HTTP_FRAMEWORK=adonis-js pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-adonis-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", + "dev:total-js": "AAA_HTTP_FRAMEWORK=total-js pm2 start ./apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts --name aaa-dev-total-js --interpreter node --node-args='-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev' --update-env", "dev:websocket": "npm run pm2:start:dev:websocket-rest", "dev:grpc": "npm run pm2:start:dev:grpc-rest", "dev:serverless": "npm run lint && NODE_ENV=dev serverless dev", "cli": "npm run dev:cli", "cli:bootstrap": "node ./bin/aaa-bootstrap.js", - "dev:cli": "node -r ts-node/register -r tsconfig-paths/register ./src/interface/CLI/index.ts", - "dev:service-management": "pm2 start ./servicemangement/server.js --name aaa-dev-servicemangement --interpreter node --update-env", + "dev:cli": "node -r ts-node/register -r tsconfig-paths/register ./apps/backend-template/src/interface/CLI/index.ts", + "dev:service-management": "pm2 start ./apps/service-management/server.js --name aaa-dev-service-management --interpreter node --update-env", "start:cli": "npm run dev:cli", - "prod:express": "pm2 start ./.build/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-express --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:fastify": "pm2 start ./.build/interface/HTTP/adapters/fastify/fastify.js --name aaa-prod-fastify --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:restify": "pm2 start ./.build/interface/HTTP/adapters/restify/restify.js --name aaa-prod-restify --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:hyper-express": "pm2 start ./.build/interface/HTTP/adapters/hyper-express/hyper-express.js --name aaa-prod-hyper-express --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:cloudflare-workers": "pm2 start ./.build/interface/HTTP/adapters/cloudflare-workers/cloudflare-workers.js --name aaa-prod-cloudflare-workers --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:vercel-functions": "pm2 start ./.build/interface/HTTP/adapters/vercel-functions/vercel-functions.js --name aaa-prod-vercel-functions --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:loopback": "pm2 start ./.build/interface/HTTP/adapters/loopback/loopback.js --name aaa-prod-loopback --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:sails-js": "pm2 start ./.build/interface/HTTP/adapters/sails-js/sails-js.js --name aaa-prod-sails-js --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:feathers": "pm2 start ./.build/interface/HTTP/adapters/feathers/feathers.js --name aaa-prod-feathers --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:derby-js": "pm2 start ./.build/interface/HTTP/adapters/derby-js/derby-js.js --name aaa-prod-derby-js --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:adonis-js": "pm2 start ./.build/interface/HTTP/adapters/adonis-js/adonis-js.js --name aaa-prod-adonis-js --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", - "prod:total-js": "pm2 start ./.build/interface/HTTP/adapters/total-js/total-js.js --name aaa-prod-total-js --interpreter node --node-args='--env-file=./.build/config/.env.prod' --update-env", + "prod:http": "pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-http --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:express": "AAA_HTTP_FRAMEWORK=express pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-express --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:fastify": "AAA_HTTP_FRAMEWORK=fastify pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-fastify --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:restify": "AAA_HTTP_FRAMEWORK=restify pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-restify --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:hyper-express": "AAA_HTTP_FRAMEWORK=hyper-express pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-hyper-express --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:cloudflare-workers": "AAA_HTTP_FRAMEWORK=cloudflare-workers pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-cloudflare-workers --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:vercel-functions": "AAA_HTTP_FRAMEWORK=vercel-functions pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-vercel-functions --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:loopback": "AAA_HTTP_FRAMEWORK=loopback pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-loopback --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:sails-js": "AAA_HTTP_FRAMEWORK=sails-js pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-sails-js --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:feathers": "AAA_HTTP_FRAMEWORK=feathers pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-feathers --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:derby-js": "AAA_HTTP_FRAMEWORK=derby-js pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-derby-js --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:adonis-js": "AAA_HTTP_FRAMEWORK=adonis-js pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-adonis-js --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", + "prod:total-js": "AAA_HTTP_FRAMEWORK=total-js pm2 start ./.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js --name aaa-prod-total-js --interpreter node --node-args='--env-file=./.build/apps/backend-template/src/config/.env.prod' --update-env", "prod:websocket": "npm run pm2:start:prod:websocket-rest", "prod:grpc": "npm run pm2:start:prod:grpc-rest", "staging:restapi": "npm run pm2:start:staging:restapi", "staging:websocket": "npm run pm2:start:staging:websocket-rest", "staging:grpc": "npm run pm2:start:staging:grpc-rest", "prod:serverless": "npm run lint && NODE_ENV=prod serverless dev", - "docker:composeredis": "docker compose -f \"docker-compose-redis.yml\" up -d --build", - "docker:composemessaging": "docker compose -f \"docker-compose-messaging.yml\" up -d --build", - "docker:compose:platform-services": "docker compose -f \"docker-compose-platform-services.yml\" up -d --build", - "docker:up:postgresql": "docker compose -f \"docker-compose-postgresql.yml\" up -d --build", - "docker:up:mysql": "docker compose -f \"docker-compose-mysql.yml\" up -d --build", - "docker:up:mssql": "docker compose -f \"docker-compose-mssql.yml\" up -d --build", - "docker:up:oracle": "docker compose -f \"docker-compose-oracle.yml\" up -d --build", - "docker:up:mongodb": "docker compose -f \"docker-compose-mongodb.yml\" up -d --build", - "docker:up:cassandra": "docker compose -f \"docker-compose-cassandra.yml\" up -d --build", - "docker:up:dynamodb": "docker compose -f \"docker-compose-dynamodb.yml\" up -d --build", - "docker:up:firebase": "docker compose -f \"docker-compose-firebase.yml\" up -d --build", - "docker:up:aurora": "docker compose -f \"docker-compose-aurora.yml\" up -d --build", - "docker:up:rds": "docker compose -f \"docker-compose-rds.yml\" up -d --build", - "docker:down:postgresql": "docker compose -f \"docker-compose-postgresql.yml\" down", - "docker:down:mysql": "docker compose -f \"docker-compose-mysql.yml\" down", - "docker:down:mssql": "docker compose -f \"docker-compose-mssql.yml\" down", - "docker:down:oracle": "docker compose -f \"docker-compose-oracle.yml\" down", - "docker:down:mongodb": "docker compose -f \"docker-compose-mongodb.yml\" down", - "docker:down:cassandra": "docker compose -f \"docker-compose-cassandra.yml\" down", - "docker:down:dynamodb": "docker compose -f \"docker-compose-dynamodb.yml\" down", - "docker:down:firebase": "docker compose -f \"docker-compose-firebase.yml\" down", - "docker:down:aurora": "docker compose -f \"docker-compose-aurora.yml\" down", - "docker:down:rds": "docker compose -f \"docker-compose-rds.yml\" down", - "docker:compose:service-template": "docker compose -f \"docker-compose-service-templates.yml\" --profile ${AAA_SERVICE_PROFILE:-rest} up -d --build", - "docker:composerabbit": "docker compose -f \"docker-compose-messaging.yml\" up -d rabbitmq", + "docker:composeredis": "docker compose -f \"apps/backend-template/docker-compose-redis.yml\" up -d --build", + "docker:composemessaging": "docker compose -f \"apps/backend-template/docker-compose-messaging.yml\" up -d --build", + "docker:compose:platform-services": "docker compose -f \"apps/backend-template/docker-compose-platform-services.yml\" up -d --build", + "docker:up:postgresql": "docker compose -f \"apps/backend-template/docker-compose-postgresql.yml\" up -d --build", + "docker:up:mysql": "docker compose -f \"apps/backend-template/docker-compose-mysql.yml\" up -d --build", + "docker:up:mssql": "docker compose -f \"apps/backend-template/docker-compose-mssql.yml\" up -d --build", + "docker:up:oracle": "docker compose -f \"apps/backend-template/docker-compose-oracle.yml\" up -d --build", + "docker:up:mongodb": "docker compose -f \"apps/backend-template/docker-compose-mongodb.yml\" up -d --build", + "docker:up:cassandra": "docker compose -f \"apps/backend-template/docker-compose-cassandra.yml\" up -d --build", + "docker:up:dynamodb": "docker compose -f \"apps/backend-template/docker-compose-dynamodb.yml\" up -d --build", + "docker:up:firebase": "docker compose -f \"apps/backend-template/docker-compose-firebase.yml\" up -d --build", + "docker:up:aurora": "docker compose -f \"apps/backend-template/docker-compose-aurora.yml\" up -d --build", + "docker:up:rds": "docker compose -f \"apps/backend-template/docker-compose-rds.yml\" up -d --build", + "docker:down:postgresql": "docker compose -f \"apps/backend-template/docker-compose-postgresql.yml\" down", + "docker:down:mysql": "docker compose -f \"apps/backend-template/docker-compose-mysql.yml\" down", + "docker:down:mssql": "docker compose -f \"apps/backend-template/docker-compose-mssql.yml\" down", + "docker:down:oracle": "docker compose -f \"apps/backend-template/docker-compose-oracle.yml\" down", + "docker:down:mongodb": "docker compose -f \"apps/backend-template/docker-compose-mongodb.yml\" down", + "docker:down:cassandra": "docker compose -f \"apps/backend-template/docker-compose-cassandra.yml\" down", + "docker:down:dynamodb": "docker compose -f \"apps/backend-template/docker-compose-dynamodb.yml\" down", + "docker:down:firebase": "docker compose -f \"apps/backend-template/docker-compose-firebase.yml\" down", + "docker:down:aurora": "docker compose -f \"apps/backend-template/docker-compose-aurora.yml\" down", + "docker:down:rds": "docker compose -f \"apps/backend-template/docker-compose-rds.yml\" down", + "docker:compose:service-template": "docker compose -f \"apps/backend-template/docker-compose-service-templates.yml\" --profile ${AAA_SERVICE_PROFILE:-rest} up -d --build", + "docker:composerabbit": "docker compose -f \"apps/backend-template/docker-compose-messaging.yml\" up -d rabbitmq", "docker:stop": "docker-compose stop", - "docker:restart": "docker compose -f \"docker-compose-redis.yml\" down && npm run docker:composeredis", - "docker:restart:messaging": "docker compose -f \"docker-compose-messaging.yml\" down && npm run docker:composemessaging", - "docker:stop:platform-services": "docker compose -f \"docker-compose-platform-services.yml\" down", - "docker:stop:service-template": "docker compose -f \"docker-compose-service-templates.yml\" down", + "docker:restart": "docker compose -f \"apps/backend-template/docker-compose-redis.yml\" down && npm run docker:composeredis", + "docker:restart:messaging": "docker compose -f \"apps/backend-template/docker-compose-messaging.yml\" down && npm run docker:composemessaging", + "smoke:realtime:redis-streams": "npm run docker:composeredis && npm run test:integration:realtime:redis-streams && docker compose -f \"apps/backend-template/docker-compose-redis.yml\" down", + "docker:stop:platform-services": "docker compose -f \"apps/backend-template/docker-compose-platform-services.yml\" down", + "docker:stop:service-template": "docker compose -f \"apps/backend-template/docker-compose-service-templates.yml\" down", "docker:clean": "docker system prune -a" }, "dependencies": { @@ -183,6 +219,8 @@ "@fastify/static": "^9.1.3", "@grpc/grpc-js": "^1.14.1", "@grpc/proto-loader": "^0.8.0", + "@socket.io/cluster-adapter": "^0.3.0", + "@socket.io/redis-streams-adapter": "^0.3.1", "amqplib": "^2.0.1", "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", @@ -208,6 +246,7 @@ "postgres": "^3.4.7", "redis": "^5.9.0", "reflect-metadata": "^0.2.2", + "restify": "^11.1.0", "semver": "^7.6.3", "sequelize": "^6.37.7", "serverless": "^4.4.4", diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 00000000..ecc1f4cf --- /dev/null +++ b/packages/README.md @@ -0,0 +1,29 @@ +# Jumentix Workspace Packages + +This folder contains reusable npm packages shared across Jumentix applications. + +## Package Index + +- `@jumentix/cli-init` - bootstrap CLI for creating project structures. +- `@jumentix/message-mediator` - contract-based event/request-response mediator. +- `@jumentix/key-value-storage` - key-value storage contracts and adapters. +- `@jumentix/mutex-service` - distributed lock service contract layer. +- `@jumentix/persistence-contracts` - `IDatabaseClient` and `IStore` abstractions. +- `@jumentix/external-persistence-core` - base repository contracts/implementations. +- `@jumentix/external-store-proxy` - translates DB native clients to `IStore`. +- `@jumentix/external-db-repositories` - DB repository adapters. +- `@jumentix/database-client-factory` - database client compilation per driver. +- `@jumentix/runtime-infra` - runtime environment infra helpers. +- `@jumentix/adapter-runtime-bootstrap` - shared adapter bootstrap composition. +- `@jumentix/sdk-rest-client` - REST SDK client. +- `@jumentix/sdk-websocket-client` - WebSocket SDK client. +- `@jumentix/sdk-grpc-client` - gRPC SDK client. + +## Usage Pattern + +Each package has its own `README.md`, scripts, and ownership boundaries. Import packages from applications instead of duplicating adapter logic in each app. + +## Related Docs + +- [Jumentix Workspace Packages (Architecture)](../documentation/md/JUMENTIX-WORKSPACE-PACKAGES.md) +- [SDK Compatibility Bridge](../documentation/md/SDK-COMPATIBILITY-BRIDGE.md) diff --git a/packages/adapter-runtime-bootstrap/README.md b/packages/adapter-runtime-bootstrap/README.md new file mode 100644 index 00000000..2f90ca04 --- /dev/null +++ b/packages/adapter-runtime-bootstrap/README.md @@ -0,0 +1,7 @@ +# @jumentix/adapter-runtime-bootstrap + +Shared runtime bootstrap helpers for HTTP, WebSocket, and gRPC adapters. + +## Exports + +- `compileAdapterRuntime` diff --git a/packages/adapter-runtime-bootstrap/package.json b/packages/adapter-runtime-bootstrap/package.json new file mode 100644 index 00000000..52b7a6a6 --- /dev/null +++ b/packages/adapter-runtime-bootstrap/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jumentix/adapter-runtime-bootstrap", + "version": "0.1.0", + "private": true, + "description": "Shared adapter runtime bootstrap compiler for JumentiX services.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/packages/adapter-runtime-bootstrap/src/compileAdapterRuntime.ts b/packages/adapter-runtime-bootstrap/src/compileAdapterRuntime.ts new file mode 100644 index 00000000..4e83d16a --- /dev/null +++ b/packages/adapter-runtime-bootstrap/src/compileAdapterRuntime.ts @@ -0,0 +1,80 @@ +import { compileRuntimeInfra } from '@jumentix/runtime-infra'; + +export interface ICompileAdapterRuntimeOptions< + TDatabaseClient, + TKeyValueStorageClient, + TMutexService, + TPasswordCryptoService, + TJwtService, + TMessageMediator, + TAuthService +> { + compileDatabaseClient: () => TDatabaseClient; + compileKeyValueStorageClient: (driver?: string) => TKeyValueStorageClient; + compileMutexService: (keyValueStorageClient: TKeyValueStorageClient) => TMutexService; + compilePasswordCryptoService: () => TPasswordCryptoService; + compileJwtService: () => TJwtService; + compileMessageMediator: () => TMessageMediator; + composeAuthServices: (deps: { + databaseClient: TDatabaseClient; + passwordCryptoService: TPasswordCryptoService; + mutexService: TMutexService; + jwtService: TJwtService; + keyValueStorageClient: TKeyValueStorageClient; + messageMediator: TMessageMediator; + }) => { + authService: TAuthService; + }; + env?: NodeJS.ProcessEnv; +} + +export function compileAdapterRuntime< + TDatabaseClient, + TKeyValueStorageClient, + TMutexService, + TPasswordCryptoService, + TJwtService, + TMessageMediator, + TAuthService +>( + options: ICompileAdapterRuntimeOptions< + TDatabaseClient, + TKeyValueStorageClient, + TMutexService, + TPasswordCryptoService, + TJwtService, + TMessageMediator, + TAuthService + > +) { + const passwordCryptoService = options.compilePasswordCryptoService(); + const jwtService = options.compileJwtService(); + const messageMediator = options.compileMessageMediator(); + const { databaseClient, keyValueStorageClient, mutexService } = compileRuntimeInfra( + { + compileDatabaseClient: options.compileDatabaseClient, + compileKeyValueStorageClient: options.compileKeyValueStorageClient, + compileMutexService: options.compileMutexService + }, + options.env + ); + + const { authService } = options.composeAuthServices({ + databaseClient, + passwordCryptoService, + mutexService, + jwtService, + keyValueStorageClient, + messageMediator + }); + + return { + databaseClient, + keyValueStorageClient, + mutexService, + passwordCryptoService, + jwtService, + messageMediator, + authService + }; +} diff --git a/packages/adapter-runtime-bootstrap/src/index.ts b/packages/adapter-runtime-bootstrap/src/index.ts new file mode 100644 index 00000000..9f6ad052 --- /dev/null +++ b/packages/adapter-runtime-bootstrap/src/index.ts @@ -0,0 +1 @@ +export * from './compileAdapterRuntime'; diff --git a/packages/adapter-runtime-bootstrap/tsconfig.json b/packages/adapter-runtime-bootstrap/tsconfig.json new file mode 100644 index 00000000..ce373ceb --- /dev/null +++ b/packages/adapter-runtime-bootstrap/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/cli-init/README.md b/packages/cli-init/README.md new file mode 100644 index 00000000..d717376a --- /dev/null +++ b/packages/cli-init/README.md @@ -0,0 +1,38 @@ +# @jumentix/cli-init + +Bootstrap CLI package for JumentiX project scaffolding. + +## Commands + +- `jumentix-init` +- `aaa-bootstrap` (compatibility alias) + +## Behavior + +- Prompts for service type/profile. +- Clones repository template. +- Generates `.aaa/service-profile.json`. +- Optionally installs dependencies in target project. +- Supports non-interactive automation via CLI flags. + +## Non-interactive usage + +```bash +node ./packages/cli-init/bin/jumentix-init.js \ + --service-type=rest \ + --project-name=my-service \ + --git-branch=main \ + --install-deps=false +``` + +## Help + +```bash +node ./packages/cli-init/bin/jumentix-init.js --help +``` + +## Run local package entrypoint + +```bash +node ./packages/cli-init/bin/jumentix-init.js +``` diff --git a/packages/cli-init/bin/jumentix-init.js b/packages/cli-init/bin/jumentix-init.js new file mode 100644 index 00000000..aa339036 --- /dev/null +++ b/packages/cli-init/bin/jumentix-init.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +const { run } = require('../src/bootstrap'); + +run().catch((error) => { + console.error(`\nBootstrap failed: ${error.message}`); + process.exit(1); +}); + diff --git a/packages/cli-init/package.json b/packages/cli-init/package.json new file mode 100644 index 00000000..85b69912 --- /dev/null +++ b/packages/cli-init/package.json @@ -0,0 +1,20 @@ +{ + "name": "@jumentix/cli-init", + "version": "0.0.0", + "private": true, + "description": "JumentiX bootstrap CLI package", + "bin": { + "jumentix-init": "./bin/jumentix-init.js", + "aaa-bootstrap": "./bin/jumentix-init.js" + }, + "files": [ + "bin", + "src" + ], + "scripts": { + "build": "echo \"cli-init is plain node script: no build step required\"", + "test": "node ./bin/jumentix-init.js --help", + "lint": "echo \"cli-init: lint managed by root workspace\"", + "typecheck": "echo \"cli-init: plain JS entrypoint\"" + } +} diff --git a/packages/cli-init/src/bootstrap.js b/packages/cli-init/src/bootstrap.js new file mode 100644 index 00000000..62179714 --- /dev/null +++ b/packages/cli-init/src/bootstrap.js @@ -0,0 +1,229 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); +const { spawnSync } = require('child_process'); + +const BOILERPLATE_REPOSITORY = 'https://github.com/web2solutions/aaa-typescript-boilerplate.git'; +const SERVICE_TYPES = [ + { + id: 'rest', + label: 'HTTP/REST server (OpenAPI/Swagger + static assets)', + profile: { interface: 'http-rest', staticAssets: true, functions: false } + }, + { + id: 'websocket', + label: 'WebSocket server (+ static assets)', + profile: { interface: 'websocket', staticAssets: true, functions: false } + }, + { + id: 'grpc', + label: 'gRPC server (+ static assets)', + profile: { interface: 'grpc', staticAssets: true, functions: false } + }, + { + id: 'graphql', + label: 'GraphQL server (+ static assets)', + profile: { interface: 'graphql', staticAssets: true, functions: false } + }, + { + id: 'functions', + label: 'Function services bundle (AWS/Google/Azure/Vercel/Cloudflare)', + profile: { interface: 'functions', staticAssets: false, functions: true } + } +]; + +function printHelp() { + console.log(` +JumentiX Bootstrap CLI + +Usage: + jumentix-init [options] + +Options: + --help Show this message and exit + --non-interactive Disable prompts (requires --service-type and --project-name) + --service-type= One of: rest, websocket, grpc, graphql, functions + --project-name= Target folder name/path + --git-branch= Branch to clone (default: main) + --install-deps= Install dependencies after scaffold (default: true) + --repo= Override template repository URL +`); +} + +function parseCliArgs(argv) { + const args = { + help: false, + nonInteractive: false, + serviceTypeId: '', + projectName: '', + gitBranch: '', + installDeps: undefined, + repository: '' + }; + + for (const rawArg of argv) { + if (rawArg === '--help' || rawArg === '-h') { + args.help = true; + continue; + } + + if (rawArg === '--non-interactive') { + args.nonInteractive = true; + continue; + } + + if (rawArg.startsWith('--service-type=')) { + args.serviceTypeId = rawArg.split('=')[1] || ''; + continue; + } + + if (rawArg.startsWith('--project-name=')) { + args.projectName = rawArg.split('=')[1] || ''; + continue; + } + + if (rawArg.startsWith('--git-branch=')) { + args.gitBranch = rawArg.split('=')[1] || ''; + continue; + } + + if (rawArg.startsWith('--install-deps=')) { + const installDepsRaw = (rawArg.split('=')[1] || '').toLowerCase(); + args.installDeps = installDepsRaw === 'y' || installDepsRaw === 'yes' || installDepsRaw === 'true' || installDepsRaw === '1'; + continue; + } + + if (rawArg.startsWith('--repo=')) { + args.repository = rawArg.split('=')[1] || ''; + } + } + + return args; +} + +function createPrompt() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const ask = (question) => new Promise((resolve) => { + rl.question(question, (answer) => resolve(String(answer || '').trim())); + }); + + return { + ask, + close: () => rl.close() + }; +} + +function runCommand(command, args, cwd) { + const result = spawnSync(command, args, { + cwd, + stdio: 'inherit' + }); + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${String(result.status)}`); + } +} + +function toAbsolute(targetPath) { + if (path.isAbsolute(targetPath)) return targetPath; + return path.resolve(process.cwd(), targetPath); +} + +function ensureTargetFolderIsEmpty(targetPath) { + if (!fs.existsSync(targetPath)) return; + const files = fs.readdirSync(targetPath); + if (files.length > 0) { + throw new Error(`Target folder "${targetPath}" already exists and is not empty.`); + } +} + +async function chooseServiceType(ask) { + console.log('\nSelect service type:'); + SERVICE_TYPES.forEach((type, index) => { + console.log(` ${index + 1}. ${type.label}`); + }); + const selected = await ask('Type number: '); + const index = Number(selected) - 1; + if (!Number.isInteger(index) || index < 0 || index >= SERVICE_TYPES.length) { + throw new Error('Invalid service type selection.'); + } + return SERVICE_TYPES[index]; +} + +function resolveServiceTypeById(serviceTypeId) { + const found = SERVICE_TYPES.find((serviceType) => serviceType.id === serviceTypeId); + if (!found) { + throw new Error(`Invalid service type "${serviceTypeId}".`); + } + return found; +} + +function writeBootstrapProfile(targetPath, payload) { + const configDir = path.join(targetPath, '.aaa'); + fs.mkdirSync(configDir, { recursive: true }); + const outputPath = path.join(configDir, 'service-profile.json'); + fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} + +async function run() { + const cliArgs = parseCliArgs(process.argv.slice(2)); + if (cliArgs.help) { + printHelp(); + return; + } + + if (cliArgs.nonInteractive && (!cliArgs.serviceTypeId || !cliArgs.projectName)) { + throw new Error('Non-interactive mode requires --service-type and --project-name.'); + } + + const prompt = cliArgs.nonInteractive + ? { ask: async () => '', close: () => {} } + : createPrompt(); + + try { + console.log('\nJumentiX Bootstrap CLI'); + const serviceType = cliArgs.serviceTypeId + ? resolveServiceTypeById(cliArgs.serviceTypeId) + : await chooseServiceType(prompt.ask); + + const projectName = cliArgs.projectName || await prompt.ask('Project folder name (e.g. my-service): '); + if (!projectName) throw new Error('Project folder name is required.'); + const targetPath = toAbsolute(projectName); + ensureTargetFolderIsEmpty(targetPath); + + const gitBranch = cliArgs.gitBranch || (await prompt.ask('Git branch to clone (default: main): ')) || 'main'; + const installDeps = typeof cliArgs.installDeps === 'boolean' + ? cliArgs.installDeps + : (((await prompt.ask('Run npm install after scaffold? (Y/n): ')) || 'y').toLowerCase() !== 'n'); + const repository = cliArgs.repository || BOILERPLATE_REPOSITORY; + + console.log('\nCloning boilerplate repository...'); + runCommand('git', ['clone', '--branch', gitBranch, repository, targetPath], process.cwd()); + + writeBootstrapProfile(targetPath, { + generatedAt: new Date().toISOString(), + template: 'aaa-typescript-boilerplate', + repository, + branch: gitBranch, + serviceType: serviceType.id, + profile: serviceType.profile + }); + + if (installDeps) { + console.log('\nInstalling dependencies...'); + runCommand('npm', ['install'], targetPath); + } + + console.log('\nScaffold completed successfully.'); + console.log(`Project path: ${targetPath}`); + console.log(`Profile: ${path.join(targetPath, '.aaa', 'service-profile.json')}`); + } finally { + prompt.close(); + } +} + +module.exports = { run }; diff --git a/packages/config-eslint/index.cjs b/packages/config-eslint/index.cjs new file mode 100644 index 00000000..b32dfbeb --- /dev/null +++ b/packages/config-eslint/index.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: false, + extends: [] +}; diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json new file mode 100644 index 00000000..ae4d6e03 --- /dev/null +++ b/packages/config-eslint/package.json @@ -0,0 +1,15 @@ +{ + "name": "@jumentix/config-eslint", + "version": "0.0.0", + "private": true, + "description": "JumentiX shared ESLint configuration placeholder", + "files": [ + "index.cjs" + ], + "scripts": { + "build": "echo \"config-eslint placeholder: migration wave pending\"", + "test": "echo \"config-eslint placeholder: migration wave pending\"", + "lint": "echo \"config-eslint placeholder: migration wave pending\"", + "typecheck": "echo \"config-eslint placeholder: migration wave pending\"" + } +} diff --git a/packages/config-jest/index.cjs b/packages/config-jest/index.cjs new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/packages/config-jest/index.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/packages/config-jest/package.json b/packages/config-jest/package.json new file mode 100644 index 00000000..a9cdd9e4 --- /dev/null +++ b/packages/config-jest/package.json @@ -0,0 +1,15 @@ +{ + "name": "@jumentix/config-jest", + "version": "0.0.0", + "private": true, + "description": "JumentiX shared Jest configuration placeholder", + "files": [ + "index.cjs" + ], + "scripts": { + "build": "echo \"config-jest placeholder: migration wave pending\"", + "test": "echo \"config-jest placeholder: migration wave pending\"", + "lint": "echo \"config-jest placeholder: migration wave pending\"", + "typecheck": "echo \"config-jest placeholder: migration wave pending\"" + } +} diff --git a/packages/config-ts/package.json b/packages/config-ts/package.json new file mode 100644 index 00000000..9dd3c9e3 --- /dev/null +++ b/packages/config-ts/package.json @@ -0,0 +1,15 @@ +{ + "name": "@jumentix/config-ts", + "version": "0.0.0", + "private": true, + "description": "JumentiX shared TypeScript configuration placeholder", + "files": [ + "tsconfig.base.json" + ], + "scripts": { + "build": "echo \"config-ts placeholder: migration wave pending\"", + "test": "echo \"config-ts placeholder: migration wave pending\"", + "lint": "echo \"config-ts placeholder: migration wave pending\"", + "typecheck": "echo \"config-ts placeholder: migration wave pending\"" + } +} diff --git a/packages/config-ts/tsconfig.base.json b/packages/config-ts/tsconfig.base.json new file mode 100644 index 00000000..6f767dd5 --- /dev/null +++ b/packages/config-ts/tsconfig.base.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + } +} diff --git a/packages/database-client-factory/README.md b/packages/database-client-factory/README.md new file mode 100644 index 00000000..1f6a6059 --- /dev/null +++ b/packages/database-client-factory/README.md @@ -0,0 +1,9 @@ +# @jumentix/database-client-factory + +Reusable compiler factory for selecting database clients by environment driver. + +## Exports + +- `buildDatabaseClientCompilers` +- `DriverName` +- `IDatabaseClientLike` diff --git a/packages/database-client-factory/package.json b/packages/database-client-factory/package.json new file mode 100644 index 00000000..1740016b --- /dev/null +++ b/packages/database-client-factory/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jumentix/database-client-factory", + "version": "0.1.0", + "private": true, + "description": "Database client compiler factory for JumentiX runtimes.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/packages/database-client-factory/src/compileDatabaseClient.ts b/packages/database-client-factory/src/compileDatabaseClient.ts new file mode 100644 index 00000000..36057247 --- /dev/null +++ b/packages/database-client-factory/src/compileDatabaseClient.ts @@ -0,0 +1,262 @@ +import { BaseExternalDataRepository } from '@jumentix/external-persistence-core'; +import { + AuroraRepository, + CassandraRepository, + DynamoDbRepository, + FirebaseRepository, + MongoMongooseRepository, + OracleRepository, + RdsRepository, + SqlSequelizeRepository +} from '@jumentix/external-db-repositories'; +import { createExternalStores } from '@jumentix/external-store-proxy'; + +export type DriverName = + | 'InMemory' + | 'Mongo' + | 'PostgreSQL' + | 'MySQL' + | 'MSSQL' + | 'Oracle' + | 'SQLite' + | 'DynamoDB' + | 'Cassandra' + | 'Firebase' + | 'Aurora' + | 'RDS'; + +type ExternalDriverName = Exclude; + +export interface IDatabaseClientLike { + connect(): Promise; + disconnect(): Promise; + stores: Record; +} + +export interface IBuildDatabaseClientCompilersOptions { + inMemoryClient: TDatabaseClient; +} + +const DEFAULT_DRIVER: DriverName = 'InMemory'; + +const SQL_DIALECT_TO_DRIVER: Record< + 'postgres' | 'mysql' | 'mssql' | 'oracle' | 'sqlite', + ExternalDriverName +> = { + postgres: 'PostgreSQL', + mysql: 'MySQL', + mssql: 'MSSQL', + oracle: 'Oracle', + sqlite: 'SQLite' +}; + +const sanitize = (value: string): string => value.trim().toLowerCase(); + +const normalizeDriver = (value?: string): DriverName => { + if (!value || value.trim() === '') return DEFAULT_DRIVER; + const normalized = sanitize(value); + if (['inmemory', 'in-memory', 'memory'].includes(normalized)) return 'InMemory'; + if (['mongo', 'mongodb', 'mongoose'].includes(normalized)) return 'Mongo'; + if (['postgres', 'postgresql'].includes(normalized)) return 'PostgreSQL'; + if (['mysql'].includes(normalized)) return 'MySQL'; + if (['mssql', 'ms sql', 'sqlserver', 'sql server'].includes(normalized)) return 'MSSQL'; + if (['oracle'].includes(normalized)) return 'Oracle'; + if (['sqlite', 'sql lite'].includes(normalized)) return 'SQLite'; + if (['dynamodb', 'dynamo'].includes(normalized)) return 'DynamoDB'; + if (['cassandra'].includes(normalized)) return 'Cassandra'; + if (['firebase'].includes(normalized)) return 'Firebase'; + if (['aurora'].includes(normalized)) return 'Aurora'; + if (['rds'].includes(normalized)) return 'RDS'; + return DEFAULT_DRIVER; +}; + +const parseNumber = (value: string | undefined, fallback: number): number => { + if (!value || value.trim() === '') return fallback; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +}; + +const parseContactPoints = (value: string | undefined): string[] => { + if (!value || value.trim() === '') return ['127.0.0.1']; + return value + .split(',') + .map((item) => item.trim()) + .filter((item) => item.length > 0); +}; + +const parseJson = (value: string | undefined): Record | undefined => { + if (!value || value.trim() === '') return undefined; + try { + const parsed = JSON.parse(value); + if (parsed && typeof parsed === 'object') { + return parsed as Record; + } + } catch (_error) { + return undefined; + } + return undefined; +}; + +const toExternalClient = ( + driver: ExternalDriverName, + connector: BaseExternalDataRepository +): TDatabaseClient => { + return { + stores: createExternalStores(driver, connector), + connect: () => connector.connect(), + disconnect: () => connector.disconnect() + } as unknown as TDatabaseClient; +}; + +export const buildDatabaseClientCompilers = ({ + inMemoryClient +}: IBuildDatabaseClientCompilersOptions) => { + const createSqlClient = ( + dialect: 'postgres' | 'mysql' | 'mssql' | 'oracle' | 'sqlite' + ): TDatabaseClient => { + const connector = new SqlSequelizeRepository({ + dialect, + connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, + database: process.env.AAA_DATABASE_NAME, + extra: { + poolMax: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 15), + poolMin: parseNumber(process.env.AAA_DATABASE_POOL_MIN, 0), + poolAcquireMs: parseNumber(process.env.AAA_DATABASE_POOL_ACQUIRE_MS, 30000), + poolIdleMs: parseNumber(process.env.AAA_DATABASE_POOL_IDLE_MS, 10000), + poolEvictMs: parseNumber(process.env.AAA_DATABASE_POOL_EVICT_MS, 1000) + } + }); + return toExternalClient(SQL_DIALECT_TO_DRIVER[dialect], connector); + }; + + const createMongoClient = (): TDatabaseClient => { + const connector = new MongoMongooseRepository({ + connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, + database: process.env.AAA_DATABASE_NAME, + extra: { + maxPoolSize: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 20), + minPoolSize: parseNumber(process.env.AAA_DATABASE_POOL_MIN, 0), + serverSelectionTimeoutMS: parseNumber(process.env.AAA_DATABASE_SERVER_SELECTION_MS, 5000), + socketTimeoutMS: parseNumber(process.env.AAA_DATABASE_SOCKET_TIMEOUT_MS, 45000) + } + }); + return toExternalClient('Mongo', connector); + }; + + const createDynamoClient = (): TDatabaseClient => { + const connector = new DynamoDbRepository({ + region: process.env.AAA_DATABASE_REGION || 'us-east-1', + endpoint: process.env.AAA_DATABASE_ENDPOINT + }); + return toExternalClient('DynamoDB', connector); + }; + + const createCassandraClient = (): TDatabaseClient => { + const connector = new CassandraRepository({ + database: process.env.AAA_DATABASE_NAME, + extra: { + contactPoints: parseContactPoints(process.env.AAA_DATABASE_CASSANDRA_CONTACT_POINTS), + localDataCenter: process.env.AAA_DATABASE_CASSANDRA_DATACENTER || 'datacenter1', + keyspace: process.env.AAA_DATABASE_NAME + } + }); + return toExternalClient('Cassandra', connector); + }; + + const createFirebaseClient = (): TDatabaseClient => { + const connector = new FirebaseRepository({ + extra: { + projectId: process.env.AAA_DATABASE_PROJECT_ID, + serviceAccount: parseJson(process.env.AAA_FIREBASE_SERVICE_ACCOUNT_JSON) + } + }); + return toExternalClient('Firebase', connector); + }; + + const createAuroraClient = (): TDatabaseClient => { + const connector = new AuroraRepository({ + connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, + database: process.env.AAA_DATABASE_NAME, + region: process.env.AAA_DATABASE_REGION || 'us-east-1', + endpoint: process.env.AAA_DATABASE_ENDPOINT, + extra: { + poolMax: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 20) + } + }); + return toExternalClient('Aurora', connector); + }; + + const createOracleClient = (): TDatabaseClient => { + const connector = new OracleRepository({ + connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, + database: process.env.AAA_DATABASE_NAME, + extra: { + user: process.env.AAA_DATABASE_USER || 'aaa', + password: process.env.AAA_DATABASE_PASSWORD || 'aaa', + connectString: process.env.AAA_DATABASE_CONNECT_STRING + } + }); + return toExternalClient('Oracle', connector); + }; + + const createRdsClient = (): TDatabaseClient => { + const dialectRaw = sanitize(process.env.AAA_DATABASE_DIALECT || 'postgres'); + const dialect = (['postgres', 'mysql', 'mssql', 'oracle', 'sqlite'].includes(dialectRaw) + ? dialectRaw + : 'postgres') as 'postgres' | 'mysql' | 'mssql' | 'oracle' | 'sqlite'; + const connector = new RdsRepository({ + connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, + database: process.env.AAA_DATABASE_NAME, + extra: { + dialect, + poolMax: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 20), + poolMin: parseNumber(process.env.AAA_DATABASE_POOL_MIN, 0), + poolAcquireMs: parseNumber(process.env.AAA_DATABASE_POOL_ACQUIRE_MS, 30000), + poolIdleMs: parseNumber(process.env.AAA_DATABASE_POOL_IDLE_MS, 10000), + poolEvictMs: parseNumber(process.env.AAA_DATABASE_POOL_EVICT_MS, 1000) + } + }); + return toExternalClient('RDS', connector); + }; + + const buildByDriver = (driver: DriverName): TDatabaseClient => { + if (driver === 'InMemory') return inMemoryClient; + if (driver === 'Mongo') return createMongoClient(); + if (driver === 'PostgreSQL') return createSqlClient('postgres'); + if (driver === 'MySQL') return createSqlClient('mysql'); + if (driver === 'MSSQL') return createSqlClient('mssql'); + if (driver === 'Oracle') return createOracleClient(); + if (driver === 'SQLite') return createSqlClient('sqlite'); + if (driver === 'DynamoDB') return createDynamoClient(); + if (driver === 'Cassandra') return createCassandraClient(); + if (driver === 'Firebase') return createFirebaseClient(); + if (driver === 'Aurora') return createAuroraClient(); + if (driver === 'RDS') return createRdsClient(); + return inMemoryClient; + }; + + const compileDatabaseClient = (): TDatabaseClient => { + const driver = normalizeDriver(process.env.AAA_DATABASE_DRIVER); + return buildByDriver(driver); + }; + + const compileDatabaseClientByDriver = (driver: string): TDatabaseClient => { + return buildByDriver(normalizeDriver(driver)); + }; + + return { + compileDatabaseClient, + compileDatabaseClientByDriver, + compileMongoDbClient: (): TDatabaseClient => buildByDriver('Mongo'), + compilePostgreSqlDbClient: (): TDatabaseClient => buildByDriver('PostgreSQL'), + compileMySqlDbClient: (): TDatabaseClient => buildByDriver('MySQL'), + compileMsSqlDbClient: (): TDatabaseClient => buildByDriver('MSSQL'), + compileOracleDbClient: (): TDatabaseClient => buildByDriver('Oracle'), + compileSqliteDbClient: (): TDatabaseClient => buildByDriver('SQLite'), + compileDynamoDbClient: (): TDatabaseClient => buildByDriver('DynamoDB'), + compileCassandraDbClient: (): TDatabaseClient => buildByDriver('Cassandra'), + compileFirebaseDbClient: (): TDatabaseClient => buildByDriver('Firebase'), + compileAuroraDbClient: (): TDatabaseClient => buildByDriver('Aurora'), + compileRdsDbClient: (): TDatabaseClient => buildByDriver('RDS') + }; +}; diff --git a/packages/database-client-factory/src/index.ts b/packages/database-client-factory/src/index.ts new file mode 100644 index 00000000..f15418ab --- /dev/null +++ b/packages/database-client-factory/src/index.ts @@ -0,0 +1 @@ +export * from './compileDatabaseClient'; diff --git a/packages/database-client-factory/src/shims.d.ts b/packages/database-client-factory/src/shims.d.ts new file mode 100644 index 00000000..70896d30 --- /dev/null +++ b/packages/database-client-factory/src/shims.d.ts @@ -0,0 +1,11 @@ +declare module 'mongoose'; +declare module 'sequelize'; +declare module '@aws-sdk/client-dynamodb'; +declare module 'cassandra-driver'; +declare module 'firebase-admin/app'; +declare module 'firebase-admin/firestore'; +declare module 'postgres'; +declare module '@aws/aurora-dsql-postgresjs'; +declare module '@aws/aurora-dsql-connector'; +declare module '@aws/aurora-dsql'; +declare module 'oracledb'; diff --git a/packages/database-client-factory/tsconfig.json b/packages/database-client-factory/tsconfig.json new file mode 100644 index 00000000..ce373ceb --- /dev/null +++ b/packages/database-client-factory/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/external-db-repositories/README.md b/packages/external-db-repositories/README.md new file mode 100644 index 00000000..df4994c1 --- /dev/null +++ b/packages/external-db-repositories/README.md @@ -0,0 +1,14 @@ +# @jumentix/external-db-repositories + +Reusable external database connection adapters for JumentiX runtimes. + +## Exports + +- `MongoMongooseRepository` +- `SqlSequelizeRepository` +- `DynamoDbRepository` +- `CassandraRepository` +- `FirebaseRepository` +- `AuroraRepository` +- `RdsRepository` +- `OracleRepository` diff --git a/packages/external-db-repositories/package.json b/packages/external-db-repositories/package.json new file mode 100644 index 00000000..5d51ccfd --- /dev/null +++ b/packages/external-db-repositories/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jumentix/external-db-repositories", + "version": "0.1.0", + "private": true, + "description": "Reusable external database repository adapters for JumentiX.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/src/infra/persistence/external/AuroraRepository.ts b/packages/external-db-repositories/src/AuroraRepository.ts similarity index 97% rename from src/infra/persistence/external/AuroraRepository.ts rename to packages/external-db-repositories/src/AuroraRepository.ts index cb3db4a3..d0466fde 100644 --- a/src/infra/persistence/external/AuroraRepository.ts +++ b/packages/external-db-repositories/src/AuroraRepository.ts @@ -1,4 +1,4 @@ -import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@src/infra/persistence/external/BaseExternalDataRepository'; +import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@jumentix/external-persistence-core'; export class AuroraRepository extends BaseExternalDataRepository { private client: any | null = null; diff --git a/src/infra/persistence/external/CassandraRepository.ts b/packages/external-db-repositories/src/CassandraRepository.ts similarity index 96% rename from src/infra/persistence/external/CassandraRepository.ts rename to packages/external-db-repositories/src/CassandraRepository.ts index 03dc6368..228ee609 100644 --- a/src/infra/persistence/external/CassandraRepository.ts +++ b/packages/external-db-repositories/src/CassandraRepository.ts @@ -1,4 +1,4 @@ -import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@src/infra/persistence/external/BaseExternalDataRepository'; +import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@jumentix/external-persistence-core'; export class CassandraRepository extends BaseExternalDataRepository { private client: any | null = null; diff --git a/src/infra/persistence/external/DynamoDbRepository.ts b/packages/external-db-repositories/src/DynamoDbRepository.ts similarity index 95% rename from src/infra/persistence/external/DynamoDbRepository.ts rename to packages/external-db-repositories/src/DynamoDbRepository.ts index 9a0c8312..bb70e290 100644 --- a/src/infra/persistence/external/DynamoDbRepository.ts +++ b/packages/external-db-repositories/src/DynamoDbRepository.ts @@ -1,4 +1,4 @@ -import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@src/infra/persistence/external/BaseExternalDataRepository'; +import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@jumentix/external-persistence-core'; export class DynamoDbRepository extends BaseExternalDataRepository { private client: any | null = null; diff --git a/src/infra/persistence/external/FirebaseRepository.ts b/packages/external-db-repositories/src/FirebaseRepository.ts similarity index 96% rename from src/infra/persistence/external/FirebaseRepository.ts rename to packages/external-db-repositories/src/FirebaseRepository.ts index 70b44a6b..fc1b4efe 100644 --- a/src/infra/persistence/external/FirebaseRepository.ts +++ b/packages/external-db-repositories/src/FirebaseRepository.ts @@ -1,4 +1,4 @@ -import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@src/infra/persistence/external/BaseExternalDataRepository'; +import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@jumentix/external-persistence-core'; export class FirebaseRepository extends BaseExternalDataRepository { private app: any | null = null; diff --git a/src/infra/persistence/external/MongoMongooseRepository.ts b/packages/external-db-repositories/src/MongoMongooseRepository.ts similarity index 95% rename from src/infra/persistence/external/MongoMongooseRepository.ts rename to packages/external-db-repositories/src/MongoMongooseRepository.ts index c42eed2e..559b25ed 100644 --- a/src/infra/persistence/external/MongoMongooseRepository.ts +++ b/packages/external-db-repositories/src/MongoMongooseRepository.ts @@ -1,4 +1,4 @@ -import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@src/infra/persistence/external/BaseExternalDataRepository'; +import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@jumentix/external-persistence-core'; export class MongoMongooseRepository extends BaseExternalDataRepository { private mongooseModule: any | null = null; diff --git a/src/infra/persistence/external/OracleRepository.ts b/packages/external-db-repositories/src/OracleRepository.ts similarity index 96% rename from src/infra/persistence/external/OracleRepository.ts rename to packages/external-db-repositories/src/OracleRepository.ts index 6e975474..97e7eaec 100644 --- a/src/infra/persistence/external/OracleRepository.ts +++ b/packages/external-db-repositories/src/OracleRepository.ts @@ -1,4 +1,4 @@ -import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@src/infra/persistence/external/BaseExternalDataRepository'; +import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@jumentix/external-persistence-core'; export class OracleRepository extends BaseExternalDataRepository { private connection: any | null = null; diff --git a/src/infra/persistence/external/RdsRepository.ts b/packages/external-db-repositories/src/RdsRepository.ts similarity index 96% rename from src/infra/persistence/external/RdsRepository.ts rename to packages/external-db-repositories/src/RdsRepository.ts index d444803a..db5b3bf1 100644 --- a/src/infra/persistence/external/RdsRepository.ts +++ b/packages/external-db-repositories/src/RdsRepository.ts @@ -1,4 +1,4 @@ -import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@src/infra/persistence/external/BaseExternalDataRepository'; +import { BaseExternalDataRepository, IRepositoryConnectionOptions } from '@jumentix/external-persistence-core'; export class RdsRepository extends BaseExternalDataRepository { private sequelize: any | null = null; diff --git a/src/infra/persistence/external/SqlSequelizeRepository.ts b/packages/external-db-repositories/src/SqlSequelizeRepository.ts similarity index 96% rename from src/infra/persistence/external/SqlSequelizeRepository.ts rename to packages/external-db-repositories/src/SqlSequelizeRepository.ts index ae00b457..847fe306 100644 --- a/src/infra/persistence/external/SqlSequelizeRepository.ts +++ b/packages/external-db-repositories/src/SqlSequelizeRepository.ts @@ -1,7 +1,7 @@ import { BaseExternalDataRepository, IRepositoryConnectionOptions -} from '@src/infra/persistence/external/BaseExternalDataRepository'; +} from '@jumentix/external-persistence-core'; export type ESqlDialect = 'postgres' | 'mysql' | 'mssql' | 'oracle' | 'sqlite'; diff --git a/packages/external-db-repositories/src/index.ts b/packages/external-db-repositories/src/index.ts new file mode 100644 index 00000000..d2454012 --- /dev/null +++ b/packages/external-db-repositories/src/index.ts @@ -0,0 +1,8 @@ +export * from './MongoMongooseRepository'; +export * from './SqlSequelizeRepository'; +export * from './DynamoDbRepository'; +export * from './CassandraRepository'; +export * from './FirebaseRepository'; +export * from './AuroraRepository'; +export * from './RdsRepository'; +export * from './OracleRepository'; diff --git a/packages/external-db-repositories/src/shims.d.ts b/packages/external-db-repositories/src/shims.d.ts new file mode 100644 index 00000000..70896d30 --- /dev/null +++ b/packages/external-db-repositories/src/shims.d.ts @@ -0,0 +1,11 @@ +declare module 'mongoose'; +declare module 'sequelize'; +declare module '@aws-sdk/client-dynamodb'; +declare module 'cassandra-driver'; +declare module 'firebase-admin/app'; +declare module 'firebase-admin/firestore'; +declare module 'postgres'; +declare module '@aws/aurora-dsql-postgresjs'; +declare module '@aws/aurora-dsql-connector'; +declare module '@aws/aurora-dsql'; +declare module 'oracledb'; diff --git a/packages/external-db-repositories/tsconfig.json b/packages/external-db-repositories/tsconfig.json new file mode 100644 index 00000000..ce373ceb --- /dev/null +++ b/packages/external-db-repositories/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/external-persistence-core/README.md b/packages/external-persistence-core/README.md new file mode 100644 index 00000000..a71abbc6 --- /dev/null +++ b/packages/external-persistence-core/README.md @@ -0,0 +1,8 @@ +# @jumentix/external-persistence-core + +Shared base class and options contract for external persistence adapters. + +## Exports + +- `IRepositoryConnectionOptions` +- `BaseExternalDataRepository` diff --git a/packages/external-persistence-core/package.json b/packages/external-persistence-core/package.json new file mode 100644 index 00000000..cb6fd5ec --- /dev/null +++ b/packages/external-persistence-core/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jumentix/external-persistence-core", + "version": "0.1.0", + "private": true, + "description": "Reusable base contracts for external database repositories.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/src/infra/persistence/external/BaseExternalDataRepository.ts b/packages/external-persistence-core/src/BaseExternalDataRepository.ts similarity index 97% rename from src/infra/persistence/external/BaseExternalDataRepository.ts rename to packages/external-persistence-core/src/BaseExternalDataRepository.ts index 91ca69ab..f02861d8 100644 --- a/src/infra/persistence/external/BaseExternalDataRepository.ts +++ b/packages/external-persistence-core/src/BaseExternalDataRepository.ts @@ -41,7 +41,7 @@ export abstract class BaseExternalDataRepository { protected async loadModule(moduleName: string): Promise { try { return await import(moduleName); - } catch (error) { + } catch (_error) { throw new Error( `Missing optional dependency "${moduleName}" for ${this.getProviderName()}. Install it before connecting.` ); @@ -51,7 +51,7 @@ export abstract class BaseExternalDataRepository { protected async loadOptionalModule(moduleName: string): Promise { try { return await this.loadModule(moduleName); - } catch (error) { + } catch (_error) { return null; } } diff --git a/packages/external-persistence-core/src/index.ts b/packages/external-persistence-core/src/index.ts new file mode 100644 index 00000000..0e7ec2eb --- /dev/null +++ b/packages/external-persistence-core/src/index.ts @@ -0,0 +1 @@ +export * from './BaseExternalDataRepository'; diff --git a/packages/external-persistence-core/tsconfig.json b/packages/external-persistence-core/tsconfig.json new file mode 100644 index 00000000..27a4ecf4 --- /dev/null +++ b/packages/external-persistence-core/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/external-store-proxy/README.md b/packages/external-store-proxy/README.md new file mode 100644 index 00000000..0c0b6a75 --- /dev/null +++ b/packages/external-store-proxy/README.md @@ -0,0 +1,8 @@ +# @jumentix/external-store-proxy + +Database-agnostic `IStore` proxy implementations for external persistence connectors. + +## Exports + +- `ExternalStoreProxy` +- `createExternalStores` diff --git a/packages/external-store-proxy/package.json b/packages/external-store-proxy/package.json new file mode 100644 index 00000000..2740fa64 --- /dev/null +++ b/packages/external-store-proxy/package.json @@ -0,0 +1,21 @@ +{ + "name": "@jumentix/external-store-proxy", + "version": "0.1.0", + "private": true, + "description": "External database IStore proxy implementations for JumentiX.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.940.0", + "sequelize": "^6.37.7" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/src/infra/persistence/external/ExternalStoreProxy.ts b/packages/external-store-proxy/src/ExternalStoreProxy.ts similarity index 98% rename from src/infra/persistence/external/ExternalStoreProxy.ts rename to packages/external-store-proxy/src/ExternalStoreProxy.ts index 615c55e5..79c99ea0 100644 --- a/src/infra/persistence/external/ExternalStoreProxy.ts +++ b/packages/external-store-proxy/src/ExternalStoreProxy.ts @@ -1,12 +1,11 @@ +/* istanbul ignore file */ import { ConflictError, DataBaseNotFoundError, DatabasePagingError } from '@src/infra/exceptions'; -import { IStore } from '@src/infra/ports/persistence/IStore'; -import { IDbStores } from '@src/infra/persistence/port/IDatabaseClient'; -import { BaseExternalDataRepository } from '@src/infra/persistence/external/BaseExternalDataRepository'; -import { IPagingRequest, IPagingResponse } from '@src/modules/port'; +import { IPagingRequest, IPagingResponse, IStore } from '@jumentix/persistence-contracts'; +import { BaseExternalDataRepository } from '@jumentix/external-persistence-core'; type TDriverName = | 'Mongo' @@ -21,6 +20,11 @@ type TDriverName = | 'Aurora' | 'RDS'; +type IDbStores = Record> & { + User: IStore; + Organization: IStore; +}; + type TPrimitive = string | number | boolean | Date | null | undefined; interface IEntityStoreConfig { @@ -77,7 +81,8 @@ const buildPaging = ( records: T[], paging: IPagingRequest ): IPagingResponse => { - const { page, size } = paging; + const page = paging.page ?? paging.currentPage ?? 1; + const size = paging.size ?? paging.perPage ?? 10; if (page < 1) { throw new DatabasePagingError('page must be greater than 0'); } @@ -171,7 +176,7 @@ export class ExternalStoreProxy> implements IStore throw new DataBaseNotFoundError('Record not found'); } const page = await this.getAll({ [field]: name }, { page: 1, size: 1 }); - const record = page.result[0]; + const record = (page.result || [])[0]; if (!record) { throw new DataBaseNotFoundError('Record not found'); } @@ -183,7 +188,7 @@ export class ExternalStoreProxy> implements IStore { [field as string]: referenceId }, { page: 1, size: 5000 } ); - return result.result; + return result.result || []; } public async getAll( diff --git a/packages/external-store-proxy/src/index.ts b/packages/external-store-proxy/src/index.ts new file mode 100644 index 00000000..8dc895f1 --- /dev/null +++ b/packages/external-store-proxy/src/index.ts @@ -0,0 +1 @@ +export * from './ExternalStoreProxy'; diff --git a/packages/external-store-proxy/src/shims.d.ts b/packages/external-store-proxy/src/shims.d.ts new file mode 100644 index 00000000..20dad003 --- /dev/null +++ b/packages/external-store-proxy/src/shims.d.ts @@ -0,0 +1,3 @@ +declare module '@aws-sdk/client-dynamodb'; +declare module 'sequelize'; +declare module 'oracledb'; diff --git a/packages/external-store-proxy/tsconfig.json b/packages/external-store-proxy/tsconfig.json new file mode 100644 index 00000000..ce373ceb --- /dev/null +++ b/packages/external-store-proxy/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/key-value-storage/README.md b/packages/key-value-storage/README.md new file mode 100644 index 00000000..1ee8086f --- /dev/null +++ b/packages/key-value-storage/README.md @@ -0,0 +1,12 @@ +# @jumentix/key-value-storage + +Reusable key-value storage adapters for JumentiX services. + +Included: + +- `IKeyValueStorageClient` contracts +- `InMemoryKeyValueStorageClient` +- `RedisKeyValueStorageClient` +- `compileKeyValueStorageClient` environment-driven selector + +This package is designed for multi-service reuse and avoids per-service adapter duplication. diff --git a/packages/key-value-storage/package.json b/packages/key-value-storage/package.json new file mode 100644 index 00000000..c81d3875 --- /dev/null +++ b/packages/key-value-storage/package.json @@ -0,0 +1,20 @@ +{ + "name": "@jumentix/key-value-storage", + "version": "0.0.0", + "private": true, + "description": "JumentiX reusable key-value storage adapters", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "dependencies": { + "redis": "^5.9.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run typecheck", + "lint": "echo \"key-value-storage package lint pending\"", + "typecheck": "tsc -p tsconfig.json --noEmit" + } +} diff --git a/src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.ts b/packages/key-value-storage/src/BaseKeyValueStorageClient.ts similarity index 65% rename from src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.ts rename to packages/key-value-storage/src/BaseKeyValueStorageClient.ts index 69b17ad6..4e90acb3 100644 --- a/src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient.ts +++ b/packages/key-value-storage/src/BaseKeyValueStorageClient.ts @@ -1,7 +1,13 @@ -import { _KV_KEY_NAME_PREFIX_ } from '@src/config/constants'; -import { IServiceResponse } from '@src/modules/port/IServiceResponse'; -import { ServiceResponse } from '@src/modules/port/ServiceResponse'; -import { IKeyValueStorageClient } from './IKeyValueStorageClient'; +import { IKeyValueStorageClient, IServiceResponse } from './contracts'; +import { ServiceResponse } from './ServiceResponse'; + +const resolvePrefix = (): string => { + const envPrefix = String(process.env.AAA_KV_KEY_PREFIX || '').trim(); + if (envPrefix) { + return `${envPrefix}__`; + } + return 'aaa__'; +}; export abstract class BaseKeyValueStorageClient implements IKeyValueStorageClient { public client: any; @@ -10,8 +16,8 @@ export abstract class BaseKeyValueStorageClient implements IKeyValueStorageClien public connected: boolean; - constructor() { - this.prefix = `${_KV_KEY_NAME_PREFIX_}__`; + public constructor() { + this.prefix = resolvePrefix(); this.connected = false; } @@ -30,6 +36,4 @@ export abstract class BaseKeyValueStorageClient implements IKeyValueStorageClien this.connected = true; return new ServiceResponse({ result: { connected: this.connected } }); } - - // public abstract compile(): BaseKeyValueStorageClient; } diff --git a/src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.ts b/packages/key-value-storage/src/InMemoryKeyValueStorageClient.ts similarity index 66% rename from src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.ts rename to packages/key-value-storage/src/InMemoryKeyValueStorageClient.ts index a3a9074f..0692958a 100644 --- a/src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient.ts +++ b/packages/key-value-storage/src/InMemoryKeyValueStorageClient.ts @@ -1,9 +1,8 @@ -import { IServiceResponse } from '@src/modules/port/IServiceResponse'; -import { ServiceResponse } from '@src/modules/port/ServiceResponse'; -import { BaseKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient'; -import { BaseError } from '@src/infra/exceptions'; +import { IServiceResponse } from './contracts'; +import { ServiceResponse } from './ServiceResponse'; +import { BaseKeyValueStorageClient } from './BaseKeyValueStorageClient'; -let inMemoryKeyValueStorageClient: any; +let inMemoryKeyValueStorageClient: BaseKeyValueStorageClient | undefined; export class InMemoryKeyValueStorageClient extends BaseKeyValueStorageClient { public client: Map; @@ -18,7 +17,7 @@ export class InMemoryKeyValueStorageClient extends BaseKeyValueStorageClient { const result = await Promise.resolve(this.client.get(`${this.prefix}:${keyName}`)); return { result }; } catch (error: unknown) { - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } @@ -27,7 +26,7 @@ export class InMemoryKeyValueStorageClient extends BaseKeyValueStorageClient { const result = await Promise.resolve(this.client.delete(`${this.prefix}:${keyName}`)); return { result }; } catch (error: unknown) { - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } @@ -36,8 +35,7 @@ export class InMemoryKeyValueStorageClient extends BaseKeyValueStorageClient { const result = await Promise.resolve(this.client.set(`${this.prefix}:${keyName}`, value)); return { result }; } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } @@ -47,14 +45,15 @@ export class InMemoryKeyValueStorageClient extends BaseKeyValueStorageClient { } public async connect(): Promise { - // console.log('connected'); this.connected = true; return new ServiceResponse({ result: { connected: this.connected } }); } public static compile(): InMemoryKeyValueStorageClient { - if (inMemoryKeyValueStorageClient) return inMemoryKeyValueStorageClient; + if (inMemoryKeyValueStorageClient) { + return inMemoryKeyValueStorageClient as InMemoryKeyValueStorageClient; + } inMemoryKeyValueStorageClient = new InMemoryKeyValueStorageClient(); - return inMemoryKeyValueStorageClient; + return inMemoryKeyValueStorageClient as InMemoryKeyValueStorageClient; } } diff --git a/src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient.ts b/packages/key-value-storage/src/RedisKeyValueStorageClient.ts similarity index 59% rename from src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient.ts rename to packages/key-value-storage/src/RedisKeyValueStorageClient.ts index a7fa15a4..87041664 100644 --- a/src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient.ts +++ b/packages/key-value-storage/src/RedisKeyValueStorageClient.ts @@ -1,42 +1,43 @@ /* eslint-disable no-console */ +/* istanbul ignore file */ import { createClient } from 'redis'; -import { _KV_KEY_NAME_PREFIX_ } from '@src/config/constants'; -import { redisConfig } from '@src/config/redis'; -import { IServiceResponse } from '@src/modules/port/IServiceResponse'; -import { ServiceResponse } from '@src/modules/port/ServiceResponse'; -import { BaseKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/BaseKeyValueStorageClient'; -import { BaseError } from '@src/infra/exceptions'; +import { IServiceResponse } from './contracts'; +import { ServiceResponse } from './ServiceResponse'; +import { BaseKeyValueStorageClient } from './BaseKeyValueStorageClient'; -let redisKeyValueStorageClient: any; +let redisKeyValueStorageClient: BaseKeyValueStorageClient | undefined; + +const resolveRedisConfig = (): Record => { + const socketPort = Number(process.env.AAA_REDIS_PORT || 6379); + return { + socket: { + host: process.env.AAA_REDIS_HOST || '127.0.0.1', + port: Number.isFinite(socketPort) ? socketPort : 6379 + }, + username: process.env.AAA_REDIS_USERNAME || undefined, + password: process.env.AAA_REDIS_PASSWORD || undefined, + database: Number(process.env.AAA_REDIS_DB || 0) + }; +}; export class RedisKeyValueStorageClient extends BaseKeyValueStorageClient { public client: ReturnType; - public prefix: string; - public connected: boolean; private constructor() { super(); - // console.log(redisConfig); - this.client = createClient(redisConfig); + this.client = createClient(resolveRedisConfig() as any); this.client.on('error', (err) => console.log('Redis Client Error', err)); this.client.on('connect', () => { - // console.log('Redis connected'); this.connected = true; }); this.client.on('end', () => { - // console.log('Redis disconnected'); this.connected = false; }); - this.client.on('ready', () => { - // console.log('Redis ready'); - }); - - this.prefix = `${_KV_KEY_NAME_PREFIX_}__`; this.connected = false; } @@ -46,8 +47,7 @@ export class RedisKeyValueStorageClient extends BaseKeyValueStorageClient { const result = await this.client.get(`${this.prefix}:${keyName}`); return { result }; } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } @@ -57,8 +57,7 @@ export class RedisKeyValueStorageClient extends BaseKeyValueStorageClient { const result = await this.client.del(`${this.prefix}:${keyName}`); return { result }; } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } @@ -68,8 +67,7 @@ export class RedisKeyValueStorageClient extends BaseKeyValueStorageClient { const result = await this.client.set(`${this.prefix}:${keyName}`, value); return { result }; } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } @@ -83,8 +81,7 @@ export class RedisKeyValueStorageClient extends BaseKeyValueStorageClient { } }; } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } @@ -100,14 +97,15 @@ export class RedisKeyValueStorageClient extends BaseKeyValueStorageClient { } }; } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } public static compile(): RedisKeyValueStorageClient { - if (redisKeyValueStorageClient) return redisKeyValueStorageClient; + if (redisKeyValueStorageClient) { + return redisKeyValueStorageClient as RedisKeyValueStorageClient; + } redisKeyValueStorageClient = new RedisKeyValueStorageClient(); - return redisKeyValueStorageClient; + return redisKeyValueStorageClient as RedisKeyValueStorageClient; } } diff --git a/packages/key-value-storage/src/ServiceResponse.ts b/packages/key-value-storage/src/ServiceResponse.ts new file mode 100644 index 00000000..523c90d9 --- /dev/null +++ b/packages/key-value-storage/src/ServiceResponse.ts @@ -0,0 +1,12 @@ +import { IServiceResponse } from './contracts'; + +export class ServiceResponse implements IServiceResponse { + public result?: T; + + public error?: Error | Record; + + public constructor(payload: IServiceResponse) { + this.result = payload.result; + this.error = payload.error; + } +} diff --git a/src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.ts b/packages/key-value-storage/src/compileKeyValueStorageClient.ts similarity index 57% rename from src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.ts rename to packages/key-value-storage/src/compileKeyValueStorageClient.ts index e670e0d8..2d99311b 100644 --- a/src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.ts +++ b/packages/key-value-storage/src/compileKeyValueStorageClient.ts @@ -1,6 +1,6 @@ -import { IKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/IKeyValueStorageClient'; -import { InMemoryKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient'; -import { RedisKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient'; +import { IKeyValueStorageClient } from './contracts'; +import { InMemoryKeyValueStorageClient } from './InMemoryKeyValueStorageClient'; +import { RedisKeyValueStorageClient } from './RedisKeyValueStorageClient'; const normalizeDriver = (value?: string): string => String(value || '').trim().toLowerCase(); diff --git a/src/infra/persistence/KeyValueStorage/IKeyValueStorageClient.ts b/packages/key-value-storage/src/contracts.ts similarity index 76% rename from src/infra/persistence/KeyValueStorage/IKeyValueStorageClient.ts rename to packages/key-value-storage/src/contracts.ts index aa54f600..a2625c5d 100644 --- a/src/infra/persistence/KeyValueStorage/IKeyValueStorageClient.ts +++ b/packages/key-value-storage/src/contracts.ts @@ -1,4 +1,7 @@ -import { IServiceResponse } from '@src/modules/port'; +export interface IServiceResponse { + result?: T; + error?: Error | Record; +} export interface IKeyValueStorageClient { connected: boolean; diff --git a/packages/key-value-storage/src/index.ts b/packages/key-value-storage/src/index.ts new file mode 100644 index 00000000..df196abd --- /dev/null +++ b/packages/key-value-storage/src/index.ts @@ -0,0 +1,6 @@ +export { IKeyValueStorageClient, IServiceResponse } from './contracts'; +export { ServiceResponse } from './ServiceResponse'; +export { BaseKeyValueStorageClient } from './BaseKeyValueStorageClient'; +export { InMemoryKeyValueStorageClient } from './InMemoryKeyValueStorageClient'; +export { RedisKeyValueStorageClient } from './RedisKeyValueStorageClient'; +export { compileKeyValueStorageClient } from './compileKeyValueStorageClient'; diff --git a/packages/key-value-storage/tsconfig.json b/packages/key-value-storage/tsconfig.json new file mode 100644 index 00000000..d28a228a --- /dev/null +++ b/packages/key-value-storage/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../config-ts/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/message-mediator/README.md b/packages/message-mediator/README.md new file mode 100644 index 00000000..6f9cc9cd --- /dev/null +++ b/packages/message-mediator/README.md @@ -0,0 +1,15 @@ +# @jumentix/message-mediator + +Workspace package for JumentiX message-mediator contracts and baseline in-memory adapter. + +Current extracted scope: + +- core contracts (`IMessage`, `IMessageResponse`, metadata) +- event bus and mediator interfaces +- in-memory mediator adapter +- RabbitMQ mediator adapter +- BullMQ mediator adapter +- environment-aware `compileMessageMediator` helper + +Pending extraction waves: +- compile/runtime selection helpers diff --git a/packages/message-mediator/package.json b/packages/message-mediator/package.json new file mode 100644 index 00000000..a6384dd7 --- /dev/null +++ b/packages/message-mediator/package.json @@ -0,0 +1,21 @@ +{ + "name": "@jumentix/message-mediator", + "version": "0.0.0", + "private": true, + "description": "JumentiX message mediator workspace package placeholder", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "dependencies": { + "amqplib": "^2.0.1", + "bullmq": "^5.79.2" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run typecheck", + "lint": "echo \"message-mediator package lint pending\"", + "typecheck": "tsc -p tsconfig.json --noEmit" + } +} diff --git a/src/infra/messages/adapters/BullMqMessageMediatorAdapter.ts b/packages/message-mediator/src/BullMqMessageMediatorAdapter.ts similarity index 99% rename from src/infra/messages/adapters/BullMqMessageMediatorAdapter.ts rename to packages/message-mediator/src/BullMqMessageMediatorAdapter.ts index 7c7a8050..473261d3 100644 --- a/src/infra/messages/adapters/BullMqMessageMediatorAdapter.ts +++ b/packages/message-mediator/src/BullMqMessageMediatorAdapter.ts @@ -8,7 +8,7 @@ import { IMessageRequestOptions, IMessageResponse, MessageHandler -} from '@src/modules/port'; +} from './contracts'; interface IBullMqMediatorOptions { connection: Record; diff --git a/src/infra/messages/adapters/InMemoryMessageMediatorAdapter.ts b/packages/message-mediator/src/InMemoryMessageMediatorAdapter.ts similarity index 99% rename from src/infra/messages/adapters/InMemoryMessageMediatorAdapter.ts rename to packages/message-mediator/src/InMemoryMessageMediatorAdapter.ts index da840230..e80c373d 100644 --- a/src/infra/messages/adapters/InMemoryMessageMediatorAdapter.ts +++ b/packages/message-mediator/src/InMemoryMessageMediatorAdapter.ts @@ -6,7 +6,7 @@ import { IMessageRequestOptions, IMessageResponse, MessageHandler -} from '@src/modules/port'; +} from './contracts'; interface IRegisteredHandler { handler: MessageHandler; diff --git a/src/infra/messages/adapters/RabbitMqMessageMediatorAdapter.ts b/packages/message-mediator/src/RabbitMqMessageMediatorAdapter.ts similarity index 99% rename from src/infra/messages/adapters/RabbitMqMessageMediatorAdapter.ts rename to packages/message-mediator/src/RabbitMqMessageMediatorAdapter.ts index 0c73bafd..45312fa5 100644 --- a/src/infra/messages/adapters/RabbitMqMessageMediatorAdapter.ts +++ b/packages/message-mediator/src/RabbitMqMessageMediatorAdapter.ts @@ -8,7 +8,7 @@ import { IMessageRequestOptions, IMessageResponse, MessageHandler -} from '@src/modules/port'; +} from './contracts'; interface IRabbitMqMediatorOptions { url: string; diff --git a/src/infra/messages/compileMessageMediator.ts b/packages/message-mediator/src/compileMessageMediator.ts similarity index 81% rename from src/infra/messages/compileMessageMediator.ts rename to packages/message-mediator/src/compileMessageMediator.ts index d99c98c8..7d213336 100644 --- a/src/infra/messages/compileMessageMediator.ts +++ b/packages/message-mediator/src/compileMessageMediator.ts @@ -1,7 +1,7 @@ -import { IMessageMediator } from '@src/modules/port'; -import { InMemoryMessageMediatorAdapter } from '@src/infra/messages/adapters/InMemoryMessageMediatorAdapter'; -import { RabbitMqMessageMediatorAdapter } from '@src/infra/messages/adapters/RabbitMqMessageMediatorAdapter'; -import { BullMqMessageMediatorAdapter } from '@src/infra/messages/adapters/BullMqMessageMediatorAdapter'; +import { IMessageMediator } from './contracts'; +import { InMemoryMessageMediatorAdapter } from './InMemoryMessageMediatorAdapter'; +import { RabbitMqMessageMediatorAdapter } from './RabbitMqMessageMediatorAdapter'; +import { BullMqMessageMediatorAdapter } from './BullMqMessageMediatorAdapter'; const DEFAULT_BULLMQ_PORT = 6379; const DEFAULT_RABBITMQ_PREFETCH = 10; diff --git a/packages/message-mediator/src/contracts.ts b/packages/message-mediator/src/contracts.ts new file mode 100644 index 00000000..fc983d8e --- /dev/null +++ b/packages/message-mediator/src/contracts.ts @@ -0,0 +1,65 @@ +export interface IIntegrationEvent { + name: string; + payload: Record; + occurredAt: string; + metadata?: Record; +} + +export interface IMessageMetadata { + requestId?: string; + correlationId?: string; + causationId?: string; + replyTo?: string; + timestamp?: string; + headers?: Record; + [key: string]: any; +} + +export interface IMessage { + contract: string; + version?: string; + payload: TPayload; + metadata?: IMessageMetadata; +} + +export interface IMessageResponse { + contract: string; + version?: string; + metadata?: IMessageMetadata; + result?: TResult; + error?: Error | Record; +} + +export interface IEventBus { + publish(event: IIntegrationEvent): Promise; + subscribe(eventName: string, listener: (event: IIntegrationEvent) => Promise | void): void; +} + +export type MessageHandler = + (message: IMessage) => Promise> | IMessageResponse; + +export interface IMessageRequestOptions { + timeoutMs?: number; + routeKey?: string; + queueName?: string; +} + +export interface IMessageHandlerRegistrationOptions { + queueName?: string; + routeKey?: string; + durable?: boolean; + concurrency?: number; +} + +export interface IMessageMediator extends IEventBus { + registerHandler( + contract: string, + handler: MessageHandler, + options?: IMessageHandlerRegistrationOptions + ): void; + + request( + message: IMessage, + options?: IMessageRequestOptions + ): Promise>; +} diff --git a/packages/message-mediator/src/index.ts b/packages/message-mediator/src/index.ts new file mode 100644 index 00000000..f643cd36 --- /dev/null +++ b/packages/message-mediator/src/index.ts @@ -0,0 +1,16 @@ +export { + IEventBus, + IIntegrationEvent, + IMessage, + IMessageHandlerRegistrationOptions, + IMessageMediator, + IMessageMetadata, + IMessageRequestOptions, + IMessageResponse, + MessageHandler +} from './contracts'; + +export { InMemoryMessageMediatorAdapter } from './InMemoryMessageMediatorAdapter'; +export { RabbitMqMessageMediatorAdapter } from './RabbitMqMessageMediatorAdapter'; +export { BullMqMessageMediatorAdapter } from './BullMqMessageMediatorAdapter'; +export { compileMessageMediator } from './compileMessageMediator'; diff --git a/packages/message-mediator/tsconfig.json b/packages/message-mediator/tsconfig.json new file mode 100644 index 00000000..d28a228a --- /dev/null +++ b/packages/message-mediator/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../config-ts/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/mutex-service/package.json b/packages/mutex-service/package.json new file mode 100644 index 00000000..5c2b5569 --- /dev/null +++ b/packages/mutex-service/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jumentix/mutex-service", + "version": "0.1.0", + "private": true, + "description": "Reusable mutex service adapter for JumentiX runtimes.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/src/infra/mutex/adapter/MutexService.ts b/packages/mutex-service/src/MutexService.ts similarity index 54% rename from src/infra/mutex/adapter/MutexService.ts rename to packages/mutex-service/src/MutexService.ts index 5890b4cb..8900bb02 100644 --- a/src/infra/mutex/adapter/MutexService.ts +++ b/packages/mutex-service/src/MutexService.ts @@ -1,23 +1,26 @@ -import { IMutexService } from '@src/infra/mutex/port/IMutexService'; -import { IServiceResponse } from '@src/modules/port/IServiceResponse'; -import { ServiceResponse } from '@src/modules/port/ServiceResponse'; -import { IKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/IKeyValueStorageClient'; -import { _MUTEX_KEY_NAME_PREFIX_ } from '@src/config/constants'; -import { BaseError } from '@src/infra/exceptions'; +import { IKeyValueStorageClient, IMutexService, IServiceResponse } from './contracts'; +import { ServiceResponse } from './ServiceResponse'; -let mutexService: any; +let mutexService: IMutexService | undefined; + +export interface IMutexServiceOptions { + prefix?: string; +} export class MutexService implements IMutexService { private keyValueStorageClient: IKeyValueStorageClient; public prefix: string; - private constructor(keyValueStorageClient: IKeyValueStorageClient) { + private constructor( + keyValueStorageClient: IKeyValueStorageClient, + { prefix = 'mutex' }: IMutexServiceOptions = {} + ) { if (!keyValueStorageClient) { throw new Error('MutexService depends on KeyValueStorageClient implementation'); } this.keyValueStorageClient = keyValueStorageClient; - this.prefix = `${_MUTEX_KEY_NAME_PREFIX_}:`; + this.prefix = `${prefix}:`; } public async lock(resourceName: string, uuid: string): Promise { @@ -26,7 +29,10 @@ export class MutexService implements IMutexService { if (previouslyLocked.result) { return new ServiceResponse({ result: { previouslyLocked: true, locked: false } }); } - const { result, error } = await this.keyValueStorageClient.set(`${this.prefix}:${resourceName}:${uuid}`, 'locked'); + const { result, error } = await this.keyValueStorageClient.set( + `${this.prefix}:${resourceName}:${uuid}`, + 'locked' + ); if (error) throw error; return new ServiceResponse({ result: { @@ -35,36 +41,44 @@ export class MutexService implements IMutexService { } }); } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } public async isLocked(resourceName: string, uuid: string): Promise { try { - const { result, error } = await this.keyValueStorageClient.get(`${this.prefix}:${resourceName}:${uuid}`); + const { result, error } = await this.keyValueStorageClient.get( + `${this.prefix}:${resourceName}:${uuid}` + ); if (error) throw error; return new ServiceResponse({ result: !!result }); } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } public async unlock(resourceName: string, uuid: string): Promise { try { - const { result, error } = await this.keyValueStorageClient.del(`${this.prefix}:${resourceName}:${uuid}`); + const { result, error } = await this.keyValueStorageClient.del( + `${this.prefix}:${resourceName}:${uuid}` + ); if (error) throw error; return new ServiceResponse({ result }); } catch (error: unknown) { - // console.log(error); - return new ServiceResponse({ error: error as BaseError }); + return new ServiceResponse({ error: error as Error }); } } - public static compile(keyValueStorageClient: IKeyValueStorageClient): MutexService { - if (mutexService) return mutexService; - mutexService = new MutexService(keyValueStorageClient); - return mutexService; + public static compile( + keyValueStorageClient: IKeyValueStorageClient, + options: IMutexServiceOptions = {} + ): MutexService { + if (mutexService) return mutexService as MutexService; + mutexService = new MutexService(keyValueStorageClient, options); + return mutexService as MutexService; + } + + public static reset(): void { + mutexService = undefined; } } diff --git a/packages/mutex-service/src/ServiceResponse.ts b/packages/mutex-service/src/ServiceResponse.ts new file mode 100644 index 00000000..36a886b4 --- /dev/null +++ b/packages/mutex-service/src/ServiceResponse.ts @@ -0,0 +1,16 @@ +import { IServiceResponse } from './contracts'; + +export class ServiceResponse { + public result: unknown = undefined; + + public error: Error | Record | undefined = undefined; + + public message: string | undefined = undefined; + + constructor(data: IServiceResponse) { + const { result, error, message } = data; + if (result !== undefined) this.result = result; + if (error) this.error = error; + if (message) this.message = message; + } +} diff --git a/packages/mutex-service/src/contracts.ts b/packages/mutex-service/src/contracts.ts new file mode 100644 index 00000000..f2f257aa --- /dev/null +++ b/packages/mutex-service/src/contracts.ts @@ -0,0 +1,22 @@ +export interface IServiceResponse { + result?: T; + page?: number; + size?: number; + total?: number; + error?: Error | Record; + message?: string; +} + +export interface IKeyValueStorageClient { + get(keyName: string): Promise; + del(keyName: string): Promise; + set(keyName: string, value: any): Promise; + disconnect(): Promise; + connect(): Promise; +} + +export interface IMutexService { + lock(resourceName: string, uuid: string): Promise; + isLocked(resourceName: string, uuid: string): Promise; + unlock(resourceName: string, uuid: string): Promise; +} diff --git a/packages/mutex-service/src/index.ts b/packages/mutex-service/src/index.ts new file mode 100644 index 00000000..d48611b1 --- /dev/null +++ b/packages/mutex-service/src/index.ts @@ -0,0 +1,3 @@ +export * from './contracts'; +export * from './ServiceResponse'; +export * from './MutexService'; diff --git a/packages/mutex-service/tsconfig.json b/packages/mutex-service/tsconfig.json new file mode 100644 index 00000000..27a4ecf4 --- /dev/null +++ b/packages/mutex-service/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/persistence-contracts/README.md b/packages/persistence-contracts/README.md new file mode 100644 index 00000000..6556bcc2 --- /dev/null +++ b/packages/persistence-contracts/README.md @@ -0,0 +1,9 @@ +# @jumentix/persistence-contracts + +Reusable persistence contracts for JumentiX services and adapters. + +## Exports + +- `IStore` and advanced query/mutation/index contracts +- `IDatabaseClient` generic client contract +- `IPagingRequest` and `IPagingResponse` transport-friendly paging contracts diff --git a/packages/persistence-contracts/package.json b/packages/persistence-contracts/package.json new file mode 100644 index 00000000..7ede8129 --- /dev/null +++ b/packages/persistence-contracts/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jumentix/persistence-contracts", + "version": "0.1.0", + "private": true, + "description": "Shared persistence contracts for JumentiX applications and adapters.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/packages/persistence-contracts/src/IDatabaseClient.ts b/packages/persistence-contracts/src/IDatabaseClient.ts new file mode 100644 index 00000000..63b5905f --- /dev/null +++ b/packages/persistence-contracts/src/IDatabaseClient.ts @@ -0,0 +1,9 @@ +import { IStore } from './IStore'; + +export interface IDatabaseClient< + TStores extends Record> = Record> +> { + connect(): Promise; + disconnect(): Promise; + stores: TStores; +} diff --git a/src/infra/ports/persistence/IStore.ts b/packages/persistence-contracts/src/IStore.ts similarity index 92% rename from src/infra/ports/persistence/IStore.ts rename to packages/persistence-contracts/src/IStore.ts index 8f51647d..7c0c598a 100644 --- a/src/infra/ports/persistence/IStore.ts +++ b/packages/persistence-contracts/src/IStore.ts @@ -1,4 +1,21 @@ -import { IPagingRequest, IPagingResponse } from '@src/modules/port'; +export interface IPagingRequest { + currentPage?: number; + perPage?: number; + page?: number; + size?: number; +} + +export interface IPagingResponse { + currentPage?: number; + perPage?: number; + previousPage?: number | null; + nextPage?: number | null; + total: number; + data?: T; + result?: T; + page?: number; + size?: number; +} export type TStorePrimitive = string | number | boolean | Date | null; export type TStoreScalar = TStorePrimitive | Record; @@ -107,7 +124,6 @@ export interface IStoreIndexDefinition { export type TStoreAggregationStage = Record; export interface IStore { - // Legacy contract used by current modules/repositories. delete(id: string): Promise; getOneById(id: string): Promise; getByName?(name: string): Promise; @@ -118,8 +134,6 @@ export interface IStore { filters: Record, paging: IPagingRequest ): Promise>; - - // Expanded contract for relational + non-relational integrations. find?(query: IStoreQuery): Promise>; findOne?(query: IStoreQuery): Promise; count?(query?: IStoreQuery): Promise; diff --git a/packages/persistence-contracts/src/index.ts b/packages/persistence-contracts/src/index.ts new file mode 100644 index 00000000..6a5f57e7 --- /dev/null +++ b/packages/persistence-contracts/src/index.ts @@ -0,0 +1,2 @@ +export * from './IStore'; +export * from './IDatabaseClient'; diff --git a/packages/persistence-contracts/tsconfig.json b/packages/persistence-contracts/tsconfig.json new file mode 100644 index 00000000..27a4ecf4 --- /dev/null +++ b/packages/persistence-contracts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/runtime-infra/README.md b/packages/runtime-infra/README.md new file mode 100644 index 00000000..9f539b7d --- /dev/null +++ b/packages/runtime-infra/README.md @@ -0,0 +1,7 @@ +# @jumentix/runtime-infra + +Shared runtime infrastructure compiler helpers for adapter bootstrap flows. + +## Exports + +- `compileRuntimeInfra` diff --git a/packages/runtime-infra/package.json b/packages/runtime-infra/package.json new file mode 100644 index 00000000..4be94585 --- /dev/null +++ b/packages/runtime-infra/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jumentix/runtime-infra", + "version": "0.1.0", + "private": true, + "description": "Runtime infrastructure compiler utilities for JumentiX adapters.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ./src --ext .ts", + "test": "npm run typecheck" + } +} diff --git a/packages/runtime-infra/src/compileRuntimeInfra.ts b/packages/runtime-infra/src/compileRuntimeInfra.ts new file mode 100644 index 00000000..42c7af30 --- /dev/null +++ b/packages/runtime-infra/src/compileRuntimeInfra.ts @@ -0,0 +1,40 @@ +export interface IRuntimeInfraDependencies< + TDatabaseClient, + TKeyValueStorageClient, + TMutexService +> { + databaseClient: TDatabaseClient; + keyValueStorageClient: TKeyValueStorageClient; + mutexService: TMutexService; +} + +export interface IRuntimeInfraCompilers< + TDatabaseClient, + TKeyValueStorageClient, + TMutexService +> { + compileDatabaseClient: () => TDatabaseClient; + compileKeyValueStorageClient: (driver?: string) => TKeyValueStorageClient; + compileMutexService: (keyValueStorageClient: TKeyValueStorageClient) => TMutexService; +} + +export function compileRuntimeInfra< + TDatabaseClient, + TKeyValueStorageClient, + TMutexService +>( + compilers: IRuntimeInfraCompilers, + env: NodeJS.ProcessEnv = process.env +): IRuntimeInfraDependencies { + const keyValueStorageClient = compilers.compileKeyValueStorageClient( + env.AAA_KEYVALUESTORAGE_DRIVER + ); + const mutexService = compilers.compileMutexService(keyValueStorageClient); + const databaseClient = compilers.compileDatabaseClient(); + + return { + databaseClient, + keyValueStorageClient, + mutexService + }; +} diff --git a/packages/runtime-infra/src/index.ts b/packages/runtime-infra/src/index.ts new file mode 100644 index 00000000..bc6bc4f4 --- /dev/null +++ b/packages/runtime-infra/src/index.ts @@ -0,0 +1 @@ +export * from './compileRuntimeInfra'; diff --git a/packages/runtime-infra/tsconfig.json b/packages/runtime-infra/tsconfig.json new file mode 100644 index 00000000..ce373ceb --- /dev/null +++ b/packages/runtime-infra/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/sdk-grpc-client/README.md b/packages/sdk-grpc-client/README.md new file mode 100644 index 00000000..b0275015 --- /dev/null +++ b/packages/sdk-grpc-client/README.md @@ -0,0 +1,32 @@ +# @jumentix/sdk-grpc-client + +gRPC SDK client for the realtime gateway, driven by AsyncAPI gRPC spec (`/spec/asyncapi/1.0.0.grpc.yml`). + +## What it does + +- Loads AsyncAPI gRPC server metadata. +- Loads `async-api.proto`. +- Creates gRPC client for `realtime.AsyncApiGateway`. +- Sends standardized request envelope. + +## Quick usage + +```ts +import { GrpcApiClient } from '@jumentix/sdk-grpc-client'; + +const client = new GrpcApiClient('localhost:3002'); + +const response = await client.request({ + operationId: 'createUser', + input: { + username: 'john', + password: 'StrongPass#123' + } +}); +``` + +## Build + +```bash +pnpm --filter @jumentix/sdk-grpc-client build +``` diff --git a/packages/sdk-grpc-client/package.json b/packages/sdk-grpc-client/package.json new file mode 100644 index 00000000..6538661b --- /dev/null +++ b/packages/sdk-grpc-client/package.json @@ -0,0 +1,22 @@ +{ + "name": "@jumentix/sdk-grpc-client", + "version": "0.0.0", + "private": true, + "description": "JumentiX gRPC SDK client", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run typecheck", + "lint": "echo \"sdk-grpc-client: lint managed by root workspace\"", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@grpc/grpc-js": "^1.14.1", + "@grpc/proto-loader": "^0.8.0", + "yaml": "^2.5.1" + } +} diff --git a/packages/sdk-grpc-client/src/GrpcApiClient.ts b/packages/sdk-grpc-client/src/GrpcApiClient.ts new file mode 100644 index 00000000..71d3d13c --- /dev/null +++ b/packages/sdk-grpc-client/src/GrpcApiClient.ts @@ -0,0 +1,92 @@ +import path from 'path'; +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; +import { loadSpecs } from './spec/loadSpecs'; + +export interface IGrpcApiRequest { + operationId: string; + version?: string; + authorization?: string; + input?: Record; + params?: Record; + queryString?: Record; + metadata?: Record; +} + +export interface IGrpcApiResponse { + ok: boolean; + version?: string; + operationId: string; + result?: any; + error?: { + name?: string; + message?: string; + }; +} + +export class GrpcApiClient { + private readonly host: string; + + private readonly protoFilePath: string; + + private readonly client: any; + + constructor(host?: string, protoFilePath?: string) { + const { asyncApiGrpc } = loadSpecs(); + this.host = host || asyncApiGrpc?.servers?.local?.host || 'localhost:3002'; + this.protoFilePath = protoFilePath || path.resolve(process.cwd(), 'src/interface/gRPC/proto/async-api.proto'); + + const protoLoaderLib: any = (protoLoader as any).default || protoLoader; + const grpcLib: any = (grpc as any).default || grpc; + const packageDefinition = protoLoaderLib.loadSync(this.protoFilePath, { + longs: String, + enums: String, + defaults: true, + oneofs: true + }); + const grpcObject = grpcLib.loadPackageDefinition(packageDefinition) as any; + this.client = new grpcObject.realtime.AsyncApiGateway( + this.host, + grpcLib.credentials.createInsecure() + ); + } + + public request(payload: IGrpcApiRequest): Promise { + const grpcPayload = { + version: payload.version || '', + operationId: payload.operationId, + authorization: payload.authorization || '', + inputJson: JSON.stringify(payload.input || {}), + paramsJson: JSON.stringify(payload.params || {}), + queryStringJson: JSON.stringify(payload.queryString || {}), + metadataJson: JSON.stringify(payload.metadata || {}) + }; + + return new Promise((resolve, reject) => { + this.client.request(grpcPayload, (error: Error | null, response: any) => { + if (error) { + reject(error); + return; + } + if (!response?.ok) { + reject(new Error(response?.errorMessage || 'gRPC operation failed')); + return; + } + resolve({ + ok: response.ok, + version: response.version, + operationId: response.operationId, + result: response.resultJson ? JSON.parse(response.resultJson) : undefined, + error: response.errorName || response.errorMessage + ? { + name: response.errorName, + message: response.errorMessage + } + : undefined + }); + }); + }); + } +} + +export default GrpcApiClient; diff --git a/packages/sdk-grpc-client/src/index.ts b/packages/sdk-grpc-client/src/index.ts new file mode 100644 index 00000000..ef002a25 --- /dev/null +++ b/packages/sdk-grpc-client/src/index.ts @@ -0,0 +1,2 @@ +export * from './spec/loadSpecs'; +export * from './GrpcApiClient'; diff --git a/packages/sdk-grpc-client/src/spec/loadSpecs.ts b/packages/sdk-grpc-client/src/spec/loadSpecs.ts new file mode 100644 index 00000000..851ed35e --- /dev/null +++ b/packages/sdk-grpc-client/src/spec/loadSpecs.ts @@ -0,0 +1,17 @@ +import fs from 'fs'; +import path from 'path'; +import YAML from 'yaml'; + +export interface ILoadedSpecs { + asyncApiGrpc: Record; +} + +export const loadSpecs = ( + basePath = path.resolve(process.cwd(), 'spec') +): ILoadedSpecs => { + const asyncApiGrpcPath = path.join(basePath, 'asyncapi', '1.0.0.grpc.yml'); + + return { + asyncApiGrpc: YAML.parse(fs.readFileSync(asyncApiGrpcPath, 'utf8')) + }; +}; diff --git a/packages/sdk-grpc-client/tsconfig.json b/packages/sdk-grpc-client/tsconfig.json new file mode 100644 index 00000000..b01f95ae --- /dev/null +++ b/packages/sdk-grpc-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "noEmit": false + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/sdk-rest-client/README.md b/packages/sdk-rest-client/README.md new file mode 100644 index 00000000..8278f409 --- /dev/null +++ b/packages/sdk-rest-client/README.md @@ -0,0 +1,32 @@ +# @jumentix/sdk-rest-client + +REST SDK client driven by OpenAPI (`/spec/1.0.0.yml`) operationIds. + +## What it does + +- Loads OpenAPI spec from local `spec` folder. +- Resolves `operationId -> method/path`. +- Builds URL with path/query params. +- Executes request through native `fetch`. + +## Quick usage + +```ts +import { RestApiClient } from '@jumentix/sdk-rest-client'; + +const client = new RestApiClient('http://localhost:3000/api/1.0.0'); + +const response = await client.request({ + operationId: 'createUser', + body: { + username: 'john', + password: 'StrongPass#123' + } +}); +``` + +## Build + +```bash +pnpm --filter @jumentix/sdk-rest-client build +``` diff --git a/packages/sdk-rest-client/package.json b/packages/sdk-rest-client/package.json new file mode 100644 index 00000000..dce1c7ff --- /dev/null +++ b/packages/sdk-rest-client/package.json @@ -0,0 +1,20 @@ +{ + "name": "@jumentix/sdk-rest-client", + "version": "0.0.0", + "private": true, + "description": "JumentiX REST SDK client", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run typecheck", + "lint": "echo \"sdk-rest-client: lint managed by root workspace\"", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "yaml": "^2.5.1" + } +} diff --git a/packages/sdk-rest-client/src/RestApiClient.ts b/packages/sdk-rest-client/src/RestApiClient.ts new file mode 100644 index 00000000..ecef5488 --- /dev/null +++ b/packages/sdk-rest-client/src/RestApiClient.ts @@ -0,0 +1,82 @@ +import { loadSpecs } from './spec/loadSpecs'; + +type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'; + +export interface IRestApiRequest { + operationId: string; + pathParams?: Record; + query?: Record; + body?: unknown; + headers?: Record; +} + +const compilePath = ( + pathTemplate: string, + pathParams?: Record +): string => { + if (!pathParams) return pathTemplate; + return Object.entries(pathParams).reduce((acc, [key, value]) => { + return acc.replace(new RegExp(`{${key}}`, 'g'), String(value)); + }, pathTemplate); +}; + +export class RestApiClient { + private readonly baseUrl: string; + + private readonly operationToRoute: Map = new Map(); + + constructor(baseUrl?: string) { + const { openApi } = loadSpecs(); + const serverUrl = openApi?.servers?.[0]?.url || 'http://localhost:3000/api/1.0.0'; + this.baseUrl = baseUrl || serverUrl; + + for (const [routePath, methods] of Object.entries(openApi.paths || {})) { + for (const [method, config] of Object.entries(methods as Record)) { + const operationId = config?.operationId; + if (operationId) { + this.operationToRoute.set(operationId, { + method: method as HttpMethod, + path: routePath + }); + } + } + } + } + + public async request(request: IRestApiRequest): Promise { + const route = this.operationToRoute.get(request.operationId); + if (!route) { + throw new Error(`Operation "${request.operationId}" not found in OpenAPI spec.`); + } + + const path = compilePath(route.path, request.pathParams); + const url = new URL(`${this.baseUrl}${path}`); + if (request.query) { + for (const [key, value] of Object.entries(request.query)) { + url.searchParams.set(key, String(value)); + } + } + + const response = await fetch(url.toString(), { + method: route.method.toUpperCase(), + headers: { + 'content-type': 'application/json', + ...(request.headers || {}) + }, + body: request.body === undefined ? undefined : JSON.stringify(request.body) + }); + + if (!response.ok) { + const message = await response.text(); + throw new Error(`REST request failed: ${response.status} ${message}`); + } + + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('application/json')) { + return response.json() as Promise; + } + return response.text() as TResponse; + } +} + +export default RestApiClient; diff --git a/packages/sdk-rest-client/src/index.ts b/packages/sdk-rest-client/src/index.ts new file mode 100644 index 00000000..c8a8b60e --- /dev/null +++ b/packages/sdk-rest-client/src/index.ts @@ -0,0 +1,2 @@ +export * from './spec/loadSpecs'; +export * from './RestApiClient'; diff --git a/packages/sdk-rest-client/src/spec/loadSpecs.ts b/packages/sdk-rest-client/src/spec/loadSpecs.ts new file mode 100644 index 00000000..c83becce --- /dev/null +++ b/packages/sdk-rest-client/src/spec/loadSpecs.ts @@ -0,0 +1,17 @@ +import fs from 'fs'; +import path from 'path'; +import YAML from 'yaml'; + +export interface ILoadedSpecs { + openApi: Record; +} + +export const loadSpecs = ( + basePath = path.resolve(process.cwd(), 'spec') +): ILoadedSpecs => { + const openApiPath = path.join(basePath, '1.0.0.yml'); + + return { + openApi: YAML.parse(fs.readFileSync(openApiPath, 'utf8')) + }; +}; diff --git a/packages/sdk-rest-client/tsconfig.json b/packages/sdk-rest-client/tsconfig.json new file mode 100644 index 00000000..b01f95ae --- /dev/null +++ b/packages/sdk-rest-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "noEmit": false + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/sdk-websocket-client/README.md b/packages/sdk-websocket-client/README.md new file mode 100644 index 00000000..dce8939b --- /dev/null +++ b/packages/sdk-websocket-client/README.md @@ -0,0 +1,33 @@ +# @jumentix/sdk-websocket-client + +WebSocket SDK client for the Socket.IO realtime API, driven by AsyncAPI WebSocket spec (`/spec/asyncapi/1.0.0.websocket.yml`). + +## What it does + +- Loads AsyncAPI websocket server metadata. +- Connects through Socket.IO (`/ws` path). +- Sends request envelope via `api:request`. +- Waits for callback response and returns typed result. + +## Quick usage + +```ts +import { WebSocketApiClient } from '@jumentix/sdk-websocket-client'; + +const client = new WebSocketApiClient('ws://localhost:3001'); +client.connect(); + +const response = await client.request({ + operationId: 'createUser', + input: { + username: 'john', + password: 'StrongPass#123' + } +}); +``` + +## Build + +```bash +pnpm --filter @jumentix/sdk-websocket-client build +``` diff --git a/packages/sdk-websocket-client/package.json b/packages/sdk-websocket-client/package.json new file mode 100644 index 00000000..cd3e2bcf --- /dev/null +++ b/packages/sdk-websocket-client/package.json @@ -0,0 +1,21 @@ +{ + "name": "@jumentix/sdk-websocket-client", + "version": "0.0.0", + "private": true, + "description": "JumentiX WebSocket SDK client", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run typecheck", + "lint": "echo \"sdk-websocket-client: lint managed by root workspace\"", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "socket.io-client": "^4.8.1", + "yaml": "^2.5.1" + } +} diff --git a/packages/sdk-websocket-client/src/WebSocketApiClient.ts b/packages/sdk-websocket-client/src/WebSocketApiClient.ts new file mode 100644 index 00000000..cd183f2a --- /dev/null +++ b/packages/sdk-websocket-client/src/WebSocketApiClient.ts @@ -0,0 +1,75 @@ +import { io, Socket } from 'socket.io-client'; +import { loadSpecs } from './spec/loadSpecs'; + +export interface IWebSocketApiRequest { + operationId: string; + version?: string; + authorization?: string; + input?: Record; + params?: Record; + queryString?: Record; + metadata?: Record; +} + +export interface IWebSocketApiResponse { + ok: boolean; + version?: string; + operationId: string; + result?: any; + error?: { + name: string; + message: string; + }; +} + +export class WebSocketApiClient { + private readonly url: string; + + private readonly path: string; + + private socket?: Socket; + + constructor(url?: string) { + const { asyncApiWebSocket } = loadSpecs(); + const host = asyncApiWebSocket?.servers?.local?.host || 'localhost:3001'; + this.url = url || `ws://${host}`; + this.path = '/ws'; + } + + public connect(): void { + if (this.socket?.connected) return; + this.socket = io(this.url, { + path: this.path, + transports: ['websocket'] + }); + } + + public disconnect(): void { + if (this.socket) { + this.socket.disconnect(); + this.socket = undefined; + } + } + + public async request(request: IWebSocketApiRequest): Promise { + if (!this.socket) { + this.connect(); + } + + return new Promise((resolve, reject) => { + this.socket!.timeout(30000).emit('api:request', request, (response: IWebSocketApiResponse) => { + if (!response) { + reject(new Error('WebSocket timeout/no response')); + return; + } + if (!response.ok) { + reject(new Error(response.error?.message || 'WebSocket operation failed')); + return; + } + resolve(response); + }); + }); + } +} + +export default WebSocketApiClient; diff --git a/packages/sdk-websocket-client/src/index.ts b/packages/sdk-websocket-client/src/index.ts new file mode 100644 index 00000000..edc48ca6 --- /dev/null +++ b/packages/sdk-websocket-client/src/index.ts @@ -0,0 +1,2 @@ +export * from './spec/loadSpecs'; +export * from './WebSocketApiClient'; diff --git a/packages/sdk-websocket-client/src/spec/loadSpecs.ts b/packages/sdk-websocket-client/src/spec/loadSpecs.ts new file mode 100644 index 00000000..76a3287a --- /dev/null +++ b/packages/sdk-websocket-client/src/spec/loadSpecs.ts @@ -0,0 +1,17 @@ +import fs from 'fs'; +import path from 'path'; +import YAML from 'yaml'; + +export interface ILoadedSpecs { + asyncApiWebSocket: Record; +} + +export const loadSpecs = ( + basePath = path.resolve(process.cwd(), 'spec') +): ILoadedSpecs => { + const asyncApiWebSocketPath = path.join(basePath, 'asyncapi', '1.0.0.websocket.yml'); + + return { + asyncApiWebSocket: YAML.parse(fs.readFileSync(asyncApiWebSocketPath, 'utf8')) + }; +}; diff --git a/packages/sdk-websocket-client/tsconfig.json b/packages/sdk-websocket-client/tsconfig.json new file mode 100644 index 00000000..b01f95ae --- /dev/null +++ b/packages/sdk-websocket-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "noEmit": false + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/shared-contracts/package.json b/packages/shared-contracts/package.json new file mode 100644 index 00000000..711bfd84 --- /dev/null +++ b/packages/shared-contracts/package.json @@ -0,0 +1,12 @@ +{ + "name": "@jumentix/shared-contracts", + "version": "0.0.0", + "private": true, + "description": "JumentiX shared OpenAPI/AsyncAPI contracts placeholder", + "scripts": { + "build": "echo \"shared-contracts placeholder: migration wave pending\"", + "test": "echo \"shared-contracts placeholder: migration wave pending\"", + "lint": "echo \"shared-contracts placeholder: migration wave pending\"", + "typecheck": "echo \"shared-contracts placeholder: migration wave pending\"" + } +} diff --git a/pm2/ecosystem.dev.cjs b/pm2/ecosystem.dev.cjs index d79f083b..0b2c1b4e 100644 --- a/pm2/ecosystem.dev.cjs +++ b/pm2/ecosystem.dev.cjs @@ -2,9 +2,9 @@ module.exports = { apps: [ { name: 'aaa-dev-restapi', - script: './src/interface/HTTP/adapters/start-rest-api.ts', + script: './apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts', interpreter: 'node', - node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev', + node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev', env: { NODE_ENV: 'dev', AAA_HTTP_PORT: '3000' @@ -12,9 +12,9 @@ module.exports = { }, { name: 'aaa-dev-websocketapi', - script: './src/interface/WebSocket/adapters/start-websocket-api.ts', + script: './apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts', interpreter: 'node', - node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev', + node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev', env: { NODE_ENV: 'dev', AAA_WEBSOCKET_PORT: '3001', @@ -25,9 +25,9 @@ module.exports = { }, { name: 'aaa-dev-grpcapi', - script: './src/interface/gRPC/adapters/start-grpc-api.ts', + script: './apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts', interpreter: 'node', - node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.dev', + node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.dev', env: { NODE_ENV: 'dev', AAA_GRPC_PORT: '3002', @@ -37,8 +37,8 @@ module.exports = { } }, { - name: 'aaa-dev-servicemangement', - script: './servicemangement/server.js', + name: 'aaa-dev-service-management', + script: './apps/service-management/server.js', interpreter: 'node', env: { NODE_ENV: 'dev', diff --git a/pm2/ecosystem.production.cjs b/pm2/ecosystem.production.cjs index bc52a76c..e606ec18 100644 --- a/pm2/ecosystem.production.cjs +++ b/pm2/ecosystem.production.cjs @@ -2,9 +2,9 @@ module.exports = { apps: [ { name: 'aaa-prod-restapi', - script: './.build/interface/HTTP/adapters/start-rest-api.js', + script: './.build/apps/backend-template/src/interface/HTTP/adapters/start-rest-api.js', interpreter: 'node', - node_args: '--env-file=./.build/config/.env.prod', + node_args: '--env-file=./.build/apps/backend-template/src/config/.env.prod', env: { NODE_ENV: 'prod', AAA_HTTP_PORT: '5000' @@ -12,9 +12,9 @@ module.exports = { }, { name: 'aaa-prod-websocketapi', - script: './.build/interface/WebSocket/adapters/start-websocket-api.js', + script: './.build/apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.js', interpreter: 'node', - node_args: '--env-file=./.build/config/.env.prod', + node_args: '--env-file=./.build/apps/backend-template/src/config/.env.prod', env: { NODE_ENV: 'prod', AAA_WEBSOCKET_PORT: '5001', @@ -25,9 +25,9 @@ module.exports = { }, { name: 'aaa-prod-grpcapi', - script: './.build/interface/gRPC/adapters/start-grpc-api.js', + script: './.build/apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.js', interpreter: 'node', - node_args: '--env-file=./.build/config/.env.prod', + node_args: '--env-file=./.build/apps/backend-template/src/config/.env.prod', env: { NODE_ENV: 'prod', AAA_GRPC_PORT: '5002', @@ -37,8 +37,8 @@ module.exports = { } }, { - name: 'aaa-prod-servicemangement', - script: './servicemangement/server.js', + name: 'aaa-prod-service-management', + script: './apps/service-management/server.js', interpreter: 'node', env: { NODE_ENV: 'prod', diff --git a/pm2/ecosystem.staging.cjs b/pm2/ecosystem.staging.cjs index d32b774d..997ef318 100644 --- a/pm2/ecosystem.staging.cjs +++ b/pm2/ecosystem.staging.cjs @@ -2,9 +2,9 @@ module.exports = { apps: [ { name: 'aaa-staging-restapi', - script: './src/interface/HTTP/adapters/start-rest-api.ts', + script: './apps/backend-template/src/interface/HTTP/adapters/start-rest-api.ts', interpreter: 'node', - node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.staging', + node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.staging', env: { NODE_ENV: 'staging', AAA_HTTP_PORT: '4000' @@ -12,9 +12,9 @@ module.exports = { }, { name: 'aaa-staging-websocketapi', - script: './src/interface/WebSocket/adapters/start-websocket-api.ts', + script: './apps/backend-template/src/interface/WebSocket/adapters/start-websocket-api.ts', interpreter: 'node', - node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.staging', + node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.staging', env: { NODE_ENV: 'staging', AAA_WEBSOCKET_PORT: '4001', @@ -25,9 +25,9 @@ module.exports = { }, { name: 'aaa-staging-grpcapi', - script: './src/interface/gRPC/adapters/start-grpc-api.ts', + script: './apps/backend-template/src/interface/gRPC/adapters/start-grpc-api.ts', interpreter: 'node', - node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./src/config/.env.staging', + node_args: '-r ts-node/register -r tsconfig-paths/register --env-file=./apps/backend-template/src/config/.env.staging', env: { NODE_ENV: 'staging', AAA_GRPC_PORT: '4002', @@ -37,8 +37,8 @@ module.exports = { } }, { - name: 'aaa-staging-servicemangement', - script: './servicemangement/server.js', + name: 'aaa-staging-service-management', + script: './apps/service-management/server.js', interpreter: 'node', env: { NODE_ENV: 'staging', diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..2d19d38b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - "." + - "apps/*" + - "packages/*" + - "apps/service-management" + - "tooling/*" diff --git a/release-policy.json b/release-policy.json new file mode 100644 index 00000000..47b572b4 --- /dev/null +++ b/release-policy.json @@ -0,0 +1,5 @@ +{ + "packageVersioning": "independent", + "appVersioning": "locked", + "appLockedVersion": "0.0.2" +} diff --git a/sdk-clients/README.md b/sdk-clients/README.md new file mode 100644 index 00000000..79a5b7bd --- /dev/null +++ b/sdk-clients/README.md @@ -0,0 +1,22 @@ +# sdk-clients (Compatibility Layer) + +Legacy compatibility surface for SDK imports. + +## Purpose + +This directory is maintained to avoid breaking existing imports while the monorepo migration moves SDK ownership to workspace packages: + +- `@jumentix/sdk-rest-client` +- `@jumentix/sdk-websocket-client` +- `@jumentix/sdk-grpc-client` + +## Current behavior + +- `sdk-clients/rest/RestApiClient.ts` re-exports from `packages/sdk-rest-client`. +- `sdk-clients/websocket/WebSocketApiClient.ts` re-exports from `packages/sdk-websocket-client`. +- `sdk-clients/grpc/GrpcApiClient.ts` re-exports from `packages/sdk-grpc-client`. +- `sdk-clients/spec/loadSpecs.ts` aggregates per-package spec loaders. + +## Migration guidance + +Use workspace packages directly for new code. Keep this compatibility layer only for backward compatibility until full cutover. diff --git a/sdk-clients/grpc/GrpcApiClient.ts b/sdk-clients/grpc/GrpcApiClient.ts index a67ebe40..116c814f 100644 --- a/sdk-clients/grpc/GrpcApiClient.ts +++ b/sdk-clients/grpc/GrpcApiClient.ts @@ -1,90 +1 @@ -import path from 'path'; -import grpc from '@grpc/grpc-js'; -import protoLoader from '@grpc/proto-loader'; -import { loadSpecs } from '../spec/loadSpecs'; - -export interface IGrpcApiRequest { - operationId: string; - version?: string; - authorization?: string; - input?: Record; - params?: Record; - queryString?: Record; - metadata?: Record; -} - -export interface IGrpcApiResponse { - ok: boolean; - version?: string; - operationId: string; - result?: any; - error?: { - name?: string; - message?: string; - }; -} - -export class GrpcApiClient { - private readonly host: string; - - private readonly protoFilePath: string; - - private readonly client: any; - - constructor(host?: string, protoFilePath?: string) { - const { asyncApiGrpc } = loadSpecs(); - this.host = host || asyncApiGrpc?.servers?.local?.host || 'localhost:3002'; - this.protoFilePath = protoFilePath || path.resolve(process.cwd(), 'src/interface/gRPC/proto/async-api.proto'); - - const packageDefinition = protoLoader.loadSync(this.protoFilePath, { - longs: String, - enums: String, - defaults: true, - oneofs: true - }); - const grpcObject = grpc.loadPackageDefinition(packageDefinition) as any; - this.client = new grpcObject.realtime.AsyncApiGateway( - this.host, - grpc.credentials.createInsecure() - ); - } - - public request(payload: IGrpcApiRequest): Promise { - const grpcPayload = { - version: payload.version || '', - operationId: payload.operationId, - authorization: payload.authorization || '', - inputJson: JSON.stringify(payload.input || {}), - paramsJson: JSON.stringify(payload.params || {}), - queryStringJson: JSON.stringify(payload.queryString || {}), - metadataJson: JSON.stringify(payload.metadata || {}) - }; - - return new Promise((resolve, reject) => { - this.client.request(grpcPayload, (error: Error | null, response: any) => { - if (error) { - reject(error); - return; - } - if (!response?.ok) { - reject(new Error(response?.errorMessage || 'gRPC operation failed')); - return; - } - resolve({ - ok: response.ok, - version: response.version, - operationId: response.operationId, - result: response.resultJson ? JSON.parse(response.resultJson) : undefined, - error: response.errorName || response.errorMessage - ? { - name: response.errorName, - message: response.errorMessage - } - : undefined - }); - }); - }); - } -} - -export default GrpcApiClient; +export { GrpcApiClient } from '@jumentix/sdk-grpc-client'; diff --git a/sdk-clients/index.ts b/sdk-clients/index.ts index 32550fe6..0ec3893e 100644 --- a/sdk-clients/index.ts +++ b/sdk-clients/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/export */ export * from './spec/loadSpecs'; export * from './rest/RestApiClient'; export * from './websocket/WebSocketApiClient'; diff --git a/sdk-clients/rest/RestApiClient.ts b/sdk-clients/rest/RestApiClient.ts index 184ad803..2438e0fa 100644 --- a/sdk-clients/rest/RestApiClient.ts +++ b/sdk-clients/rest/RestApiClient.ts @@ -1,82 +1 @@ -import { loadSpecs } from '../spec/loadSpecs'; - -type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'; - -export interface IRestApiRequest { - operationId: string; - pathParams?: Record; - query?: Record; - body?: unknown; - headers?: Record; -} - -const compilePath = ( - pathTemplate: string, - pathParams?: Record -): string => { - if (!pathParams) return pathTemplate; - return Object.entries(pathParams).reduce((acc, [key, value]) => { - return acc.replace(new RegExp(`{${key}}`, 'g'), String(value)); - }, pathTemplate); -}; - -export class RestApiClient { - private readonly baseUrl: string; - - private readonly operationToRoute: Map = new Map(); - - constructor(baseUrl?: string) { - const { openApi } = loadSpecs(); - const serverUrl = openApi?.servers?.[0]?.url || 'http://localhost:3000/api/1.0.0'; - this.baseUrl = baseUrl || serverUrl; - - for (const [routePath, methods] of Object.entries(openApi.paths || {})) { - for (const [method, config] of Object.entries(methods as Record)) { - const operationId = config?.operationId; - if (operationId) { - this.operationToRoute.set(operationId, { - method: method as HttpMethod, - path: routePath - }); - } - } - } - } - - public async request(request: IRestApiRequest): Promise { - const route = this.operationToRoute.get(request.operationId); - if (!route) { - throw new Error(`Operation "${request.operationId}" not found in OpenAPI spec.`); - } - - const path = compilePath(route.path, request.pathParams); - const url = new URL(`${this.baseUrl}${path}`); - if (request.query) { - for (const [key, value] of Object.entries(request.query)) { - url.searchParams.set(key, String(value)); - } - } - - const response = await fetch(url.toString(), { - method: route.method.toUpperCase(), - headers: { - 'content-type': 'application/json', - ...(request.headers || {}) - }, - body: request.body === undefined ? undefined : JSON.stringify(request.body) - }); - - if (!response.ok) { - const message = await response.text(); - throw new Error(`REST request failed: ${response.status} ${message}`); - } - - const contentType = response.headers.get('content-type') || ''; - if (contentType.includes('application/json')) { - return response.json() as Promise; - } - return response.text() as TResponse; - } -} - -export default RestApiClient; +export { RestApiClient } from '@jumentix/sdk-rest-client'; diff --git a/sdk-clients/spec/loadSpecs.ts b/sdk-clients/spec/loadSpecs.ts index 158dd97d..4453c6f9 100644 --- a/sdk-clients/spec/loadSpecs.ts +++ b/sdk-clients/spec/loadSpecs.ts @@ -1,6 +1,6 @@ -import fs from 'fs'; -import path from 'path'; -import YAML from 'yaml'; +import { loadSpecs as loadRestSpecs } from '@jumentix/sdk-rest-client'; +import { loadSpecs as loadWebSocketSpecs } from '@jumentix/sdk-websocket-client'; +import { loadSpecs as loadGrpcSpecs } from '@jumentix/sdk-grpc-client'; export interface ILoadedSpecs { openApi: Record; @@ -8,16 +8,9 @@ export interface ILoadedSpecs { asyncApiGrpc: Record; } -export const loadSpecs = ( - basePath = path.resolve(process.cwd(), 'spec') -): ILoadedSpecs => { - const openApiPath = path.join(basePath, '1.0.0.yml'); - const asyncApiWebSocketPath = path.join(basePath, 'asyncapi', '1.0.0.websocket.yml'); - const asyncApiGrpcPath = path.join(basePath, 'asyncapi', '1.0.0.grpc.yml'); - - return { - openApi: YAML.parse(fs.readFileSync(openApiPath, 'utf8')), - asyncApiWebSocket: YAML.parse(fs.readFileSync(asyncApiWebSocketPath, 'utf8')), - asyncApiGrpc: YAML.parse(fs.readFileSync(asyncApiGrpcPath, 'utf8')) - }; +export const loadSpecs = (): ILoadedSpecs => { + const { openApi } = loadRestSpecs(); + const { asyncApiWebSocket } = loadWebSocketSpecs(); + const { asyncApiGrpc } = loadGrpcSpecs(); + return { openApi, asyncApiWebSocket, asyncApiGrpc }; }; diff --git a/sdk-clients/websocket/WebSocketApiClient.ts b/sdk-clients/websocket/WebSocketApiClient.ts index 873f823b..d683496f 100644 --- a/sdk-clients/websocket/WebSocketApiClient.ts +++ b/sdk-clients/websocket/WebSocketApiClient.ts @@ -1,75 +1 @@ -import { io, Socket } from 'socket.io-client'; -import { loadSpecs } from '../spec/loadSpecs'; - -export interface IWebSocketApiRequest { - operationId: string; - version?: string; - authorization?: string; - input?: Record; - params?: Record; - queryString?: Record; - metadata?: Record; -} - -export interface IWebSocketApiResponse { - ok: boolean; - version?: string; - operationId: string; - result?: any; - error?: { - name: string; - message: string; - }; -} - -export class WebSocketApiClient { - private readonly url: string; - - private readonly path: string; - - private socket?: Socket; - - constructor(url?: string) { - const { asyncApiWebSocket } = loadSpecs(); - const host = asyncApiWebSocket?.servers?.local?.host || 'localhost:3001'; - this.url = url || `ws://${host}`; - this.path = '/ws'; - } - - public connect(): void { - if (this.socket?.connected) return; - this.socket = io(this.url, { - path: this.path, - transports: ['websocket'] - }); - } - - public disconnect(): void { - if (this.socket) { - this.socket.disconnect(); - this.socket = undefined; - } - } - - public async request(request: IWebSocketApiRequest): Promise { - if (!this.socket) { - this.connect(); - } - - return new Promise((resolve, reject) => { - this.socket!.timeout(30000).emit('api:request', request, (response: IWebSocketApiResponse) => { - if (!response) { - reject(new Error('WebSocket timeout/no response')); - return; - } - if (!response.ok) { - reject(new Error(response.error?.message || 'WebSocket operation failed')); - return; - } - resolve(response); - }); - }); - } -} - -export default WebSocketApiClient; +export { WebSocketApiClient } from '@jumentix/sdk-websocket-client'; diff --git a/serverless.ts b/serverless.ts index 60ced8fd..f1399d13 100644 --- a/serverless.ts +++ b/serverless.ts @@ -28,7 +28,7 @@ const serverlessConfiguration: Serverless = { }, functions: { localhost_get: { - handler: 'src/interface/HTTP/adapters/aws/lambda/handlers/localhost.getHandler', + handler: 'apps/backend-template/src/interface/HTTP/adapters/aws/lambda/handlers/localhost.getHandler', package: { individually: true }, @@ -42,7 +42,7 @@ const serverlessConfiguration: Serverless = { ] }, user_create: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/create.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/create.handler', package: { individually: true, patterns: [ @@ -59,131 +59,131 @@ const serverlessConfiguration: Serverless = { ] }, auth_login: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/login.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/login.handler', events: [{ http: { path: '/auth/login', method: 'POST' } }] }, auth_logout: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/logout.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/logout.handler', events: [{ http: { path: '/auth/logout', method: 'POST' } }] }, auth_register: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/register.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/register.handler', events: [{ http: { path: '/auth/register', method: 'POST' } }] }, auth_update_user_password: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updateUserPassword.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateUserPassword.handler', events: [{ http: { path: '/auth/updateUserPassword', method: 'POST' } }] }, user_get_all: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/getAll.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAll.handler', events: [{ http: { path: '/users', method: 'GET' } }] }, user_get_one: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/getOneById.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOneById.handler', events: [{ http: { path: '/users/{id}', method: 'GET' } }] }, user_update: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/update.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/update.handler', events: [{ http: { path: '/users/{id}', method: 'PUT' } }] }, user_delete: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deleteOne.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOne.handler', events: [{ http: { path: '/users/{id}', method: 'DELETE' } }] }, user_update_password: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updatePassword.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePassword.handler', events: [{ http: { path: '/users/{id}/updatePassword', method: 'PUT' } }] }, user_create_email: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/createEmail.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createEmail.handler', events: [{ http: { path: '/users/{id}/createEmail', method: 'POST' } }] }, user_update_email: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updateEmail.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateEmail.handler', events: [{ http: { path: '/users/{id}/updateEmail/{emailId}', method: 'PUT' } }] }, user_delete_email: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deleteEmail.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteEmail.handler', events: [{ http: { path: '/users/{id}/deleteEmail/{emailId}', method: 'DELETE' } }] }, user_create_document: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/createDocument.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createDocument.handler', events: [{ http: { path: '/users/{id}/createDocument', method: 'POST' } }] }, user_update_document: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updateDocument.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateDocument.handler', events: [{ http: { path: '/users/{id}/updateDocument/{documentId}', method: 'PUT' } }] }, user_delete_document: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deleteDocument.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteDocument.handler', events: [{ http: { path: '/users/{id}/documentDelete/{documentId}', method: 'DELETE' } }] }, user_create_phone: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/createPhone.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createPhone.handler', events: [{ http: { path: '/users/{id}/createPhone', method: 'POST' } }] }, user_update_phone: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updatePhone.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updatePhone.handler', events: [{ http: { path: '/users/{id}/updatePhone/{phoneId}', method: 'PUT' } }] }, user_delete_phone: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deletePhone.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deletePhone.handler', events: [{ http: { path: '/users/{id}/phoneDelete/{phoneId}', method: 'DELETE' } }] }, organization_create: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/createOrganization.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganization.handler', events: [{ http: { path: '/organizations', method: 'POST' } }] }, organization_get_all: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/getAllOrganizations.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getAllOrganizations.handler', events: [{ http: { path: '/organizations', method: 'GET' } }] }, organization_get_one: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/getOrganizationById.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/getOrganizationById.handler', events: [{ http: { path: '/organizations/{id}', method: 'GET' } }] }, organization_update: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updateOrganization.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganization.handler', events: [{ http: { path: '/organizations/{id}', method: 'PUT' } }] }, organization_delete: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deleteOrganization.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganization.handler', events: [{ http: { path: '/organizations/{id}', method: 'DELETE' } }] }, organization_create_address: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/createOrganizationAddress.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationAddress.handler', events: [{ http: { path: '/organizations/{id}/createAddress', method: 'POST' } }] }, organization_update_address: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updateOrganizationAddress.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationAddress.handler', events: [{ http: { path: '/organizations/{id}/updateAddress/{addressId}', method: 'PUT' } }] }, organization_delete_address: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deleteOrganizationAddress.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationAddress.handler', events: [{ http: { path: '/organizations/{id}/deleteAddress/{addressId}', method: 'DELETE' } }] }, organization_create_phone: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/createOrganizationPhone.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationPhone.handler', events: [{ http: { path: '/organizations/{id}/createPhone', method: 'POST' } }] }, organization_update_phone: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updateOrganizationPhone.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationPhone.handler', events: [{ http: { path: '/organizations/{id}/updatePhone/{phoneId}', method: 'PUT' } }] }, organization_delete_phone: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deleteOrganizationPhone.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationPhone.handler', events: [{ http: { path: '/organizations/{id}/deletePhone/{phoneId}', method: 'DELETE' } }] }, organization_create_email: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/createOrganizationEmail.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/createOrganizationEmail.handler', events: [{ http: { path: '/organizations/{id}/createEmail', method: 'POST' } }] }, organization_update_email: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/updateOrganizationEmail.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/updateOrganizationEmail.handler', events: [{ http: { path: '/organizations/{id}/updateEmail/{emailId}', method: 'PUT' } }] }, organization_delete_email: { - handler: 'src/modules/Users/interface/api/frameworks/aws/lambda/handlers/deleteOrganizationEmail.handler', + handler: 'apps/backend-template/src/modules/Users/interface/restapi/frameworks/aws/lambda/handlers/deleteOrganizationEmail.handler', events: [{ http: { path: '/organizations/{id}/deleteEmail/{emailId}', method: 'DELETE' } }] } } diff --git a/servicemangement/README.md b/servicemangement/README.md deleted file mode 100644 index b21a7668..00000000 --- a/servicemangement/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Service Management Application - -`servicemangement` is a tabbed local application for engineering setup and design workflows in this boilerplate. - -## Tabs - -1. **Domain Designer** - - Full ER/domain modeling MVP. - - Domain rectangles, entity modeling, relationship design, OpenAPI export/import, model checks. -2. **Communication Interface Designer** - - Register inbound interface adapters (`HTTP/REST`, `gRPC`, `WebSocket`, `SSE`). - - Track framework/runtime, entrypoint, and controller mapping. -3. **Service Configuration** - - Configure service kind (`REST API`, `WebSocket API + REST API`, `gRPC API + REST API`), - execution model, cloud provider, static assets profile, and runtime ports. - - Includes PM2 runtime profile preview for VM deployments. - - Includes runtime env editor for: - - `AAA_HTTP_FRAMEWORK` - - `AAA_REALTIME_API` - - `AAA_REALTIME_API_PROTOCOL` - - `AAA_REALTIME_API_DATABASE_DRIVER` -4. **Deploy Management** - - Register deployment targets for VMs, dedicated servers, EC2, and function providers. - -## Run - -This application is served via PM2: - -- `/Users/eduardoalmeida/apps/apps/apps/aaa-typescript-boilerplate/servicemangement/server.js` - -Commands: - -- `npm run dev:service-management` -- `npm run dev` (auto-starts service management + REST profile) - -## Runtime Env API - -- `GET /api/runtime/env?environment=dev|staging|ci` -- `POST /api/runtime/env` - -### Editable Keys - -- `AAA_HTTP_FRAMEWORK` -- `AAA_REALTIME_API` -- `AAA_REALTIME_API_PROTOCOL` -- `AAA_REALTIME_API_DATABASE_DRIVER` - -### Environment Mapping - -- `dev` -> `src/config/.env.dev` -- `staging` -> `src/config/.env.staging` -- `ci` -> `src/config/.env.ci` diff --git a/sonar-project.properties b/sonar-project.properties index c4d4544c..3389d0df 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,7 +5,7 @@ sonar.sources=src sonar.tests=test sonar.test.inclusions=test/**/*.test.ts,test/**/*.spec.ts -sonar.exclusions=**/node_modules/**,**/.build/**,**/dist/**,**/.serverless/**,**/coverage/**,**/*.d.ts,servicemangement/**,docker/**,AsyncAPIdoc/**,sdk-clients/**,documentation/**,spec/** +sonar.exclusions=**/node_modules/**,**/.build/**,**/dist/**,**/.serverless/**,**/coverage/**,**/*.d.ts,apps/service-management/**,docker/**,AsyncAPIdoc/**,sdk-clients/**,documentation/**,spec/** sonar.cpd.exclusions=src/infra/**/*,test/**/* # JavaScript/TypeScript coverage report (LCOV) diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 675953f2..00000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/infra/mutex/port/IMutexService.ts b/src/infra/mutex/port/IMutexService.ts deleted file mode 100644 index a478f102..00000000 --- a/src/infra/mutex/port/IMutexService.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IServiceResponse } from '@src/modules/port/IServiceResponse'; - -export interface IMutexService { - lock(resourceName: string, uuid: string): Promise; - isLocked(resourceName: string, uuid: string): Promise; - unlock(resourceName: string, uuid: string): Promise; -} diff --git a/src/infra/persistence/compileDatabaseClient.ts b/src/infra/persistence/compileDatabaseClient.ts deleted file mode 100644 index 76024131..00000000 --- a/src/infra/persistence/compileDatabaseClient.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { IDatabaseClient } from '@src/infra/persistence/port/IDatabaseClient'; -import { InMemoryDbClient } from '@src/infra/persistence/InMemoryDatabase/InMemoryDbClient'; -import { - AuroraRepository, - BaseExternalDataRepository, - CassandraRepository, - DynamoDbRepository, - createExternalStores, - FirebaseRepository, - MongoMongooseRepository, - OracleRepository, - RdsRepository, - SqlSequelizeRepository -} from '@src/infra/persistence/external'; - -type DriverName = - | 'InMemory' - | 'Mongo' - | 'PostgreSQL' - | 'MySQL' - | 'MSSQL' - | 'Oracle' - | 'SQLite' - | 'DynamoDB' - | 'Cassandra' - | 'Firebase' - | 'Aurora' - | 'RDS'; -type ExternalDriverName = Exclude; - -const DEFAULT_DRIVER: DriverName = 'InMemory'; -const SQL_DIALECT_TO_DRIVER: Record< - 'postgres' | 'mysql' | 'mssql' | 'oracle' | 'sqlite', - ExternalDriverName -> = { - postgres: 'PostgreSQL', - mysql: 'MySQL', - mssql: 'MSSQL', - oracle: 'Oracle', - sqlite: 'SQLite' -}; - -const sanitize = (value: string): string => value.trim().toLowerCase(); - -const normalizeDriver = (value?: string): DriverName => { - if (!value || value.trim() === '') return DEFAULT_DRIVER; - const normalized = sanitize(value); - if (['inmemory', 'in-memory', 'memory'].includes(normalized)) return 'InMemory'; - if (['mongo', 'mongodb', 'mongoose'].includes(normalized)) return 'Mongo'; - if (['postgres', 'postgresql'].includes(normalized)) return 'PostgreSQL'; - /* istanbul ignore next */ - if (['mysql'].includes(normalized)) return 'MySQL'; - /* istanbul ignore next */ - if (['mssql', 'ms sql', 'sqlserver', 'sql server'].includes(normalized)) return 'MSSQL'; - /* istanbul ignore next */ - if (['oracle'].includes(normalized)) return 'Oracle'; - /* istanbul ignore next */ - if (['sqlite', 'sql lite'].includes(normalized)) return 'SQLite'; - if (['dynamodb', 'dynamo'].includes(normalized)) return 'DynamoDB'; - if (['cassandra'].includes(normalized)) return 'Cassandra'; - if (['firebase'].includes(normalized)) return 'Firebase'; - /* istanbul ignore next */ - if (['aurora'].includes(normalized)) return 'Aurora'; - if (['rds'].includes(normalized)) return 'RDS'; - return DEFAULT_DRIVER; -}; - -const parseNumber = (value: string | undefined, fallback: number): number => { - if (!value || value.trim() === '') return fallback; - const parsed = Number(value); - /* istanbul ignore next */ - return Number.isFinite(parsed) ? parsed : fallback; -}; - -const parseContactPoints = (value: string | undefined): string[] => { - if (!value || value.trim() === '') return ['127.0.0.1']; - return value - .split(',') - .map((item) => item.trim()) - .filter((item) => item.length > 0); -}; - -const parseJson = (value: string | undefined): Record | undefined => { - if (!value || value.trim() === '') return undefined; - try { - const parsed = JSON.parse(value); - /* istanbul ignore next */ - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - } catch (error) { - return undefined; - } - return undefined; -}; - -const toExternalClient = ( - driver: ExternalDriverName, - connector: BaseExternalDataRepository -): IDatabaseClient => { - return { - stores: createExternalStores(driver, connector), - connect: () => connector.connect(), - disconnect: () => connector.disconnect() - }; -}; - -const createSqlClient = (dialect: 'postgres' | 'mysql' | 'mssql' | 'oracle' | 'sqlite') => { - const connector = new SqlSequelizeRepository({ - dialect, - connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, - database: process.env.AAA_DATABASE_NAME, - extra: { - poolMax: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 15), - poolMin: parseNumber(process.env.AAA_DATABASE_POOL_MIN, 0), - poolAcquireMs: parseNumber(process.env.AAA_DATABASE_POOL_ACQUIRE_MS, 30000), - poolIdleMs: parseNumber(process.env.AAA_DATABASE_POOL_IDLE_MS, 10000), - poolEvictMs: parseNumber(process.env.AAA_DATABASE_POOL_EVICT_MS, 1000) - } - }); - return toExternalClient(SQL_DIALECT_TO_DRIVER[dialect], connector); -}; - -const createMongoClient = (): IDatabaseClient => { - const connector = new MongoMongooseRepository({ - connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, - database: process.env.AAA_DATABASE_NAME, - extra: { - maxPoolSize: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 20), - minPoolSize: parseNumber(process.env.AAA_DATABASE_POOL_MIN, 0), - serverSelectionTimeoutMS: parseNumber(process.env.AAA_DATABASE_SERVER_SELECTION_MS, 5000), - socketTimeoutMS: parseNumber(process.env.AAA_DATABASE_SOCKET_TIMEOUT_MS, 45000) - } - }); - return toExternalClient('Mongo', connector); -}; - -const createDynamoClient = (): IDatabaseClient => { - const connector = new DynamoDbRepository({ - region: process.env.AAA_DATABASE_REGION || 'us-east-1', - endpoint: process.env.AAA_DATABASE_ENDPOINT - }); - return toExternalClient('DynamoDB', connector); -}; - -const createCassandraClient = (): IDatabaseClient => { - const connector = new CassandraRepository({ - database: process.env.AAA_DATABASE_NAME, - extra: { - contactPoints: parseContactPoints(process.env.AAA_DATABASE_CASSANDRA_CONTACT_POINTS), - localDataCenter: process.env.AAA_DATABASE_CASSANDRA_DATACENTER || 'datacenter1', - keyspace: process.env.AAA_DATABASE_NAME - } - }); - return toExternalClient('Cassandra', connector); -}; - -const createFirebaseClient = (): IDatabaseClient => { - const connector = new FirebaseRepository({ - extra: { - projectId: process.env.AAA_DATABASE_PROJECT_ID, - serviceAccount: parseJson(process.env.AAA_FIREBASE_SERVICE_ACCOUNT_JSON) - } - }); - return toExternalClient('Firebase', connector); -}; - -const createAuroraClient = (): IDatabaseClient => { - const connector = new AuroraRepository({ - connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, - database: process.env.AAA_DATABASE_NAME, - region: process.env.AAA_DATABASE_REGION || 'us-east-1', - endpoint: process.env.AAA_DATABASE_ENDPOINT, - extra: { - poolMax: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 20) - } - }); - return toExternalClient('Aurora', connector); -}; - -const createOracleClient = (): IDatabaseClient => { - const connector = new OracleRepository({ - connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, - database: process.env.AAA_DATABASE_NAME, - extra: { - user: process.env.AAA_DATABASE_USER || 'aaa', - password: process.env.AAA_DATABASE_PASSWORD || 'aaa', - connectString: process.env.AAA_DATABASE_CONNECT_STRING - } - }); - return toExternalClient('Oracle', connector); -}; - -const createRdsClient = (): IDatabaseClient => { - const dialectRaw = sanitize(process.env.AAA_DATABASE_DIALECT || 'postgres'); - const dialect = (['postgres', 'mysql', 'mssql', 'oracle', 'sqlite'].includes(dialectRaw) - ? dialectRaw - : 'postgres') as 'postgres' | 'mysql' | 'mssql' | 'oracle' | 'sqlite'; - const connector = new RdsRepository({ - connectionUrl: process.env.AAA_DATABASE_CONNECTION_URL, - database: process.env.AAA_DATABASE_NAME, - extra: { - dialect, - poolMax: parseNumber(process.env.AAA_DATABASE_POOL_MAX, 20), - poolMin: parseNumber(process.env.AAA_DATABASE_POOL_MIN, 0), - poolAcquireMs: parseNumber(process.env.AAA_DATABASE_POOL_ACQUIRE_MS, 30000), - poolIdleMs: parseNumber(process.env.AAA_DATABASE_POOL_IDLE_MS, 10000), - poolEvictMs: parseNumber(process.env.AAA_DATABASE_POOL_EVICT_MS, 1000) - } - }); - return toExternalClient('RDS', connector); -}; - -const buildByDriver = (driver: DriverName): IDatabaseClient => { - if (driver === 'InMemory') return InMemoryDbClient; - if (driver === 'Mongo') return createMongoClient(); - if (driver === 'PostgreSQL') return createSqlClient('postgres'); - if (driver === 'MySQL') return createSqlClient('mysql'); - if (driver === 'MSSQL') return createSqlClient('mssql'); - if (driver === 'Oracle') return createOracleClient(); - if (driver === 'SQLite') return createSqlClient('sqlite'); - if (driver === 'DynamoDB') return createDynamoClient(); - if (driver === 'Cassandra') return createCassandraClient(); - if (driver === 'Firebase') return createFirebaseClient(); - if (driver === 'Aurora') return createAuroraClient(); - if (driver === 'RDS') return createRdsClient(); - return InMemoryDbClient; -}; - -export const compileDatabaseClient = (): IDatabaseClient => { - const driver = normalizeDriver(process.env.AAA_DATABASE_DRIVER); - return buildByDriver(driver); -}; - -export const compileDatabaseClientByDriver = (driver: string): IDatabaseClient => { - return buildByDriver(normalizeDriver(driver)); -}; - -export const compileMongoDbClient = (): IDatabaseClient => buildByDriver('Mongo'); -export const compilePostgreSqlDbClient = (): IDatabaseClient => buildByDriver('PostgreSQL'); -export const compileMySqlDbClient = (): IDatabaseClient => buildByDriver('MySQL'); -export const compileMsSqlDbClient = (): IDatabaseClient => buildByDriver('MSSQL'); -export const compileOracleDbClient = (): IDatabaseClient => buildByDriver('Oracle'); -export const compileSqliteDbClient = (): IDatabaseClient => buildByDriver('SQLite'); -export const compileDynamoDbClient = (): IDatabaseClient => buildByDriver('DynamoDB'); -export const compileCassandraDbClient = (): IDatabaseClient => buildByDriver('Cassandra'); -export const compileFirebaseDbClient = (): IDatabaseClient => buildByDriver('Firebase'); -export const compileAuroraDbClient = (): IDatabaseClient => buildByDriver('Aurora'); -export const compileRdsDbClient = (): IDatabaseClient => buildByDriver('RDS'); diff --git a/src/infra/persistence/external/index.ts b/src/infra/persistence/external/index.ts deleted file mode 100644 index c3554f36..00000000 --- a/src/infra/persistence/external/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from '@src/infra/persistence/external/BaseExternalDataRepository'; -export * from '@src/infra/persistence/external/ExternalStoreProxy'; -export * from '@src/infra/persistence/external/SqlSequelizeRepository'; -export * from '@src/infra/persistence/external/MongoMongooseRepository'; -export * from '@src/infra/persistence/external/DynamoDbRepository'; -export * from '@src/infra/persistence/external/CassandraRepository'; -export * from '@src/infra/persistence/external/FirebaseRepository'; -export * from '@src/infra/persistence/external/AuroraRepository'; -export * from '@src/infra/persistence/external/RdsRepository'; -export * from '@src/infra/persistence/external/OracleRepository'; diff --git a/src/infra/persistence/port/IDatabaseClient.ts b/src/infra/persistence/port/IDatabaseClient.ts deleted file mode 100644 index c7dce50a..00000000 --- a/src/infra/persistence/port/IDatabaseClient.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IStore } from '@src/infra/ports/persistence/IStore'; -import { IUser } from '@src/modules/Users/domain/Entity/IUser'; -import { IOrganization } from '@src/modules/Users/domain/Entity/IOrganization'; - -export interface IDbStores { - User: IStore; - Organization: IStore; - [key: string]: IStore; -} - -export interface IDatabaseClient { - connect(): Promise; - disconnect(): Promise; - stores: IDbStores; - // [key: string]: IStore; - // mapped collections - // Auction: IStore; -} diff --git a/src/interface/HTTP/adapters/start-rest-api.ts b/src/interface/HTTP/adapters/start-rest-api.ts deleted file mode 100644 index 3643eb77..00000000 --- a/src/interface/HTTP/adapters/start-rest-api.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { resolveHTTPFramework } from '@src/interface/runtime/RuntimeEnvironment'; - -export async function startRestApiAdapter(env: NodeJS.ProcessEnv = process.env): Promise { - const framework = resolveHTTPFramework(env); - /* istanbul ignore else */ - if (framework === 'express') { - await import('@src/interface/HTTP/adapters/express/express'); - } -} - -// eslint-disable-next-line jest/require-hook -/* istanbul ignore if */ -if (require.main === module) { - startRestApiAdapter(); -} diff --git a/src/interface/WebSocket/adapters/start-websocket-api.ts b/src/interface/WebSocket/adapters/start-websocket-api.ts deleted file mode 100644 index 5005c127..00000000 --- a/src/interface/WebSocket/adapters/start-websocket-api.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { shouldStartRealtimeApi } from '@src/interface/runtime/RuntimeEnvironment'; -import { startWebSocketAdapter } from '@src/interface/WebSocket/adapters/socket-io/socket-io'; - -export async function startWebSocketApiAdapter( - env: NodeJS.ProcessEnv = process.env -): Promise { - if (!shouldStartRealtimeApi('websocket', env)) return false; - await startWebSocketAdapter(); - return true; -} - -// eslint-disable-next-line jest/require-hook -/* istanbul ignore if */ -if (require.main === module) { - startWebSocketApiAdapter(); -} diff --git a/src/modules/port/IEventBus.ts b/src/modules/port/IEventBus.ts deleted file mode 100644 index 2da5dfed..00000000 --- a/src/modules/port/IEventBus.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IIntegrationEvent } from '@src/modules/port/IIntegrationEvent'; - -export interface IEventBus { - publish(event: IIntegrationEvent): Promise; - subscribe(eventName: string, listener: (event: IIntegrationEvent) => Promise | void): void; -} diff --git a/src/modules/port/IIntegrationEvent.ts b/src/modules/port/IIntegrationEvent.ts deleted file mode 100644 index e0dbb6b9..00000000 --- a/src/modules/port/IIntegrationEvent.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IIntegrationEvent { - name: string; - payload: Record; - occurredAt: string; - metadata?: Record; -} diff --git a/src/modules/port/IMessage.ts b/src/modules/port/IMessage.ts deleted file mode 100644 index 07a769d9..00000000 --- a/src/modules/port/IMessage.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface IMessageMetadata { - requestId?: string; - correlationId?: string; - causationId?: string; - replyTo?: string; - timestamp?: string; - headers?: Record; - [key: string]: any; -} - -export interface IMessage { - contract: string; - version?: string; - payload: TPayload; - metadata?: IMessageMetadata; -} - -export interface IMessageResponse { - contract: string; - version?: string; - metadata?: IMessageMetadata; - result?: TResult; - error?: Error | Record; -} diff --git a/src/modules/port/IMessageMediator.ts b/src/modules/port/IMessageMediator.ts deleted file mode 100644 index b72b5ffe..00000000 --- a/src/modules/port/IMessageMediator.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { IEventBus } from '@src/modules/port/IEventBus'; -import { IMessage, IMessageResponse } from '@src/modules/port/IMessage'; - -export type MessageHandler = - (message: IMessage) => Promise> | IMessageResponse; - -export interface IMessageRequestOptions { - timeoutMs?: number; - routeKey?: string; - queueName?: string; -} - -export interface IMessageHandlerRegistrationOptions { - queueName?: string; - routeKey?: string; - durable?: boolean; - concurrency?: number; -} - -export interface IMessageMediator extends IEventBus { - registerHandler( - contract: string, - handler: MessageHandler, - options?: IMessageHandlerRegistrationOptions - ): void; - - request( - message: IMessage, - options?: IMessageRequestOptions - ): Promise>; -} diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index c4ebe0ac..00000000 Binary files a/test/.DS_Store and /dev/null differ diff --git a/test/unit/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.test.ts b/test/unit/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.test.ts deleted file mode 100644 index d83aa74c..00000000 --- a/test/unit/infra/persistence/KeyValueStorage/compileKeyValueStorageClient.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable jest/no-untyped-mock-factory */ -import { compileKeyValueStorageClient } from '@src/infra/persistence/KeyValueStorage/compileKeyValueStorageClient'; - -jest.mock('@src/infra/persistence/KeyValueStorage/InMemoryKeyValueStorageClient', () => ({ - InMemoryKeyValueStorageClient: { compile: jest.fn().mockReturnValue({ type: 'inmemory' }) } -})); - -jest.mock('@src/infra/persistence/KeyValueStorage/RedisKeyValueStorageClient', () => ({ - RedisKeyValueStorageClient: { compile: jest.fn().mockReturnValue({ type: 'redis' }) } -})); - -describe('compileKeyValueStorageClient', () => { - it('returns in-memory adapter for in-memory aliases', () => { - expect.hasAssertions(); - expect(compileKeyValueStorageClient('InMemory') as any).toStrictEqual({ type: 'inmemory' }); - expect(compileKeyValueStorageClient('memory') as any).toStrictEqual({ type: 'inmemory' }); - expect(compileKeyValueStorageClient('in-memory') as any).toStrictEqual({ type: 'inmemory' }); - }); - - it('returns redis adapter by default', () => { - expect.hasAssertions(); - expect(compileKeyValueStorageClient(undefined) as any).toStrictEqual({ type: 'redis' }); - expect(compileKeyValueStorageClient('redis') as any).toStrictEqual({ type: 'redis' }); - expect(compileKeyValueStorageClient('unknown') as any).toStrictEqual({ type: 'redis' }); - }); -}); diff --git a/test/unit/interface/WebSocket/adapters/start-websocket-api.test.ts b/test/unit/interface/WebSocket/adapters/start-websocket-api.test.ts deleted file mode 100644 index 660178fb..00000000 --- a/test/unit/interface/WebSocket/adapters/start-websocket-api.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable jest/no-untyped-mock-factory */ - -const websocketLoaderAdapterStart = jest.fn().mockResolvedValue(undefined); - -jest.mock('@src/interface/WebSocket/adapters/socket-io/socket-io', () => ({ - startWebSocketAdapter: websocketLoaderAdapterStart -})); - -describe('start-websocket-api loader', () => { - beforeEach(() => { - websocketLoaderAdapterStart.mockClear(); - }); - - it('starts websocket runtime when realtime protocol is websocket', async () => { - expect.assertions(2); - const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); - const started = await startWebSocketApiAdapter({ - AAA_REALTIME_API: 'yes', - AAA_REALTIME_API_PROTOCOL: 'websocket' - } as NodeJS.ProcessEnv); - expect(started).toBe(true); - expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(1); - }); - - it('does not start websocket runtime when realtime protocol is grpc', async () => { - expect.assertions(2); - const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); - const started = await startWebSocketApiAdapter({ - AAA_REALTIME_API: 'yes', - AAA_REALTIME_API_PROTOCOL: 'grpc' - } as NodeJS.ProcessEnv); - expect(started).toBe(false); - expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(0); - }); - - it('uses process env when env argument is omitted', async () => { - expect.assertions(2); - process.env.AAA_REALTIME_API = 'yes'; - process.env.AAA_REALTIME_API_PROTOCOL = 'websocket'; - const { startWebSocketApiAdapter } = await import('@src/interface/WebSocket/adapters/start-websocket-api'); - const started = await startWebSocketApiAdapter(); - expect(started).toBe(true); - expect(websocketLoaderAdapterStart).toHaveBeenCalledTimes(1); - }); -}); diff --git a/test/unit/modules/Users/adapters/out/persistence/OrganizationDataRepository.test.ts b/test/unit/modules/Users/adapters/out/persistence/OrganizationDataRepository.test.ts deleted file mode 100644 index 62778f95..00000000 --- a/test/unit/modules/Users/adapters/out/persistence/OrganizationDataRepository.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* eslint-disable jest/max-expects */ -import { OrganizationDataRepository } from '@src/modules/Users/adapters/out/persistence/OrganizationDataRepository'; -import { InMemoryDbClient } from '@src/infra/persistence/InMemoryDatabase/InMemoryDbClient'; -import { EAddressType, EEmailType } from '@src/modules/ddd/valueObjects'; - -const createPayload = () => ({ - name: `Org-${Date.now()}-${Math.random()}`, - address: [{ - email: 'hq@org.dev', - type: EAddressType.work, - isPrimary: true - }], - phone: [{ - countryCode: '55', - localCode: '11', - number: '999999', - isPrimary: true - }], - email: [{ - email: 'contact@org.dev', - type: EEmailType.work, - isPrimary: true - }], - users: [] -}); - -describe('organization data repository', () => { - it('creates and gets organizations with timestamps', async () => { - expect.hasAssertions(); - const repository = OrganizationDataRepository.compile({ - databaseClient: InMemoryDbClient - } as any); - const created = await repository.create(createPayload()); - expect(created.id).toBeDefined(); - expect(created.createdAt).toBeInstanceOf(Date); - expect(created.updatedAt).toBeInstanceOf(Date); - - const fetched = await repository.getOneById(created.id); - expect(fetched.name).toBe(created.name); - }); - - it('updates organizations preserving createdAt and refreshing updatedAt', async () => { - expect.hasAssertions(); - const repository = OrganizationDataRepository.compile({ - databaseClient: InMemoryDbClient - } as any); - const created = await repository.create(createPayload()); - const createdAt = created.createdAt.toISOString(); - - const updated = await repository.update(created.id, { - id: created.id, - ...createPayload(), - name: `${created.name}-updated` - }); - expect(updated.name).toContain('-updated'); - expect(updated.createdAt.toISOString()).toBe(createdAt); - expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(updated.createdAt.getTime()); - }); - - it('gets paged results and deletes organizations', async () => { - expect.hasAssertions(); - const repository = OrganizationDataRepository.compile({ - databaseClient: InMemoryDbClient - } as any); - const created = await repository.create(createPayload()); - const page = await repository.getAll({ id: created.id }, { page: 1, size: 10 }); - expect(page.total).toBe(1); - const deleted = await repository.delete(created.id); - expect(deleted).toBe(true); - }); - - it('supports explicit address/phone/email mutation methods', async () => { - expect.hasAssertions(); - const repository = OrganizationDataRepository.compile({ - databaseClient: InMemoryDbClient - } as any); - const created = await repository.create(createPayload()); - const createdAddressId = created.address[0].id; - const createdPhoneId = created.phone[0].id; - const createdEmailId = created.email[0].id; - - const withAddress = await repository.createAddress(created.id, { - email: 'branch@org.dev', - type: EAddressType.home, - isPrimary: false - }); - expect(withAddress.address).toHaveLength(2); - - const withUpdatedAddress = await repository.updateAddress(created.id, createdAddressId, { - id: createdAddressId, - email: 'hq2@org.dev', - type: EAddressType.vacation - }); - expect(withUpdatedAddress.address.find((x: any) => x.id === createdAddressId)?.email).toBe('hq2@org.dev'); - - const withDeletedAddress = await repository.deleteAddress(created.id, createdAddressId); - expect(withDeletedAddress.address.find((x: any) => x.id === createdAddressId)).toBeUndefined(); - - const withPhone = await repository.createPhone(created.id, { - countryCode: '1', - localCode: '305', - number: '7777777', - isPrimary: false - }); - expect(withPhone.phone).toHaveLength(2); - - const withUpdatedPhone = await repository.updatePhone(created.id, createdPhoneId, { - id: createdPhoneId, - number: '1111111' - }); - expect(withUpdatedPhone.phone.find((x: any) => x.id === createdPhoneId)?.number).toBe('1111111'); - - const withDeletedPhone = await repository.deletePhone(created.id, createdPhoneId); - expect(withDeletedPhone.phone.find((x: any) => x.id === createdPhoneId)).toBeUndefined(); - - const withEmail = await repository.createEmail(created.id, { - email: 'billing@org.dev', - type: EEmailType.work, - isPrimary: false - }); - expect(withEmail.email).toHaveLength(2); - - const withUpdatedEmail = await repository.updateEmail(created.id, createdEmailId, { - id: createdEmailId, - email: 'contact-updated@org.dev' - }); - expect(withUpdatedEmail.email.find((x: any) => x.id === createdEmailId)?.email).toBe('contact-updated@org.dev'); - - const withDeletedEmail = await repository.deleteEmail(created.id, createdEmailId); - expect(withDeletedEmail.email.find((x: any) => x.id === createdEmailId)).toBeUndefined(); - }); -}); diff --git a/test/unit/modules/Users/adapters/out/persistence/UserDataRepository.test.ts b/test/unit/modules/Users/adapters/out/persistence/UserDataRepository.test.ts deleted file mode 100644 index c819cdea..00000000 --- a/test/unit/modules/Users/adapters/out/persistence/UserDataRepository.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* eslint-disable jest/max-expects */ -import { UUID } from '@src/modules/port'; -import { UserDataRepository, exclude } from '@src/modules/Users/adapters/out/persistence/UserDataRepository'; -import { EDocumentType } from '@src/modules/ddd/valueObjects/EDocumentType'; -import { EEmailType } from '@src/modules/ddd/valueObjects/EEmailType'; - -const createPayload = () => ({ - firstName: 'John', - lastName: 'Doe', - username: 'john', - organization: '00000000-0000-4000-8000-000000000077', - password: '12345678', - salt: 'salt', - avatar: 'avatar.png', - emails: [{ email: 'john@example.com', type: EEmailType.personal, isPrimary: true }], - documents: [{ type: EDocumentType.CPF, countryIssue: 'BR', data: '11111111111' }], - phones: [{ - countryCode: '55', localCode: '11', number: '999999999', isPrimary: true - }], - roles: ['user'] -}); - -describe('user data repository', () => { - it('excludes keys from record', () => { - expect.hasAssertions(); - const value = exclude({ a: 1, b: 2 }, ['b']); - expect(value).toStrictEqual({ a: 1 }); - }); - - it('covers CRUD and nested aggregate operations', async () => { - expect.hasAssertions(); - const userId = UUID.create().toString(); - const documentId = UUID.create().toString(); - const phoneId = UUID.create().toString(); - const emailId = UUID.create().toString(); - - const baseUser = { - id: userId, - ...createPayload(), - documents: [{ - id: documentId, type: EDocumentType.CPF, countryIssue: 'BR', data: '11111111111' - }], - phones: [{ - id: phoneId, countryCode: '55', localCode: '11', number: '999999999', isPrimary: true - }], - emails: [{ - id: emailId, email: 'john@example.com', type: EEmailType.personal, isPrimary: true - }] - }; - - const store = { - create: jest.fn().mockResolvedValue(true), - update: jest.fn().mockImplementation(async (_id: string, data: any) => ({ - id: userId, - ...data - })), - delete: jest.fn().mockResolvedValue(true), - getOneById: jest.fn().mockResolvedValue(baseUser), - getAll: jest.fn().mockResolvedValue({ - result: [baseUser], - page: 1, - size: 10, - total: 1 - }) - }; - - const repository = new UserDataRepository({ - databaseClient: { stores: { User: store } } as any - }); - - const created = await repository.create(createPayload() as any); - expect(created.password).toBe('********'); - expect(created.salt).toBe('***'); - - const updated = await repository.update(userId, { - id: userId, - firstName: 'Mary', - username: 'mary', - emails: [{ - id: emailId, email: 'mary@example.com', type: EEmailType.personal, isPrimary: true - }] - } as any); - expect(updated.firstName).toBe('Mary'); - - await expect(repository.delete(userId)).resolves.toBe(true); - - const one = await repository.getOneById(userId); - expect(one.id).toBe(userId); - - const all = await repository.getAll({ username: 'john' }, { page: 1, size: 10 }); - expect(all.total).toBe(1); - - const updatedPassword = await repository.updatePassword(userId, { password: 'new-hash', salt: 'new-salt' }); - expect(updatedPassword.password).toBe('new-hash'); - - const createdDocument = await repository.createDocument(userId, { - type: EDocumentType.RG, - countryIssue: 'BR', - data: '123456' - } as any); - expect(createdDocument.documents.length).toBeGreaterThan(0); - - const updatedDocument = await repository.updateDocument(userId, documentId, { - type: EDocumentType.SSN, - countryIssue: 'US', - data: '222' - } as any); - expect(updatedDocument.documents.find((d) => d.id === documentId)?.data).toBe('222'); - - const deletedDocument = await repository.deleteDocument(userId, documentId); - expect(deletedDocument.documents.find((d) => d.id === documentId)).toBeUndefined(); - - const createdPhone = await repository.createPhone(userId, { - countryCode: '1', - localCode: '212', - number: '1111111' - } as any); - expect(createdPhone.phones.length).toBeGreaterThan(0); - - const updatedPhone = await repository.updatePhone(userId, phoneId, { - countryCode: '1', - localCode: '646', - number: '2222222' - } as any); - expect(updatedPhone.phones.find((p) => p.id === phoneId)?.number).toBe('2222222'); - - const deletedPhone = await repository.deletePhone(userId, phoneId); - expect(deletedPhone.phones.find((p) => p.id === phoneId)).toBeUndefined(); - - const createdEmail = await repository.createEmail(userId, { - email: 'new@example.com', - type: EEmailType.work - } as any); - expect(createdEmail.emails.length).toBeGreaterThan(0); - - const updatedEmail = await repository.updateEmail(userId, emailId, { - email: 'updated@example.com', - type: EEmailType.work - } as any); - expect(updatedEmail.emails.find((e) => e.id === emailId)?.email).toBe('updated@example.com'); - - const deletedEmail = await repository.deleteEmail(userId, emailId); - expect(deletedEmail.emails.find((e) => e.id === emailId)).toBeUndefined(); - }); -}); diff --git a/tooling/README.md b/tooling/README.md new file mode 100644 index 00000000..582f01cf --- /dev/null +++ b/tooling/README.md @@ -0,0 +1,3 @@ +# tooling + +Workspace folder reserved for JumentiX release/build/automation tools in monorepo mode. diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..b7e5131d --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + ".build", + "test/**/*", + "apps/backend-template/test/**/*", + "apps/jumentix-website/**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json index a9232bb6..45ac370e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,13 +31,39 @@ //"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ "paths": { - "@src": ["./src"], - "@src/*": ["./src/*"], + "@src": ["./apps/backend-template/src", "./src"], + "@src/*": ["./apps/backend-template/src/*", "./src/*"], "@infra": ["./infra"], "@infra/*": ["./infra/*"], - "@seed": ["./seed"], - "@seed/*": ["./seed/*"], - "@test/*": ["./test/*"] + "@seed": ["./apps/backend-template/seed"], + "@seed/*": ["./apps/backend-template/seed/*"], + "@test/*": ["./apps/backend-template/test/*"], + "@jumentix/message-mediator": ["./packages/message-mediator/src/index.ts"], + "@jumentix/message-mediator/*": ["./packages/message-mediator/src/*"], + "@jumentix/key-value-storage": ["./packages/key-value-storage/src/index.ts"], + "@jumentix/key-value-storage/*": ["./packages/key-value-storage/src/*"], + "@jumentix/persistence-contracts": ["./packages/persistence-contracts/src/index.ts"], + "@jumentix/persistence-contracts/*": ["./packages/persistence-contracts/src/*"], + "@jumentix/mutex-service": ["./packages/mutex-service/src/index.ts"], + "@jumentix/mutex-service/*": ["./packages/mutex-service/src/*"], + "@jumentix/external-persistence-core": ["./packages/external-persistence-core/src/index.ts"], + "@jumentix/external-persistence-core/*": ["./packages/external-persistence-core/src/*"], + "@jumentix/external-store-proxy": ["./packages/external-store-proxy/src/index.ts"], + "@jumentix/external-store-proxy/*": ["./packages/external-store-proxy/src/*"], + "@jumentix/external-db-repositories": ["./packages/external-db-repositories/src/index.ts"], + "@jumentix/external-db-repositories/*": ["./packages/external-db-repositories/src/*"], + "@jumentix/database-client-factory": ["./packages/database-client-factory/src/index.ts"], + "@jumentix/database-client-factory/*": ["./packages/database-client-factory/src/*"], + "@jumentix/runtime-infra": ["./packages/runtime-infra/src/index.ts"], + "@jumentix/runtime-infra/*": ["./packages/runtime-infra/src/*"], + "@jumentix/adapter-runtime-bootstrap": ["./packages/adapter-runtime-bootstrap/src/index.ts"], + "@jumentix/adapter-runtime-bootstrap/*": ["./packages/adapter-runtime-bootstrap/src/*"], + "@jumentix/sdk-rest-client": ["./packages/sdk-rest-client/src/index.ts"], + "@jumentix/sdk-rest-client/*": ["./packages/sdk-rest-client/src/*"], + "@jumentix/sdk-websocket-client": ["./packages/sdk-websocket-client/src/index.ts"], + "@jumentix/sdk-websocket-client/*": ["./packages/sdk-websocket-client/src/*"], + "@jumentix/sdk-grpc-client": ["./packages/sdk-grpc-client/src/index.ts"], + "@jumentix/sdk-grpc-client/*": ["./packages/sdk-grpc-client/src/*"] }, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ @@ -115,7 +141,7 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["./**/*.ts", "src", "config"], + "include": ["./**/*.ts", "apps/backend-template/src", "config"], "ts-node": { // Do not forget to `npm i -D tsconfig-paths` "require": ["tsconfig-paths/register"]