@cdmbase/wiki-browser 12.0.18-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/lib/components/Logo.d.ts +4 -0
- package/lib/components/Logo.d.ts.map +1 -0
- package/lib/components/Logo.js +16 -0
- package/lib/components/Logo.js.map +1 -0
- package/lib/components/help/SidebarSearch.d.ts +8 -0
- package/lib/components/help/SidebarSearch.d.ts.map +1 -0
- package/lib/components/help/SidebarSearch.js +111 -0
- package/lib/components/help/SidebarSearch.js.map +1 -0
- package/lib/components/help/index.d.ts +2 -0
- package/lib/components/help/index.d.ts.map +1 -0
- package/lib/components/landing/FeatureCard.d.ts +13 -0
- package/lib/components/landing/FeatureCard.d.ts.map +1 -0
- package/lib/components/landing/FeatureCard.js +85 -0
- package/lib/components/landing/FeatureCard.js.map +1 -0
- package/lib/components/landing/QuickLinkCard.d.ts +8 -0
- package/lib/components/landing/QuickLinkCard.d.ts.map +1 -0
- package/lib/components/landing/QuickLinkCard.js +26 -0
- package/lib/components/landing/QuickLinkCard.js.map +1 -0
- package/lib/components/landing/SearchInput.d.ts +10 -0
- package/lib/components/landing/SearchInput.d.ts.map +1 -0
- package/lib/components/landing/SearchInput.js +223 -0
- package/lib/components/landing/SearchInput.js.map +1 -0
- package/lib/components/landing/index.d.ts +4 -0
- package/lib/components/landing/index.d.ts.map +1 -0
- package/lib/components/welcome.d.ts +3 -0
- package/lib/components/welcome.d.ts.map +1 -0
- package/lib/compute.d.ts +4 -0
- package/lib/compute.d.ts.map +1 -0
- package/lib/compute.js +96 -0
- package/lib/compute.js.map +1 -0
- package/lib/config/env-config.d.ts +4 -0
- package/lib/config/env-config.d.ts.map +1 -0
- package/lib/config/env-config.js +7 -0
- package/lib/config/env-config.js.map +1 -0
- package/lib/docs.config.d.ts +48 -0
- package/lib/docs.config.d.ts.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -0
- package/lib/loaders/search.d.ts +1 -0
- package/lib/loaders/search.d.ts.map +1 -0
- package/lib/module.d.ts +4 -0
- package/lib/module.d.ts.map +1 -0
- package/lib/module.js +11 -0
- package/lib/module.js.map +1 -0
- package/lib/pages/ArticlePage/ArticlePage.d.ts +4 -0
- package/lib/pages/ArticlePage/ArticlePage.d.ts.map +1 -0
- package/lib/pages/ArticlePage/ArticlePage.js +222 -0
- package/lib/pages/ArticlePage/ArticlePage.js.map +1 -0
- package/lib/pages/ArticlePage/index.d.ts +3 -0
- package/lib/pages/ArticlePage/index.d.ts.map +1 -0
- package/lib/pages/ArticlePage/index.js +3 -0
- package/lib/pages/ArticlePage/index.js.map +1 -0
- package/lib/pages/CategoryCollection/CategoryCollection.d.ts +4 -0
- package/lib/pages/CategoryCollection/CategoryCollection.d.ts.map +1 -0
- package/lib/pages/CategoryCollection/CategoryCollection.js +103 -0
- package/lib/pages/CategoryCollection/CategoryCollection.js.map +1 -0
- package/lib/pages/CategoryCollection/index.d.ts +3 -0
- package/lib/pages/CategoryCollection/index.d.ts.map +1 -0
- package/lib/pages/CategoryCollection/index.js +3 -0
- package/lib/pages/CategoryCollection/index.js.map +1 -0
- package/lib/pages/Help/HelpIndex.d.ts +4 -0
- package/lib/pages/Help/HelpIndex.d.ts.map +1 -0
- package/lib/pages/Help/HelpIndex.js +44 -0
- package/lib/pages/Help/HelpIndex.js.map +1 -0
- package/lib/pages/Help/index.d.ts +4 -0
- package/lib/pages/Help/index.d.ts.map +1 -0
- package/lib/pages/Help/index.js +226 -0
- package/lib/pages/Help/index.js.map +1 -0
- package/lib/pages/Landing/index.d.ts +3 -0
- package/lib/pages/Landing/index.d.ts.map +1 -0
- package/lib/pages/Landing/index.js +281 -0
- package/lib/pages/Landing/index.js.map +1 -0
- package/lib/routes.json +2533 -0
- package/lib/seo.d.ts +22 -0
- package/lib/seo.d.ts.map +1 -0
- package/lib/slot-fill/FooterFill.d.ts +3 -0
- package/lib/slot-fill/FooterFill.d.ts.map +1 -0
- package/lib/slot-fill/FooterFill.js +18 -0
- package/lib/slot-fill/FooterFill.js.map +1 -0
- package/lib/slot-fill/LogoFill.d.ts +5 -0
- package/lib/slot-fill/LogoFill.d.ts.map +1 -0
- package/lib/slot-fill/LogoFill.js +74 -0
- package/lib/slot-fill/LogoFill.js.map +1 -0
- package/lib/slot-fill/consts.d.ts +5 -0
- package/lib/slot-fill/consts.d.ts.map +1 -0
- package/lib/slot-fill/consts.js +1 -0
- package/lib/slot-fill/consts.js.map +1 -0
- package/lib/slot-fill/index.d.ts +4 -0
- package/lib/slot-fill/index.d.ts.map +1 -0
- package/lib/templates/assets/images/add-link-frontend.png +0 -0
- package/lib/templates/assets/images/add-package-backend.png +0 -0
- package/lib/templates/assets/images/add-to-backend-module.png +0 -0
- package/lib/templates/assets/images/add-upload-client-frontend.png +0 -0
- package/lib/templates/assets/images/additional-parameters.png +0 -0
- package/lib/templates/assets/images/aeh-implementation.png +0 -0
- package/lib/templates/assets/images/aeh-usage.png +0 -0
- package/lib/templates/assets/images/apollo-client/recommendation_cache_mgmt.png +0 -0
- package/lib/templates/assets/images/app-deploy-new-version/jenkins1.PNG +0 -0
- package/lib/templates/assets/images/app-deploy-new-version/jenkins2.PNG +0 -0
- package/lib/templates/assets/images/auth-wrapper-code.png +0 -0
- package/lib/templates/assets/images/cdebase.png +0 -0
- package/lib/templates/assets/images/cdm-locales-directory.png +0 -0
- package/lib/templates/assets/images/client-settings.png +0 -0
- package/lib/templates/assets/images/codegen_file_update.png +0 -0
- package/lib/templates/assets/images/configuration.png +0 -0
- package/lib/templates/assets/images/copy-plugin.png +0 -0
- package/lib/templates/assets/images/docusaurus.png +0 -0
- package/lib/templates/assets/images/error-link.png +0 -0
- package/lib/templates/assets/images/error-sample.png +0 -0
- package/lib/templates/assets/images/extension copy.png +0 -0
- package/lib/templates/assets/images/extension.png +0 -0
- package/lib/templates/assets/images/graphql/graphql-folder-backend.png +0 -0
- package/lib/templates/assets/images/graphql/graphql-folder-with-gql.png +0 -0
- package/lib/templates/assets/images/i18n-config.png +0 -0
- package/lib/templates/assets/images/image.png +0 -0
- package/lib/templates/assets/images/logo.svg +10 -0
- package/lib/templates/assets/images/logo1.svg +1 -0
- package/lib/templates/assets/images/modify-upload-false-server.png +0 -0
- package/lib/templates/assets/images/navigation-auth-enabled.png +0 -0
- package/lib/templates/assets/images/org-dashboard-navigation.png +0 -0
- package/lib/templates/assets/images/org-navigation.png +0 -0
- package/lib/templates/assets/images/preferences_graphql_type.png +0 -0
- package/lib/templates/assets/images/provider.png +0 -0
- package/lib/templates/assets/images/route-config.png +0 -0
- package/lib/templates/assets/images/service-accounts.png +0 -0
- package/lib/templates/assets/images/source-code/source-code-environments.png +0 -0
- package/lib/templates/assets/images/source-code/source-code-organization.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/jenkins-changes.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/lerna-changes.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/root-package-json-changes.png +0 -0
- package/lib/templates/assets/images/spin-clone-develop-deployment/values-dev-changes.png +0 -0
- package/lib/templates/assets/images/sso-mappers.png +0 -0
- package/lib/templates/assets/images/sso-picture-mapper.png +0 -0
- package/lib/templates/assets/images/sso-settings.png +0 -0
- package/lib/templates/assets/images/timesheet_apollo_cache.png +0 -0
- package/lib/templates/assets/images/timesheet_query.png +0 -0
- package/lib/templates/assets/images/tutorial/docsVersionDropdown.png +0 -0
- package/lib/templates/assets/images/tutorial/localeDropdown.png +0 -0
- package/lib/templates/assets/images/unauthenticated.png +0 -0
- package/lib/templates/assets/images/undraw_docusaurus_mountain.svg +170 -0
- package/lib/templates/assets/images/undraw_docusaurus_react.svg +169 -0
- package/lib/templates/assets/images/undraw_docusaurus_tree.svg +1 -0
- package/lib/templates/assets/images/vite-plugin-config.png +0 -0
- package/lib/templates/content/docs/Generators/Project/generate-fullproject.md +12 -0
- package/lib/templates/content/docs/LLM/Logger.llm.md +194 -0
- package/lib/templates/content/docs/LLM/backend-proxies-services-llm.md +2687 -0
- package/lib/templates/content/docs/LLM/backend-service-llm.md +3384 -0
- package/lib/templates/content/docs/LLM/db_migration_llm.md +954 -0
- package/lib/templates/content/docs/LLM/frontend/REMIX-15.3-upgrade-llm.md +1245 -0
- package/lib/templates/content/docs/LLM/inngest/INNGEST_FUNCTION_DEVELOPMENT_GUIDE_LLM.md +1241 -0
- package/lib/templates/content/docs/LLM/inngest/INNGEST_NAMESPACE_LLM.md +384 -0
- package/lib/templates/content/docs/LLM/llm_workflow_namespace.md +384 -0
- package/lib/templates/content/docs/LLM/organization-components-form-llm.md +1395 -0
- package/lib/templates/content/docs/LLM/page-component-llm.md +173 -0
- package/lib/templates/content/docs/LLM/preferences-settings-llm.md +2781 -0
- package/lib/templates/content/docs/LLM/tailwind-css-llm.md +502 -0
- package/lib/templates/content/docs/UI/SchemaBasedUI.md +334 -0
- package/lib/templates/content/docs/UI/SlotFillComponent.md +334 -0
- package/lib/templates/content/docs/adminide-modules/account/auth0-login.md +31 -0
- package/lib/templates/content/docs/adminide-modules/account/index.md +14 -0
- package/lib/templates/content/docs/adminide-modules/account/keycloak-remix-setup.md +86 -0
- package/lib/templates/content/docs/adminide-modules/account/remix-auth-setup.md +79 -0
- package/lib/templates/content/docs/adminide-modules/account/various-auth-qatest.md +157 -0
- package/lib/templates/content/docs/adminide-modules/api-builders/graphql.md +906 -0
- package/lib/templates/content/docs/adminide-modules/billing/payments/index.md +14 -0
- package/lib/templates/content/docs/adminide-modules/billing/payments/stripe/index.md +14 -0
- package/lib/templates/content/docs/adminide-modules/billing/payments/stripe/settingup-stripe-locally.md +25 -0
- package/lib/templates/content/docs/adminide-modules/billing/tier-config.md +293 -0
- package/lib/templates/content/docs/adminide-modules/connectors/Connector.md +207 -0
- package/lib/templates/content/docs/adminide-modules/file-upload/index.md +16 -0
- package/lib/templates/content/docs/adminide-modules/file-upload/setup.md +435 -0
- package/lib/templates/content/docs/adminide-modules/file-upload/upload-file-using-signed-url.md +161 -0
- package/lib/templates/content/docs/adminide-modules/preferences/AddAdditionalPermissions.md +151 -0
- package/lib/templates/content/docs/adminide-modules/preferences/Configuration.md +241 -0
- package/lib/templates/content/docs/adminide-modules/preferences/Policy-Configuration.md +61 -0
- package/lib/templates/content/docs/adminide-modules/preferences/UI-components/ResourceSettingsLoader.md +319 -0
- package/lib/templates/content/docs/adminide-modules/preferences/contribute_scope_target.md +280 -0
- package/lib/templates/content/docs/adminide-modules/preferences/generate-urii.md +94 -0
- package/lib/templates/content/docs/adminide-modules/preferences/index.md +28 -0
- package/lib/templates/content/docs/adminide-modules/preferences/machine-configuration.md +157 -0
- package/lib/templates/content/docs/adminide-modules/preferences/pageSettings/generateCdecodeUri.md +1289 -0
- package/lib/templates/content/docs/adminide-modules/preferences/pageSettings/migratingFromUseSettings.md +215 -0
- package/lib/templates/content/docs/adminide-modules/preferences/permissions/Roles-Permissions.md +72 -0
- package/lib/templates/content/docs/adminide-modules/preferences/permissions/settingUserPermissions.md +139 -0
- package/lib/templates/content/docs/adminide-modules/preferences/preference-dependency.md +138 -0
- package/lib/templates/content/docs/adminide-modules/preferences/route-based-configuration.md +41 -0
- package/lib/templates/content/docs/adminide-modules/preferences/schema-configuration.md +71 -0
- package/lib/templates/content/docs/adminide-modules/preferences/supported.md +24 -0
- package/lib/templates/content/docs/adminide-modules/preferences/useSettingsLoader.md +248 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/auth-providers.md +1317 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/keycloak-guide.md +543 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/tenant-management/tenant-based-authentication.md +846 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/tenant-management/tenant-management.md +708 -0
- package/lib/templates/content/docs/adminide-modules/project-tools/tenant-management/tenants.md +1117 -0
- package/lib/templates/content/docs/chrome-extension/index.md +14 -0
- package/lib/templates/content/docs/chrome-extension/setup.md +30 -0
- package/lib/templates/content/docs/contributing/adding-package.md +23 -0
- package/lib/templates/content/docs/contributing/adding_new_modules.md +99 -0
- package/lib/templates/content/docs/contributing/architecture-updates.md +19 -0
- package/lib/templates/content/docs/contributing/avoid-using-promises-ui.md +116 -0
- package/lib/templates/content/docs/contributing/coding-guidelines.md +111 -0
- package/lib/templates/content/docs/contributing/do-and-dont.md +42 -0
- package/lib/templates/content/docs/contributing/faq.md +22 -0
- package/lib/templates/content/docs/contributing/folder-setup/browser.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/config.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/containers-server.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/core.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/graphql.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/index.md +30 -0
- package/lib/templates/content/docs/contributing/folder-setup/module.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/server.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/services.md +12 -0
- package/lib/templates/content/docs/contributing/folder-setup/store.md +12 -0
- package/lib/templates/content/docs/contributing/frontend-coding.md +30 -0
- package/lib/templates/content/docs/contributing/git-subtree-sharing.md +73 -0
- package/lib/templates/content/docs/contributing/graphql-subscriptions.md +69 -0
- package/lib/templates/content/docs/contributing/how-to-contribute.md +30 -0
- package/lib/templates/content/docs/contributing/how_to_check_pure_esm.md +29 -0
- package/lib/templates/content/docs/contributing/index.md +60 -0
- package/lib/templates/content/docs/contributing/installation-issues.md +23 -0
- package/lib/templates/content/docs/contributing/keyboard-shortcut.md +131 -0
- package/lib/templates/content/docs/contributing/language/locale-support.md +12 -0
- package/lib/templates/content/docs/contributing/lerna-build-tools.md +516 -0
- package/lib/templates/content/docs/contributing/lerna-yarn-workspaces.md +95 -0
- package/lib/templates/content/docs/contributing/lint-and-formatter.md +20 -0
- package/lib/templates/content/docs/contributing/mobile-setup.md +16 -0
- package/lib/templates/content/docs/contributing/project-setup.md +233 -0
- package/lib/templates/content/docs/contributing/react/index.md +14 -0
- package/lib/templates/content/docs/contributing/react/lazy-component.md +70 -0
- package/lib/templates/content/docs/contributing/run-various-options.md +124 -0
- package/lib/templates/content/docs/contributing/schema-first-graphql-types.md +37 -0
- package/lib/templates/content/docs/contributing/source-code-organization.md +57 -0
- package/lib/templates/content/docs/contributing/staging-docker.md +88 -0
- package/lib/templates/content/docs/contributing/third-party/apollo-client-v3-tutorials.md +28 -0
- package/lib/templates/content/docs/contributing/third-party/index.md +18 -0
- package/lib/templates/content/docs/contributing/typescript-contribution.md +16 -0
- package/lib/templates/content/docs/devops/app-deploy-new-version.md +30 -0
- package/lib/templates/content/docs/devops/index.md +14 -0
- package/lib/templates/content/docs/devops/mobile-jenkins-build.md +40 -0
- package/lib/templates/content/docs/devops/versioning-the-project.md +128 -0
- package/lib/templates/content/docs/error-handler/application-error-handler.md +40 -0
- package/lib/templates/content/docs/error-handler/error-handling.md +26 -0
- package/lib/templates/content/docs/error-handler/index.md +16 -0
- package/lib/templates/content/docs/error-handler/logging-errors.md +14 -0
- package/lib/templates/content/docs/feature-api/copy-operation.md +427 -0
- package/lib/templates/content/docs/feature-api/feature-browser/assets.md +46 -0
- package/lib/templates/content/docs/feature-api/feature-browser/auth-permissions.md +12 -0
- package/lib/templates/content/docs/feature-api/feature-browser/feature.md +131 -0
- package/lib/templates/content/docs/feature-api/feature-browser/index.md +22 -0
- package/lib/templates/content/docs/feature-api/feature-browser/routes-menu.md +110 -0
- package/lib/templates/content/docs/feature-api/feature-browser/routing-convention.md +124 -0
- package/lib/templates/content/docs/feature-api/feature-browser/routing.md +338 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/auth-permissions.md +20 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/feature.md +130 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/index.md +18 -0
- package/lib/templates/content/docs/feature-api/feature-mobile/navigation.md +187 -0
- package/lib/templates/content/docs/feature-api/feature-server/Scheduling.md +44 -0
- package/lib/templates/content/docs/feature-api/feature-server/dataloader.md +320 -0
- package/lib/templates/content/docs/feature-api/feature-server/dependency-injection.md +81 -0
- package/lib/templates/content/docs/feature-api/feature-server/feature.md +65 -0
- package/lib/templates/content/docs/feature-api/feature-server/generic-dataloader.md +135 -0
- package/lib/templates/content/docs/feature-api/feature-server/index.md +40 -0
- package/lib/templates/content/docs/feature-api/feature-server/migration.md +127 -0
- package/lib/templates/content/docs/feature-api/feature-server/mongo-model.md +72 -0
- package/lib/templates/content/docs/feature-api/feature-server/permissions.md +12 -0
- package/lib/templates/content/docs/feature-api/feature-server/policies.md +57 -0
- package/lib/templates/content/docs/feature-api/feature-server/preferences.md +57 -0
- package/lib/templates/content/docs/feature-api/feature-server/repositories.md +114 -0
- package/lib/templates/content/docs/feature-api/feature-server/resolvers.md +126 -0
- package/lib/templates/content/docs/feature-api/feature-server/rules.md +132 -0
- package/lib/templates/content/docs/feature-api/feature-server/schema.md +12 -0
- package/lib/templates/content/docs/feature-api/feature-server/services.md +102 -0
- package/lib/templates/content/docs/feature-api/feature-server/setup-resource-crud.md +359 -0
- package/lib/templates/content/docs/feature-api/index.md +18 -0
- package/lib/templates/content/docs/graphql/apolloClient-mutation.md +94 -0
- package/lib/templates/content/docs/graphql/index.md +14 -0
- package/lib/templates/content/docs/graphql/scalars.md +15 -0
- package/lib/templates/content/docs/help/index.md +14 -0
- package/lib/templates/content/docs/help/intro.md +16 -0
- package/lib/templates/content/docs/intl/ant-design-menu-translation.md +74 -0
- package/lib/templates/content/docs/intl/intl-namespace.md +129 -0
- package/lib/templates/content/docs/intl/vite-plugin-intl.md +87 -0
- package/lib/templates/content/docs/intl/webpack-plugin-intl.md +12 -0
- package/lib/templates/content/docs/intro.md +18 -0
- package/lib/templates/content/docs/knowledge/basic-fullstack.md +238 -0
- package/lib/templates/content/docs/mailing/index.md +14 -0
- package/lib/templates/content/docs/mailing/mailing-template.md +148 -0
- package/lib/templates/content/docs/mobile/App-navigation-generator.md +410 -0
- package/lib/templates/content/docs/mobile/MobileTestCases.md +264 -0
- package/lib/templates/content/docs/mobile/eas-profile-build.md +107 -0
- package/lib/templates/content/docs/mobile/expo-push-notification-setup.md +216 -0
- package/lib/templates/content/docs/mobile/index.md +14 -0
- package/lib/templates/content/docs/mobile/routes.md +83 -0
- package/lib/templates/content/docs/organization/adding-account-context.md +116 -0
- package/lib/templates/content/docs/organization/adding-org-mobile-navigation.md +22 -0
- package/lib/templates/content/docs/organization/adding-org-web-navigation.md +12 -0
- package/lib/templates/content/docs/organization/index.md +20 -0
- package/lib/templates/content/docs/organization/initialization.md +20 -0
- package/lib/templates/content/docs/organization/organization-resource-vs-resource.md +112 -0
- package/lib/templates/content/docs/remix/configuration/component-structure-best-practices.md +152 -0
- package/lib/templates/content/docs/remix/configuration/configurations.md +218 -0
- package/lib/templates/content/docs/remix/configuration/css-import-and-stylesheets.md +142 -0
- package/lib/templates/content/docs/remix/configuration/dont-subcomponent-network.md +166 -0
- package/lib/templates/content/docs/remix/configuration/generated-data-loaders.md +122 -0
- package/lib/templates/content/docs/remix/configuration/generated-resource-loaders.md +257 -0
- package/lib/templates/content/docs/remix/configuration/query-params-generator.md +216 -0
- package/lib/templates/content/docs/remix/configuration/routes-extra-icons.md +103 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-advanced.md +86 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-auth.md +113 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-best-practices.md +55 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-fields.md +79 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-graphql.md +79 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-index.md +112 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-loaders.md +165 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-middleware.md +196 -0
- package/lib/templates/content/docs/remix/configuration/routes-json-overview.md +53 -0
- package/lib/templates/content/docs/remix/data-loaders.md +43 -0
- package/lib/templates/content/docs/remix/devtools/remix-devtools.md +58 -0
- package/lib/templates/content/docs/remix/examples/changes-using-servercode.md +79 -0
- package/lib/templates/content/docs/remix/extra-icons.md +62 -0
- package/lib/templates/content/docs/remix/extra-links.md +65 -0
- package/lib/templates/content/docs/remix/generated-data-loaders.md +114 -0
- package/lib/templates/content/docs/remix/queryParamsGenerator.md +89 -0
- package/lib/templates/content/docs/remix/resources.md +16 -0
- package/lib/templates/content/docs/remix/styles.md +132 -0
- package/lib/templates/content/docs/remix/wiki.md +12 -0
- package/lib/templates/content/docs/security/auth-wrapper/auth-wrapper.md +24 -0
- package/lib/templates/content/docs/security/index.md +18 -0
- package/lib/templates/content/docs/security/secure-button-mobilenative.md +88 -0
- package/lib/templates/content/docs/security/secure-button-web.md +89 -0
- package/lib/templates/content/docs/server-side/account-customization.md +82 -0
- package/lib/templates/content/docs/server-side/apollo/caching.md +164 -0
- package/lib/templates/content/docs/server-side/backend-architecture/FINAL-DECISION.md +209 -0
- package/lib/templates/content/docs/server-side/backend-architecture/TRUE-FINAL-ARCHITECTURE.md +603 -0
- package/lib/templates/content/docs/server-side/backend-architecture/index1.md +0 -0
- package/lib/templates/content/docs/server-side/backend-coding.md +839 -0
- package/lib/templates/content/docs/server-side/e2b/manageing-template.md +197 -0
- package/lib/templates/content/docs/server-side/index.md +14 -0
- package/lib/templates/content/docs/server-side/inngest-functions-module.md +309 -0
- package/lib/templates/content/docs/server-side/listen-stripe-events.md +43 -0
- package/lib/templates/content/docs/server-side/slug-service.md +323 -0
- package/lib/templates/content/docs/tests/index.md +18 -0
- package/lib/templates/content/docs/tests/jest-test-debug-vscode.md +40 -0
- package/lib/templates/content/docs/tests/known-errors.md +116 -0
- package/lib/templates/content/docs/tests/service-test-template.md +118 -0
- package/lib/templates/content/docs/tests/test-setup.md +93 -0
- package/lib/templates/content/docs/xstate.md +23 -0
- package/lib/types.d.ts +37 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/utils/docsNavigation.d.ts +9 -0
- package/lib/utils/docsNavigation.d.ts.map +1 -0
- package/lib/utils/docsNavigation.js +37 -0
- package/lib/utils/docsNavigation.js.map +1 -0
- package/lib/utils/helpCenterUtils.d.ts +26 -0
- package/lib/utils/helpCenterUtils.d.ts.map +1 -0
- package/lib/utils/index.d.ts +3 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +3 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/markdownLoader.d.ts +36 -0
- package/lib/utils/markdownLoader.d.ts.map +1 -0
- package/lib/utils/markdownLoader.js +2242 -0
- package/lib/utils/markdownLoader.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,3384 @@
|
|
|
1
|
+
# LLM Template: Complete Backend Service Creation Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This is a comprehensive template for creating backend services in the adminIde-stack architecture. Follow this exact sequence to create new services with GraphQL, Mongoose, templates, and proper module integration.
|
|
6
|
+
|
|
7
|
+
## ⚠️ CRITICAL: Naming Conventions and Conflict Prevention
|
|
8
|
+
|
|
9
|
+
### **Module-Prefixed Naming for Large Projects**
|
|
10
|
+
|
|
11
|
+
**In large monorepo projects, name conflicts are common and destructive. ALWAYS use module prefixes for ALL GraphQL types, enums, queries, and mutations.**
|
|
12
|
+
|
|
13
|
+
#### **GraphQL Naming Pattern:**
|
|
14
|
+
|
|
15
|
+
```graphql
|
|
16
|
+
# ✅ CORRECT - Module prefixed (prevents conflicts)
|
|
17
|
+
type MarketplacePublisher @entity {
|
|
18
|
+
id: ID! @id
|
|
19
|
+
# ... fields
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type MarketplacePublisherStats @entity(embedded: true) {
|
|
23
|
+
totalDownloads: Int!
|
|
24
|
+
# ... fields
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enum MarketplacePublisherStatus {
|
|
28
|
+
ACTIVE
|
|
29
|
+
INACTIVE
|
|
30
|
+
VERIFIED
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
input CreateMarketplacePublisherInput {
|
|
34
|
+
# ... fields
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
extend type Query {
|
|
38
|
+
getMarketplacePublisher(id: ID!): MarketplacePublisher @auth
|
|
39
|
+
listMarketplacePublishers(filter: MarketplacePublisherFilter): [MarketplacePublisher!]! @auth
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
extend type Mutation {
|
|
43
|
+
createMarketplacePublisher(input: CreateMarketplacePublisherInput!): MarketplacePublisher! @auth
|
|
44
|
+
updateMarketplacePublisher(id: ID!, input: UpdateMarketplacePublisherInput!): MarketplacePublisher! @auth
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# ❌ WRONG - No prefix (will conflict with other modules)
|
|
48
|
+
type Publisher @entity {
|
|
49
|
+
id: ID! @id
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
enum PublisherStatus {
|
|
53
|
+
ACTIVE
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
extend type Query {
|
|
57
|
+
getPublisher(id: ID!): Publisher @auth # Conflicts with other Publisher types
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### **Why Module Prefixes Matter:**
|
|
62
|
+
|
|
63
|
+
1. **Conflict Prevention**: Multiple modules may have `Publisher`, `User`, `Project` types
|
|
64
|
+
2. **GraphQL Schema Merging**: All modules merge into single schema - conflicts break builds
|
|
65
|
+
3. **Generated Type Safety**: TypeScript generation creates clean, non-conflicting interfaces
|
|
66
|
+
4. **API Clarity**: Clear which module/domain the type belongs to
|
|
67
|
+
5. **Maintenance**: Easy to identify type ownership across large teams
|
|
68
|
+
|
|
69
|
+
#### **Prefix Examples by Module:**
|
|
70
|
+
|
|
71
|
+
- **Marketplace**: `MarketplacePublisher`, `MarketplaceExtension`, `MarketplaceStats`
|
|
72
|
+
- **Account**: `AccountUser`, `AccountOrganization`, `AccountTeam`
|
|
73
|
+
- **Project**: `ProjectTask`, `ProjectTemplate`, `ProjectStatus`
|
|
74
|
+
- **Extension**: `ExtensionRegistry`, `ExtensionInstalled`, `ExtensionManifest`
|
|
75
|
+
- **Billing**: `BillingPlan`, `BillingInvoice`, `BillingSubscription`
|
|
76
|
+
|
|
77
|
+
### **Complete Service Creation Workflow**
|
|
78
|
+
|
|
79
|
+
After defining GraphQL schemas with proper prefixes, follow this exact sequence:
|
|
80
|
+
|
|
81
|
+
1. **Create GraphQL Schemas** (with module prefixes)
|
|
82
|
+
2. **Create Templates** (Service/Repository interfaces)
|
|
83
|
+
3. **Run Template Generation** (copies to common package)
|
|
84
|
+
4. **Implement Services & Repositories** (using generated interfaces)
|
|
85
|
+
5. **Container Binding** (dependency injection setup)
|
|
86
|
+
6. **Module Integration** (createServiceFunc setup)
|
|
87
|
+
7. **Resolver Implementation** (GraphQL endpoint logic)
|
|
88
|
+
8. **Build & Test** (verify integration)
|
|
89
|
+
|
|
90
|
+
**This workflow ensures:**
|
|
91
|
+
|
|
92
|
+
- ✅ Type safety across the entire stack
|
|
93
|
+
- ✅ Proper dependency injection
|
|
94
|
+
- ✅ No naming conflicts
|
|
95
|
+
- ✅ Consistent architecture patterns
|
|
96
|
+
- ✅ Template-driven development (not manual interfaces)
|
|
97
|
+
|
|
98
|
+
## 📋 Complete Implementation Checklist
|
|
99
|
+
|
|
100
|
+
### Phase 1: GraphQL Schema Design
|
|
101
|
+
|
|
102
|
+
**File**: `packages-modules/{module}/server/src/graphql/schemas/{entity}.graphql`
|
|
103
|
+
|
|
104
|
+
```graphql
|
|
105
|
+
# Main Entity with Database Mapping - ALWAYS USE MODULE PREFIX
|
|
106
|
+
type {ModulePrefix}{EntityName} implements Node @entity {
|
|
107
|
+
id: ID! @id
|
|
108
|
+
tenantId: String! @column
|
|
109
|
+
|
|
110
|
+
# Object References - USE ObjectId pattern for relationships
|
|
111
|
+
{relationField}: {ModulePrefix}{RelatedType} @column(overrideType: "ObjectId")
|
|
112
|
+
|
|
113
|
+
# String/Primitive Fields
|
|
114
|
+
{fieldName}: String! @column
|
|
115
|
+
{optionalField}: String @column
|
|
116
|
+
|
|
117
|
+
# Embedded Documents
|
|
118
|
+
{embeddedField}: {ModulePrefix}{EmbeddedType}! @embedded
|
|
119
|
+
|
|
120
|
+
# Status/Enum Fields - MODULE PREFIXED
|
|
121
|
+
status: {ModulePrefix}{EntityName}Status! @column
|
|
122
|
+
|
|
123
|
+
# Audit Fields
|
|
124
|
+
createdAt: DateTime! @column(overrideType: "Date")
|
|
125
|
+
updatedAt: DateTime @column(overrideType: "Date")
|
|
126
|
+
createdBy: UserAccount! @column(overrideType: "ObjectId")
|
|
127
|
+
updatedBy: UserAccount @column(overrideType: "ObjectId")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Embedded Types - NO @entity directive, use @entity(embedded: true) - MODULE PREFIXED
|
|
131
|
+
type {ModulePrefix}{EmbeddedTypeName} @entity(embedded: true) {
|
|
132
|
+
{field}: String! @column
|
|
133
|
+
{nestedField}: Int @column
|
|
134
|
+
{nestedObject}: {ModulePrefix}{NestedType} @embedded
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Status Enums - MODULE PREFIXED
|
|
138
|
+
enum {ModulePrefix}{EntityName}Status {
|
|
139
|
+
ACTIVE
|
|
140
|
+
INACTIVE
|
|
141
|
+
PENDING
|
|
142
|
+
ARCHIVED
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Input Types for GraphQL Operations - MODULE PREFIXED
|
|
146
|
+
input Create{ModulePrefix}{EntityName}Input {
|
|
147
|
+
{fieldName}: String!
|
|
148
|
+
{optionalField}: String
|
|
149
|
+
{embeddedInput}: {ModulePrefix}{EmbeddedTypeInput}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
input Update{ModulePrefix}{EntityName}Input {
|
|
153
|
+
{fieldName}: String
|
|
154
|
+
{optionalField}: String
|
|
155
|
+
{embeddedInput}: {ModulePrefix}{EmbeddedTypeInput}
|
|
156
|
+
status: {ModulePrefix}{EntityName}Status
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
input {ModulePrefix}{EmbeddedTypeInput} {
|
|
160
|
+
{field}: String
|
|
161
|
+
{nestedField}: Int
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Query Extensions - MODULE PREFIXED
|
|
165
|
+
extend type Query {
|
|
166
|
+
get{ModulePrefix}{EntityName}(id: ID!): {ModulePrefix}{EntityName} @auth
|
|
167
|
+
list{ModulePrefix}{EntityName}s(tenantId: String, filter: {ModulePrefix}{EntityName}Filter): [{ModulePrefix}{EntityName}!]! @auth
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Mutation Extensions - MODULE PREFIXED
|
|
171
|
+
extend type Mutation {
|
|
172
|
+
create{ModulePrefix}{EntityName}(input: Create{ModulePrefix}{EntityName}Input!): {ModulePrefix}{EntityName}! @auth
|
|
173
|
+
update{ModulePrefix}{EntityName}(id: ID!, input: Update{ModulePrefix}{EntityName}Input!): {ModulePrefix}{EntityName}! @auth
|
|
174
|
+
delete{ModulePrefix}{EntityName}(id: ID!): Boolean! @auth
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Key Points:**
|
|
179
|
+
|
|
180
|
+
1. **Object References**: Use `@column(overrideType: "ObjectId")` for relationships
|
|
181
|
+
2. **Embedded Types**: Use `@entity(embedded: true)` for nested objects
|
|
182
|
+
3. **Field Mapping**: API uses human-readable IDs, database stores ObjectId references
|
|
183
|
+
4. **Audit Fields**: Always include createdAt, updatedAt, createdBy, updatedBy
|
|
184
|
+
|
|
185
|
+
### Phase 2: Event System in service.graphql
|
|
186
|
+
|
|
187
|
+
**File**: `packages-modules/{module}/server/src/graphql/schemas/service.graphql`
|
|
188
|
+
|
|
189
|
+
```graphql
|
|
190
|
+
# Extension-specific event types (NO @entity directives for events!)
|
|
191
|
+
type {EntityName}CreatedEvent {
|
|
192
|
+
{entity}Id: String!
|
|
193
|
+
tenantId: String!
|
|
194
|
+
createdBy: String!
|
|
195
|
+
createdAt: String!
|
|
196
|
+
{fieldName}: String!
|
|
197
|
+
status: String!
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type {EntityName}UpdatedEvent {
|
|
201
|
+
{entity}Id: String!
|
|
202
|
+
tenantId: String!
|
|
203
|
+
updatedBy: String!
|
|
204
|
+
updatedAt: String!
|
|
205
|
+
changes: JSON
|
|
206
|
+
previousStatus: String
|
|
207
|
+
newStatus: String
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type {EntityName}DeletedEvent {
|
|
211
|
+
{entity}Id: String!
|
|
212
|
+
tenantId: String!
|
|
213
|
+
deletedBy: String!
|
|
214
|
+
deletedAt: String!
|
|
215
|
+
reason: String
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type {EntityName}StatusChangedEvent {
|
|
219
|
+
{entity}Id: String!
|
|
220
|
+
tenantId: String!
|
|
221
|
+
changedBy: String!
|
|
222
|
+
changedAt: String!
|
|
223
|
+
fromStatus: String!
|
|
224
|
+
toStatus: String!
|
|
225
|
+
reason: String
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Phase 3: Service Template Creation
|
|
230
|
+
|
|
231
|
+
**File**: `packages-modules/{module}/server/src/templates/services/{EntityName}Service.ts.template`
|
|
232
|
+
|
|
233
|
+
````typescript
|
|
234
|
+
// from package: {module}-server
|
|
235
|
+
import {
|
|
236
|
+
I{EntityName}Model,
|
|
237
|
+
ICreate{EntityName}Input,
|
|
238
|
+
IUpdate{EntityName}Input,
|
|
239
|
+
I{EntityName}Filter,
|
|
240
|
+
I{EntityName}CreatedEvent,
|
|
241
|
+
I{EntityName}UpdatedEvent,
|
|
242
|
+
I{EntityName}DeletedEvent,
|
|
243
|
+
} from 'common/server';
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Extended input type for creating {entity} with server-side fields
|
|
247
|
+
*/
|
|
248
|
+
export interface ICreate{EntityName}ServerInput extends ICreate{EntityName}Input {
|
|
249
|
+
tenantId: string;
|
|
250
|
+
createdBy: string; // User ObjectId as string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Extended input type for updating {entity} with server-side fields
|
|
255
|
+
*/
|
|
256
|
+
export interface IUpdate{EntityName}ServerInput extends IUpdate{EntityName}Input {
|
|
257
|
+
updatedBy: string; // User ObjectId as string
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Service interface for managing {entity} entities
|
|
262
|
+
* IMPORTANT: All service methods that return domain entities MUST use AsDomainType<IEntityModel>
|
|
263
|
+
* This ensures proper type compatibility between MongoDB models and GraphQL domain types
|
|
264
|
+
*/
|
|
265
|
+
export interface I{EntityName}Service {
|
|
266
|
+
/**
|
|
267
|
+
* Create a new {entity}
|
|
268
|
+
*/
|
|
269
|
+
create{EntityName}(input: ICreate{EntityName}ServerInput): Promise<AsDomainType<I{EntityName}Model>>;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Update an existing {entity}
|
|
273
|
+
*/
|
|
274
|
+
update{EntityName}(id: string, input: IUpdate{EntityName}ServerInput): Promise<AsDomainType<I{EntityName}Model>>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get a {entity} by ID
|
|
278
|
+
*/
|
|
279
|
+
get{EntityName}(id: string, tenantId: string): Promise<AsDomainType<I{EntityName}Model> | null>;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* List {entity} entities with filtering
|
|
283
|
+
*/
|
|
284
|
+
list{EntityName}s(filter: I{EntityName}Filter): Promise<AsDomainType<I{EntityName}Model>[]>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Delete a {entity}
|
|
288
|
+
*/
|
|
289
|
+
delete{EntityName}(id: string, tenantId: string, deletedBy: string): Promise<boolean>;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Update {entity} status
|
|
293
|
+
*/
|
|
294
|
+
update{EntityName}Status(
|
|
295
|
+
id: string,
|
|
296
|
+
tenantId: string,
|
|
297
|
+
status: string,
|
|
298
|
+
changedBy: string,
|
|
299
|
+
): Promise<AsDomainType<I{EntityName}Model>>;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if {entity} exists
|
|
303
|
+
*/
|
|
304
|
+
{entity}Exists(id: string, tenantId: string): Promise<boolean>;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
## 🔄 Critical Service Return Type Pattern
|
|
308
|
+
|
|
309
|
+
**MANDATORY**: All service methods returning domain entities MUST use `AsDomainType<IEntityModel>`
|
|
310
|
+
|
|
311
|
+
### Why AsDomainType is Required
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// ❌ WRONG - Direct model return
|
|
315
|
+
Promise<IEntityModel>
|
|
316
|
+
|
|
317
|
+
// ✅ CORRECT - AsDomainType wrapper
|
|
318
|
+
Promise<AsDomainType<IEntityModel>>
|
|
319
|
+
Promise<AsDomainType<IEntityModel> | null>
|
|
320
|
+
Promise<AsDomainType<IEntityModel>[]>
|
|
321
|
+
````
|
|
322
|
+
|
|
323
|
+
### Service Implementation Pattern
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// In service implementation - ALWAYS cast the return
|
|
327
|
+
public async create{EntityName}(input: ICreate{EntityName}Input): Promise<AsDomainType<I{EntityName}Model>> {
|
|
328
|
+
const result = await this.repository.create(input);
|
|
329
|
+
|
|
330
|
+
// CRITICAL: Cast repository result via unknown for type compatibility
|
|
331
|
+
return result as unknown as AsDomainType<I{EntityName}Model>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// For nullable returns
|
|
335
|
+
public async get{EntityName}(id: string): Promise<AsDomainType<I{EntityName}Model> | null> {
|
|
336
|
+
const result = await this.repository.findById(id);
|
|
337
|
+
|
|
338
|
+
// Handle null case explicitly
|
|
339
|
+
return result ? (result as unknown as AsDomainType<I{EntityName}Model>) : null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// For arrays
|
|
343
|
+
public async list{EntityName}s(filter: IFilter): Promise<AsDomainType<I{EntityName}Model>[]> {
|
|
344
|
+
const results = await this.repository.findMany(filter);
|
|
345
|
+
|
|
346
|
+
// Cast entire array
|
|
347
|
+
return results as unknown as AsDomainType<I{EntityName}Model>[];
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Import Requirements
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { AsDomainType } from 'common/server';
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Note**: AsDomainType bridges the gap between MongoDB models (with `_id`) and GraphQL domain types (with `id`). The type casting via `unknown` is necessary for TypeScript compatibility.
|
|
358
|
+
|
|
359
|
+
/\*\*
|
|
360
|
+
|
|
361
|
+
- This file augments the ServerContext interface from the central apollo-context.
|
|
362
|
+
- Through declaration merging, the {entity}Service will be added to the ServerContext.
|
|
363
|
+
\*/
|
|
364
|
+
declare module '../apollo-context' {
|
|
365
|
+
export interface ServerContext {
|
|
366
|
+
{entity}Service: I{EntityName}Service;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
````
|
|
371
|
+
|
|
372
|
+
### Phase 4: Repository Template Creation
|
|
373
|
+
|
|
374
|
+
**File**: `packages-modules/{module}/server/src/templates/repositories/{EntityName}Repository.ts.template`
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// from package: {module}-server
|
|
378
|
+
import { I{EntityName}Model, I{EntityName}Filter } from 'common/server';
|
|
379
|
+
import { ICreate{EntityName}ServerInput, IUpdate{EntityName}ServerInput } from '../services/{EntityName}Service';
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Repository interface for {EntityName} data access
|
|
383
|
+
*/
|
|
384
|
+
export interface I{EntityName}Repository {
|
|
385
|
+
/**
|
|
386
|
+
* Create a new {entity}
|
|
387
|
+
*/
|
|
388
|
+
create(input: ICreate{EntityName}ServerInput): Promise<I{EntityName}Model>;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Update an existing {entity}
|
|
392
|
+
*/
|
|
393
|
+
update(id: string, input: IUpdate{EntityName}ServerInput): Promise<I{EntityName}Model>;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Find a {entity} by ID and tenant
|
|
397
|
+
*/
|
|
398
|
+
findById(id: string, tenantId: string): Promise<I{EntityName}Model | null>;
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Find multiple {entity} entities with filtering
|
|
402
|
+
*/
|
|
403
|
+
find(filter: I{EntityName}Filter): Promise<I{EntityName}Model[]>;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Delete a {entity}
|
|
407
|
+
*/
|
|
408
|
+
delete(id: string, tenantId: string): Promise<boolean>;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check if {entity} exists
|
|
412
|
+
*/
|
|
413
|
+
exists(id: string, tenantId: string): Promise<boolean>;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Update {entity} status
|
|
417
|
+
*/
|
|
418
|
+
updateStatus(id: string, tenantId: string, status: string, updatedBy: string): Promise<I{EntityName}Model>;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get {entity} count with optional filtering
|
|
422
|
+
*/
|
|
423
|
+
count(filter?: Partial<I{EntityName}Filter>): Promise<number>;
|
|
424
|
+
}
|
|
425
|
+
````
|
|
426
|
+
|
|
427
|
+
### Phase 5: Constants Template Update
|
|
428
|
+
|
|
429
|
+
**File**: `packages-modules/{module}/server/src/templates/constants/DB_COLL_NAMES.ts.template`
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
export const DB_COLL_NAMES = {
|
|
433
|
+
// ... existing collections
|
|
434
|
+
{EntityName}: '{entityName}s',
|
|
435
|
+
// Add other related collections if needed
|
|
436
|
+
} as const;
|
|
437
|
+
|
|
438
|
+
export const SERVER_TYPES = {
|
|
439
|
+
// ... existing types
|
|
440
|
+
I{EntityName}Service: Symbol.for('I{EntityName}Service'),
|
|
441
|
+
I{EntityName}Repository: Symbol.for('I{EntityName}Repository'),
|
|
442
|
+
} as const;
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Phase 6: Run Template Generation
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# CRITICAL: Navigate to ROOT of the project (not module directory)
|
|
449
|
+
cd /path/to/your/project-root
|
|
450
|
+
|
|
451
|
+
# CRITICAL: Always run these commands in sequence from PROJECT ROOT
|
|
452
|
+
# 1. Regenerate templates (copies to common package but may remove codegen files)
|
|
453
|
+
yarn regenerateGraphql
|
|
454
|
+
|
|
455
|
+
# 2. IMMEDIATELY generate GraphQL types (restores codegen files)
|
|
456
|
+
yarn generateGraphql
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**⚠️ CRITICAL**:
|
|
460
|
+
|
|
461
|
+
- Both commands MUST be run from the PROJECT ROOT, not the module directory
|
|
462
|
+
- Always run `yarn regenerateGraphql && yarn generateGraphql` in sequence
|
|
463
|
+
- The regenerateGraphql command copies templates to the common package but can remove codegen files
|
|
464
|
+
- The generateGraphql command must be run immediately after to regenerate TypeScript types
|
|
465
|
+
|
|
466
|
+
### Phase 7: Mongoose Model Creation
|
|
467
|
+
|
|
468
|
+
**File**: `packages-modules/{module}/server/src/store/models/{entity-name}-model.ts`
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { Schema, Types } from 'mongoose';
|
|
472
|
+
import type { I{EntityName}Model, I{EmbeddedType}Model, {EntityName}Status } from 'common/server';
|
|
473
|
+
import { DB_COLL_NAMES } from 'common/server';
|
|
474
|
+
|
|
475
|
+
// Embedded Schema (if applicable)
|
|
476
|
+
const {EmbeddedType}Schema = new Schema<I{EmbeddedType}Model>({
|
|
477
|
+
{field}: {
|
|
478
|
+
type: String,
|
|
479
|
+
required: true,
|
|
480
|
+
},
|
|
481
|
+
{nestedField}: {
|
|
482
|
+
type: Number,
|
|
483
|
+
default: 0,
|
|
484
|
+
},
|
|
485
|
+
}, { _id: false }); // No _id for embedded documents
|
|
486
|
+
|
|
487
|
+
// Main Entity Schema
|
|
488
|
+
const {EntityName}Schema = new Schema<I{EntityName}Model>({
|
|
489
|
+
tenantId: {
|
|
490
|
+
type: String,
|
|
491
|
+
required: true,
|
|
492
|
+
index: true,
|
|
493
|
+
},
|
|
494
|
+
{relationField}: {
|
|
495
|
+
type: Schema.Types.ObjectId,
|
|
496
|
+
ref: DB_COLL_NAMES.{RelatedEntity}, // Reference to related collection
|
|
497
|
+
required: true,
|
|
498
|
+
index: true,
|
|
499
|
+
},
|
|
500
|
+
{fieldName}: {
|
|
501
|
+
type: String,
|
|
502
|
+
required: true,
|
|
503
|
+
index: true,
|
|
504
|
+
},
|
|
505
|
+
{optionalField}: {
|
|
506
|
+
type: String,
|
|
507
|
+
index: true,
|
|
508
|
+
},
|
|
509
|
+
{embeddedField}: {
|
|
510
|
+
type: {EmbeddedType}Schema,
|
|
511
|
+
required: true,
|
|
512
|
+
},
|
|
513
|
+
status: {
|
|
514
|
+
type: String,
|
|
515
|
+
enum: Object.values({EntityName}Status), // Use enum from codegen
|
|
516
|
+
default: {EntityName}Status.Active,
|
|
517
|
+
index: true,
|
|
518
|
+
},
|
|
519
|
+
createdAt: {
|
|
520
|
+
type: Date,
|
|
521
|
+
default: Date.now,
|
|
522
|
+
index: true,
|
|
523
|
+
},
|
|
524
|
+
updatedAt: {
|
|
525
|
+
type: Date,
|
|
526
|
+
index: true,
|
|
527
|
+
},
|
|
528
|
+
createdBy: {
|
|
529
|
+
type: Schema.Types.ObjectId,
|
|
530
|
+
ref: DB_COLL_NAMES.UserAccount,
|
|
531
|
+
required: true,
|
|
532
|
+
index: true,
|
|
533
|
+
},
|
|
534
|
+
updatedBy: {
|
|
535
|
+
type: Schema.Types.ObjectId,
|
|
536
|
+
ref: DB_COLL_NAMES.UserAccount,
|
|
537
|
+
index: true,
|
|
538
|
+
},
|
|
539
|
+
}, {
|
|
540
|
+
timestamps: true, // Automatically manage createdAt/updatedAt
|
|
541
|
+
collection: DB_COLL_NAMES.{EntityName},
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Virtual for id field
|
|
545
|
+
{EntityName}Schema.virtual('id').get(function() {
|
|
546
|
+
return this._id?.toHexString();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Ensure virtual fields are serialized
|
|
550
|
+
{EntityName}Schema.set('toJSON', {
|
|
551
|
+
virtuals: true,
|
|
552
|
+
transform: (doc, ret) => {
|
|
553
|
+
delete ret._id;
|
|
554
|
+
delete ret.__v;
|
|
555
|
+
return ret;
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
{EntityName}Schema.set('toObject', { virtuals: true });
|
|
560
|
+
|
|
561
|
+
// Indexes for common queries
|
|
562
|
+
{EntityName}Schema.index({ tenantId: 1, status: 1 });
|
|
563
|
+
{EntityName}Schema.index({ tenantId: 1, {relationField}: 1 });
|
|
564
|
+
{EntityName}Schema.index({ createdAt: -1 });
|
|
565
|
+
|
|
566
|
+
// Export model function
|
|
567
|
+
export const {EntityName}ModelFunc = (conn: any) =>
|
|
568
|
+
conn.model(DB_COLL_NAMES.{EntityName}, {EntityName}Schema);
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Phase 8: Repository Implementation
|
|
572
|
+
|
|
573
|
+
**File**: `packages-modules/{module}/server/src/store/repositories/{entity-name}-repository.ts`
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
import { injectable } from 'inversify';
|
|
577
|
+
import { Connection } from 'mongoose';
|
|
578
|
+
import { BaseRepository } from '@cdm-logger/server';
|
|
579
|
+
import type { I{EntityName}Model, I{EntityName}Filter } from 'common/server';
|
|
580
|
+
import type { I{EntityName}Repository, ICreate{EntityName}ServerInput, IUpdate{EntityName}ServerInput } from 'common/server';
|
|
581
|
+
import { {EntityName}ModelFunc } from '../models';
|
|
582
|
+
|
|
583
|
+
@injectable()
|
|
584
|
+
export class {EntityName}Repository extends BaseRepository<I{EntityName}Model> implements I{EntityName}Repository {
|
|
585
|
+
constructor(connection: Connection) {
|
|
586
|
+
const {EntityName}Model = {EntityName}ModelFunc(connection);
|
|
587
|
+
super({EntityName}Model);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async create(input: ICreate{EntityName}ServerInput): Promise<I{EntityName}Model> {
|
|
591
|
+
const {entity}Data = {
|
|
592
|
+
...input,
|
|
593
|
+
createdAt: new Date(),
|
|
594
|
+
};
|
|
595
|
+
return await this.create(entityData);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async update(id: string, input: IUpdate{EntityName}ServerInput): Promise<I{EntityName}Model> {
|
|
599
|
+
const updateData = {
|
|
600
|
+
...input,
|
|
601
|
+
updatedAt: new Date(),
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const updated = await this.update(
|
|
605
|
+
{ _id: id, tenantId: input.tenantId },
|
|
606
|
+
updateData,
|
|
607
|
+
{ new: true }
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
if (!updated) {
|
|
611
|
+
throw new Error(`{EntityName} not found: ${id}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return updated;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async findById(id: string, tenantId: string): Promise<I{EntityName}Model | null> {
|
|
618
|
+
return await this.get({ _id: id, tenantId });
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async find(filter: I{EntityName}Filter): Promise<I{EntityName}Model[]> {
|
|
622
|
+
const conditions: any = {};
|
|
623
|
+
|
|
624
|
+
if (filter.tenantId) {
|
|
625
|
+
conditions.tenantId = filter.tenantId;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (filter.status?.length) {
|
|
629
|
+
conditions.status = { $in: filter.status };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (filter.{fieldName}) {
|
|
633
|
+
conditions.{fieldName} = new RegExp(filter.{fieldName}, 'i');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (filter.createdBy) {
|
|
637
|
+
conditions.createdBy = filter.createdBy;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return await this.getAll({
|
|
641
|
+
conditions,
|
|
642
|
+
sort: { createdAt: -1 },
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async delete(id: string, tenantId: string): Promise<boolean> {
|
|
647
|
+
const result = await this.delete({ _id: id, tenantId });
|
|
648
|
+
return result.deletedCount > 0;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async exists(id: string, tenantId: string): Promise<boolean> {
|
|
652
|
+
const count = await this.count({ _id: id, tenantId });
|
|
653
|
+
return count > 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async updateStatus(
|
|
657
|
+
id: string,
|
|
658
|
+
tenantId: string,
|
|
659
|
+
status: string,
|
|
660
|
+
updatedBy: string
|
|
661
|
+
): Promise<I{EntityName}Model> {
|
|
662
|
+
return await this.update(id, {
|
|
663
|
+
status,
|
|
664
|
+
updatedBy,
|
|
665
|
+
tenantId,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async count(filter?: Partial<I{EntityName}Filter>): Promise<number> {
|
|
670
|
+
const conditions: any = {};
|
|
671
|
+
|
|
672
|
+
if (filter?.tenantId) {
|
|
673
|
+
conditions.tenantId = filter.tenantId;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (filter?.status?.length) {
|
|
677
|
+
conditions.status = { $in: filter.status };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return await this.count(conditions);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Phase 9: Service Implementation
|
|
686
|
+
|
|
687
|
+
**File**: `packages-modules/{module}/server/src/services/{entity-name}-service.ts`
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
import { injectable, inject } from 'inversify';
|
|
691
|
+
import { Emitter } from '@common-stack/utils';
|
|
692
|
+
import { BaseService } from '@cdm-logger/server';
|
|
693
|
+
import { SERVER_TYPES } from 'common/server';
|
|
694
|
+
import type {
|
|
695
|
+
I{EntityName}Model,
|
|
696
|
+
I{EntityName}Service,
|
|
697
|
+
I{EntityName}Repository,
|
|
698
|
+
I{EntityName}Filter,
|
|
699
|
+
I{EntityName}CreatedEvent,
|
|
700
|
+
I{EntityName}UpdatedEvent,
|
|
701
|
+
I{EntityName}DeletedEvent,
|
|
702
|
+
AsDomainType,
|
|
703
|
+
} from 'common/server';
|
|
704
|
+
import type { ICreate{EntityName}ServerInput, IUpdate{EntityName}ServerInput } from 'common/server';
|
|
705
|
+
|
|
706
|
+
@injectable()
|
|
707
|
+
export class {EntityName}Service extends BaseService<I{EntityName}Model> implements I{EntityName}Service {
|
|
708
|
+
// Event emitters for broadcasting operations
|
|
709
|
+
protected readonly on{EntityName}Created = new Emitter<I{EntityName}CreatedEvent>();
|
|
710
|
+
protected readonly on{EntityName}Updated = new Emitter<I{EntityName}UpdatedEvent>();
|
|
711
|
+
protected readonly on{EntityName}Deleted = new Emitter<I{EntityName}DeletedEvent>();
|
|
712
|
+
protected readonly on{EntityName}StatusChanged = new Emitter<I{EntityName}StatusChangedEvent>();
|
|
713
|
+
|
|
714
|
+
constructor(
|
|
715
|
+
@inject(SERVER_TYPES.I{EntityName}Repository)
|
|
716
|
+
private {entity}Repository: I{EntityName}Repository,
|
|
717
|
+
) {
|
|
718
|
+
super();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Event getters for external access
|
|
722
|
+
public get {entity}Created() {
|
|
723
|
+
return this.on{EntityName}Created.event;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
public get {entity}Updated() {
|
|
727
|
+
return this.on{EntityName}Updated.event;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
public get {entity}Deleted() {
|
|
731
|
+
return this.on{EntityName}Deleted.event;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
public get {entity}StatusChanged() {
|
|
735
|
+
return this.on{EntityName}StatusChanged.event;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async create{EntityName}(input: ICreate{EntityName}ServerInput): Promise<I{EntityName}Model> {
|
|
739
|
+
// Business logic validation
|
|
740
|
+
const existing = await this.{entity}Repository.exists(
|
|
741
|
+
input.{fieldName}, // or relevant unique field
|
|
742
|
+
input.tenantId
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
if (existing) {
|
|
746
|
+
throw new Error(`{EntityName} with {fieldName} '${input.{fieldName}}' already exists`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Create the entity
|
|
750
|
+
const {entity} = await this.{entity}Repository.create(input);
|
|
751
|
+
|
|
752
|
+
// Fire event
|
|
753
|
+
const event: I{EntityName}CreatedEvent = {
|
|
754
|
+
{entity}Id: {entity}.id!,
|
|
755
|
+
tenantId: {entity}.tenantId,
|
|
756
|
+
createdBy: input.createdBy,
|
|
757
|
+
createdAt: {entity}.createdAt.toISOString(),
|
|
758
|
+
{fieldName}: {entity}.{fieldName},
|
|
759
|
+
status: {entity}.status,
|
|
760
|
+
};
|
|
761
|
+
this.on{EntityName}Created.fire(event);
|
|
762
|
+
|
|
763
|
+
return {entity};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async update{EntityName}(id: string, input: IUpdate{EntityName}ServerInput): Promise<I{EntityName}Model> {
|
|
767
|
+
// Get current state for change detection
|
|
768
|
+
const current = await this.{entity}Repository.findById(id, input.tenantId);
|
|
769
|
+
if (!current) {
|
|
770
|
+
throw new Error(`{EntityName} not found: ${id}`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Update the entity
|
|
774
|
+
const updated = await this.{entity}Repository.update(id, input);
|
|
775
|
+
|
|
776
|
+
// Fire event
|
|
777
|
+
const event: I{EntityName}UpdatedEvent = {
|
|
778
|
+
{entity}Id: updated.id!,
|
|
779
|
+
tenantId: updated.tenantId,
|
|
780
|
+
updatedBy: input.updatedBy,
|
|
781
|
+
updatedAt: updated.updatedAt!.toISOString(),
|
|
782
|
+
changes: this.getChanges(current, updated),
|
|
783
|
+
previousStatus: current.status,
|
|
784
|
+
newStatus: updated.status,
|
|
785
|
+
};
|
|
786
|
+
this.on{EntityName}Updated.fire(event);
|
|
787
|
+
|
|
788
|
+
return updated;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async get{EntityName}(id: string, tenantId: string): Promise<I{EntityName}Model | null> {
|
|
792
|
+
return await this.{entity}Repository.findById(id, tenantId);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async list{EntityName}s(filter: I{EntityName}Filter): Promise<I{EntityName}Model[]> {
|
|
796
|
+
return await this.{entity}Repository.find(filter);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async delete{EntityName}(id: string, tenantId: string, deletedBy: string): Promise<boolean> {
|
|
800
|
+
// Get entity before deletion for event
|
|
801
|
+
const {entity} = await this.{entity}Repository.findById(id, tenantId);
|
|
802
|
+
if (!{entity}) {
|
|
803
|
+
throw new Error(`{EntityName} not found: ${id}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Delete the entity
|
|
807
|
+
const deleted = await this.{entity}Repository.delete(id, tenantId);
|
|
808
|
+
|
|
809
|
+
if (deleted) {
|
|
810
|
+
// Fire event
|
|
811
|
+
const event: I{EntityName}DeletedEvent = {
|
|
812
|
+
{entity}Id: id,
|
|
813
|
+
tenantId,
|
|
814
|
+
deletedBy,
|
|
815
|
+
deletedAt: new Date().toISOString(),
|
|
816
|
+
reason: 'Manual deletion',
|
|
817
|
+
};
|
|
818
|
+
this.on{EntityName}Deleted.fire(event);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return deleted;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async update{EntityName}Status(
|
|
825
|
+
id: string,
|
|
826
|
+
tenantId: string,
|
|
827
|
+
status: string,
|
|
828
|
+
changedBy: string,
|
|
829
|
+
): Promise<I{EntityName}Model> {
|
|
830
|
+
const current = await this.{entity}Repository.findById(id, tenantId);
|
|
831
|
+
if (!current) {
|
|
832
|
+
throw new Error(`{EntityName} not found: ${id}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const previousStatus = current.status;
|
|
836
|
+
const updated = await this.{entity}Repository.updateStatus(id, tenantId, status, changedBy);
|
|
837
|
+
|
|
838
|
+
// Fire status change event
|
|
839
|
+
const event: I{EntityName}StatusChangedEvent = {
|
|
840
|
+
{entity}Id: id,
|
|
841
|
+
tenantId,
|
|
842
|
+
changedBy,
|
|
843
|
+
changedAt: new Date().toISOString(),
|
|
844
|
+
fromStatus: previousStatus,
|
|
845
|
+
toStatus: status,
|
|
846
|
+
reason: 'Status update',
|
|
847
|
+
};
|
|
848
|
+
this.on{EntityName}StatusChanged.fire(event);
|
|
849
|
+
|
|
850
|
+
return updated;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async {entity}Exists(id: string, tenantId: string): Promise<boolean> {
|
|
854
|
+
return await this.{entity}Repository.exists(id, tenantId);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Helper method to detect changes
|
|
858
|
+
private getChanges(before: I{EntityName}Model, after: I{EntityName}Model): any {
|
|
859
|
+
const changes: any = {};
|
|
860
|
+
|
|
861
|
+
if (before.{fieldName} !== after.{fieldName}) {
|
|
862
|
+
changes.{fieldName} = { from: before.{fieldName}, to: after.{fieldName} };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (before.status !== after.status) {
|
|
866
|
+
changes.status = { from: before.status, to: after.status };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return changes;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Cleanup method
|
|
873
|
+
dispose(): void {
|
|
874
|
+
this.on{EntityName}Created.dispose();
|
|
875
|
+
this.on{EntityName}Updated.dispose();
|
|
876
|
+
this.on{EntityName}Deleted.dispose();
|
|
877
|
+
this.on{EntityName}StatusChanged.dispose();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### Phase 10: Moleculer Service Extension (Optional)
|
|
883
|
+
|
|
884
|
+
**File**: `packages-modules/{module}/server/src/services/{entity-name}-service-ext.ts`
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
import { injectable, inject } from 'inversify';
|
|
888
|
+
import { Moleculer } from '@common-stack/server-core';
|
|
889
|
+
import { {EntityName}Service } from './{entity-name}-service';
|
|
890
|
+
import { MoleculerServiceName, {EntityName}ServiceAction } from '../constants';
|
|
891
|
+
import type { I{EntityName}CreatedEvent, I{EntityName}UpdatedEvent, I{EntityName}DeletedEvent } from 'common/server';
|
|
892
|
+
|
|
893
|
+
@injectable()
|
|
894
|
+
export class {EntityName}ServiceExt extends {EntityName}Service {
|
|
895
|
+
private broker: any;
|
|
896
|
+
|
|
897
|
+
constructor(
|
|
898
|
+
@inject('Environment') environment: any,
|
|
899
|
+
...args: any[]
|
|
900
|
+
) {
|
|
901
|
+
super(...args);
|
|
902
|
+
this.broker = environment.broker;
|
|
903
|
+
this.setupEventListeners();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private setupEventListeners(): void {
|
|
907
|
+
// Broadcast created event
|
|
908
|
+
this.{entity}Created((event: I{EntityName}CreatedEvent) => {
|
|
909
|
+
this.broker.broadcast({EntityName}ServiceAction.On{EntityName}Created, { event }, [
|
|
910
|
+
MoleculerServiceName.NotificationService,
|
|
911
|
+
MoleculerServiceName.AuditService,
|
|
912
|
+
]);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// Broadcast updated event
|
|
916
|
+
this.{entity}Updated((event: I{EntityName}UpdatedEvent) => {
|
|
917
|
+
this.broker.broadcast({EntityName}ServiceAction.On{EntityName}Updated, { event }, [
|
|
918
|
+
MoleculerServiceName.NotificationService,
|
|
919
|
+
MoleculerServiceName.AuditService,
|
|
920
|
+
]);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Broadcast deleted event
|
|
924
|
+
this.{entity}Deleted((event: I{EntityName}DeletedEvent) => {
|
|
925
|
+
this.broker.broadcast({EntityName}ServiceAction.On{EntityName}Deleted, { event }, [
|
|
926
|
+
MoleculerServiceName.NotificationService,
|
|
927
|
+
MoleculerServiceName.AuditService,
|
|
928
|
+
]);
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### Phase 11: DataLoader Implementation (for Object Resolution)
|
|
935
|
+
|
|
936
|
+
**Files**:
|
|
937
|
+
|
|
938
|
+
- `packages-modules/{module}/server/src/dataloaders/{entity}-data-loader.ts`
|
|
939
|
+
- `packages-modules/{module}/server/src/dataloaders/index.ts`
|
|
940
|
+
- `packages-modules/{module}/server/src/templates/services/{EntityName}DataLoader.ts.template`
|
|
941
|
+
|
|
942
|
+
```typescript
|
|
943
|
+
// DataLoader Implementation
|
|
944
|
+
import { I{EntityName}, I{EntityName}Service, SERVER_TYPES, IBaseService, AsDomainType } from 'common/server';
|
|
945
|
+
import { injectable, inject } from 'inversify';
|
|
946
|
+
import { BulkDataLoader2 } from '@common-stack/store-mongo';
|
|
947
|
+
|
|
948
|
+
@injectable()
|
|
949
|
+
export class {EntityName}DataLoader extends BulkDataLoader2<AsDomainType<I{EntityName}>> {
|
|
950
|
+
constructor(
|
|
951
|
+
@inject(SERVER_TYPES.I{EntityName}Service)
|
|
952
|
+
{entity}Service: I{EntityName}Service,
|
|
953
|
+
) {
|
|
954
|
+
super({entity}Service as unknown as IBaseService<AsDomainType<I{EntityName}>>);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**DataLoader Index**:
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
// File: packages-modules/{module}/server/src/dataloaders/index.ts
|
|
963
|
+
export { {EntityName}DataLoader } from './{entity}-data-loader';
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
**Template File**:
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
// File: packages-modules/{module}/server/src/templates/services/{EntityName}DataLoader.ts.template
|
|
970
|
+
import { I{EntityName}, IDataLoader } from 'common/server';
|
|
971
|
+
export type I{EntityName}DataLoader = IDataLoader<I{EntityName}>;
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### Phase 12: GraphQL Resolver Implementation
|
|
975
|
+
|
|
976
|
+
**File**: `packages-modules/{module}/server/src/graphql/resolvers/{entity-name}-resolver.ts`
|
|
977
|
+
|
|
978
|
+
````typescript
|
|
979
|
+
import { IResolvers } from '@graphql-tools/utils';
|
|
980
|
+
import { IResolverOptions } from '@common-stack/server-core';
|
|
981
|
+
import type {
|
|
982
|
+
I{EntityName}Model,
|
|
983
|
+
ICreate{EntityName}Input,
|
|
984
|
+
IUpdate{EntityName}Input,
|
|
985
|
+
ServerContext
|
|
986
|
+
} from 'common/server';
|
|
987
|
+
|
|
988
|
+
// Define context interface for this resolver
|
|
989
|
+
interface I{EntityName}Context extends ServerContext {
|
|
990
|
+
{entity}Service: any; // Will be injected via createServiceFunc
|
|
991
|
+
{entity}DataLoader: any; // DataLoader for resolving object references
|
|
992
|
+
userContext: {
|
|
993
|
+
tenantId: string;
|
|
994
|
+
userId: string;
|
|
995
|
+
accountId: string;
|
|
996
|
+
emailId: string;
|
|
997
|
+
organization?: {
|
|
998
|
+
id: string;
|
|
999
|
+
name: string;
|
|
1000
|
+
};
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
export const resolvers: (options: IResolverOptions) => IResolvers = (options) => ({
|
|
1005
|
+
// Field resolvers for nested data relationships
|
|
1006
|
+
{EntityName}: {
|
|
1007
|
+
// Resolve object references using DataLoaders
|
|
1008
|
+
{relationField}: async (
|
|
1009
|
+
parent: I{EntityName}Model,
|
|
1010
|
+
_: any,
|
|
1011
|
+
{ {relatedEntity}DataLoader }: I{EntityName}Context,
|
|
1012
|
+
) => {
|
|
1013
|
+
// Check if the relation field exists and is not null before calling dataloader
|
|
1014
|
+
if (!parent.{relationField} || parent.{relationField} === null || parent.{relationField} === undefined) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
const relationId = String(parent.{relationField});
|
|
1018
|
+
return {relatedEntity}DataLoader.load(relationId);
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
// Resolve computed fields or complex transformations
|
|
1022
|
+
displayName: (parent: I{EntityName}Model) => {
|
|
1023
|
+
return `${parent.{fieldName}} (${parent.status})`;
|
|
1024
|
+
},
|
|
1025
|
+
|
|
1026
|
+
// Resolve configuration or settings URIs (following team-resolver pattern)
|
|
1027
|
+
settingsUri: (params: I{EntityName}Model, args: any, { userContext }: I{EntityName}Context) => {
|
|
1028
|
+
// Create a resource URI for configuration management
|
|
1029
|
+
return createStandardResourceUri(
|
|
1030
|
+
ConfigCollectionName.{EntityName}s, // Collection name
|
|
1031
|
+
params.id,
|
|
1032
|
+
params.{relationField} as unknown as string, // Parent relation
|
|
1033
|
+
{
|
|
1034
|
+
tenantId: userContext.tenantId,
|
|
1035
|
+
fragment: ConfigFragmentName.Settings,
|
|
1036
|
+
},
|
|
1037
|
+
);
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1041
|
+
Query: {
|
|
1042
|
+
get{EntityName}: async (
|
|
1043
|
+
_: any,
|
|
1044
|
+
{ id }: { id: string },
|
|
1045
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1046
|
+
): Promise<I{EntityName} | null> => {
|
|
1047
|
+
options.logger.trace('(Query.get{EntityName}) id [%j]', id);
|
|
1048
|
+
|
|
1049
|
+
if (!userContext?.tenantId) {
|
|
1050
|
+
throw new Error('Authentication required');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Cast service result to GraphQL interface type
|
|
1054
|
+
// Field resolvers will handle ObjectId → object transformations via DataLoaders
|
|
1055
|
+
const result = await {entity}Service.get{EntityName}(id, userContext.tenantId);
|
|
1056
|
+
return result as unknown as I{EntityName} | null;
|
|
1057
|
+
},
|
|
1058
|
+
|
|
1059
|
+
list{EntityName}s: async (
|
|
1060
|
+
_: any,
|
|
1061
|
+
{ filter }: { filter?: any },
|
|
1062
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1063
|
+
): Promise<I{EntityName}[]> => {
|
|
1064
|
+
options.logger.trace('(Query.list{EntityName}s) filter [%j]', filter);
|
|
1065
|
+
|
|
1066
|
+
if (!userContext?.tenantId) {
|
|
1067
|
+
throw new Error('Authentication required');
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const searchFilter = {
|
|
1071
|
+
tenantId: userContext.tenantId,
|
|
1072
|
+
...filter,
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// Cast service result to GraphQL interface type
|
|
1076
|
+
// Field resolvers will handle ObjectId → object transformations via DataLoaders
|
|
1077
|
+
const result = await {entity}Service.list{EntityName}s(searchFilter);
|
|
1078
|
+
return result as unknown as I{EntityName}[];
|
|
1079
|
+
},
|
|
1080
|
+
|
|
1081
|
+
// User-specific queries
|
|
1082
|
+
getUser{EntityName}s: async (
|
|
1083
|
+
_: any,
|
|
1084
|
+
args: any,
|
|
1085
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1086
|
+
): Promise<I{EntityName}Model[]> => {
|
|
1087
|
+
options.logger.trace('(Query.getUser{EntityName}s) args [%j]', args);
|
|
1088
|
+
|
|
1089
|
+
if (!userContext) {
|
|
1090
|
+
return [];
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return await {entity}Service.list{EntityName}s({
|
|
1094
|
+
tenantId: userContext.tenantId,
|
|
1095
|
+
createdBy: userContext.accountId,
|
|
1096
|
+
});
|
|
1097
|
+
},
|
|
1098
|
+
|
|
1099
|
+
// Organization/tenant-wide queries with permissions
|
|
1100
|
+
getOrganization{EntityName}s: async (
|
|
1101
|
+
_: any,
|
|
1102
|
+
{ orgName }: { orgName?: string },
|
|
1103
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1104
|
+
): Promise<I{EntityName}Model[]> => {
|
|
1105
|
+
options.logger.trace('(Query.getOrganization{EntityName}s) orgName [%j]', orgName);
|
|
1106
|
+
|
|
1107
|
+
if (!userContext) {
|
|
1108
|
+
return [];
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const { permissions, accountId } = userContext;
|
|
1112
|
+
let fullAccess = false;
|
|
1113
|
+
|
|
1114
|
+
// Check permissions for viewing organization-wide data
|
|
1115
|
+
if (permissions?.organization?.{entity}s?.viewOthers === PermissionType.Allow) {
|
|
1116
|
+
fullAccess = true;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return await {entity}Service.getOrganization{EntityName}s(
|
|
1120
|
+
orgName || userContext?.organization?.name,
|
|
1121
|
+
accountId,
|
|
1122
|
+
fullAccess,
|
|
1123
|
+
);
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
|
|
1127
|
+
Mutation: {
|
|
1128
|
+
create{EntityName}: async (
|
|
1129
|
+
_: any,
|
|
1130
|
+
{ input }: { input: ICreate{EntityName}Input },
|
|
1131
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1132
|
+
): Promise<I{EntityName}> => {
|
|
1133
|
+
options.logger.trace('(Mutation.create{EntityName}) input [%j]', input);
|
|
1134
|
+
|
|
1135
|
+
if (!userContext?.tenantId || !userContext?.userId) {
|
|
1136
|
+
throw new Error('Authentication required');
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const serverInput = {
|
|
1140
|
+
...input,
|
|
1141
|
+
tenantId: userContext.tenantId,
|
|
1142
|
+
createdBy: userContext.userId,
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
const result = await {entity}Service.create{EntityName}(serverInput);
|
|
1147
|
+
options.logger.trace('(Mutation.create{EntityName}) result [%j]', result);
|
|
1148
|
+
// Cast service result to GraphQL interface type
|
|
1149
|
+
return result as unknown as I{EntityName};
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
options.logger.error('Error creating {entity}:', error);
|
|
1152
|
+
throw error;
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
|
|
1156
|
+
update{EntityName}: async (
|
|
1157
|
+
_: any,
|
|
1158
|
+
{ id, input }: { id: string; input: IUpdate{EntityName}Input },
|
|
1159
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1160
|
+
): Promise<I{EntityName}> => {
|
|
1161
|
+
options.logger.trace('(Mutation.update{EntityName}) args [%j, %j]', id, input);
|
|
1162
|
+
|
|
1163
|
+
if (!userContext?.tenantId || !userContext?.userId) {
|
|
1164
|
+
throw new Error('Authentication required');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const serverInput = {
|
|
1168
|
+
...input,
|
|
1169
|
+
tenantId: userContext.tenantId,
|
|
1170
|
+
updatedBy: userContext.userId,
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
try {
|
|
1174
|
+
const result = await {entity}Service.update{EntityName}(id, serverInput);
|
|
1175
|
+
options.logger.trace('(Mutation.update{EntityName}) result [%j]', result);
|
|
1176
|
+
// Cast service result to GraphQL interface type
|
|
1177
|
+
return result as unknown as I{EntityName};
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
options.logger.error('Error updating {entity}:', error);
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
|
|
1184
|
+
delete{EntityName}: async (
|
|
1185
|
+
_: any,
|
|
1186
|
+
{ id }: { id: string },
|
|
1187
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1188
|
+
): Promise<boolean> => {
|
|
1189
|
+
options.logger.trace('(Mutation.delete{EntityName}) id [%j]', id);
|
|
1190
|
+
|
|
1191
|
+
if (!userContext?.tenantId || !userContext?.userId) {
|
|
1192
|
+
throw new Error('Authentication required');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
try {
|
|
1196
|
+
const result = await {entity}Service.delete{EntityName}(
|
|
1197
|
+
id,
|
|
1198
|
+
userContext.tenantId,
|
|
1199
|
+
userContext.userId
|
|
1200
|
+
);
|
|
1201
|
+
options.logger.trace('(Mutation.delete{EntityName}) result [%j]', result);
|
|
1202
|
+
return result;
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
options.logger.error('Error deleting {entity}:', error);
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
|
|
1209
|
+
// Status update mutations
|
|
1210
|
+
update{EntityName}Status: async (
|
|
1211
|
+
_: any,
|
|
1212
|
+
{ id, status }: { id: string; status: string },
|
|
1213
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1214
|
+
): Promise<I{EntityName}Model> => {
|
|
1215
|
+
options.logger.trace('(Mutation.update{EntityName}Status) args [%j, %j]', id, status);
|
|
1216
|
+
|
|
1217
|
+
if (!userContext?.tenantId || !userContext?.userId) {
|
|
1218
|
+
throw new Error('Authentication required');
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
try {
|
|
1222
|
+
const result = await {entity}Service.update{EntityName}Status(
|
|
1223
|
+
id,
|
|
1224
|
+
userContext.tenantId,
|
|
1225
|
+
status,
|
|
1226
|
+
userContext.userId,
|
|
1227
|
+
);
|
|
1228
|
+
return result;
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
options.logger.error('Error updating {entity} status:', error);
|
|
1231
|
+
throw error;
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
|
|
1235
|
+
// Complex business logic operations (like extension installation)
|
|
1236
|
+
install{EntityName}: async (
|
|
1237
|
+
_: any,
|
|
1238
|
+
{ {entity}Id }: { {entity}Id: string },
|
|
1239
|
+
{ {entity}Service, registryService, userContext }: I{EntityName}Context,
|
|
1240
|
+
): Promise<I{EntityName}Model> => {
|
|
1241
|
+
options.logger.trace('(Mutation.install{EntityName}) {entity}Id [%j]', {entity}Id);
|
|
1242
|
+
|
|
1243
|
+
if (!userContext?.tenantId || !userContext?.userId) {
|
|
1244
|
+
throw new Error('Authentication required');
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
try {
|
|
1248
|
+
// First check if the entity exists in registry
|
|
1249
|
+
const registryEntity = await registryService.find{EntityName}({entity}Id);
|
|
1250
|
+
if (!registryEntity) {
|
|
1251
|
+
throw new Error(`{EntityName} ${{entity}Id} not found in registry`);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Check if already installed
|
|
1255
|
+
const existingInstallation = await {entity}Service.get{EntityName}(
|
|
1256
|
+
userContext.tenantId,
|
|
1257
|
+
{entity}Id
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
if (existingInstallation) {
|
|
1261
|
+
throw new Error(`{EntityName} ${{entity}Id} is already installed`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Perform installation with proper input mapping
|
|
1265
|
+
const installInput = {
|
|
1266
|
+
tenantId: userContext.tenantId,
|
|
1267
|
+
registryRef: registryEntity._id,
|
|
1268
|
+
{entity}Id,
|
|
1269
|
+
installedVersion: registryEntity.version,
|
|
1270
|
+
installedBy: userContext.userId,
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const result = await {entity}Service.install{EntityName}(installInput);
|
|
1274
|
+
options.logger.trace('(Mutation.install{EntityName}) result [%j]', result);
|
|
1275
|
+
return result;
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
options.logger.error('Error installing {entity}:', error);
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
|
|
1282
|
+
uninstall{EntityName}: async (
|
|
1283
|
+
_: any,
|
|
1284
|
+
{ {entity}Id }: { {entity}Id: string },
|
|
1285
|
+
{ {entity}Service, userContext }: I{EntityName}Context,
|
|
1286
|
+
): Promise<boolean> => {
|
|
1287
|
+
options.logger.trace('(Mutation.uninstall{EntityName}) {entity}Id [%j]', {entity}Id);
|
|
1288
|
+
|
|
1289
|
+
if (!userContext?.tenantId || !userContext?.userId) {
|
|
1290
|
+
throw new Error('Authentication required');
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
try {
|
|
1294
|
+
const result = await {entity}Service.uninstall{EntityName}(
|
|
1295
|
+
userContext.tenantId,
|
|
1296
|
+
{entity}Id,
|
|
1297
|
+
userContext.userId
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
if (!result) {
|
|
1301
|
+
throw new Error(`{EntityName} ${{entity}Id} was not installed`);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
options.logger.trace('(Mutation.uninstall{EntityName}) result [%j]', result);
|
|
1305
|
+
return result;
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
options.logger.error('Error uninstalling {entity}:', error);
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
},
|
|
1312
|
+
});
|
|
1313
|
+
# Key Points for GraphQL Resolvers:**
|
|
1314
|
+
|
|
1315
|
+
1. **Use IResolverOptions Pattern**: Follow the `(options: IResolverOptions) => IResolvers` pattern
|
|
1316
|
+
2. **Logging**: Use `options.logger.trace()` for request/response logging
|
|
1317
|
+
3. **Context Typing**: Define specific context interfaces for type safety
|
|
1318
|
+
4. **Error Handling**: Wrap mutations in try-catch with proper error logging
|
|
1319
|
+
5. **Authentication**: Always check `userContext` for authentication
|
|
1320
|
+
6. **DataLoader Integration**: Use DataLoaders for nested field resolution to prevent N+1 queries
|
|
1321
|
+
7. **Permission Checks**: Implement proper authorization logic
|
|
1322
|
+
8. **Batch Operations**: Support batch operations where appropriate
|
|
1323
|
+
|
|
1324
|
+
### Phase 13: Resolver Index and Integration
|
|
1325
|
+
|
|
1326
|
+
**File**: `packages-modules/{module}/server/src/graphql/resolvers/index.ts`
|
|
1327
|
+
|
|
1328
|
+
The resolver index file combines all individual resolver files into a single export array. Each resolver file exports a `resolver` function that takes `pubsub` and `logger` parameters.
|
|
1329
|
+
|
|
1330
|
+
#### **Individual Resolver File Pattern:**
|
|
1331
|
+
```typescript
|
|
1332
|
+
// File: {entity-name}-resolver.ts
|
|
1333
|
+
import { PubSub } from 'graphql-subscriptions';
|
|
1334
|
+
import { CdmLogger } from '@cdm-logger/core';
|
|
1335
|
+
import { IResolvers } from 'common/server';
|
|
1336
|
+
|
|
1337
|
+
export const resolver = (pubsub: PubSub, logger?: CdmLogger.ILogger): IResolvers => ({
|
|
1338
|
+
// Field resolvers with DataLoaders
|
|
1339
|
+
{EntityName}: {
|
|
1340
|
+
{relationField}: (root, args, { {entity}DataLoader }) => {
|
|
1341
|
+
if (!root.{relationField}) return null;
|
|
1342
|
+
return {entity}DataLoader.load(String(root.{relationField}));
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
|
|
1346
|
+
Query: {
|
|
1347
|
+
get{EntityName}: async (_, { id }, { {entity}Service, userContext }) => {
|
|
1348
|
+
const result = await {entity}Service.get{EntityName}(id, userContext.tenantId);
|
|
1349
|
+
return result as unknown as I{EntityName}; // Type casting for DataLoader compatibility
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
|
|
1353
|
+
Mutation: {
|
|
1354
|
+
create{EntityName}: async (_, { input }, { {entity}Service, userContext }) => {
|
|
1355
|
+
const result = await {entity}Service.create{EntityName}({
|
|
1356
|
+
...input,
|
|
1357
|
+
tenantId: userContext.tenantId,
|
|
1358
|
+
createdBy: userContext.accountId,
|
|
1359
|
+
});
|
|
1360
|
+
return result as unknown as I{EntityName}; // Type casting for DataLoader compatibility
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
});
|
|
1364
|
+
````
|
|
1365
|
+
|
|
1366
|
+
#### **Index File Combination Pattern:**
|
|
1367
|
+
|
|
1368
|
+
```typescript
|
|
1369
|
+
// File: packages-modules/{module}/server/src/graphql/resolvers/index.ts
|
|
1370
|
+
import { resolver as registryResolver } from './registry-extension-resolver';
|
|
1371
|
+
import { resolver as installedExtensionResolver } from './installed-extension-resolver';
|
|
1372
|
+
import { resolver as {entity}Resolver } from './{entity-name}-resolver';
|
|
1373
|
+
|
|
1374
|
+
// Export array of resolver functions
|
|
1375
|
+
export const resolvers = [
|
|
1376
|
+
registryResolver,
|
|
1377
|
+
installedExtensionResolver,
|
|
1378
|
+
{entity}Resolver,
|
|
1379
|
+
];
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
#### **Alternative IResolverOptions Pattern:**
|
|
1383
|
+
|
|
1384
|
+
For resolvers following the newer IResolverOptions pattern (like team-resolver), use this approach:
|
|
1385
|
+
|
|
1386
|
+
```typescript
|
|
1387
|
+
// Individual resolver file
|
|
1388
|
+
export const resolvers: (options: IResolverOptions) => IResolvers = (options) => ({
|
|
1389
|
+
// ... resolver implementation with options.logger
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// Index file
|
|
1393
|
+
import { merge } from 'lodash-es';
|
|
1394
|
+
import { resolvers as {entity}Resolvers } from './{entity-name}-resolver';
|
|
1395
|
+
import { IResolverOptions } from '@common-stack/server-core';
|
|
1396
|
+
|
|
1397
|
+
export const resolvers = (options: IResolverOptions) =>
|
|
1398
|
+
merge(
|
|
1399
|
+
{entity}Resolvers(options),
|
|
1400
|
+
// Add other resolver functions here
|
|
1401
|
+
);
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
#### **Key Integration Points:**
|
|
1405
|
+
|
|
1406
|
+
1. **Function Signature**: Each resolver exports a `resolver` function taking `(pubsub, logger?)` parameters
|
|
1407
|
+
2. **Array Export**: Index combines resolvers into an array for GraphQL schema execution
|
|
1408
|
+
3. **Naming Convention**: Use `{entity-name}-resolver.ts` for individual files (singular)
|
|
1409
|
+
4. **Import Aliasing**: Use `resolver as {entity}Resolver` for clarity in index
|
|
1410
|
+
5. **Type Casting**: Always cast service results with `as unknown as I{EntityName}` for DataLoader compatibility
|
|
1411
|
+
6. **Context Usage**: Use proper destructuring for `{ userContext, {entity}Service, {entity}DataLoader }`
|
|
1412
|
+
|
|
1413
|
+
**File**: `packages-modules/{module}/server/src/graphql/schemas/index.ts`
|
|
1414
|
+
|
|
1415
|
+
```typescript
|
|
1416
|
+
import { loadSchemaSync } from '@graphql-tools/load';
|
|
1417
|
+
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
|
|
1418
|
+
import { join } from 'path';
|
|
1419
|
+
|
|
1420
|
+
const schema = loadSchemaSync(join(__dirname, './*.graphql'), {
|
|
1421
|
+
loaders: [new GraphQLFileLoader()],
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
export { schema };
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
**File**: `packages-modules/{module}/server/src/graphql/index.ts`
|
|
1428
|
+
|
|
1429
|
+
```typescript
|
|
1430
|
+
export { resolvers } from './resolvers';
|
|
1431
|
+
export { schema } from './schemas';
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
**File**: `packages-modules/{module}/server/src/config/env-config.ts`
|
|
1435
|
+
|
|
1436
|
+
```typescript
|
|
1437
|
+
import { str, cleanEnv, num, bool } from 'envalid';
|
|
1438
|
+
|
|
1439
|
+
export const config = cleanEnv(
|
|
1440
|
+
process.env,
|
|
1441
|
+
{
|
|
1442
|
+
NODE_ENV: str({ choices: ['production', 'test', 'staging', 'development'], default: 'production' }),
|
|
1443
|
+
|
|
1444
|
+
// Database Configuration
|
|
1445
|
+
MONGO_URL: str({ desc: 'MongoDB connection string' }),
|
|
1446
|
+
|
|
1447
|
+
// Service-specific Configuration
|
|
1448
|
+
{ENTITY_NAME}_AUTO_CLEANUP_ENABLED: bool({ default: false }),
|
|
1449
|
+
{ENTITY_NAME}_CLEANUP_INTERVAL_HOURS: num({ default: 24 }),
|
|
1450
|
+
{ENTITY_NAME}_MAX_RETENTION_DAYS: num({ default: 90 }),
|
|
1451
|
+
|
|
1452
|
+
// Feature Flags
|
|
1453
|
+
{ENTITY_NAME}_NOTIFICATIONS_ENABLED: bool({ default: true }),
|
|
1454
|
+
{ENTITY_NAME}_AUDIT_ENABLED: bool({ default: true }),
|
|
1455
|
+
|
|
1456
|
+
// External Service URLs
|
|
1457
|
+
NOTIFICATION_SERVICE_URL: str({ default: '' }),
|
|
1458
|
+
AUDIT_SERVICE_URL: str({ default: '' }),
|
|
1459
|
+
|
|
1460
|
+
// Security
|
|
1461
|
+
{ENTITY_NAME}_RATE_LIMIT_PER_HOUR: num({ default: 100 }),
|
|
1462
|
+
|
|
1463
|
+
// GraphQL
|
|
1464
|
+
GRAPHQL_URL: str({ desc: 'GraphQL endpoint URL' }),
|
|
1465
|
+
|
|
1466
|
+
// Common settings
|
|
1467
|
+
APP_NAME: str({ default: 'CDEBASE.IO' }),
|
|
1468
|
+
CLIENT_URL: str({ desc: 'Client application URL' }),
|
|
1469
|
+
},
|
|
1470
|
+
);
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
### Phase 14: Container Module with createServiceFunc
|
|
1474
|
+
|
|
1475
|
+
**File**: `packages-modules/{module}/server/src/containers/module.ts`
|
|
1476
|
+
|
|
1477
|
+
```typescript
|
|
1478
|
+
import { ContainerModule, interfaces } from 'inversify';
|
|
1479
|
+
import { SERVER_TYPES } from 'common/server';
|
|
1480
|
+
import type {
|
|
1481
|
+
I{EntityName}Service,
|
|
1482
|
+
I{EntityName}Repository,
|
|
1483
|
+
ServerContext
|
|
1484
|
+
} from 'common/server';
|
|
1485
|
+
import { {EntityName}ServiceExt } from '../services/{entity-name}-service-ext';
|
|
1486
|
+
import { {EntityName}Repository } from '../store/repositories/{entity-name}-repository';
|
|
1487
|
+
|
|
1488
|
+
export const {entity}Module: (settings: any) => interfaces.ContainerModule = (settings: any) =>
|
|
1489
|
+
new ContainerModule((bind: interfaces.Bind) => {
|
|
1490
|
+
// MongoDB Connection
|
|
1491
|
+
bind(SERVER_TYPES.I{Entity}MongoConnection).toConstantValue(settings.mongoConnection);
|
|
1492
|
+
|
|
1493
|
+
// Repository
|
|
1494
|
+
bind<I{EntityName}Repository>(SERVER_TYPES.I{EntityName}Repository)
|
|
1495
|
+
.to({EntityName}Repository as any)
|
|
1496
|
+
.inSingletonScope()
|
|
1497
|
+
.whenTargetIsDefault();
|
|
1498
|
+
|
|
1499
|
+
// Service (use ServiceExt for Moleculer integration)
|
|
1500
|
+
bind<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service)
|
|
1501
|
+
.to({EntityName}ServiceExt)
|
|
1502
|
+
.inSingletonScope()
|
|
1503
|
+
.whenTargetIsDefault();
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
// Service creation function for ServerContext
|
|
1507
|
+
const createServiceFunc = (container: interfaces.Container): Partial<ServerContext> => ({
|
|
1508
|
+
{entity}Service: container.get<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service),
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
export { createServiceFunc };
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
### Phase 14: Main Module File
|
|
1515
|
+
|
|
1516
|
+
**File**: `packages-modules/{module}/server/src/module.ts`
|
|
1517
|
+
|
|
1518
|
+
```typescript
|
|
1519
|
+
import { interfaces } from 'inversify';
|
|
1520
|
+
import { Feature } from '@common-stack/server-core';
|
|
1521
|
+
import type { ServerContext } from 'common/server';
|
|
1522
|
+
import { SERVER_TYPES, I{EntityName}Service } from 'common/server';
|
|
1523
|
+
import { {entity}Module, createServiceFunc } from './containers/module';
|
|
1524
|
+
import { resolvers, schema } from './graphql';
|
|
1525
|
+
import { rules } from './permissions';
|
|
1526
|
+
import { {EntityName}MoleculerService } from './plugins';
|
|
1527
|
+
|
|
1528
|
+
// Service generation function - REQUIRED TYPE: (container: interfaces.Container) => Partial<ServerContext>
|
|
1529
|
+
const {entity}ServiceGen = (container: interfaces.Container): Partial<ServerContext> => {
|
|
1530
|
+
const environment = container.get('Environment');
|
|
1531
|
+
return {
|
|
1532
|
+
{entity}Service: container.get<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service),
|
|
1533
|
+
};
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
export default new Feature({
|
|
1537
|
+
schema,
|
|
1538
|
+
createResolversFunc: resolvers,
|
|
1539
|
+
createContainerFunc: [{entity}Module],
|
|
1540
|
+
createServiceFunc: {entity}ServiceGen, // This must return Partial<ServerContext>
|
|
1541
|
+
rules,
|
|
1542
|
+
addBrokerMainServiceClass: [{EntityName}MoleculerService],
|
|
1543
|
+
middleware: [
|
|
1544
|
+
// Add custom middleware here
|
|
1545
|
+
(app) => {
|
|
1546
|
+
app.get('/api/{entity}/health', (req, res) => {
|
|
1547
|
+
res.json({ status: 'OK', service: '{entity}Service', timestamp: new Date().toISOString() });
|
|
1548
|
+
});
|
|
1549
|
+
},
|
|
1550
|
+
],
|
|
1551
|
+
});
|
|
1552
|
+
```
|
|
1553
|
+
|
|
1554
|
+
### Phase 15: Main Module File
|
|
1555
|
+
|
|
1556
|
+
**File**: `packages-modules/{module}/server/src/module.ts`
|
|
1557
|
+
|
|
1558
|
+
```typescript
|
|
1559
|
+
import { interfaces } from 'inversify';
|
|
1560
|
+
import { Feature } from '@common-stack/server-core';
|
|
1561
|
+
import type { ServerContext } from 'common/server';
|
|
1562
|
+
import { SERVER_TYPES, I{EntityName}Service } from 'common/server';
|
|
1563
|
+
import { {entity}Module, createServiceFunc } from './containers/module';
|
|
1564
|
+
import { resolvers, schema } from './graphql';
|
|
1565
|
+
import { rules } from './permissions';
|
|
1566
|
+
import { {EntityName}MoleculerService } from './plugins';
|
|
1567
|
+
|
|
1568
|
+
// Service generation function - REQUIRED TYPE: (container: interfaces.Container) => Partial<ServerContext>
|
|
1569
|
+
const {entity}ServiceGen = (container: interfaces.Container): Partial<ServerContext> => {
|
|
1570
|
+
const environment = container.get('Environment');
|
|
1571
|
+
return {
|
|
1572
|
+
{entity}Service: container.get<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service),
|
|
1573
|
+
{entity}DataLoader: container.get(SERVER_TYPES.{EntityName}DataLoader), // Include DataLoader
|
|
1574
|
+
};
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
export default new Feature({
|
|
1578
|
+
schema,
|
|
1579
|
+
createResolversFunc: resolvers,
|
|
1580
|
+
createContainerFunc: [{entity}Module],
|
|
1581
|
+
createServiceFunc: {entity}ServiceGen, // This must return Partial<ServerContext>
|
|
1582
|
+
rules,
|
|
1583
|
+
addBrokerMainServiceClass: [{EntityName}MoleculerService],
|
|
1584
|
+
middleware: [
|
|
1585
|
+
// Add custom middleware here
|
|
1586
|
+
(app) => {
|
|
1587
|
+
app.get('/api/{entity}/health', (req, res) => {
|
|
1588
|
+
res.json({ status: 'OK', service: '{entity}Service', timestamp: new Date().toISOString() });
|
|
1589
|
+
});
|
|
1590
|
+
},
|
|
1591
|
+
],
|
|
1592
|
+
});
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
### Phase 16: Index Files and Exports
|
|
1596
|
+
|
|
1597
|
+
**File**: `packages-modules/{module}/server/src/store/models/index.ts`
|
|
1598
|
+
|
|
1599
|
+
```typescript
|
|
1600
|
+
export { {EntityName}ModelFunc } from './{entity-name}-model';
|
|
1601
|
+
```
|
|
1602
|
+
|
|
1603
|
+
**File**: `packages-modules/{module}/server/src/store/repositories/index.ts`
|
|
1604
|
+
|
|
1605
|
+
```typescript
|
|
1606
|
+
export { {EntityName}Repository } from './{entity-name}-repository';
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
**File**: `packages-modules/{module}/server/src/services/index.ts`
|
|
1610
|
+
|
|
1611
|
+
```typescript
|
|
1612
|
+
export { {EntityName}Service } from './{entity-name}-service';
|
|
1613
|
+
export { {EntityName}ServiceExt } from './{entity-name}-service-ext';
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
**File**: `packages-modules/{module}/server/src/dataloaders/index.ts`
|
|
1617
|
+
|
|
1618
|
+
```typescript
|
|
1619
|
+
export { {EntityName}DataLoader } from './{entity-name}-data-loader';
|
|
1620
|
+
```
|
|
1621
|
+
|
|
1622
|
+
**File**: `packages-modules/{module}/server/src/graphql/resolvers/index.ts`
|
|
1623
|
+
|
|
1624
|
+
```typescript
|
|
1625
|
+
import { merge } from 'lodash-es';
|
|
1626
|
+
import { {entity}Resolvers } from './{entity-name}-resolver';
|
|
1627
|
+
|
|
1628
|
+
export const resolvers = () => merge({entity}Resolvers);
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
**File**: `packages-modules/{module}/server/src/graphql/schemas/index.ts`
|
|
1632
|
+
|
|
1633
|
+
```typescript
|
|
1634
|
+
import { loadSchemaSync } from '@graphql-tools/load';
|
|
1635
|
+
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
|
|
1636
|
+
import { join } from 'path';
|
|
1637
|
+
|
|
1638
|
+
const schema = loadSchemaSync(join(__dirname, './*.graphql'), {
|
|
1639
|
+
loaders: [new GraphQLFileLoader()],
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
export { schema };
|
|
1643
|
+
```
|
|
1644
|
+
|
|
1645
|
+
### Phase 17: Constants and Actions
|
|
1646
|
+
|
|
1647
|
+
**File**: `packages-modules/{module}/server/src/constants/moleculer-actions.ts`
|
|
1648
|
+
|
|
1649
|
+
```typescript
|
|
1650
|
+
export const {EntityName}ServiceAction = {
|
|
1651
|
+
On{EntityName}Created: '{entity}.created',
|
|
1652
|
+
On{EntityName}Updated: '{entity}.updated',
|
|
1653
|
+
On{EntityName}Deleted: '{entity}.deleted',
|
|
1654
|
+
On{EntityName}StatusChanged: '{entity}.status.changed',
|
|
1655
|
+
} as const;
|
|
1656
|
+
|
|
1657
|
+
export const MoleculerServiceName = {
|
|
1658
|
+
NotificationService: 'notification',
|
|
1659
|
+
AuditService: 'audit',
|
|
1660
|
+
ConfigurationService: 'configuration',
|
|
1661
|
+
} as const;
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
### Phase 18: Final Build and Verification
|
|
1665
|
+
|
|
1666
|
+
```bash
|
|
1667
|
+
# 1. CRITICAL: Navigate to PROJECT ROOT (not module directory)
|
|
1668
|
+
cd /path/to/your/project-root
|
|
1669
|
+
|
|
1670
|
+
# 2. CRITICAL: Regenerate templates and immediately generate types from ROOT
|
|
1671
|
+
# (regenerateGraphql can remove codegen files, so generateGraphql must follow immediately)
|
|
1672
|
+
yarn regenerateGraphql && yarn generateGraphql
|
|
1673
|
+
|
|
1674
|
+
# 3. Build specific module using Lerna
|
|
1675
|
+
lerna exec --scope=@adminide-stack/{module}-module-server yarn build
|
|
1676
|
+
|
|
1677
|
+
# Alternative builds:
|
|
1678
|
+
# For marketplace module specifically:
|
|
1679
|
+
lerna exec --scope=@adminide-stack/marketplace-module-server yarn build
|
|
1680
|
+
|
|
1681
|
+
# For extension module:
|
|
1682
|
+
lerna exec --scope=@adminide-stack/extension-module-server yarn build
|
|
1683
|
+
|
|
1684
|
+
# For general module build:
|
|
1685
|
+
yarn build
|
|
1686
|
+
|
|
1687
|
+
# 5. Run tests
|
|
1688
|
+
yarn test
|
|
1689
|
+
|
|
1690
|
+
# 6. Full monorepo build (if needed)
|
|
1691
|
+
yarn build:all
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
### Phase 19: Verification Checklist
|
|
1695
|
+
|
|
1696
|
+
✅ **Files Created:**
|
|
1697
|
+
|
|
1698
|
+
- [ ] GraphQL schema: `{entity}.graphql`
|
|
1699
|
+
- [ ] Event types: `service.graphql`
|
|
1700
|
+
- [ ] Templates: Service and Repository templates
|
|
1701
|
+
- [ ] Constants: DB_COLL_NAMES and SERVER_TYPES
|
|
1702
|
+
- [ ] Model: Mongoose schema with proper typing
|
|
1703
|
+
- [ ] Repository: Data access with BaseRepository
|
|
1704
|
+
- [ ] Service: Business logic with event emitters
|
|
1705
|
+
- [ ] ServiceExt: Moleculer broadcasting
|
|
1706
|
+
- [ ] Resolver: GraphQL operations
|
|
1707
|
+
- [ ] Container: Dependency injection bindings
|
|
1708
|
+
- [ ] Module: Feature integration with createServiceFunc
|
|
1709
|
+
- [ ] Config: Environment configuration
|
|
1710
|
+
- [ ] Constants: Moleculer actions and service names
|
|
1711
|
+
|
|
1712
|
+
✅ **Generated Files:**
|
|
1713
|
+
|
|
1714
|
+
- [ ] `packages/common/src/services/{EntityName}Service.ts`
|
|
1715
|
+
- [ ] `packages/common/src/repositories/{EntityName}Repository.ts`
|
|
1716
|
+
- [ ] `packages/common/src/constants/DB_COLL_NAMES.ts` (updated)
|
|
1717
|
+
- [ ] `packages/common/src/generated/generated-models.ts` (updated)
|
|
1718
|
+
|
|
1719
|
+
✅ **Type Safety:**
|
|
1720
|
+
|
|
1721
|
+
- [ ] All imports from `common/server` resolve
|
|
1722
|
+
- [ ] No TypeScript compilation errors
|
|
1723
|
+
- [ ] Proper ServerContext augmentation
|
|
1724
|
+
- [ ] Event types generated without @entity
|
|
1725
|
+
|
|
1726
|
+
✅ **Architecture:**
|
|
1727
|
+
|
|
1728
|
+
- [ ] Repository extends BaseRepository
|
|
1729
|
+
- [ ] Service extends BaseService
|
|
1730
|
+
- [ ] ServiceExt for Moleculer integration
|
|
1731
|
+
- [ ] Proper dependency injection
|
|
1732
|
+
- [ ] Event-driven architecture
|
|
1733
|
+
- [ ] Tenant isolation implemented
|
|
1734
|
+
- [ ] Audit fields included
|
|
1735
|
+
|
|
1736
|
+
## 🎯 Key Patterns Summary
|
|
1737
|
+
|
|
1738
|
+
### **1. Field Mapping Architecture**
|
|
1739
|
+
|
|
1740
|
+
```typescript
|
|
1741
|
+
// GraphQL API Layer (human-readable)
|
|
1742
|
+
input InstallExtensionInput {
|
|
1743
|
+
extensionID: String! // "publisher/extension-name"
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Database Layer (ObjectId references)
|
|
1747
|
+
type InstalledExtension @entity {
|
|
1748
|
+
extension: RegistryExtension @column(overrideType: "ObjectId")
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Resolver handles the mapping
|
|
1752
|
+
const installInput = {
|
|
1753
|
+
tenantId: userContext.tenantId,
|
|
1754
|
+
registryRef: registryEntity._id, // ObjectId reference
|
|
1755
|
+
extensionID, // Human-readable ID for API
|
|
1756
|
+
installedVersion: registryEntity.version,
|
|
1757
|
+
installedBy: userContext.userId,
|
|
1758
|
+
};
|
|
1759
|
+
```
|
|
1760
|
+
|
|
1761
|
+
### **2. Service Creation Function Pattern**
|
|
1762
|
+
|
|
1763
|
+
```typescript
|
|
1764
|
+
// REQUIRED signature for createServiceFunc
|
|
1765
|
+
const createServiceFunc = (container: interfaces.Container): Partial<ServerContext> => ({
|
|
1766
|
+
{entity}Service: container.get<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service),
|
|
1767
|
+
});
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
### **3. Event System Pattern**
|
|
1771
|
+
|
|
1772
|
+
```typescript
|
|
1773
|
+
// Events in service.graphql (NO @entity)
|
|
1774
|
+
type {EntityName}CreatedEvent {
|
|
1775
|
+
{entity}Id: String!
|
|
1776
|
+
tenantId: String!
|
|
1777
|
+
// ... other fields
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Service emits events
|
|
1781
|
+
protected readonly on{EntityName}Created = new Emitter<I{EntityName}CreatedEvent>();
|
|
1782
|
+
```
|
|
1783
|
+
|
|
1784
|
+
### **4. Repository Pattern**
|
|
1785
|
+
|
|
1786
|
+
```typescript
|
|
1787
|
+
// Repository extends BaseRepository
|
|
1788
|
+
@injectable()
|
|
1789
|
+
export class {EntityName}Repository extends BaseRepository<I{EntityName}Model>
|
|
1790
|
+
implements I{EntityName}Repository {
|
|
1791
|
+
|
|
1792
|
+
constructor(connection: Connection) {
|
|
1793
|
+
const {EntityName}Model = {EntityName}ModelFunc(connection);
|
|
1794
|
+
super({EntityName}Model);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
```
|
|
1798
|
+
|
|
1799
|
+
### **5. Container Binding Pattern**
|
|
1800
|
+
|
|
1801
|
+
```typescript
|
|
1802
|
+
// Container module with proper typing
|
|
1803
|
+
bind<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service)
|
|
1804
|
+
.to({EntityName}ServiceExt) // Use ServiceExt for Moleculer
|
|
1805
|
+
.inSingletonScope()
|
|
1806
|
+
.whenTargetIsDefault();
|
|
1807
|
+
```
|
|
1808
|
+
|
|
1809
|
+
### **6. DataLoader Template Pattern**
|
|
1810
|
+
|
|
1811
|
+
```typescript
|
|
1812
|
+
// File: packages-modules/{module}/server/src/templates/services/{EntityName}DataLoader.ts.template
|
|
1813
|
+
import { I{EntityName}, IDataLoader } from 'common/server';
|
|
1814
|
+
export type I{EntityName}DataLoader = IDataLoader<I{EntityName}>;
|
|
1815
|
+
```
|
|
1816
|
+
|
|
1817
|
+
### **7. SERVER_TYPES for DataLoaders**
|
|
1818
|
+
|
|
1819
|
+
```typescript
|
|
1820
|
+
// Add to SERVER_TYPES.ts (automatically generated from templates)
|
|
1821
|
+
export const SERVER_TYPES = {
|
|
1822
|
+
// ... existing types
|
|
1823
|
+
{EntityName}DataLoader: Symbol('{EntityName}DataLoader'),
|
|
1824
|
+
} as const;
|
|
1825
|
+
```
|
|
1826
|
+
|
|
1827
|
+
## 🔄 **DataLoader Implementation for Object Resolution**
|
|
1828
|
+
|
|
1829
|
+
### **Problem**:
|
|
1830
|
+
|
|
1831
|
+
When GraphQL schema defines object relationships but database stores ObjectId references, you need DataLoaders to resolve these efficiently and prevent N+1 query problems.
|
|
1832
|
+
|
|
1833
|
+
### **Solution Pattern**:
|
|
1834
|
+
|
|
1835
|
+
#### **1. DataLoader Implementation**
|
|
1836
|
+
|
|
1837
|
+
```typescript
|
|
1838
|
+
// File: packages-modules/{module}/server/src/dataloaders/{entity}-data-loader.ts
|
|
1839
|
+
import { I{EntityName}, I{EntityName}Service, SERVER_TYPES, IBaseService, AsDomainType } from 'common/server';
|
|
1840
|
+
import { injectable, inject } from 'inversify';
|
|
1841
|
+
import { BulkDataLoader2 } from '@common-stack/store-mongo';
|
|
1842
|
+
|
|
1843
|
+
@injectable()
|
|
1844
|
+
export class {EntityName}DataLoader extends BulkDataLoader2<AsDomainType<I{EntityName}>> {
|
|
1845
|
+
constructor(
|
|
1846
|
+
@inject(SERVER_TYPES.I{EntityName}Service)
|
|
1847
|
+
{entity}Service: I{EntityName}Service,
|
|
1848
|
+
) {
|
|
1849
|
+
super({entity}Service as unknown as IBaseService<AsDomainType<I{EntityName}>>);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
#### **2. Field Resolver with DataLoader**
|
|
1855
|
+
|
|
1856
|
+
```typescript
|
|
1857
|
+
// In your GraphQL resolver
|
|
1858
|
+
{ParentEntity}: {
|
|
1859
|
+
// Field resolver for object references using DataLoaders
|
|
1860
|
+
{relationField}: (root, args, { {entity}DataLoader }) => {
|
|
1861
|
+
// CRITICAL: Check if relation field exists and is not null before calling dataloader
|
|
1862
|
+
if (!root.{relationField} || root.{relationField} === null || root.{relationField} === undefined) {
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
const relationId = String(root.{relationField}); // Convert ObjectId to string
|
|
1866
|
+
return {entity}DataLoader.load(relationId);
|
|
1867
|
+
},
|
|
1868
|
+
|
|
1869
|
+
// Example: Resolving organization from ObjectId reference
|
|
1870
|
+
organization: (root, args, { organizationDataLoader }) => {
|
|
1871
|
+
// Check if organization exists and is not null/undefined before calling dataloader
|
|
1872
|
+
if (!root.organization || root.organization === null || root.organization === undefined) {
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
const orgId = String(root.organization);
|
|
1876
|
+
return organizationDataLoader.load(orgId);
|
|
1877
|
+
},
|
|
1878
|
+
|
|
1879
|
+
// Example: Resolving user from ObjectId reference
|
|
1880
|
+
user: (root, args, { accountUserDataLoader }) => {
|
|
1881
|
+
// Check if user exists and is not null/undefined before calling dataloader
|
|
1882
|
+
if (!root.user || root.user === null || root.user === undefined) {
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
const userId = String(root.user);
|
|
1886
|
+
return accountUserDataLoader.load(userId);
|
|
1887
|
+
},
|
|
1888
|
+
|
|
1889
|
+
// Example: Resolving extension from ObjectId reference in installed extension
|
|
1890
|
+
extension: (root, args, { registryExtensionDataLoader }) => {
|
|
1891
|
+
if (!root.extension || root.extension === null || root.extension === undefined) {
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
const extensionID = String(root.extension);
|
|
1895
|
+
return registryExtensionDataLoader.load(extensionID);
|
|
1896
|
+
},
|
|
1897
|
+
},
|
|
1898
|
+
```
|
|
1899
|
+
|
|
1900
|
+
#### **3. Context Integration**
|
|
1901
|
+
|
|
1902
|
+
```typescript
|
|
1903
|
+
// Context interface must include DataLoaders
|
|
1904
|
+
interface I{EntityName}Context extends ServerContext {
|
|
1905
|
+
// Service dependencies
|
|
1906
|
+
{entity}Service: I{EntityName}Service;
|
|
1907
|
+
organizationService: IOrganizationService;
|
|
1908
|
+
|
|
1909
|
+
// DataLoader dependencies for resolving ObjectId references
|
|
1910
|
+
{entity}DataLoader: {EntityName}DataLoader;
|
|
1911
|
+
organizationDataLoader: OrganizationDataLoader;
|
|
1912
|
+
accountUserDataLoader: AccountUserDataLoader;
|
|
1913
|
+
registryExtensionDataLoader: RegistryExtensionDataLoader;
|
|
1914
|
+
|
|
1915
|
+
// User context
|
|
1916
|
+
userContext: {
|
|
1917
|
+
tenantId: string;
|
|
1918
|
+
userId: string;
|
|
1919
|
+
accountId: string;
|
|
1920
|
+
emailId: string;
|
|
1921
|
+
organization?: {
|
|
1922
|
+
id: string;
|
|
1923
|
+
name: string;
|
|
1924
|
+
};
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
```
|
|
1928
|
+
|
|
1929
|
+
#### **4. Container Module DataLoader Registration**
|
|
1930
|
+
|
|
1931
|
+
```typescript
|
|
1932
|
+
// File: packages-modules/{module}/server/src/containers/module.ts
|
|
1933
|
+
import { {EntityName}DataLoader } from '../dataloaders/{entity}-data-loader';
|
|
1934
|
+
|
|
1935
|
+
export const {entity}Module: (settings: any) => interfaces.ContainerModule = (settings: any) =>
|
|
1936
|
+
new ContainerModule((bind: interfaces.Bind) => {
|
|
1937
|
+
// ... existing bindings
|
|
1938
|
+
|
|
1939
|
+
// DataLoader - CRITICAL: Use request scope for caching
|
|
1940
|
+
bind<{EntityName}DataLoader>(SERVER_TYPES.{EntityName}DataLoader)
|
|
1941
|
+
.to({EntityName}DataLoader)
|
|
1942
|
+
.inRequestScope(); // Important: Request scope for DataLoader caching
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
// Update createServiceFunc to include DataLoader
|
|
1946
|
+
const createServiceFunc = (container: interfaces.Container): Partial<ServerContext> => ({
|
|
1947
|
+
{entity}Service: container.get<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service),
|
|
1948
|
+
{entity}DataLoader: container.get<{EntityName}DataLoader>(SERVER_TYPES.{EntityName}DataLoader),
|
|
1949
|
+
});
|
|
1950
|
+
```
|
|
1951
|
+
|
|
1952
|
+
### **🔧 CRITICAL: Template Configuration for DataLoaders**
|
|
1953
|
+
|
|
1954
|
+
#### **1. Add DataLoader Templates to package.json**
|
|
1955
|
+
|
|
1956
|
+
```json
|
|
1957
|
+
{
|
|
1958
|
+
"cdecode": {
|
|
1959
|
+
"common": {
|
|
1960
|
+
"constants": [
|
|
1961
|
+
"./${libDir}/templates/constants/SERVER_TYPES.ts.template",
|
|
1962
|
+
"./${libDir}/templates/constants/DB_COLL_NAMES.ts.template"
|
|
1963
|
+
],
|
|
1964
|
+
"services": [
|
|
1965
|
+
"./${libDir}/templates/services/{EntityName}Service.ts.template",
|
|
1966
|
+
"./${libDir}/templates/services/{EntityName}DataLoader.ts.template"
|
|
1967
|
+
],
|
|
1968
|
+
"repositories": ["./${libDir}/templates/repositories/{EntityName}Repository.ts.template"]
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
```
|
|
1973
|
+
|
|
1974
|
+
#### **2. Create DataLoader Template File**
|
|
1975
|
+
|
|
1976
|
+
```typescript
|
|
1977
|
+
// File: packages-modules/{module}/server/src/templates/services/{EntityName}DataLoader.ts.template
|
|
1978
|
+
import { I{EntityName}, IDataLoader } from 'common/server';
|
|
1979
|
+
export type I{EntityName}DataLoader = IDataLoader<I{EntityName}>;
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
#### **3. Add SERVER_TYPES Template**
|
|
1983
|
+
|
|
1984
|
+
```typescript
|
|
1985
|
+
// File: packages-modules/{module}/server/src/templates/constants/SERVER_TYPES.ts.template
|
|
1986
|
+
export const SERVER_TYPES = {
|
|
1987
|
+
// ... existing types from other modules
|
|
1988
|
+
|
|
1989
|
+
// Add DataLoader types for this module
|
|
1990
|
+
{EntityName}DataLoader: Symbol('{EntityName}DataLoader'),
|
|
1991
|
+
};
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
#### **4. Regenerate Types**
|
|
1995
|
+
|
|
1996
|
+
```bash
|
|
1997
|
+
# Run these commands to regenerate templates
|
|
1998
|
+
yarn regenerateGraphql
|
|
1999
|
+
yarn generateGraphql
|
|
2000
|
+
```
|
|
2001
|
+
|
|
2002
|
+
This will automatically:
|
|
2003
|
+
|
|
2004
|
+
- Generate `I{EntityName}DataLoader` type in `common/server`
|
|
2005
|
+
- Add `{EntityName}DataLoader` to SERVER_TYPES constants
|
|
2006
|
+
- Make DataLoader available in GraphQL context for resolvers
|
|
2007
|
+
|
|
2008
|
+
#### **5. GraphQL Schema Definition**
|
|
2009
|
+
|
|
2010
|
+
```graphql
|
|
2011
|
+
# Schema showing ObjectId stored as references but resolved as objects
|
|
2012
|
+
type InstalledExtension @entity {
|
|
2013
|
+
id: ID!
|
|
2014
|
+
tenantId: String! @column
|
|
2015
|
+
|
|
2016
|
+
# Database stores ObjectId reference to RegistryExtension
|
|
2017
|
+
extension: RegistryExtension @column(overrideType: "ObjectId")
|
|
2018
|
+
|
|
2019
|
+
# Database stores string but resolves to object via DataLoader
|
|
2020
|
+
extensionID: String! @column
|
|
2021
|
+
|
|
2022
|
+
installedVersion: String! @column
|
|
2023
|
+
installedBy: String! @column
|
|
2024
|
+
|
|
2025
|
+
createdAt: Date! @column(overrideType: "Date")
|
|
2026
|
+
updatedAt: Date! @column(overrideType: "Date")
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
type RegistryExtension @entity {
|
|
2030
|
+
id: ID!
|
|
2031
|
+
extensionID: String! @column
|
|
2032
|
+
version: String! @column
|
|
2033
|
+
name: String! @column
|
|
2034
|
+
# ... other fields
|
|
2035
|
+
}
|
|
2036
|
+
```
|
|
2037
|
+
|
|
2038
|
+
#### **6. Complete Real-World Example: Installed Extension**
|
|
2039
|
+
|
|
2040
|
+
```typescript
|
|
2041
|
+
export const resolvers: (options: IResolverOptions) => IResolvers = (options) => ({
|
|
2042
|
+
// Field resolvers for InstalledExtension
|
|
2043
|
+
InstalledExtension: {
|
|
2044
|
+
// Resolve extension ObjectId reference to actual RegistryExtension object
|
|
2045
|
+
extension: (root, args, { registryExtensionDataLoader }) => {
|
|
2046
|
+
if (!root.extension || root.extension === null || root.extension === undefined) {
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
const extensionID = String(root.extension); // Convert ObjectId to string
|
|
2050
|
+
return registryExtensionDataLoader.load(extensionID);
|
|
2051
|
+
},
|
|
2052
|
+
|
|
2053
|
+
// Resolve installedBy ObjectId reference to User object
|
|
2054
|
+
installedByUser: (root, args, { accountUserDataLoader }) => {
|
|
2055
|
+
if (!root.installedBy || root.installedBy === null || root.installedBy === undefined) {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
const userId = String(root.installedBy);
|
|
2059
|
+
return accountUserDataLoader.load(userId);
|
|
2060
|
+
},
|
|
2061
|
+
|
|
2062
|
+
// Computed field combining data from both sources
|
|
2063
|
+
displayName: async (root, args, { registryExtensionDataLoader }) => {
|
|
2064
|
+
if (!root.extension) return root.extensionID;
|
|
2065
|
+
|
|
2066
|
+
const extension = await registryExtensionDataLoader.load(String(root.extension));
|
|
2067
|
+
return extension ? extension.name : root.extensionID;
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
|
|
2071
|
+
// Query resolvers
|
|
2072
|
+
Query: {
|
|
2073
|
+
getInstalledExtensions: async (
|
|
2074
|
+
_: any,
|
|
2075
|
+
{ filter }: { filter?: any },
|
|
2076
|
+
{ installedExtensionService, userContext }: InstalledExtensionContext,
|
|
2077
|
+
): Promise<IInstalledExtensionModel[]> => {
|
|
2078
|
+
options.logger.trace('(Query.getInstalledExtensions) filter [%j]', filter);
|
|
2079
|
+
|
|
2080
|
+
if (!userContext?.tenantId) {
|
|
2081
|
+
throw new Error('Authentication required');
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const searchFilter = {
|
|
2085
|
+
tenantId: userContext.tenantId,
|
|
2086
|
+
...filter,
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
// Service returns models with ObjectId references
|
|
2090
|
+
// Field resolvers will use DataLoaders to resolve them to actual objects
|
|
2091
|
+
return await installedExtensionService.getInstalledExtensions(searchFilter);
|
|
2092
|
+
},
|
|
2093
|
+
},
|
|
2094
|
+
|
|
2095
|
+
// Mutation resolvers
|
|
2096
|
+
Mutation: {
|
|
2097
|
+
installExtension: async (
|
|
2098
|
+
_: any,
|
|
2099
|
+
{ extensionID }: { extensionID: string },
|
|
2100
|
+
{ installedExtensionService, registryExtensionService, userContext }: InstalledExtensionContext,
|
|
2101
|
+
) => {
|
|
2102
|
+
options.logger.trace('(Mutation.installExtension) extensionID [%j]', extensionID);
|
|
2103
|
+
|
|
2104
|
+
if (!userContext?.tenantId || !userContext?.userId) {
|
|
2105
|
+
throw new Error('Authentication required');
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
try {
|
|
2109
|
+
// First get the registry extension (this will be stored as ObjectId reference)
|
|
2110
|
+
const extension = await registryExtensionService.findExtension(extensionID);
|
|
2111
|
+
if (!extension) {
|
|
2112
|
+
throw new Error(`Extension ${extensionID} not found`);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// Install extension - stores extension._id as ObjectId reference
|
|
2116
|
+
const installInput = {
|
|
2117
|
+
tenantId: userContext.tenantId,
|
|
2118
|
+
extension: extension._id, // ObjectId reference
|
|
2119
|
+
extensionID, // Human-readable ID
|
|
2120
|
+
installedVersion: extension.version,
|
|
2121
|
+
installedBy: userContext.userId, // Also ObjectId reference
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
const result = await installedExtensionService.installExtension(installInput);
|
|
2125
|
+
|
|
2126
|
+
// When returned, field resolvers will use DataLoaders to resolve:
|
|
2127
|
+
// - result.extension (ObjectId) → RegistryExtension object
|
|
2128
|
+
// - result.installedBy (ObjectId) → User object
|
|
2129
|
+
|
|
2130
|
+
return result;
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
options.logger.error('Error installing extension:', error);
|
|
2133
|
+
throw error;
|
|
2134
|
+
}
|
|
2135
|
+
},
|
|
2136
|
+
},
|
|
2137
|
+
});
|
|
2138
|
+
```
|
|
2139
|
+
|
|
2140
|
+
### **Key Benefits of DataLoader Pattern:**
|
|
2141
|
+
|
|
2142
|
+
1. **Performance**: Prevents N+1 query problems by batching database requests
|
|
2143
|
+
2. **Caching**: DataLoaders cache results within a single request
|
|
2144
|
+
3. **Type Safety**: Resolves ObjectId references to properly typed objects
|
|
2145
|
+
4. **Clean Separation**: Database stores efficient ObjectId references, GraphQL returns rich objects
|
|
2146
|
+
5. **Consistency**: All object references resolved through same pattern
|
|
2147
|
+
|
|
2148
|
+
### **Critical Implementation Notes:**
|
|
2149
|
+
|
|
2150
|
+
1. **Null Checks**: Always check if reference field exists before calling DataLoader
|
|
2151
|
+
2. **String Conversion**: Convert ObjectId to string before passing to DataLoader
|
|
2152
|
+
3. **Request Scope**: DataLoaders should be bound in request scope for proper caching
|
|
2153
|
+
4. **Field Mapping**: Database stores ObjectId, GraphQL schema exposes as object type
|
|
2154
|
+
5. **Error Handling**: DataLoaders handle missing references gracefully by returning null
|
|
2155
|
+
6. **Type Casting**: When field resolvers use DataLoaders to resolve ObjectId references, you must cast return types in queries/mutations
|
|
2156
|
+
|
|
2157
|
+
### **Type Casting Pattern for DataLoader Resolvers:**
|
|
2158
|
+
|
|
2159
|
+
When you have field resolvers that use DataLoaders to resolve ObjectId references to objects, the service layer returns `Model` types but GraphQL expects the interface types. You need to cast the return types:
|
|
2160
|
+
|
|
2161
|
+
```typescript
|
|
2162
|
+
// Query resolvers need type casting
|
|
2163
|
+
Query: {
|
|
2164
|
+
// Single object query
|
|
2165
|
+
get{EntityName}: async (_, { id }, { {entity}Service, userContext }) => {
|
|
2166
|
+
return await {entity}Service.get{EntityName}(id, userContext.tenantId) as unknown as I{EntityName};
|
|
2167
|
+
},
|
|
2168
|
+
|
|
2169
|
+
// Array query
|
|
2170
|
+
list{EntityName}s: async (_, { filter }, { {entity}Service, userContext }) => {
|
|
2171
|
+
const result = await {entity}Service.list{EntityName}s({
|
|
2172
|
+
tenantId: userContext.tenantId,
|
|
2173
|
+
...filter,
|
|
2174
|
+
});
|
|
2175
|
+
return result as unknown as I{EntityName}[];
|
|
2176
|
+
},
|
|
2177
|
+
},
|
|
2178
|
+
|
|
2179
|
+
// Mutation resolvers also need type casting
|
|
2180
|
+
Mutation: {
|
|
2181
|
+
create{EntityName}: async (_, { input }, { {entity}Service, userContext }) => {
|
|
2182
|
+
const result = await {entity}Service.create{EntityName}({
|
|
2183
|
+
...input,
|
|
2184
|
+
tenantId: userContext.tenantId,
|
|
2185
|
+
createdBy: userContext.accountId,
|
|
2186
|
+
});
|
|
2187
|
+
return result as unknown as I{EntityName};
|
|
2188
|
+
},
|
|
2189
|
+
|
|
2190
|
+
// When returning objects in mutation responses
|
|
2191
|
+
installExtension: async (_, { input }, { installedExtensionService, userContext }) => {
|
|
2192
|
+
const installedExtension = await installedExtensionService.installExtension({
|
|
2193
|
+
...input,
|
|
2194
|
+
tenantId: userContext.tenantId,
|
|
2195
|
+
installedBy: userContext.accountId,
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
return {
|
|
2199
|
+
success: true,
|
|
2200
|
+
extension: installedExtension as unknown as IInstalledExtension,
|
|
2201
|
+
message: `Extension installed successfully`,
|
|
2202
|
+
};
|
|
2203
|
+
},
|
|
2204
|
+
},
|
|
2205
|
+
```
|
|
2206
|
+
|
|
2207
|
+
### **Why Type Casting is Required:**
|
|
2208
|
+
|
|
2209
|
+
1. **Service Layer**: Returns `I{EntityName}Model` (Mongoose document with ObjectId fields)
|
|
2210
|
+
2. **GraphQL Schema**: Expects `I{EntityName}` (interface with resolved object references)
|
|
2211
|
+
3. **Field Resolvers**: Use DataLoaders to transform ObjectId → actual objects during GraphQL execution
|
|
2212
|
+
4. **Type System**: TypeScript can't infer that field resolvers will handle the transformation
|
|
2213
|
+
|
|
2214
|
+
### **Example with InstalledExtension:**
|
|
2215
|
+
|
|
2216
|
+
```typescript
|
|
2217
|
+
// Database stores this
|
|
2218
|
+
type IInstalledExtensionModel = {
|
|
2219
|
+
_id: ObjectId;
|
|
2220
|
+
extension: ObjectId; // Reference to RegistryExtension
|
|
2221
|
+
extensionID: string;
|
|
2222
|
+
tenantId: string;
|
|
2223
|
+
// ...
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// GraphQL expects this after field resolution
|
|
2227
|
+
type IInstalledExtension = {
|
|
2228
|
+
id: string;
|
|
2229
|
+
extension: IRegistryExtension; // Resolved object via DataLoader
|
|
2230
|
+
extensionID: string;
|
|
2231
|
+
tenantId: string;
|
|
2232
|
+
// ...
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// Field resolver handles the transformation
|
|
2236
|
+
InstalledExtension: {
|
|
2237
|
+
extension: (root, args, { registryExtensionDataLoader }) => {
|
|
2238
|
+
if (!root.extension) return null;
|
|
2239
|
+
return registryExtensionDataLoader.load(String(root.extension)); // ObjectId → IRegistryExtension
|
|
2240
|
+
},
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// Query must cast the type
|
|
2244
|
+
Query: {
|
|
2245
|
+
installedExtensions: async (_, args, { installedExtensionService, userContext }) => {
|
|
2246
|
+
// Service returns IInstalledExtensionModel[]
|
|
2247
|
+
const result = await installedExtensionService.getInstalledExtensions({
|
|
2248
|
+
tenantId: userContext.tenantId
|
|
2249
|
+
});
|
|
2250
|
+
// Cast to what GraphQL expects - field resolver will handle extension transformation
|
|
2251
|
+
return result as unknown as IInstalledExtension[];
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
```
|
|
2255
|
+
|
|
2256
|
+
This pattern ensures efficient object resolution while maintaining clean GraphQL schemas and preventing performance issues.
|
|
2257
|
+
|
|
2258
|
+
## 🎯 **Complete DataLoader Implementation Checklist**
|
|
2259
|
+
|
|
2260
|
+
When implementing DataLoaders for object resolution, follow this complete checklist:
|
|
2261
|
+
|
|
2262
|
+
### ✅ **1. Create DataLoader Class**
|
|
2263
|
+
|
|
2264
|
+
```typescript
|
|
2265
|
+
// File: src/dataloaders/{entity}-data-loader.ts
|
|
2266
|
+
@injectable()
|
|
2267
|
+
export class {EntityName}DataLoader extends BulkDataLoader2<AsDomainType<I{EntityName}>> {
|
|
2268
|
+
constructor(@inject(SERVER_TYPES.I{EntityName}Service) {entity}Service: I{EntityName}Service) {
|
|
2269
|
+
super({entity}Service as unknown as IBaseService<AsDomainType<I{EntityName}>>);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
```
|
|
2273
|
+
|
|
2274
|
+
### ✅ **2. Add to SERVER_TYPES**
|
|
2275
|
+
|
|
2276
|
+
```typescript
|
|
2277
|
+
// packages/common/src/constants/SERVER_TYPES.ts
|
|
2278
|
+
I{EntityName}DataLoader: Symbol.for('I{EntityName}DataLoader'),
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
### ✅ **3. Register in Container**
|
|
2282
|
+
|
|
2283
|
+
```typescript
|
|
2284
|
+
// src/containers/module.ts
|
|
2285
|
+
bind<{EntityName}DataLoader>(SERVER_TYPES.I{EntityName}DataLoader)
|
|
2286
|
+
.to({EntityName}DataLoader)
|
|
2287
|
+
.inRequestScope();
|
|
2288
|
+
```
|
|
2289
|
+
|
|
2290
|
+
### ✅ **4. Add to Service Context**
|
|
2291
|
+
|
|
2292
|
+
```typescript
|
|
2293
|
+
// src/containers/module.ts - createServiceFunc
|
|
2294
|
+
const createServiceFunc = (container: interfaces.Container) => ({
|
|
2295
|
+
{entity}DataLoader: container.get<{EntityName}DataLoader>(SERVER_TYPES.I{EntityName}DataLoader),
|
|
2296
|
+
});
|
|
2297
|
+
```
|
|
2298
|
+
|
|
2299
|
+
### ✅ **5. Create Field Resolvers**
|
|
2300
|
+
|
|
2301
|
+
```typescript
|
|
2302
|
+
// src/graphql/resolvers/{entity}-resolver.ts
|
|
2303
|
+
{ParentEntity}: {
|
|
2304
|
+
{relationField}: (root, args, { {entity}DataLoader }) => {
|
|
2305
|
+
if (!root.{relationField}) return null;
|
|
2306
|
+
return {entity}DataLoader.load(String(root.{relationField}));
|
|
2307
|
+
},
|
|
2308
|
+
}
|
|
2309
|
+
```
|
|
2310
|
+
|
|
2311
|
+
### ✅ **6. Add Type Casting in Queries/Mutations**
|
|
2312
|
+
|
|
2313
|
+
```typescript
|
|
2314
|
+
Query: {
|
|
2315
|
+
get{EntityName}: async (_, { id }, { {entity}Service }) => {
|
|
2316
|
+
const result = await {entity}Service.get{EntityName}(id);
|
|
2317
|
+
return result as unknown as I{EntityName};
|
|
2318
|
+
},
|
|
2319
|
+
}
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2322
|
+
### ✅ **7. Run Template Regeneration**
|
|
2323
|
+
|
|
2324
|
+
```bash
|
|
2325
|
+
yarn regenerateGraphql && yarn generateGraphql
|
|
2326
|
+
```
|
|
2327
|
+
|
|
2328
|
+
This complete implementation ensures proper DataLoader integration with the adminIde-stack architecture! 🚀
|
|
2329
|
+
|
|
2330
|
+
## 📋 **Complete GraphQL Resolver Architecture Pattern**
|
|
2331
|
+
|
|
2332
|
+
### **File Structure:**
|
|
2333
|
+
|
|
2334
|
+
```
|
|
2335
|
+
src/graphql/resolvers/
|
|
2336
|
+
├── index.ts # Resolver combination and export
|
|
2337
|
+
├── {entity-name}-resolver.ts # Individual entity resolvers
|
|
2338
|
+
├── registry-extension-resolver.ts
|
|
2339
|
+
├── installed-extension-resolver.ts
|
|
2340
|
+
└── ...
|
|
2341
|
+
```
|
|
2342
|
+
|
|
2343
|
+
### **Individual Resolver Structure:**
|
|
2344
|
+
|
|
2345
|
+
```typescript
|
|
2346
|
+
// File: {entity-name}-resolver.ts
|
|
2347
|
+
import { PubSub } from 'graphql-subscriptions';
|
|
2348
|
+
import { CdmLogger } from '@cdm-logger/core';
|
|
2349
|
+
import { I{EntityName}, IResolvers } from 'common/server';
|
|
2350
|
+
|
|
2351
|
+
export const resolver = (pubsub: PubSub, logger?: CdmLogger.ILogger): IResolvers => ({
|
|
2352
|
+
// 1. Field Resolvers - Handle ObjectId → Object transformations
|
|
2353
|
+
{EntityName}: {
|
|
2354
|
+
{relationField}: (root, args, { {relation}DataLoader }) => {
|
|
2355
|
+
if (!root.{relationField}) return null;
|
|
2356
|
+
return {relation}DataLoader.load(String(root.{relationField}));
|
|
2357
|
+
},
|
|
2358
|
+
},
|
|
2359
|
+
|
|
2360
|
+
// 2. Query Resolvers - Always cast return types
|
|
2361
|
+
Query: {
|
|
2362
|
+
get{EntityName}: async (_, { id }, { {entity}Service, userContext }) => {
|
|
2363
|
+
if (!userContext?.tenantId) throw new Error('Authentication required');
|
|
2364
|
+
const result = await {entity}Service.get{EntityName}(id, userContext.tenantId);
|
|
2365
|
+
return result as unknown as I{EntityName};
|
|
2366
|
+
},
|
|
2367
|
+
},
|
|
2368
|
+
|
|
2369
|
+
// 3. Mutation Resolvers - Always cast return types
|
|
2370
|
+
Mutation: {
|
|
2371
|
+
create{EntityName}: async (_, { input }, { {entity}Service, userContext }) => {
|
|
2372
|
+
if (!userContext?.tenantId) throw new Error('Authentication required');
|
|
2373
|
+
const result = await {entity}Service.create{EntityName}({
|
|
2374
|
+
...input,
|
|
2375
|
+
tenantId: userContext.tenantId,
|
|
2376
|
+
createdBy: userContext.accountId,
|
|
2377
|
+
});
|
|
2378
|
+
return result as unknown as I{EntityName};
|
|
2379
|
+
},
|
|
2380
|
+
},
|
|
2381
|
+
});
|
|
2382
|
+
```
|
|
2383
|
+
|
|
2384
|
+
### **Index Resolver Combination:**
|
|
2385
|
+
|
|
2386
|
+
```typescript
|
|
2387
|
+
// File: index.ts
|
|
2388
|
+
import { resolver as entityResolver } from './{entity-name}-resolver';
|
|
2389
|
+
import { resolver as registryResolver } from './registry-extension-resolver';
|
|
2390
|
+
|
|
2391
|
+
export const resolvers = [entityResolver, registryResolver];
|
|
2392
|
+
```
|
|
2393
|
+
|
|
2394
|
+
### **Context Integration Pattern:**
|
|
2395
|
+
|
|
2396
|
+
The resolvers receive context with these properties:
|
|
2397
|
+
|
|
2398
|
+
- `userContext`: Authentication and tenant information
|
|
2399
|
+
- `{entity}Service`: Business logic services
|
|
2400
|
+
- `{entity}DataLoader`: Efficient object resolution
|
|
2401
|
+
- `pubsub`: GraphQL subscriptions (if needed)
|
|
2402
|
+
- `logger`: Logging functionality
|
|
2403
|
+
|
|
2404
|
+
### **Critical Rules:**
|
|
2405
|
+
|
|
2406
|
+
1. **🔑 Authentication**: Always check `userContext?.tenantId` in resolvers
|
|
2407
|
+
2. **🔄 Type Casting**: Always use `as unknown as I{EntityName}` for service returns
|
|
2408
|
+
3. **🚀 DataLoaders**: Use for all ObjectId → Object field resolutions
|
|
2409
|
+
4. **🛡️ Null Checks**: Check field existence before calling DataLoaders
|
|
2410
|
+
5. **📝 Logging**: Log important operations for debugging
|
|
2411
|
+
6. **🎯 Context**: Use proper destructuring for clean code
|
|
2412
|
+
|
|
2413
|
+
## 🎯 **CRITICAL: DataLoader Requirements for LLM Service Creation**
|
|
2414
|
+
|
|
2415
|
+
### **Mandatory Steps When Creating Any New Service:**
|
|
2416
|
+
|
|
2417
|
+
1. **Create DataLoader Template**:
|
|
2418
|
+
|
|
2419
|
+
```typescript
|
|
2420
|
+
// File: src/templates/services/{EntityName}DataLoader.ts.template
|
|
2421
|
+
import { I{EntityName}, IDataLoader } from 'common/server';
|
|
2422
|
+
export type I{EntityName}DataLoader = IDataLoader<I{EntityName}>;
|
|
2423
|
+
```
|
|
2424
|
+
|
|
2425
|
+
2. **Add to package.json cdecode**:
|
|
2426
|
+
|
|
2427
|
+
```json
|
|
2428
|
+
"services": [
|
|
2429
|
+
"./${libDir}/templates/services/{EntityName}Service.ts.template",
|
|
2430
|
+
"./${libDir}/templates/services/{EntityName}DataLoader.ts.template"
|
|
2431
|
+
]
|
|
2432
|
+
```
|
|
2433
|
+
|
|
2434
|
+
3. **Add SERVER_TYPES symbols**:
|
|
2435
|
+
|
|
2436
|
+
```typescript
|
|
2437
|
+
// In templates/constants/SERVER_TYPES.ts.template
|
|
2438
|
+
{EntityName}DataLoader: Symbol('{EntityName}DataLoader'),
|
|
2439
|
+
```
|
|
2440
|
+
|
|
2441
|
+
4. **Container Binding (Request Scope)**:
|
|
2442
|
+
|
|
2443
|
+
```typescript
|
|
2444
|
+
bind<{EntityName}DataLoader>(SERVER_TYPES.{EntityName}DataLoader)
|
|
2445
|
+
.to({EntityName}DataLoader)
|
|
2446
|
+
.inRequestScope(); // CRITICAL for caching
|
|
2447
|
+
```
|
|
2448
|
+
|
|
2449
|
+
5. **Include in createServiceFunc**:
|
|
2450
|
+
|
|
2451
|
+
```typescript
|
|
2452
|
+
const createServiceFunc = (container: interfaces.Container): Partial<ServerContext> => ({
|
|
2453
|
+
{entity}Service: container.get<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service),
|
|
2454
|
+
{entity}DataLoader: container.get<{EntityName}DataLoader>(SERVER_TYPES.{EntityName}DataLoader),
|
|
2455
|
+
});
|
|
2456
|
+
```
|
|
2457
|
+
|
|
2458
|
+
6. **Field Resolver Pattern**:
|
|
2459
|
+
```typescript
|
|
2460
|
+
{relationField}: (root, args, { {entity}DataLoader }) => {
|
|
2461
|
+
if (!root.{relationField} || root.{relationField} === null || root.{relationField} === undefined) {
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
const relationId = String(root.{relationField});
|
|
2465
|
+
return {entity}DataLoader.load(relationId);
|
|
2466
|
+
},
|
|
2467
|
+
```
|
|
2468
|
+
|
|
2469
|
+
### **Why This Matters:**
|
|
2470
|
+
|
|
2471
|
+
- **Performance**: Prevents N+1 query problems
|
|
2472
|
+
- **Type Safety**: Generated types ensure consistent context
|
|
2473
|
+
- **Maintainability**: Templates ensure all services follow same pattern
|
|
2474
|
+
- **Regeneration**: `yarn regenerateGraphql` updates all types automatically
|
|
2475
|
+
|
|
2476
|
+
**⚠️ CRITICAL**:
|
|
2477
|
+
|
|
2478
|
+
- Always run `yarn regenerateGraphql && yarn generateGraphql` from PROJECT ROOT (not module directory)
|
|
2479
|
+
- These commands must be run in sequence after creating DataLoader templates to generate proper types in common/server package
|
|
2480
|
+
- Running from module directory will fail - commands only work from project root
|
|
2481
|
+
|
|
2482
|
+
This comprehensive template covers all aspects of creating a backend service following the established patterns. Use this as a complete guide for LLM-assisted development of similar services.
|
|
2483
|
+
|
|
2484
|
+
## 🧪 Phase 19: Service Testing
|
|
2485
|
+
|
|
2486
|
+
**File**: `packages-modules/{module}/server/src/services/{entity-name}-service.test.ts`
|
|
2487
|
+
|
|
2488
|
+
```typescript
|
|
2489
|
+
import { MongoMemoryServer } from 'mongodb-memory-server';
|
|
2490
|
+
import mongoose from 'mongoose';
|
|
2491
|
+
import { CdmLogger } from '@cdm-logger/core';
|
|
2492
|
+
import {
|
|
2493
|
+
I{EntityName}Model,
|
|
2494
|
+
I{EntityName}Service,
|
|
2495
|
+
ICreate{EntityName}ServerInput,
|
|
2496
|
+
IUpdate{EntityName}ServerInput,
|
|
2497
|
+
I{EntityName}Filter,
|
|
2498
|
+
{EntityName}Status,
|
|
2499
|
+
} from 'common/server';
|
|
2500
|
+
import { {EntityName}Service } from './{entity-name}-service';
|
|
2501
|
+
|
|
2502
|
+
// Mock dependencies
|
|
2503
|
+
const mockBroker = {
|
|
2504
|
+
broadcast: jest.fn(),
|
|
2505
|
+
emit: jest.fn(),
|
|
2506
|
+
};
|
|
2507
|
+
|
|
2508
|
+
describe('{EntityName}Service', () => {
|
|
2509
|
+
let mongoServer: MongoMemoryServer;
|
|
2510
|
+
let connection: mongoose.Connection;
|
|
2511
|
+
let {entity}Service: {EntityName}Service;
|
|
2512
|
+
let mock{EntityName}Repository: any;
|
|
2513
|
+
let logger: CdmLogger.ILogger;
|
|
2514
|
+
|
|
2515
|
+
beforeAll(async () => {
|
|
2516
|
+
mongoServer = await MongoMemoryServer.create();
|
|
2517
|
+
const mongoUri = mongoServer.getUri();
|
|
2518
|
+
connection = await mongoose.createConnection(mongoUri);
|
|
2519
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
beforeEach(() => {
|
|
2523
|
+
// Initialize logger mock
|
|
2524
|
+
logger = {
|
|
2525
|
+
child: () => logger,
|
|
2526
|
+
info: jest.fn(),
|
|
2527
|
+
error: jest.fn(),
|
|
2528
|
+
warn: jest.fn(),
|
|
2529
|
+
debug: jest.fn(),
|
|
2530
|
+
} as any;
|
|
2531
|
+
|
|
2532
|
+
// Mock repository with all required methods
|
|
2533
|
+
mock{EntityName}Repository = {
|
|
2534
|
+
create: jest.fn(),
|
|
2535
|
+
update: jest.fn(),
|
|
2536
|
+
findById: jest.fn(),
|
|
2537
|
+
find: jest.fn(),
|
|
2538
|
+
delete: jest.fn(),
|
|
2539
|
+
exists: jest.fn(),
|
|
2540
|
+
updateStatus: jest.fn(),
|
|
2541
|
+
count: jest.fn(),
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
// Initialize service with mocked dependencies
|
|
2545
|
+
{entity}Service = new {EntityName}Service(
|
|
2546
|
+
mock{EntityName}Repository,
|
|
2547
|
+
mockBroker as any,
|
|
2548
|
+
logger,
|
|
2549
|
+
);
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
afterAll(async () => {
|
|
2553
|
+
await connection.close();
|
|
2554
|
+
await mongoServer.stop();
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
// Test helpers
|
|
2558
|
+
const createMock{EntityName} = (overrides: Partial<I{EntityName}Model> = {}): I{EntityName}Model => ({
|
|
2559
|
+
_id: new mongoose.Types.ObjectId(),
|
|
2560
|
+
tenantId: 'test-tenant',
|
|
2561
|
+
{fieldName}: 'test-value',
|
|
2562
|
+
status: {EntityName}Status.Active,
|
|
2563
|
+
createdAt: new Date(),
|
|
2564
|
+
updatedAt: new Date(),
|
|
2565
|
+
createdBy: new mongoose.Types.ObjectId(),
|
|
2566
|
+
...overrides,
|
|
2567
|
+
} as I{EntityName}Model);
|
|
2568
|
+
|
|
2569
|
+
const createInput = (overrides: Partial<ICreate{EntityName}ServerInput> = {}): ICreate{EntityName}ServerInput => ({
|
|
2570
|
+
{fieldName}: 'test-value',
|
|
2571
|
+
tenantId: 'test-tenant',
|
|
2572
|
+
createdBy: new mongoose.Types.ObjectId().toString(),
|
|
2573
|
+
...overrides,
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
describe('create{EntityName}', () => {
|
|
2577
|
+
it('should create a new {entity} successfully', async () => {
|
|
2578
|
+
// Arrange
|
|
2579
|
+
const input = createInput();
|
|
2580
|
+
const mock{EntityName} = createMock{EntityName}();
|
|
2581
|
+
|
|
2582
|
+
mock{EntityName}Repository.exists.mockResolvedValue(false);
|
|
2583
|
+
mock{EntityName}Repository.create.mockResolvedValue(mock{EntityName});
|
|
2584
|
+
|
|
2585
|
+
// Act
|
|
2586
|
+
const result = await {entity}Service.create{EntityName}(input);
|
|
2587
|
+
|
|
2588
|
+
// Assert
|
|
2589
|
+
expect(result).toBeDefined();
|
|
2590
|
+
expect(result.tenantId).toBe(input.tenantId);
|
|
2591
|
+
expect(mock{EntityName}Repository.create).toHaveBeenCalledWith(input);
|
|
2592
|
+
});
|
|
2593
|
+
|
|
2594
|
+
it('should throw error when {entity} already exists', async () => {
|
|
2595
|
+
// Arrange
|
|
2596
|
+
const input = createInput();
|
|
2597
|
+
mock{EntityName}Repository.exists.mockResolvedValue(true);
|
|
2598
|
+
|
|
2599
|
+
// Act & Assert
|
|
2600
|
+
await expect({entity}Service.create{EntityName}(input)).rejects.toThrow(
|
|
2601
|
+
'{EntityName} with {fieldName}'
|
|
2602
|
+
);
|
|
2603
|
+
});
|
|
2604
|
+
});
|
|
2605
|
+
|
|
2606
|
+
describe('update{EntityName}', () => {
|
|
2607
|
+
it('should update an existing {entity}', async () => {
|
|
2608
|
+
// Arrange
|
|
2609
|
+
const id = new mongoose.Types.ObjectId().toString();
|
|
2610
|
+
const tenantId = 'test-tenant';
|
|
2611
|
+
const current = createMock{EntityName}();
|
|
2612
|
+
const updated = { ...current, {fieldName}: 'updated-value' };
|
|
2613
|
+
const updateInput: IUpdate{EntityName}ServerInput = {
|
|
2614
|
+
{fieldName}: 'updated-value',
|
|
2615
|
+
updatedBy: new mongoose.Types.ObjectId().toString(),
|
|
2616
|
+
};
|
|
2617
|
+
|
|
2618
|
+
mock{EntityName}Repository.findById.mockResolvedValue(current);
|
|
2619
|
+
mock{EntityName}Repository.update.mockResolvedValue(updated);
|
|
2620
|
+
|
|
2621
|
+
// Act
|
|
2622
|
+
const result = await {entity}Service.update{EntityName}(id, updateInput);
|
|
2623
|
+
|
|
2624
|
+
// Assert
|
|
2625
|
+
expect(result).toBeDefined();
|
|
2626
|
+
expect(result.{fieldName}).toBe('updated-value');
|
|
2627
|
+
expect(mock{EntityName}Repository.update).toHaveBeenCalledWith(id, updateInput);
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
it('should throw error when {entity} not found', async () => {
|
|
2631
|
+
// Arrange
|
|
2632
|
+
const id = new mongoose.Types.ObjectId().toString();
|
|
2633
|
+
const updateInput: IUpdate{EntityName}ServerInput = {
|
|
2634
|
+
{fieldName}: 'updated-value',
|
|
2635
|
+
updatedBy: new mongoose.Types.ObjectId().toString(),
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
mock{EntityName}Repository.findById.mockResolvedValue(null);
|
|
2639
|
+
|
|
2640
|
+
// Act & Assert
|
|
2641
|
+
await expect({entity}Service.update{EntityName}(id, updateInput)).rejects.toThrow(
|
|
2642
|
+
'{EntityName} not found'
|
|
2643
|
+
);
|
|
2644
|
+
});
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
describe('get{EntityName}', () => {
|
|
2648
|
+
it('should return {entity} when found', async () => {
|
|
2649
|
+
// Arrange
|
|
2650
|
+
const id = new mongoose.Types.ObjectId().toString();
|
|
2651
|
+
const tenantId = 'test-tenant';
|
|
2652
|
+
const mock{EntityName} = createMock{EntityName}();
|
|
2653
|
+
|
|
2654
|
+
mock{EntityName}Repository.findById.mockResolvedValue(mock{EntityName});
|
|
2655
|
+
|
|
2656
|
+
// Act
|
|
2657
|
+
const result = await {entity}Service.get{EntityName}(id, tenantId);
|
|
2658
|
+
|
|
2659
|
+
// Assert
|
|
2660
|
+
expect(result).toBeDefined();
|
|
2661
|
+
expect(result!.tenantId).toBe(tenantId);
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
it('should return null when not found', async () => {
|
|
2665
|
+
// Arrange
|
|
2666
|
+
const id = new mongoose.Types.ObjectId().toString();
|
|
2667
|
+
const tenantId = 'test-tenant';
|
|
2668
|
+
|
|
2669
|
+
mock{EntityName}Repository.findById.mockResolvedValue(null);
|
|
2670
|
+
|
|
2671
|
+
// Act
|
|
2672
|
+
const result = await {entity}Service.get{EntityName}(id, tenantId);
|
|
2673
|
+
|
|
2674
|
+
// Assert
|
|
2675
|
+
expect(result).toBeNull();
|
|
2676
|
+
});
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2679
|
+
describe('list{EntityName}s', () => {
|
|
2680
|
+
it('should return all {entity} entities for tenant', async () => {
|
|
2681
|
+
// Arrange
|
|
2682
|
+
const mock{EntityName}s = [
|
|
2683
|
+
createMock{EntityName}({ {fieldName}: 'entity-1' }),
|
|
2684
|
+
createMock{EntityName}({ {fieldName}: 'entity-2' }),
|
|
2685
|
+
];
|
|
2686
|
+
|
|
2687
|
+
mock{EntityName}Repository.find.mockResolvedValue(mock{EntityName}s);
|
|
2688
|
+
|
|
2689
|
+
const filter: I{EntityName}Filter = { tenantId: 'test-tenant' };
|
|
2690
|
+
|
|
2691
|
+
// Act
|
|
2692
|
+
const result = await {entity}Service.list{EntityName}s(filter);
|
|
2693
|
+
|
|
2694
|
+
// Assert
|
|
2695
|
+
expect(result).toHaveLength(2);
|
|
2696
|
+
expect(result.every(e => e.tenantId === 'test-tenant')).toBe(true);
|
|
2697
|
+
});
|
|
2698
|
+
|
|
2699
|
+
it('should handle empty results', async () => {
|
|
2700
|
+
// Arrange
|
|
2701
|
+
mock{EntityName}Repository.find.mockResolvedValue([]);
|
|
2702
|
+
|
|
2703
|
+
// Act
|
|
2704
|
+
const result = await {entity}Service.list{EntityName}s({ tenantId: 'empty-tenant' });
|
|
2705
|
+
|
|
2706
|
+
// Assert
|
|
2707
|
+
expect(result).toEqual([]);
|
|
2708
|
+
});
|
|
2709
|
+
});
|
|
2710
|
+
|
|
2711
|
+
describe('delete{EntityName}', () => {
|
|
2712
|
+
it('should delete existing {entity}', async () => {
|
|
2713
|
+
// Arrange
|
|
2714
|
+
const id = new mongoose.Types.ObjectId().toString();
|
|
2715
|
+
const tenantId = 'test-tenant';
|
|
2716
|
+
const mock{EntityName} = createMock{EntityName}();
|
|
2717
|
+
|
|
2718
|
+
mock{EntityName}Repository.findById.mockResolvedValue(mock{EntityName});
|
|
2719
|
+
mock{EntityName}Repository.delete.mockResolvedValue(true);
|
|
2720
|
+
|
|
2721
|
+
// Act
|
|
2722
|
+
const result = await {entity}Service.delete{EntityName}(id, tenantId, 'user-id');
|
|
2723
|
+
|
|
2724
|
+
// Assert
|
|
2725
|
+
expect(result).toBe(true);
|
|
2726
|
+
expect(mock{EntityName}Repository.delete).toHaveBeenCalledWith(id, tenantId);
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
it('should throw error when {entity} not found', async () => {
|
|
2730
|
+
// Arrange
|
|
2731
|
+
const id = new mongoose.Types.ObjectId().toString();
|
|
2732
|
+
const tenantId = 'test-tenant';
|
|
2733
|
+
|
|
2734
|
+
mock{EntityName}Repository.findById.mockResolvedValue(null);
|
|
2735
|
+
|
|
2736
|
+
// Act & Assert
|
|
2737
|
+
await expect({entity}Service.delete{EntityName}(id, tenantId, 'user-id')).rejects.toThrow(
|
|
2738
|
+
'{EntityName} not found'
|
|
2739
|
+
);
|
|
2740
|
+
});
|
|
2741
|
+
});
|
|
2742
|
+
|
|
2743
|
+
describe('error handling', () => {
|
|
2744
|
+
it('should handle repository errors gracefully', async () => {
|
|
2745
|
+
// Arrange
|
|
2746
|
+
const input = createInput();
|
|
2747
|
+
mock{EntityName}Repository.exists.mockRejectedValue(new Error('Database error'));
|
|
2748
|
+
|
|
2749
|
+
// Act & Assert
|
|
2750
|
+
await expect({entity}Service.create{EntityName}(input)).rejects.toThrow('Database error');
|
|
2751
|
+
});
|
|
2752
|
+
});
|
|
2753
|
+
|
|
2754
|
+
describe('event system', () => {
|
|
2755
|
+
it('should emit events on {entity} operations', async () => {
|
|
2756
|
+
// Arrange
|
|
2757
|
+
const input = createInput();
|
|
2758
|
+
const mock{EntityName} = createMock{EntityName}();
|
|
2759
|
+
|
|
2760
|
+
mock{EntityName}Repository.exists.mockResolvedValue(false);
|
|
2761
|
+
mock{EntityName}Repository.create.mockResolvedValue(mock{EntityName});
|
|
2762
|
+
|
|
2763
|
+
// Act
|
|
2764
|
+
await {entity}Service.create{EntityName}(input);
|
|
2765
|
+
|
|
2766
|
+
// Assert - Events are emitted through service emitters
|
|
2767
|
+
expect(mockBroker).toBeDefined();
|
|
2768
|
+
});
|
|
2769
|
+
});
|
|
2770
|
+
});
|
|
2771
|
+
```
|
|
2772
|
+
|
|
2773
|
+
### Test Running Commands
|
|
2774
|
+
|
|
2775
|
+
```bash
|
|
2776
|
+
# Run specific service tests
|
|
2777
|
+
cd packages-modules/{module}/server
|
|
2778
|
+
yarn test --testPathPattern='{entity-name}-service.test.ts'
|
|
2779
|
+
|
|
2780
|
+
# Run all tests with coverage
|
|
2781
|
+
yarn test --coverage
|
|
2782
|
+
|
|
2783
|
+
# Run tests in watch mode during development
|
|
2784
|
+
yarn test --watch
|
|
2785
|
+
```
|
|
2786
|
+
|
|
2787
|
+
### Test Best Practices
|
|
2788
|
+
|
|
2789
|
+
1. **Use Real Implementations**: Use actual MongoDB in-memory server for integration testing
|
|
2790
|
+
2. **Mock Dependencies**: Mock external services and repositories for unit testing
|
|
2791
|
+
3. **Test Business Logic**: Focus on service business logic, not repository implementation
|
|
2792
|
+
4. **Event Testing**: Verify events are emitted correctly through service emitters
|
|
2793
|
+
5. **Error Scenarios**: Test all error conditions and edge cases
|
|
2794
|
+
6. **Data Validation**: Test input validation and business rule enforcement
|
|
2795
|
+
|
|
2796
|
+
This comprehensive template covers all aspects of creating a backend service following the established patterns. Use this as a complete guide for LLM-assisted development of similar services.
|
|
2797
|
+
{entityName}(id: ID!): {EntityName}
|
|
2798
|
+
{entityNamePlural}(filter: {EntityName}Filter): [{EntityName}!]!
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
# Mutations
|
|
2802
|
+
|
|
2803
|
+
extend type Mutation {
|
|
2804
|
+
create{EntityName}(input: {EntityName}Input!): {EntityName}!
|
|
2805
|
+
update{EntityName}(id: ID!, input: Update{EntityName}Input!): {EntityName}!
|
|
2806
|
+
delete{EntityName}(id: ID!): Boolean!
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
````
|
|
2810
|
+
|
|
2811
|
+
**Key Principles**:
|
|
2812
|
+
- Use `@entity` for database-mapped types
|
|
2813
|
+
- Use `@column` for database fields
|
|
2814
|
+
- Use `@column(overrideType: "ObjectId")` for object references
|
|
2815
|
+
- Use `@column(overrideType: "Date")` for timestamps
|
|
2816
|
+
- Use `@embedded` for nested objects
|
|
2817
|
+
- No `@entity` directive for pure input types or event types
|
|
2818
|
+
|
|
2819
|
+
### 2. Database Collection Names
|
|
2820
|
+
**Location**: `packages-modules/{module}/server/src/templates/constants/DB_COLL_NAMES.ts.template`
|
|
2821
|
+
|
|
2822
|
+
```typescript
|
|
2823
|
+
// Add to existing DB_COLL_NAMES constant
|
|
2824
|
+
export const DB_COLL_NAMES = {
|
|
2825
|
+
// ... existing collections
|
|
2826
|
+
{EntityName}: '{module}_{entity_name}',
|
|
2827
|
+
// Example: InstalledExtension: 'marketplace_installed_extensions'
|
|
2828
|
+
} as const;
|
|
2829
|
+
````
|
|
2830
|
+
|
|
2831
|
+
**Map in package.json cdecode**:
|
|
2832
|
+
|
|
2833
|
+
```json
|
|
2834
|
+
{
|
|
2835
|
+
"cdecode": {
|
|
2836
|
+
"templates": {
|
|
2837
|
+
"constants": {
|
|
2838
|
+
"src": "src/templates/constants",
|
|
2839
|
+
"dest": "../../packages/common/src/constants"
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
```
|
|
2845
|
+
|
|
2846
|
+
### 3. Mongoose Model
|
|
2847
|
+
|
|
2848
|
+
**Location**: `packages-modules/{module}/server/src/store/models/{entity}-model.ts`
|
|
2849
|
+
|
|
2850
|
+
```typescript
|
|
2851
|
+
import {
|
|
2852
|
+
DB_COLL_NAMES,
|
|
2853
|
+
I{EntityName}Model,
|
|
2854
|
+
// Import embedded type interfaces
|
|
2855
|
+
} from 'common/server';
|
|
2856
|
+
import { Connection, Schema } from 'mongoose';
|
|
2857
|
+
|
|
2858
|
+
// Embedded Schemas First
|
|
2859
|
+
const {EmbeddedType}Schema = new Schema<I{EmbeddedType}Model>({
|
|
2860
|
+
{field}: {
|
|
2861
|
+
type: String,
|
|
2862
|
+
required: true,
|
|
2863
|
+
},
|
|
2864
|
+
{optionalField}: {
|
|
2865
|
+
type: String,
|
|
2866
|
+
},
|
|
2867
|
+
});
|
|
2868
|
+
|
|
2869
|
+
// Main Schema
|
|
2870
|
+
const {EntityName}Schema = new Schema<I{EntityName}Model>(
|
|
2871
|
+
{
|
|
2872
|
+
tenantId: {
|
|
2873
|
+
type: String,
|
|
2874
|
+
required: true,
|
|
2875
|
+
index: true,
|
|
2876
|
+
},
|
|
2877
|
+
{relationField}: {
|
|
2878
|
+
type: Schema.Types.ObjectId,
|
|
2879
|
+
ref: DB_COLL_NAMES.{RelatedEntity},
|
|
2880
|
+
required: true,
|
|
2881
|
+
index: true,
|
|
2882
|
+
},
|
|
2883
|
+
{fieldName}: {
|
|
2884
|
+
type: String,
|
|
2885
|
+
required: true,
|
|
2886
|
+
},
|
|
2887
|
+
{embeddedField}: {
|
|
2888
|
+
type: {EmbeddedType}Schema,
|
|
2889
|
+
default: () => ({}),
|
|
2890
|
+
},
|
|
2891
|
+
createdBy: {
|
|
2892
|
+
type: Schema.Types.ObjectId,
|
|
2893
|
+
ref: DB_COLL_NAMES.Accounts,
|
|
2894
|
+
required: true,
|
|
2895
|
+
},
|
|
2896
|
+
},
|
|
2897
|
+
{
|
|
2898
|
+
timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
|
|
2899
|
+
collection: DB_COLL_NAMES.{EntityName},
|
|
2900
|
+
}
|
|
2901
|
+
);
|
|
2902
|
+
|
|
2903
|
+
// Indexes
|
|
2904
|
+
{EntityName}Schema.index({ tenantId: 1, {keyField}: 1 }, { unique: true });
|
|
2905
|
+
|
|
2906
|
+
// Virtuals (for GraphQL id field)
|
|
2907
|
+
{EntityName}Schema.virtual('id').get(function () {
|
|
2908
|
+
return this._id.toHexString();
|
|
2909
|
+
});
|
|
2910
|
+
|
|
2911
|
+
// Transform for JSON output
|
|
2912
|
+
{EntityName}Schema.set('toJSON', {
|
|
2913
|
+
virtuals: true,
|
|
2914
|
+
transform: (doc, ret) => {
|
|
2915
|
+
delete ret._id;
|
|
2916
|
+
delete ret.__v;
|
|
2917
|
+
return ret;
|
|
2918
|
+
},
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
export const {EntityName}ModelFunc = (connection: Connection) => {
|
|
2922
|
+
return connection.model<I{EntityName}Model>(DB_COLL_NAMES.{EntityName}, {EntityName}Schema);
|
|
2923
|
+
};
|
|
2924
|
+
```
|
|
2925
|
+
|
|
2926
|
+
### 4. Service Templates
|
|
2927
|
+
|
|
2928
|
+
**Location**: `packages-modules/{module}/server/src/templates/services/{EntityName}Service.ts.template`
|
|
2929
|
+
|
|
2930
|
+
```typescript
|
|
2931
|
+
import {
|
|
2932
|
+
I{EntityName}Model,
|
|
2933
|
+
I{EntityName}Input,
|
|
2934
|
+
IUpdate{EntityName}Input,
|
|
2935
|
+
I{EmbeddedType}Input,
|
|
2936
|
+
} from 'common/server';
|
|
2937
|
+
|
|
2938
|
+
/**
|
|
2939
|
+
* Input type for creating {entityName} with additional server-side fields
|
|
2940
|
+
*/
|
|
2941
|
+
export interface ICreate{EntityName}Input extends I{EntityName}Input {
|
|
2942
|
+
tenantId: string;
|
|
2943
|
+
createdBy: string; // User ObjectId as string
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
/**
|
|
2947
|
+
* Input type for updating {entityName}
|
|
2948
|
+
*/
|
|
2949
|
+
export interface IUpdate{EntityName}Input {
|
|
2950
|
+
{field}?: string;
|
|
2951
|
+
{embeddedField}?: I{EmbeddedType}Input;
|
|
2952
|
+
updatedBy?: string;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Filter input for querying {entityName}
|
|
2957
|
+
*/
|
|
2958
|
+
export interface I{EntityName}Filter {
|
|
2959
|
+
tenantId?: string;
|
|
2960
|
+
{keyField}?: string;
|
|
2961
|
+
createdBy?: string;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
/**
|
|
2965
|
+
* Service interface for managing {entityName}
|
|
2966
|
+
*/
|
|
2967
|
+
export interface I{EntityName}Service {
|
|
2968
|
+
/**
|
|
2969
|
+
* Create a new {entityName}
|
|
2970
|
+
*/
|
|
2971
|
+
create{EntityName}(input: ICreate{EntityName}Input): Promise<I{EntityName}Model>;
|
|
2972
|
+
|
|
2973
|
+
/**
|
|
2974
|
+
* Update an existing {entityName}
|
|
2975
|
+
*/
|
|
2976
|
+
update{EntityName}(
|
|
2977
|
+
tenantId: string,
|
|
2978
|
+
id: string,
|
|
2979
|
+
update: IUpdate{EntityName}Input,
|
|
2980
|
+
): Promise<I{EntityName}Model>;
|
|
2981
|
+
|
|
2982
|
+
/**
|
|
2983
|
+
* Get a specific {entityName}
|
|
2984
|
+
*/
|
|
2985
|
+
get{EntityName}(tenantId: string, id: string): Promise<I{EntityName}Model | null>;
|
|
2986
|
+
|
|
2987
|
+
/**
|
|
2988
|
+
* Get all {entityName} for a tenant with optional filtering
|
|
2989
|
+
*/
|
|
2990
|
+
get{EntityNamePlural}(filter: I{EntityName}Filter): Promise<I{EntityName}Model[]>;
|
|
2991
|
+
|
|
2992
|
+
/**
|
|
2993
|
+
* Delete a {entityName}
|
|
2994
|
+
*/
|
|
2995
|
+
delete{EntityName}(tenantId: string, id: string, deletedBy: string): Promise<boolean>;
|
|
2996
|
+
}
|
|
2997
|
+
```
|
|
2998
|
+
|
|
2999
|
+
### 5. Repository Templates
|
|
3000
|
+
|
|
3001
|
+
**Location**: `packages-modules/{module}/server/src/templates/repositories/{EntityName}Repository.ts.template`
|
|
3002
|
+
|
|
3003
|
+
```typescript
|
|
3004
|
+
import { ObjectId } from 'mongodb';
|
|
3005
|
+
import {
|
|
3006
|
+
I{EntityName}Model,
|
|
3007
|
+
} from 'common/server';
|
|
3008
|
+
import {
|
|
3009
|
+
ICreate{EntityName}Input,
|
|
3010
|
+
IUpdate{EntityName}Input,
|
|
3011
|
+
I{EntityName}Filter,
|
|
3012
|
+
} from '../services/{EntityName}Service';
|
|
3013
|
+
|
|
3014
|
+
/**
|
|
3015
|
+
* Repository interface for {entityName} data access layer
|
|
3016
|
+
*/
|
|
3017
|
+
export interface I{EntityName}Repository {
|
|
3018
|
+
/**
|
|
3019
|
+
* Create a new {entityName} record
|
|
3020
|
+
*/
|
|
3021
|
+
create(input: ICreate{EntityName}Input): Promise<I{EntityName}Model>;
|
|
3022
|
+
|
|
3023
|
+
/**
|
|
3024
|
+
* Update an existing {entityName}
|
|
3025
|
+
*/
|
|
3026
|
+
updateById(
|
|
3027
|
+
tenantId: string,
|
|
3028
|
+
id: string,
|
|
3029
|
+
update: IUpdate{EntityName}Input,
|
|
3030
|
+
): Promise<I{EntityName}Model | null>;
|
|
3031
|
+
|
|
3032
|
+
/**
|
|
3033
|
+
* Find a specific {entityName}
|
|
3034
|
+
*/
|
|
3035
|
+
findById(tenantId: string, id: string): Promise<I{EntityName}Model | null>;
|
|
3036
|
+
|
|
3037
|
+
/**
|
|
3038
|
+
* Find {entityName} by criteria
|
|
3039
|
+
*/
|
|
3040
|
+
findByCriteria(filter: I{EntityName}Filter): Promise<I{EntityName}Model[]>;
|
|
3041
|
+
|
|
3042
|
+
/**
|
|
3043
|
+
* Check if {entityName} exists
|
|
3044
|
+
*/
|
|
3045
|
+
exists(tenantId: string, {keyField}: string): Promise<boolean>;
|
|
3046
|
+
|
|
3047
|
+
/**
|
|
3048
|
+
* Delete a {entityName}
|
|
3049
|
+
*/
|
|
3050
|
+
deleteById(tenantId: string, id: string): Promise<boolean>;
|
|
3051
|
+
|
|
3052
|
+
/**
|
|
3053
|
+
* Count {entityName} by criteria
|
|
3054
|
+
*/
|
|
3055
|
+
countByCriteria(filter: I{EntityName}Filter): Promise<number>;
|
|
3056
|
+
}
|
|
3057
|
+
```
|
|
3058
|
+
|
|
3059
|
+
### 6. Event Types (if needed)
|
|
3060
|
+
|
|
3061
|
+
**Location**: `packages-modules/{module}/server/src/graphql/schemas/service.graphql`
|
|
3062
|
+
|
|
3063
|
+
```graphql
|
|
3064
|
+
# Add to existing service enum
|
|
3065
|
+
extend enum MoleculerServiceName {
|
|
3066
|
+
{EntityName}Service
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
enum {EntityName}ServiceAction {
|
|
3070
|
+
On{EntityName}Created
|
|
3071
|
+
On{EntityName}Updated
|
|
3072
|
+
On{EntityName}Deleted
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
# Event Types (no @entity directive for events)
|
|
3076
|
+
type {EntityName}CreatedEvent {
|
|
3077
|
+
{entityName}Id: String!
|
|
3078
|
+
tenantId: String!
|
|
3079
|
+
createdBy: String!
|
|
3080
|
+
createdAt: String!
|
|
3081
|
+
{relevantField}: String!
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
type {EntityName}UpdatedEvent {
|
|
3085
|
+
{entityName}Id: String!
|
|
3086
|
+
tenantId: String!
|
|
3087
|
+
updatedBy: String!
|
|
3088
|
+
updatedAt: String!
|
|
3089
|
+
changes: JSON!
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
type {EntityName}DeletedEvent {
|
|
3093
|
+
{entityName}Id: String!
|
|
3094
|
+
tenantId: String!
|
|
3095
|
+
deletedBy: String!
|
|
3096
|
+
deletedAt: String!
|
|
3097
|
+
}
|
|
3098
|
+
```
|
|
3099
|
+
|
|
3100
|
+
### 7. Template Configuration
|
|
3101
|
+
|
|
3102
|
+
**Location**: `packages-modules/{module}/server/package.json`
|
|
3103
|
+
|
|
3104
|
+
```json
|
|
3105
|
+
{
|
|
3106
|
+
"cdecode": {
|
|
3107
|
+
"templates": {
|
|
3108
|
+
"services": {
|
|
3109
|
+
"src": "src/templates/services",
|
|
3110
|
+
"dest": "../../packages/common/src/services"
|
|
3111
|
+
},
|
|
3112
|
+
"repositories": {
|
|
3113
|
+
"src": "src/templates/repositories",
|
|
3114
|
+
"dest": "../../packages/common/src/repositories"
|
|
3115
|
+
},
|
|
3116
|
+
"constants": {
|
|
3117
|
+
"src": "src/templates/constants",
|
|
3118
|
+
"dest": "../../packages/common/src/constants"
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
```
|
|
3124
|
+
|
|
3125
|
+
### 8. SERVER_TYPES Constants
|
|
3126
|
+
|
|
3127
|
+
**Location**: `packages/common/src/constants/SERVER_TYPES.ts` (add to existing)
|
|
3128
|
+
|
|
3129
|
+
```typescript
|
|
3130
|
+
export const SERVER_TYPES = {
|
|
3131
|
+
// ... existing types
|
|
3132
|
+
I{EntityName}Service: Symbol.for('I{EntityName}Service'),
|
|
3133
|
+
I{EntityName}Repository: Symbol.for('I{EntityName}Repository'),
|
|
3134
|
+
I{EntityName}MongoConnection: Symbol.for('I{EntityName}MongoConnection'),
|
|
3135
|
+
} as const;
|
|
3136
|
+
```
|
|
3137
|
+
|
|
3138
|
+
### 9. Service Implementation
|
|
3139
|
+
|
|
3140
|
+
**Location**: `packages-modules/{module}/server/src/services/{entity}-service.ts`
|
|
3141
|
+
|
|
3142
|
+
```typescript
|
|
3143
|
+
import { inject, injectable } from 'inversify';
|
|
3144
|
+
import { CdmLogger } from '@cdm-logger/core';
|
|
3145
|
+
import { ServiceBroker } from 'moleculer';
|
|
3146
|
+
import { CommonType } from '@common-stack/core';
|
|
3147
|
+
import { Disposable, DisposableCollection, Emitter } from '@adminide-stack/core';
|
|
3148
|
+
import {
|
|
3149
|
+
SERVER_TYPES,
|
|
3150
|
+
I{EntityName}Service,
|
|
3151
|
+
I{EntityName}Repository,
|
|
3152
|
+
I{EntityName}Model,
|
|
3153
|
+
ICreate{EntityName}Input,
|
|
3154
|
+
IUpdate{EntityName}Input,
|
|
3155
|
+
I{EntityName}Filter,
|
|
3156
|
+
// Event types if needed
|
|
3157
|
+
I{EntityName}CreatedEvent,
|
|
3158
|
+
I{EntityName}UpdatedEvent,
|
|
3159
|
+
I{EntityName}DeletedEvent,
|
|
3160
|
+
} from 'common/server';
|
|
3161
|
+
|
|
3162
|
+
@injectable()
|
|
3163
|
+
export class {EntityName}Service implements I{EntityName}Service, Disposable {
|
|
3164
|
+
// Event emitters (if needed)
|
|
3165
|
+
protected readonly on{EntityName}Created = new Emitter<I{EntityName}CreatedEvent>();
|
|
3166
|
+
protected readonly on{EntityName}Updated = new Emitter<I{EntityName}UpdatedEvent>();
|
|
3167
|
+
protected readonly on{EntityName}Deleted = new Emitter<I{EntityName}DeletedEvent>();
|
|
3168
|
+
|
|
3169
|
+
protected readonly toDispose = new DisposableCollection(
|
|
3170
|
+
this.on{EntityName}Created,
|
|
3171
|
+
this.on{EntityName}Updated,
|
|
3172
|
+
this.on{EntityName}Deleted,
|
|
3173
|
+
);
|
|
3174
|
+
|
|
3175
|
+
private logger: CdmLogger.ILogger;
|
|
3176
|
+
|
|
3177
|
+
constructor(
|
|
3178
|
+
@inject(SERVER_TYPES.I{EntityName}Repository)
|
|
3179
|
+
private {entityName}Repository: I{EntityName}Repository,
|
|
3180
|
+
|
|
3181
|
+
@inject(CommonType.MOLECULER_BROKER)
|
|
3182
|
+
protected broker: ServiceBroker,
|
|
3183
|
+
|
|
3184
|
+
@inject(CommonType.LOGGER)
|
|
3185
|
+
logger: CdmLogger.ILogger,
|
|
3186
|
+
) {
|
|
3187
|
+
this.logger = logger.child({ className: {EntityName}Service.name });
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
public dispose(): void {
|
|
3191
|
+
this.toDispose.dispose();
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
public async create{EntityName}(input: ICreate{EntityName}Input): Promise<AsDomainType<I{EntityName}Model>> {
|
|
3195
|
+
this.logger.info(`Creating {entityName} for tenant ${input.tenantId}`);
|
|
3196
|
+
|
|
3197
|
+
// Validation logic here
|
|
3198
|
+
|
|
3199
|
+
const {entityName} = await this.{entityName}Repository.create(input);
|
|
3200
|
+
|
|
3201
|
+
// Fire creation event
|
|
3202
|
+
const event: I{EntityName}CreatedEvent = {
|
|
3203
|
+
{entityName}Id: {entityName}.id,
|
|
3204
|
+
tenantId: input.tenantId,
|
|
3205
|
+
createdBy: input.createdBy,
|
|
3206
|
+
createdAt: new Date().toISOString(),
|
|
3207
|
+
{relevantField}: {entityName}.{relevantField},
|
|
3208
|
+
};
|
|
3209
|
+
|
|
3210
|
+
this.on{EntityName}Created.fire(event);
|
|
3211
|
+
|
|
3212
|
+
this.logger.info(`Successfully created {entityName} ${{entityName}.id}`);
|
|
3213
|
+
|
|
3214
|
+
// CRITICAL: Cast repository result to AsDomainType for GraphQL compatibility
|
|
3215
|
+
return {entityName} as unknown as AsDomainType<I{EntityName}Model>;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
public async update{EntityName}(
|
|
3219
|
+
tenantId: string,
|
|
3220
|
+
id: string,
|
|
3221
|
+
update: IUpdate{EntityName}Input,
|
|
3222
|
+
): Promise<AsDomainType<I{EntityName}Model>> {
|
|
3223
|
+
this.logger.info(`Updating {entityName} ${id} for tenant ${tenantId}`);
|
|
3224
|
+
|
|
3225
|
+
const updated{EntityName} = await this.{entityName}Repository.updateById(tenantId, id, update);
|
|
3226
|
+
|
|
3227
|
+
if (!updated{EntityName}) {
|
|
3228
|
+
throw new Error(`{EntityName} ${id} not found for tenant ${tenantId}`);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// Fire update event
|
|
3232
|
+
const event: I{EntityName}UpdatedEvent = {
|
|
3233
|
+
{entityName}Id: id,
|
|
3234
|
+
tenantId,
|
|
3235
|
+
updatedBy: update.updatedBy || 'system',
|
|
3236
|
+
updatedAt: new Date().toISOString(),
|
|
3237
|
+
changes: update,
|
|
3238
|
+
};
|
|
3239
|
+
|
|
3240
|
+
this.on{EntityName}Updated.fire(event);
|
|
3241
|
+
|
|
3242
|
+
// CRITICAL: Cast repository result to AsDomainType for GraphQL compatibility
|
|
3243
|
+
return updated{EntityName} as unknown as AsDomainType<I{EntityName}Model>;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
public async get{EntityName}(tenantId: string, id: string): Promise<I{EntityName}Model | null> {
|
|
3247
|
+
return this.{entityName}Repository.findById(tenantId, id);
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
public async get{EntityNamePlural}(filter: I{EntityName}Filter): Promise<I{EntityName}Model[]> {
|
|
3251
|
+
return this.{entityName}Repository.findByCriteria(filter);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
public async delete{EntityName}(tenantId: string, id: string, deletedBy: string): Promise<boolean> {
|
|
3255
|
+
this.logger.info(`Deleting {entityName} ${id} for tenant ${tenantId}`);
|
|
3256
|
+
|
|
3257
|
+
const {entityName} = await this.get{EntityName}(tenantId, id);
|
|
3258
|
+
if (!{entityName}) {
|
|
3259
|
+
throw new Error(`{EntityName} ${id} not found for tenant ${tenantId}`);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
const deleted = await this.{entityName}Repository.deleteById(tenantId, id);
|
|
3263
|
+
|
|
3264
|
+
if (deleted) {
|
|
3265
|
+
// Fire deletion event
|
|
3266
|
+
const event: I{EntityName}DeletedEvent = {
|
|
3267
|
+
{entityName}Id: id,
|
|
3268
|
+
tenantId,
|
|
3269
|
+
deletedBy,
|
|
3270
|
+
deletedAt: new Date().toISOString(),
|
|
3271
|
+
};
|
|
3272
|
+
|
|
3273
|
+
this.on{EntityName}Deleted.fire(event);
|
|
3274
|
+
this.logger.info(`Successfully deleted {entityName} ${id}`);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
return deleted;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
```
|
|
3281
|
+
|
|
3282
|
+
### 10. Container Module
|
|
3283
|
+
|
|
3284
|
+
**Location**: `packages-modules/{module}/server/src/containers/module.ts`
|
|
3285
|
+
|
|
3286
|
+
```typescript
|
|
3287
|
+
import { ContainerModule, interfaces } from 'inversify';
|
|
3288
|
+
import {
|
|
3289
|
+
SERVER_TYPES,
|
|
3290
|
+
I{EntityName}Service,
|
|
3291
|
+
I{EntityName}Repository
|
|
3292
|
+
} from 'common/server';
|
|
3293
|
+
import { {EntityName}Service } from '../services/{entity}-service';
|
|
3294
|
+
import { {EntityName}ServiceExt } from '../services/{entity}-service-ext';
|
|
3295
|
+
import { {EntityName}Repository } from '../store/repositories/{entity}-repository';
|
|
3296
|
+
|
|
3297
|
+
export const {module}Module: (settings: any) => interfaces.ContainerModule = (settings: any) =>
|
|
3298
|
+
new ContainerModule((bind: interfaces.Bind) => {
|
|
3299
|
+
bind(SERVER_TYPES.I{EntityName}MongoConnection).toConstantValue(settings.mongoConnection);
|
|
3300
|
+
|
|
3301
|
+
// {EntityName} Service
|
|
3302
|
+
bind<I{EntityName}Service>(SERVER_TYPES.I{EntityName}Service)
|
|
3303
|
+
.to({EntityName}ServiceExt)
|
|
3304
|
+
.inSingletonScope()
|
|
3305
|
+
.whenTargetIsDefault();
|
|
3306
|
+
|
|
3307
|
+
// {EntityName} Repository
|
|
3308
|
+
bind<I{EntityName}Repository>(SERVER_TYPES.I{EntityName}Repository)
|
|
3309
|
+
.to({EntityName}Repository as any)
|
|
3310
|
+
.inSingletonScope()
|
|
3311
|
+
.whenTargetIsDefault();
|
|
3312
|
+
});
|
|
3313
|
+
```
|
|
3314
|
+
|
|
3315
|
+
## 🔄 Build Process
|
|
3316
|
+
|
|
3317
|
+
### 1. Template Generation
|
|
3318
|
+
|
|
3319
|
+
```bash
|
|
3320
|
+
cd /path/to/workspace
|
|
3321
|
+
yarn regenerateGraphql
|
|
3322
|
+
```
|
|
3323
|
+
|
|
3324
|
+
### 2. GraphQL Code Generation
|
|
3325
|
+
|
|
3326
|
+
```bash
|
|
3327
|
+
yarn generateGraphql
|
|
3328
|
+
```
|
|
3329
|
+
|
|
3330
|
+
### 3. Verification Steps
|
|
3331
|
+
|
|
3332
|
+
1. Check generated types in `packages/common/src/generated/generated-models.ts`
|
|
3333
|
+
2. Verify interfaces in `packages/common/src/services/`
|
|
3334
|
+
3. Verify interfaces in `packages/common/src/repositories/`
|
|
3335
|
+
4. Check constants in `packages/common/src/constants/`
|
|
3336
|
+
|
|
3337
|
+
## 🎯 Key Patterns and Principles
|
|
3338
|
+
|
|
3339
|
+
### GraphQL Schema Design
|
|
3340
|
+
|
|
3341
|
+
- **Entity Types**: Use `@entity` for database-mapped types
|
|
3342
|
+
- **Object References**: Use `@column(overrideType: "ObjectId")` for database relations
|
|
3343
|
+
- **Date Fields**: Use `@column(overrideType: "Date")` for timestamps
|
|
3344
|
+
- **Embedded Types**: Use `@embedded` for nested objects, `@entity(embedded: true)` for the type definition
|
|
3345
|
+
- **Events**: Never use `@entity` directive for event types
|
|
3346
|
+
- **API vs Storage**: Use human-readable IDs in API (`extensionID: String!`) but store as ObjectId references (`extension: ObjectId`)
|
|
3347
|
+
|
|
3348
|
+
### Service Layer Architecture
|
|
3349
|
+
|
|
3350
|
+
- **Interface First**: Always define service interfaces in templates
|
|
3351
|
+
- **Repository Pattern**: Services depend on repository interfaces, not implementations
|
|
3352
|
+
- **Event Driven**: Services emit events for important operations
|
|
3353
|
+
- **Validation**: Services handle business logic and validation
|
|
3354
|
+
- **Error Handling**: Services throw meaningful errors with context
|
|
3355
|
+
|
|
3356
|
+
### Database Design
|
|
3357
|
+
|
|
3358
|
+
- **Collection Naming**: Use `{module}_{entity_name}` pattern
|
|
3359
|
+
- **Indexes**: Always index tenant + key fields
|
|
3360
|
+
- **References**: Use ObjectId references with proper ref
|
|
3361
|
+
- **Timestamps**: Use mongoose timestamps option
|
|
3362
|
+
- **Virtuals**: Add `id` virtual for GraphQL compatibility
|
|
3363
|
+
|
|
3364
|
+
### Template System
|
|
3365
|
+
|
|
3366
|
+
- **Separation of Concerns**: Services define business interfaces, repositories define data access
|
|
3367
|
+
- **Code Generation**: Use templates to generate interfaces in common package
|
|
3368
|
+
- **Type Safety**: Generated types ensure consistency between GraphQL and TypeScript
|
|
3369
|
+
- **Reusability**: Templates create reusable patterns across all modules
|
|
3370
|
+
|
|
3371
|
+
## 🚀 Usage Example
|
|
3372
|
+
|
|
3373
|
+
To create a new "ProjectTask" service in the "project-mgmt" module:
|
|
3374
|
+
|
|
3375
|
+
1. Replace `{EntityName}` with `ProjectTask`
|
|
3376
|
+
2. Replace `{entityName}` with `projectTask`
|
|
3377
|
+
3. Replace `{entityNamePlural}` with `projectTasks`
|
|
3378
|
+
4. Replace `{module}` with `project-mgmt`
|
|
3379
|
+
5. Replace `{entity}` with `project-task`
|
|
3380
|
+
6. Replace `{keyField}` with appropriate unique field (e.g., `taskId`)
|
|
3381
|
+
7. Replace `{RelatedEntity}` with related entities (e.g., `Project`)
|
|
3382
|
+
8. Follow all steps in the checklist
|
|
3383
|
+
|
|
3384
|
+
This template ensures consistency, type safety, and follows established patterns across the entire adminIde-stack architecture.
|