@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,2781 @@
|
|
|
1
|
+
# AdminIDE Stack Preferences System - Complete LLM Guide
|
|
2
|
+
|
|
3
|
+
**Purpose:** This guide teaches how to create preferences in AdminIDE Stack, covering the complete workflow from flattened configuration keys to nested GraphQL types and database registration.
|
|
4
|
+
|
|
5
|
+
**Last Updated:** January 19, 2025 | **Version:** 1.0.0
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Quick Start](#quick-start)
|
|
12
|
+
2. [Architecture Overview](#architecture-overview)
|
|
13
|
+
3. [Complete Implementation Guide](#complete-implementation-guide)
|
|
14
|
+
4. [Mapping Rules & Patterns](#mapping-rules--patterns)
|
|
15
|
+
5. [Real-World Examples](#real-world-examples)
|
|
16
|
+
6. [Frontend Consumption - Practical UI Usage](#frontend-consumption---practical-ui-usage)
|
|
17
|
+
7. [Testing & Validation](#testing--validation)
|
|
18
|
+
8. [Best Practices](#best-practices)
|
|
19
|
+
9. [Troubleshooting](#troubleshooting)
|
|
20
|
+
10. [Reference](#reference)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### TL;DR - The 4-Step Pattern
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
1. Create flattened keys → *-contribution.ts
|
|
30
|
+
2. Create migration → Add*ConfigurationsMigration.ts
|
|
31
|
+
3. Create nested GraphQL → preferences.graphql
|
|
32
|
+
4. Bind migration → module.ts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Minimal Working Example
|
|
36
|
+
|
|
37
|
+
**Step 1: Contribution (Flattened Keys)**
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// notifications-contribution.ts
|
|
41
|
+
import { IConfigurationSchemaMap } from '@adminide-stack/core';
|
|
42
|
+
import * as nls from '@vscode-alt/monaco-editor/esm/vs/nls';
|
|
43
|
+
import { ConfigurationScope } from 'common/server';
|
|
44
|
+
|
|
45
|
+
export const NotificationsContribution: IConfigurationSchemaMap = {
|
|
46
|
+
'app.notifications.email.enabled': {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
default: true,
|
|
49
|
+
description: nls.localize('appNotificationsEmailEnabled', 'Enable email notifications'),
|
|
50
|
+
scope: ConfigurationScope.APPLICATION,
|
|
51
|
+
},
|
|
52
|
+
'app.notifications.email.frequency': {
|
|
53
|
+
type: 'string',
|
|
54
|
+
enum: ['instant', 'daily', 'weekly'],
|
|
55
|
+
default: 'daily',
|
|
56
|
+
description: nls.localize('appNotificationsEmailFrequency', 'Email notification frequency'),
|
|
57
|
+
scope: ConfigurationScope.APPLICATION,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Step 2: Migration**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// AddNotificationsConfigurationsMigration.ts
|
|
66
|
+
import { IConfigurationRegistry } from '@adminide-stack/core';
|
|
67
|
+
import { inject, injectable } from 'inversify';
|
|
68
|
+
import type { Connection } from 'mongoose';
|
|
69
|
+
import { CdmLogger } from '@cdm-logger/core';
|
|
70
|
+
import { SERVER_TYPES, IRedisCacheManager } from 'common/server';
|
|
71
|
+
import { BaseConfigurationsMigration } from '@adminide-stack/platform-server';
|
|
72
|
+
import { NotificationsContribution } from '../preferences/settings';
|
|
73
|
+
|
|
74
|
+
@injectable()
|
|
75
|
+
export class AddNotificationsConfigurationsMigration extends BaseConfigurationsMigration {
|
|
76
|
+
name = 'notifications';
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
@inject('MongoDBConnection') db: Connection,
|
|
80
|
+
@inject(SERVER_TYPES.IConfigurationRegistry) configurationRegistry: IConfigurationRegistry,
|
|
81
|
+
@inject('Logger') logger: CdmLogger.ILogger,
|
|
82
|
+
@inject(SERVER_TYPES.IRedisCacheManager) redisCacheManager: IRedisCacheManager,
|
|
83
|
+
) {
|
|
84
|
+
super(db, configurationRegistry, redisCacheManager, logger);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get id() {
|
|
88
|
+
return `${AddNotificationsConfigurationsMigration.name}_20250119`;
|
|
89
|
+
}
|
|
90
|
+
get policies() {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
get roles() {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
get permissions() {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
get permissionsOverride() {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
get settings() {
|
|
103
|
+
return NotificationsContribution;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Step 3: GraphQL (Nested Structure)**
|
|
109
|
+
|
|
110
|
+
```graphql
|
|
111
|
+
# preferences.graphql
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
Email notification settings
|
|
115
|
+
Maps to: app.notifications.email.*
|
|
116
|
+
"""
|
|
117
|
+
type Preference_App_Notifications_Email {
|
|
118
|
+
"""Maps to: app.notifications.email.enabled"""
|
|
119
|
+
enabled: Boolean
|
|
120
|
+
|
|
121
|
+
"""Maps to: app.notifications.email.frequency"""
|
|
122
|
+
frequency: NotificationFrequency
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
enum NotificationFrequency {
|
|
126
|
+
INSTANT
|
|
127
|
+
DAILY
|
|
128
|
+
WEEKLY
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
Application notification preferences
|
|
133
|
+
Maps to: app.notifications.*
|
|
134
|
+
"""
|
|
135
|
+
type Preference_App_Notifications {
|
|
136
|
+
email: Preference_App_Notifications_Email
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
Application preferences
|
|
141
|
+
Maps to: app.*
|
|
142
|
+
"""
|
|
143
|
+
type Preference_App {
|
|
144
|
+
notifications: Preference_App_Notifications
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
Extend the global Preferences type
|
|
149
|
+
"""
|
|
150
|
+
extend type Preferences {
|
|
151
|
+
app: Preference_App
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Step 4: Bind in Container**
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// module.ts
|
|
159
|
+
import { ContainerModule, interfaces } from 'inversify';
|
|
160
|
+
import { AddNotificationsConfigurationsMigration } from '../migrations';
|
|
161
|
+
|
|
162
|
+
export const notificationsModule = () =>
|
|
163
|
+
new ContainerModule((bind: interfaces.Bind) => {
|
|
164
|
+
// ... other bindings ...
|
|
165
|
+
|
|
166
|
+
bind('MongodbMigration')
|
|
167
|
+
.to(AddNotificationsConfigurationsMigration)
|
|
168
|
+
.whenTargetNamed(AddNotificationsConfigurationsMigration.name);
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Architecture Overview
|
|
175
|
+
|
|
176
|
+
### System Flow
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
180
|
+
│ 1. CONTRIBUTION LAYER │
|
|
181
|
+
│ Flattened Keys: 'account.notification.primaryEmail' │
|
|
182
|
+
│ ───────────────────────────────────────────────────────── │
|
|
183
|
+
│ Files: preferences/settings/*-contribution.ts │
|
|
184
|
+
│ Format: IConfigurationSchemaMap with dot notation │
|
|
185
|
+
│ Purpose: Define configuration schema with types & defaults │
|
|
186
|
+
└──────────────────────────┬───────────────────────────────────┘
|
|
187
|
+
│
|
|
188
|
+
↓
|
|
189
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
190
|
+
│ 2. MIGRATION LAYER │
|
|
191
|
+
│ Registers configurations to MongoDB │
|
|
192
|
+
│ ───────────────────────────────────────────────────────── │
|
|
193
|
+
│ Files: migrations/dbMigrations/Add*Configurations*.ts │
|
|
194
|
+
│ Extends: BaseConfigurationsMigration │
|
|
195
|
+
│ Purpose: Load settings into configuration registry │
|
|
196
|
+
└──────────────────────────┬───────────────────────────────────┘
|
|
197
|
+
│
|
|
198
|
+
↓
|
|
199
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
200
|
+
│ 3. GRAPHQL LAYER │
|
|
201
|
+
│ Nested Types: Preference_Account.notification.primaryEmail │
|
|
202
|
+
│ ───────────────────────────────────────────────────────── │
|
|
203
|
+
│ Files: graphql/schema/preferences.graphql │
|
|
204
|
+
│ Format: Nested type definitions │
|
|
205
|
+
│ Purpose: Expose preferences via GraphQL API │
|
|
206
|
+
└──────────────────────────┬───────────────────────────────────┘
|
|
207
|
+
│
|
|
208
|
+
↓
|
|
209
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
210
|
+
│ 4. DEPENDENCY INJECTION │
|
|
211
|
+
│ Binds migration to container │
|
|
212
|
+
│ ───────────────────────────────────────────────────────── │
|
|
213
|
+
│ Files: containers/module.ts │
|
|
214
|
+
│ Framework: InversifyJS │
|
|
215
|
+
│ Purpose: Make migration discoverable & executable │
|
|
216
|
+
└─────────────────────────────────────────────────────────────┘
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Key Components
|
|
220
|
+
|
|
221
|
+
| Component | Purpose | Format |
|
|
222
|
+
| ---------------- | --------------------------- | ----------------------------------- |
|
|
223
|
+
| **Contribution** | Define configuration schema | Flattened keys with metadata |
|
|
224
|
+
| **Migration** | Register to database | Extends BaseConfigurationsMigration |
|
|
225
|
+
| **GraphQL** | API exposure | Nested type definitions |
|
|
226
|
+
| **DI Binding** | Make discoverable | InversifyJS binding |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Complete Implementation Guide
|
|
231
|
+
|
|
232
|
+
### Step 1: Create Configuration Contribution
|
|
233
|
+
|
|
234
|
+
**Location:** `packages-modules/{module}/server/src/preferences/settings/{feature}-contribution.ts`
|
|
235
|
+
|
|
236
|
+
**Purpose:** Define all configuration properties with flattened dot-notation keys.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { IConfigurationSchemaMap } from '@adminide-stack/core';
|
|
240
|
+
import * as nls from '@vscode-alt/monaco-editor/esm/vs/nls';
|
|
241
|
+
import { ConfigurationScope } from 'common/server';
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Account preferences contribution
|
|
245
|
+
* Uses flattened dot notation for all configuration keys
|
|
246
|
+
* Pattern: {module}.{section}.{subsection}.{property}
|
|
247
|
+
*/
|
|
248
|
+
export const AccountsContribution: IConfigurationSchemaMap = {
|
|
249
|
+
// ========================================
|
|
250
|
+
// Notification Settings
|
|
251
|
+
// ========================================
|
|
252
|
+
'account.notification.primaryEmail': {
|
|
253
|
+
type: 'string',
|
|
254
|
+
default: '',
|
|
255
|
+
description: nls.localize('accountNotificationPrimaryEmail', 'Notification Primary Email'),
|
|
256
|
+
scope: ConfigurationScope.APPLICATION,
|
|
257
|
+
},
|
|
258
|
+
'account.notification.onChangeAccountSettings': {
|
|
259
|
+
type: 'boolean',
|
|
260
|
+
default: true,
|
|
261
|
+
description: nls.localize(
|
|
262
|
+
'notifyOnChangeAccountSettings',
|
|
263
|
+
'Notifies you when you change your account settings',
|
|
264
|
+
),
|
|
265
|
+
scope: ConfigurationScope.APPLICATION,
|
|
266
|
+
},
|
|
267
|
+
'account.notification.billing': {
|
|
268
|
+
type: 'boolean',
|
|
269
|
+
default: true,
|
|
270
|
+
description: nls.localize('SubscribeToBillingNotifications', 'Subscribe to all billing changes'),
|
|
271
|
+
scope: ConfigurationScope.APPLICATION,
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// Nested professional information
|
|
275
|
+
'account.notification.professional.companyName': {
|
|
276
|
+
type: 'string',
|
|
277
|
+
default: '',
|
|
278
|
+
description: nls.localize('professionalCompanyName', 'Company Name'),
|
|
279
|
+
scope: ConfigurationScope.APPLICATION,
|
|
280
|
+
},
|
|
281
|
+
'account.notification.professional.companyEmail': {
|
|
282
|
+
type: 'string',
|
|
283
|
+
default: '',
|
|
284
|
+
description: nls.localize('professionalCompanyEmail', 'Company Email'),
|
|
285
|
+
scope: ConfigurationScope.APPLICATION,
|
|
286
|
+
},
|
|
287
|
+
'account.notification.professional.companyPhone': {
|
|
288
|
+
type: 'string',
|
|
289
|
+
default: '',
|
|
290
|
+
description: nls.localize('professionalCompanyPhone', 'Company Phone'),
|
|
291
|
+
scope: ConfigurationScope.APPLICATION,
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
// ========================================
|
|
295
|
+
// Default Settings
|
|
296
|
+
// ========================================
|
|
297
|
+
'account.default.organization': {
|
|
298
|
+
type: 'string',
|
|
299
|
+
default: '',
|
|
300
|
+
description: nls.localize('accountDefaultOrganization', 'Default organization for your account'),
|
|
301
|
+
scope: ConfigurationScope.APPLICATION,
|
|
302
|
+
},
|
|
303
|
+
'account.default.language': {
|
|
304
|
+
type: 'string',
|
|
305
|
+
default: 'en',
|
|
306
|
+
description: nls.localize('accountDefaultLanguage', 'Preferred language'),
|
|
307
|
+
scope: ConfigurationScope.APPLICATION,
|
|
308
|
+
},
|
|
309
|
+
'account.default.timezone': {
|
|
310
|
+
type: 'string',
|
|
311
|
+
default: 'UTC',
|
|
312
|
+
description: nls.localize('accountDefaultTimezone', 'Default timezone'),
|
|
313
|
+
scope: ConfigurationScope.APPLICATION,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Organization preferences contribution
|
|
319
|
+
*/
|
|
320
|
+
export const OrganizationContribution: IConfigurationSchemaMap = {
|
|
321
|
+
'organization.notification.notifyOrgManagersOnUserJoined': {
|
|
322
|
+
type: 'boolean',
|
|
323
|
+
default: false,
|
|
324
|
+
description: nls.localize('notifyOrgManagersOnUserJoined', 'Notify organization manager about new member'),
|
|
325
|
+
scope: ConfigurationScope.WINDOW,
|
|
326
|
+
},
|
|
327
|
+
'organization.notification.notifyOrgOwnerOnUserJoined': {
|
|
328
|
+
type: 'boolean',
|
|
329
|
+
default: false,
|
|
330
|
+
description: nls.localize('notifyOrgOwnerOnUserJoined', 'Notify owner about new member'),
|
|
331
|
+
scope: ConfigurationScope.WINDOW,
|
|
332
|
+
},
|
|
333
|
+
'organization.teams.visibility': {
|
|
334
|
+
type: 'string',
|
|
335
|
+
enum: ['private', 'public'],
|
|
336
|
+
default: 'private',
|
|
337
|
+
enumDescriptions: [
|
|
338
|
+
nls.localize(
|
|
339
|
+
'project.team.shareType.private',
|
|
340
|
+
'Only people you give access to will be able to view this team',
|
|
341
|
+
),
|
|
342
|
+
nls.localize('project.team.shareType.public', 'Anyone on the internet can view the team.'),
|
|
343
|
+
],
|
|
344
|
+
description: nls.localize('organizationTeamsVisibility', 'Teams Visibility'),
|
|
345
|
+
scope: ConfigurationScope.RESOURCE,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Configuration Properties:**
|
|
351
|
+
|
|
352
|
+
| Property | Type | Required | Description |
|
|
353
|
+
| ------------------ | ------------------ | -------- | ----------------------------------------------------------- |
|
|
354
|
+
| `type` | string | Yes | Data type: 'string', 'boolean', 'number', 'array', 'object' |
|
|
355
|
+
| `default` | any | Yes | Default value for the setting |
|
|
356
|
+
| `description` | string | Yes | Human-readable description (use nls.localize) |
|
|
357
|
+
| `scope` | ConfigurationScope | Yes | Where the setting applies |
|
|
358
|
+
| `enum` | array | No | Allowed values for the setting |
|
|
359
|
+
| `enumDescriptions` | array | No | Descriptions for each enum value |
|
|
360
|
+
|
|
361
|
+
**Configuration Scopes:**
|
|
362
|
+
|
|
363
|
+
| Scope | Use Case | Example |
|
|
364
|
+
| ---------------------- | ------------------------------- | ------------------------ |
|
|
365
|
+
| `APPLICATION` | User-specific settings | Email preferences, theme |
|
|
366
|
+
| `WINDOW` | Workspace/Organization settings | Org notifications |
|
|
367
|
+
| `RESOURCE` | Project/Team specific | Project visibility |
|
|
368
|
+
| `LANGUAGE_OVERRIDABLE` | Per-language settings | Code formatting |
|
|
369
|
+
| `MACHINE` | System-wide settings | Installation path |
|
|
370
|
+
| `MACHINE_OVERRIDABLE` | System with user override | Proxy settings |
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### Step 2: Export Contributions
|
|
375
|
+
|
|
376
|
+
**Location:** `packages-modules/{module}/server/src/preferences/settings/index.ts`
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { TeamsContribution } from './teams-contribution';
|
|
380
|
+
import { AccountsContribution } from './accounts-contribution';
|
|
381
|
+
import { OrganizationContribution } from './organization-contribution';
|
|
382
|
+
import { GlobalContribution } from './global-contribution';
|
|
383
|
+
import { UiLayoutContribution } from './ui-layout-contribution';
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Combine all setting contributions
|
|
387
|
+
* These will be merged and registered in the migration
|
|
388
|
+
*/
|
|
389
|
+
export const SettingsContributions = [
|
|
390
|
+
TeamsContribution,
|
|
391
|
+
AccountsContribution,
|
|
392
|
+
OrganizationContribution,
|
|
393
|
+
GlobalContribution,
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
// Export special contributions separately if they need different handling
|
|
397
|
+
export { UiLayoutContribution };
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Main Export:**
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// preferences/index.ts
|
|
404
|
+
export * from './settings';
|
|
405
|
+
export * from './roles';
|
|
406
|
+
export * from './policies';
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### Step 3: Create Migration Script
|
|
412
|
+
|
|
413
|
+
**Location:** `packages-modules/{module}/server/src/migrations/dbMigrations/Add{Module}ConfigurationsMigration.ts`
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import { IConfigurationRegistry } from '@adminide-stack/core';
|
|
417
|
+
import { inject, injectable } from 'inversify';
|
|
418
|
+
import type { Connection } from 'mongoose';
|
|
419
|
+
import { CdmLogger } from '@cdm-logger/core';
|
|
420
|
+
import { SERVER_TYPES, IRedisCacheManager, ContributionSchemaId, ContributionFragmentName } from 'common/server';
|
|
421
|
+
import { BaseConfigurationsMigration, ISchemaConfiguration, ISchemaOverride } from '@adminide-stack/platform-server';
|
|
422
|
+
import {
|
|
423
|
+
AccountPermissionsContribution,
|
|
424
|
+
AccountRolesContribution,
|
|
425
|
+
PoliciesContribution,
|
|
426
|
+
SettingsContributions,
|
|
427
|
+
AccountRolesPermissionOverwrite,
|
|
428
|
+
UiLayoutContribution,
|
|
429
|
+
} from '../../preferences';
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Migration to register all account-related configurations
|
|
433
|
+
*
|
|
434
|
+
* This migration:
|
|
435
|
+
* 1. Registers settings, permissions, roles, and policies
|
|
436
|
+
* 2. Loads them into the configuration registry (MongoDB)
|
|
437
|
+
* 3. Provides up/down methods for migration control
|
|
438
|
+
*
|
|
439
|
+
* @injectable Marks this class for dependency injection
|
|
440
|
+
*/
|
|
441
|
+
@injectable()
|
|
442
|
+
export class AddAccountsConfigurationsMigration extends BaseConfigurationsMigration {
|
|
443
|
+
/**
|
|
444
|
+
* Module name - used for namespacing configurations
|
|
445
|
+
* Must match the module identifier in the system
|
|
446
|
+
*/
|
|
447
|
+
name = 'account';
|
|
448
|
+
|
|
449
|
+
constructor(
|
|
450
|
+
@inject('MongoDBConnection') db: Connection,
|
|
451
|
+
@inject(SERVER_TYPES.IConfigurationRegistry) configurationRegistry: IConfigurationRegistry,
|
|
452
|
+
@inject('Logger') logger: CdmLogger.ILogger,
|
|
453
|
+
@inject(SERVER_TYPES.IRedisCacheManager) redisCacheManager: IRedisCacheManager,
|
|
454
|
+
) {
|
|
455
|
+
super(db, configurationRegistry, redisCacheManager, logger);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Unique ID for this migration
|
|
460
|
+
*
|
|
461
|
+
* Format: {ClassName}_{YYYYMMDD}
|
|
462
|
+
* IMPORTANT: Never reuse migration IDs, always update the date
|
|
463
|
+
*/
|
|
464
|
+
get id() {
|
|
465
|
+
return `${AddAccountsConfigurationsMigration.name}_20250719`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Policy configurations
|
|
470
|
+
* Define access policies for resources
|
|
471
|
+
*
|
|
472
|
+
* @returns Merged policy contributions
|
|
473
|
+
*/
|
|
474
|
+
get policies() {
|
|
475
|
+
return PoliciesContribution.reduce(
|
|
476
|
+
(acc, curr) => ({
|
|
477
|
+
...acc,
|
|
478
|
+
...curr,
|
|
479
|
+
}),
|
|
480
|
+
{},
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Role configurations
|
|
486
|
+
* Define available roles in the system
|
|
487
|
+
*
|
|
488
|
+
* Example structure:
|
|
489
|
+
* {
|
|
490
|
+
* 'account:admin': { description: 'Account Administrator', ... },
|
|
491
|
+
* 'account:user': { description: 'Regular User', ... }
|
|
492
|
+
* }
|
|
493
|
+
*/
|
|
494
|
+
get roles() {
|
|
495
|
+
return AccountRolesContribution;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Permission configurations
|
|
500
|
+
* Define granular permissions
|
|
501
|
+
*
|
|
502
|
+
* Example structure:
|
|
503
|
+
* {
|
|
504
|
+
* 'account.read': { description: 'Read account data', ... },
|
|
505
|
+
* 'account.write': { description: 'Modify account data', ... }
|
|
506
|
+
* }
|
|
507
|
+
*/
|
|
508
|
+
get permissions() {
|
|
509
|
+
return AccountPermissionsContribution;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Settings configurations
|
|
514
|
+
* Merge all setting contributions into a single object
|
|
515
|
+
*
|
|
516
|
+
* This combines:
|
|
517
|
+
* - AccountsContribution
|
|
518
|
+
* - OrganizationContribution
|
|
519
|
+
* - TeamsContribution
|
|
520
|
+
* - GlobalContribution
|
|
521
|
+
*/
|
|
522
|
+
get settings() {
|
|
523
|
+
return SettingsContributions.reduce(
|
|
524
|
+
(acc, curr) => ({
|
|
525
|
+
...acc,
|
|
526
|
+
...curr,
|
|
527
|
+
}),
|
|
528
|
+
{},
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Permission overrides
|
|
534
|
+
* Override default permissions for specific roles
|
|
535
|
+
*
|
|
536
|
+
* Example structure:
|
|
537
|
+
* {
|
|
538
|
+
* 'account:admin': {
|
|
539
|
+
* 'account.read': true,
|
|
540
|
+
* 'account.write': true,
|
|
541
|
+
* 'account.delete': true
|
|
542
|
+
* }
|
|
543
|
+
* }
|
|
544
|
+
*/
|
|
545
|
+
get permissionsOverride() {
|
|
546
|
+
return AccountRolesPermissionOverwrite;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Additional schema configurations
|
|
551
|
+
* Use this to add custom schemas beyond the standard ones
|
|
552
|
+
* (settings, policies, permissions, roles)
|
|
553
|
+
*
|
|
554
|
+
* @returns Array of additional schema configurations
|
|
555
|
+
*/
|
|
556
|
+
protected getAdditionalSchemaConfigurations(): ISchemaConfiguration[] {
|
|
557
|
+
return [
|
|
558
|
+
this.addSchemaConfiguration(
|
|
559
|
+
ContributionFragmentName.Settings,
|
|
560
|
+
ContributionSchemaId.UiLayout,
|
|
561
|
+
UiLayoutContribution,
|
|
562
|
+
{
|
|
563
|
+
priority: 50,
|
|
564
|
+
condition: () => true, // Always enable UI Layout
|
|
565
|
+
},
|
|
566
|
+
),
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Additional schema overrides
|
|
572
|
+
* Use this to override schemas with custom values
|
|
573
|
+
*
|
|
574
|
+
* @returns Array of schema overrides
|
|
575
|
+
*/
|
|
576
|
+
protected getAdditionalSchemaOverrides(): ISchemaOverride[] {
|
|
577
|
+
return [
|
|
578
|
+
this.addSchemaOverride(
|
|
579
|
+
ContributionSchemaId.UiLayout,
|
|
580
|
+
{}, // No overrides needed for UI Layout
|
|
581
|
+
{ priority: 50 },
|
|
582
|
+
),
|
|
583
|
+
];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Migration Lifecycle:**
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
// Migration is automatically discovered and executed
|
|
592
|
+
// Up: migration.up() - Registers configurations
|
|
593
|
+
// Down: migration.down() - Deregisters configurations and clears cache
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
### Step 4: Create GraphQL Schema
|
|
599
|
+
|
|
600
|
+
**Location:** `packages-modules/{module}/server/src/graphql/schema/preferences.graphql`
|
|
601
|
+
|
|
602
|
+
**Purpose:** Define nested GraphQL types that map to flattened configuration keys.
|
|
603
|
+
|
|
604
|
+
```graphql
|
|
605
|
+
# ============================================
|
|
606
|
+
# SYSTEM EXTENSIONS
|
|
607
|
+
# ============================================
|
|
608
|
+
|
|
609
|
+
"""
|
|
610
|
+
Extend the system contribution names
|
|
611
|
+
Add your module name here
|
|
612
|
+
"""
|
|
613
|
+
extend enum SystemContributionExtensionNames {
|
|
614
|
+
account
|
|
615
|
+
organization
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
# ============================================
|
|
619
|
+
# ACCOUNT PREFERENCES
|
|
620
|
+
# Flattened: account.*
|
|
621
|
+
# ============================================
|
|
622
|
+
|
|
623
|
+
"""
|
|
624
|
+
Professional account information
|
|
625
|
+
Maps to flattened keys: account.notification.professional.*
|
|
626
|
+
"""
|
|
627
|
+
type Preference_Account_Professional {
|
|
628
|
+
"""
|
|
629
|
+
Company name
|
|
630
|
+
Flattened key: account.notification.professional.companyName
|
|
631
|
+
"""
|
|
632
|
+
companyName: String
|
|
633
|
+
|
|
634
|
+
"""
|
|
635
|
+
Company email address
|
|
636
|
+
Flattened key: account.notification.professional.companyEmail
|
|
637
|
+
"""
|
|
638
|
+
companyEmail: String
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
Company phone number
|
|
642
|
+
Flattened key: account.notification.professional.companyPhone
|
|
643
|
+
"""
|
|
644
|
+
companyPhone: String
|
|
645
|
+
|
|
646
|
+
"""
|
|
647
|
+
Company website URL
|
|
648
|
+
Flattened key: account.notification.professional.companyWebsite
|
|
649
|
+
"""
|
|
650
|
+
companyWebsite: String
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
"""
|
|
654
|
+
Account notification preferences
|
|
655
|
+
Maps to flattened keys: account.notification.*
|
|
656
|
+
"""
|
|
657
|
+
type Preference_Account_Notification {
|
|
658
|
+
"""
|
|
659
|
+
Primary email for notifications
|
|
660
|
+
Flattened key: account.notification.primaryEmail
|
|
661
|
+
"""
|
|
662
|
+
primaryEmail: String
|
|
663
|
+
|
|
664
|
+
"""
|
|
665
|
+
Notify on account settings changes
|
|
666
|
+
Flattened key: account.notification.onChangeAccountSettings
|
|
667
|
+
"""
|
|
668
|
+
onChangeAccountSettings: Boolean
|
|
669
|
+
|
|
670
|
+
"""
|
|
671
|
+
Subscribe to billing notifications
|
|
672
|
+
Flattened key: account.notification.billing
|
|
673
|
+
"""
|
|
674
|
+
billing: Boolean
|
|
675
|
+
|
|
676
|
+
"""
|
|
677
|
+
Professional information (nested object)
|
|
678
|
+
Maps to: account.notification.professional.*
|
|
679
|
+
"""
|
|
680
|
+
professional: Preference_Account_Professional
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
"""
|
|
684
|
+
Account default settings
|
|
685
|
+
Maps to flattened keys: account.default.*
|
|
686
|
+
"""
|
|
687
|
+
type Preference_Account_Default {
|
|
688
|
+
"""
|
|
689
|
+
Preferred language
|
|
690
|
+
Flattened key: account.default.language
|
|
691
|
+
"""
|
|
692
|
+
language: String
|
|
693
|
+
|
|
694
|
+
"""
|
|
695
|
+
Default timezone
|
|
696
|
+
Flattened key: account.default.timezone
|
|
697
|
+
"""
|
|
698
|
+
timezone: String
|
|
699
|
+
|
|
700
|
+
"""
|
|
701
|
+
Default organization ID
|
|
702
|
+
Flattened key: account.default.organization
|
|
703
|
+
"""
|
|
704
|
+
organization: String
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
"""
|
|
708
|
+
Root account preferences
|
|
709
|
+
Maps to flattened keys: account.*
|
|
710
|
+
"""
|
|
711
|
+
type Preference_Account {
|
|
712
|
+
"""
|
|
713
|
+
Account roles (array)
|
|
714
|
+
Flattened key: account.roles
|
|
715
|
+
"""
|
|
716
|
+
roles: [String]
|
|
717
|
+
|
|
718
|
+
"""
|
|
719
|
+
Whether account is soft-deleted
|
|
720
|
+
Flattened key: account.isDeleted
|
|
721
|
+
"""
|
|
722
|
+
isDeleted: Boolean
|
|
723
|
+
|
|
724
|
+
"""
|
|
725
|
+
Whether account is verified
|
|
726
|
+
Flattened key: account.isVerified
|
|
727
|
+
"""
|
|
728
|
+
isVerified: Boolean
|
|
729
|
+
|
|
730
|
+
"""
|
|
731
|
+
Default settings
|
|
732
|
+
Maps to: account.default.*
|
|
733
|
+
"""
|
|
734
|
+
default: Preference_Account_Default
|
|
735
|
+
|
|
736
|
+
"""
|
|
737
|
+
Notification settings
|
|
738
|
+
Maps to: account.notification.*
|
|
739
|
+
"""
|
|
740
|
+
notification: Preference_Account_Notification
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
# ============================================
|
|
744
|
+
# ORGANIZATION PREFERENCES
|
|
745
|
+
# Flattened: organization.*
|
|
746
|
+
# ============================================
|
|
747
|
+
|
|
748
|
+
"""
|
|
749
|
+
Organization notification preferences
|
|
750
|
+
Maps to flattened keys: organization.notification.*
|
|
751
|
+
"""
|
|
752
|
+
type Pref_Org_Notification {
|
|
753
|
+
"""
|
|
754
|
+
Notify managers when new user joins
|
|
755
|
+
Flattened key: organization.notification.notifyOrgManagersOnUserJoined
|
|
756
|
+
"""
|
|
757
|
+
notifyOrgManagersOnUserJoined: Boolean
|
|
758
|
+
|
|
759
|
+
"""
|
|
760
|
+
Notify owner when new user joins
|
|
761
|
+
Flattened key: organization.notification.notifyOrgOwnerOnUserJoined
|
|
762
|
+
"""
|
|
763
|
+
notifyOrgOwnerOnUserJoined: Boolean
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
"""
|
|
767
|
+
Team preferences
|
|
768
|
+
Maps to flattened keys: organization.teams.*
|
|
769
|
+
"""
|
|
770
|
+
type Preference_Teams {
|
|
771
|
+
"""
|
|
772
|
+
Team visibility setting
|
|
773
|
+
Flattened key: organization.teams.visibility
|
|
774
|
+
"""
|
|
775
|
+
visibility: TeamVisibility
|
|
776
|
+
|
|
777
|
+
"""
|
|
778
|
+
How new members can join the team
|
|
779
|
+
"""
|
|
780
|
+
joinMethod: TeamJoinMethod
|
|
781
|
+
|
|
782
|
+
"""
|
|
783
|
+
Invite code for link-based joining
|
|
784
|
+
"""
|
|
785
|
+
inviteCode: String
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
"""
|
|
789
|
+
Organization-level preferences
|
|
790
|
+
Groups organization.* preferences
|
|
791
|
+
"""
|
|
792
|
+
type OrganizationPreferences {
|
|
793
|
+
"""
|
|
794
|
+
Notification settings
|
|
795
|
+
Maps to: organization.notification.*
|
|
796
|
+
"""
|
|
797
|
+
notification: Pref_Org_Notification
|
|
798
|
+
|
|
799
|
+
"""
|
|
800
|
+
Team settings
|
|
801
|
+
Maps to: organization.teams.*
|
|
802
|
+
"""
|
|
803
|
+
teams: Preference_Teams
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
# ============================================
|
|
807
|
+
# EXTEND BASE PREFERENCES TYPE
|
|
808
|
+
# ============================================
|
|
809
|
+
|
|
810
|
+
"""
|
|
811
|
+
Extend the global Preferences type
|
|
812
|
+
This is the root query type for all preferences
|
|
813
|
+
Each module extends this with its own preferences
|
|
814
|
+
"""
|
|
815
|
+
extend type Preferences {
|
|
816
|
+
"""
|
|
817
|
+
Account-specific preferences
|
|
818
|
+
Contains all account.* settings from AccountsContribution
|
|
819
|
+
"""
|
|
820
|
+
account: Preference_Account
|
|
821
|
+
|
|
822
|
+
"""
|
|
823
|
+
Organization-specific preferences
|
|
824
|
+
Contains all organization.* settings from OrganizationContribution
|
|
825
|
+
"""
|
|
826
|
+
organization: OrganizationPreferences
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
# ============================================
|
|
830
|
+
# INPUT TYPES (for mutations)
|
|
831
|
+
# ============================================
|
|
832
|
+
|
|
833
|
+
"""
|
|
834
|
+
Input type for updating professional information
|
|
835
|
+
"""
|
|
836
|
+
input Preference_Account_ProfessionalInput {
|
|
837
|
+
companyName: String
|
|
838
|
+
companyEmail: String
|
|
839
|
+
companyPhone: String
|
|
840
|
+
companyWebsite: String
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
"""
|
|
844
|
+
Input type for updating notification preferences
|
|
845
|
+
"""
|
|
846
|
+
input Preference_Account_NotificationInput {
|
|
847
|
+
primaryEmail: String
|
|
848
|
+
onChangeAccountSettings: Boolean
|
|
849
|
+
billing: Boolean
|
|
850
|
+
professional: Preference_Account_ProfessionalInput
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
"""
|
|
854
|
+
Input type for updating default settings
|
|
855
|
+
"""
|
|
856
|
+
input Preference_Account_DefaultInput {
|
|
857
|
+
language: String
|
|
858
|
+
timezone: String
|
|
859
|
+
organization: String
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
"""
|
|
863
|
+
Input type for updating account preferences
|
|
864
|
+
"""
|
|
865
|
+
input Preference_AccountInput {
|
|
866
|
+
default: Preference_Account_DefaultInput
|
|
867
|
+
notification: Preference_Account_NotificationInput
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
"""
|
|
871
|
+
Input type for updating organization notification preferences
|
|
872
|
+
"""
|
|
873
|
+
input Pref_Org_NotificationInput {
|
|
874
|
+
notifyOrgManagersOnUserJoined: Boolean
|
|
875
|
+
notifyOrgOwnerOnUserJoined: Boolean
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
"""
|
|
879
|
+
Input type for updating team preferences
|
|
880
|
+
"""
|
|
881
|
+
input Preference_TeamsInput {
|
|
882
|
+
visibility: TeamVisibility
|
|
883
|
+
joinMethod: TeamJoinMethod
|
|
884
|
+
inviteCode: String
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
"""
|
|
888
|
+
Input type for updating organization preferences
|
|
889
|
+
"""
|
|
890
|
+
input OrganizationPreferencesInput {
|
|
891
|
+
notification: Pref_Org_NotificationInput
|
|
892
|
+
teams: Preference_TeamsInput
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
# ============================================
|
|
896
|
+
# ENUMS
|
|
897
|
+
# ============================================
|
|
898
|
+
|
|
899
|
+
enum TeamVisibility {
|
|
900
|
+
PRIVATE
|
|
901
|
+
PUBLIC
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
enum TeamJoinMethod {
|
|
905
|
+
INVITE_ONLY
|
|
906
|
+
LINK
|
|
907
|
+
AUTO
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
### Step 5: Register in DI Container
|
|
914
|
+
|
|
915
|
+
**Location:** `packages-modules/{module}/server/src/containers/module.ts`
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
import { ContainerModule, interfaces } from 'inversify';
|
|
919
|
+
import {
|
|
920
|
+
AddAccountsRoleMigration,
|
|
921
|
+
AddStrategyPrefixToAliasesMigration,
|
|
922
|
+
MigrateOrganizationMembersToCollection,
|
|
923
|
+
AddOrganizationRolesMigration,
|
|
924
|
+
AddTeamRolesAndRenameMembersMigration,
|
|
925
|
+
AddAccountsConfigurationsMigration,
|
|
926
|
+
MigrateTeamsToNewSchema,
|
|
927
|
+
DropTeamsIndexMigration,
|
|
928
|
+
} from '../migrations';
|
|
929
|
+
|
|
930
|
+
export const accountsModule: (settings: any, pubsub) => interfaces.ContainerModule = (settings) =>
|
|
931
|
+
new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind) => {
|
|
932
|
+
// ... other bindings (repositories, services, data loaders) ...
|
|
933
|
+
|
|
934
|
+
// =============================================
|
|
935
|
+
// REGISTER MIGRATIONS
|
|
936
|
+
// =============================================
|
|
937
|
+
|
|
938
|
+
// Configuration Migration - MUST be registered
|
|
939
|
+
bind('MongodbMigration')
|
|
940
|
+
.to(AddAccountsConfigurationsMigration)
|
|
941
|
+
.whenTargetNamed(AddAccountsConfigurationsMigration.name);
|
|
942
|
+
|
|
943
|
+
// Other migrations
|
|
944
|
+
bind('MongodbMigration').to(AddAccountsRoleMigration).whenTargetNamed(AddAccountsRoleMigration.name);
|
|
945
|
+
|
|
946
|
+
bind('MongodbMigration').to(AddOrganizationRolesMigration).whenTargetNamed(AddOrganizationRolesMigration.name);
|
|
947
|
+
|
|
948
|
+
// ... more migrations ...
|
|
949
|
+
});
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
**Key Points:**
|
|
953
|
+
|
|
954
|
+
- Use `bind('MongodbMigration')` as the service identifier
|
|
955
|
+
- Use `.whenTargetNamed()` with migration class name
|
|
956
|
+
- Migrations are automatically discovered and executed by the system
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
## Mapping Rules & Patterns
|
|
961
|
+
|
|
962
|
+
### Flattened to Nested Conversion
|
|
963
|
+
|
|
964
|
+
| Flattened Key (TypeScript) | GraphQL Nested Path | GraphQL Type |
|
|
965
|
+
| ------------------------------- | ------------------------------------------ | --------------------------------- |
|
|
966
|
+
| `account.notification.email` | `Preference_Account.notification.email` | `Preference_Account_Notification` |
|
|
967
|
+
| `account.default.organization` | `Preference_Account.default.organization` | `Preference_Account_Default` |
|
|
968
|
+
| `organization.teams.visibility` | `OrganizationPreferences.teams.visibility` | `Preference_Teams` |
|
|
969
|
+
| `app.settings.ui.theme` | `Preference_App.settings.ui.theme` | `Preference_App_Settings_Ui` |
|
|
970
|
+
|
|
971
|
+
### Type Naming Convention
|
|
972
|
+
|
|
973
|
+
**Pattern:** `Preference_{Module}_{Section}_{Subsection}`
|
|
974
|
+
|
|
975
|
+
**Examples:**
|
|
976
|
+
|
|
977
|
+
```graphql
|
|
978
|
+
# Simple: account.notification.email
|
|
979
|
+
type Preference_Account_Notification {
|
|
980
|
+
email: String
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
# Nested: account.notification.professional.companyName
|
|
984
|
+
type Preference_Account_Notification_Professional {
|
|
985
|
+
companyName: String
|
|
986
|
+
}
|
|
987
|
+
type Preference_Account_Notification {
|
|
988
|
+
professional: Preference_Account_Notification_Professional
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
# Deep nesting: app.settings.editor.formatting.indentSize
|
|
992
|
+
type Preference_App_Settings_Editor_Formatting {
|
|
993
|
+
indentSize: Int
|
|
994
|
+
}
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
### Conversion Algorithm
|
|
998
|
+
|
|
999
|
+
```typescript
|
|
1000
|
+
/**
|
|
1001
|
+
* Convert flattened key to GraphQL path
|
|
1002
|
+
*
|
|
1003
|
+
* @param flatKey - Flattened key (e.g., 'account.notification.email')
|
|
1004
|
+
* @returns GraphQL path (e.g., 'Preference_Account.notification.email')
|
|
1005
|
+
*/
|
|
1006
|
+
function flattenedToGraphQL(flatKey: string): string {
|
|
1007
|
+
const parts = flatKey.split('.');
|
|
1008
|
+
const typeName = `Preference_${parts.map(capitalize).join('_')}`;
|
|
1009
|
+
const path = parts.join('.');
|
|
1010
|
+
return `${typeName} => ${path}`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Example:
|
|
1014
|
+
flattenedToGraphQL('account.notification.email');
|
|
1015
|
+
// Returns: "Preference_Account_Notification_Email => account.notification.email"
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## Real-World Examples
|
|
1021
|
+
|
|
1022
|
+
### Example 1: Billing Module
|
|
1023
|
+
|
|
1024
|
+
**Contribution:**
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
// billing-contribution.ts
|
|
1028
|
+
export const BillingContribution: IConfigurationSchemaMap = {
|
|
1029
|
+
'billing.payment.defaultMethod': {
|
|
1030
|
+
type: 'string',
|
|
1031
|
+
enum: ['credit_card', 'bank_transfer', 'paypal'],
|
|
1032
|
+
default: 'credit_card',
|
|
1033
|
+
description: nls.localize('billingDefaultPaymentMethod', 'Default payment method'),
|
|
1034
|
+
scope: ConfigurationScope.APPLICATION,
|
|
1035
|
+
},
|
|
1036
|
+
'billing.payment.autoPay': {
|
|
1037
|
+
type: 'boolean',
|
|
1038
|
+
default: false,
|
|
1039
|
+
description: nls.localize('billingAutoPay', 'Enable automatic payments'),
|
|
1040
|
+
scope: ConfigurationScope.APPLICATION,
|
|
1041
|
+
},
|
|
1042
|
+
'billing.notification.invoiceEmail': {
|
|
1043
|
+
type: 'string',
|
|
1044
|
+
default: '',
|
|
1045
|
+
description: nls.localize('billingInvoiceEmail', 'Email for invoice notifications'),
|
|
1046
|
+
scope: ConfigurationScope.APPLICATION,
|
|
1047
|
+
},
|
|
1048
|
+
'billing.notification.sendReminders': {
|
|
1049
|
+
type: 'boolean',
|
|
1050
|
+
default: true,
|
|
1051
|
+
description: nls.localize('billingSendReminders', 'Send payment reminders'),
|
|
1052
|
+
scope: ConfigurationScope.APPLICATION,
|
|
1053
|
+
},
|
|
1054
|
+
'billing.subscription.plan': {
|
|
1055
|
+
type: 'string',
|
|
1056
|
+
enum: ['free', 'pro', 'enterprise'],
|
|
1057
|
+
default: 'free',
|
|
1058
|
+
description: nls.localize('billingSubscriptionPlan', 'Subscription plan'),
|
|
1059
|
+
scope: ConfigurationScope.APPLICATION,
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
**Migration:**
|
|
1065
|
+
|
|
1066
|
+
```typescript
|
|
1067
|
+
// AddBillingConfigurationsMigration.ts
|
|
1068
|
+
@injectable()
|
|
1069
|
+
export class AddBillingConfigurationsMigration extends BaseConfigurationsMigration {
|
|
1070
|
+
name = 'billing';
|
|
1071
|
+
|
|
1072
|
+
constructor(
|
|
1073
|
+
@inject('MongoDBConnection') db: Connection,
|
|
1074
|
+
@inject(SERVER_TYPES.IConfigurationRegistry) configurationRegistry: IConfigurationRegistry,
|
|
1075
|
+
@inject('Logger') logger: CdmLogger.ILogger,
|
|
1076
|
+
@inject(SERVER_TYPES.IRedisCacheManager) redisCacheManager: IRedisCacheManager,
|
|
1077
|
+
) {
|
|
1078
|
+
super(db, configurationRegistry, redisCacheManager, logger);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
get id() {
|
|
1082
|
+
return `${AddBillingConfigurationsMigration.name}_20250119`;
|
|
1083
|
+
}
|
|
1084
|
+
get policies() {
|
|
1085
|
+
return {};
|
|
1086
|
+
}
|
|
1087
|
+
get roles() {
|
|
1088
|
+
return {};
|
|
1089
|
+
}
|
|
1090
|
+
get permissions() {
|
|
1091
|
+
return {};
|
|
1092
|
+
}
|
|
1093
|
+
get permissionsOverride() {
|
|
1094
|
+
return {};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
get settings() {
|
|
1098
|
+
return BillingContribution;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
**GraphQL:**
|
|
1104
|
+
|
|
1105
|
+
```graphql
|
|
1106
|
+
# billing.graphql
|
|
1107
|
+
|
|
1108
|
+
enum PaymentMethod {
|
|
1109
|
+
CREDIT_CARD
|
|
1110
|
+
BANK_TRANSFER
|
|
1111
|
+
PAYPAL
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
enum SubscriptionPlan {
|
|
1115
|
+
FREE
|
|
1116
|
+
PRO
|
|
1117
|
+
ENTERPRISE
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
type Preference_Billing_Payment {
|
|
1121
|
+
"""
|
|
1122
|
+
Maps to: billing.payment.defaultMethod
|
|
1123
|
+
"""
|
|
1124
|
+
defaultMethod: PaymentMethod
|
|
1125
|
+
|
|
1126
|
+
"""
|
|
1127
|
+
Maps to: billing.payment.autoPay
|
|
1128
|
+
"""
|
|
1129
|
+
autoPay: Boolean
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
type Preference_Billing_Notification {
|
|
1133
|
+
"""
|
|
1134
|
+
Maps to: billing.notification.invoiceEmail
|
|
1135
|
+
"""
|
|
1136
|
+
invoiceEmail: String
|
|
1137
|
+
|
|
1138
|
+
"""
|
|
1139
|
+
Maps to: billing.notification.sendReminders
|
|
1140
|
+
"""
|
|
1141
|
+
sendReminders: Boolean
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
type Preference_Billing_Subscription {
|
|
1145
|
+
"""
|
|
1146
|
+
Maps to: billing.subscription.plan
|
|
1147
|
+
"""
|
|
1148
|
+
plan: SubscriptionPlan
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
type Preference_Billing {
|
|
1152
|
+
payment: Preference_Billing_Payment
|
|
1153
|
+
notification: Preference_Billing_Notification
|
|
1154
|
+
subscription: Preference_Billing_Subscription
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
extend type Preferences {
|
|
1158
|
+
billing: Preference_Billing
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
**Query Example:**
|
|
1163
|
+
|
|
1164
|
+
```graphql
|
|
1165
|
+
query GetBillingPreferences {
|
|
1166
|
+
preferences {
|
|
1167
|
+
billing {
|
|
1168
|
+
payment {
|
|
1169
|
+
defaultMethod
|
|
1170
|
+
autoPay
|
|
1171
|
+
}
|
|
1172
|
+
notification {
|
|
1173
|
+
invoiceEmail
|
|
1174
|
+
sendReminders
|
|
1175
|
+
}
|
|
1176
|
+
subscription {
|
|
1177
|
+
plan
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
### Example 2: Project Management with Deep Nesting
|
|
1185
|
+
|
|
1186
|
+
**Contribution:**
|
|
1187
|
+
|
|
1188
|
+
```typescript
|
|
1189
|
+
// project-contribution.ts
|
|
1190
|
+
export const ProjectContribution: IConfigurationSchemaMap = {
|
|
1191
|
+
// Three levels deep
|
|
1192
|
+
'project.settings.access.defaultVisibility': {
|
|
1193
|
+
type: 'string',
|
|
1194
|
+
enum: ['private', 'public', 'team'],
|
|
1195
|
+
default: 'private',
|
|
1196
|
+
description: nls.localize('projectDefaultVisibility', 'Default project visibility'),
|
|
1197
|
+
scope: ConfigurationScope.RESOURCE,
|
|
1198
|
+
},
|
|
1199
|
+
'project.settings.access.allowExternalCollaborators': {
|
|
1200
|
+
type: 'boolean',
|
|
1201
|
+
default: false,
|
|
1202
|
+
description: nls.localize('projectAllowExternal', 'Allow external collaborators'),
|
|
1203
|
+
scope: ConfigurationScope.RESOURCE,
|
|
1204
|
+
},
|
|
1205
|
+
'project.settings.notifications.emailOnUpdate': {
|
|
1206
|
+
type: 'boolean',
|
|
1207
|
+
default: true,
|
|
1208
|
+
description: nls.localize('projectEmailOnUpdate', 'Email on project updates'),
|
|
1209
|
+
scope: ConfigurationScope.RESOURCE,
|
|
1210
|
+
},
|
|
1211
|
+
'project.settings.notifications.digestFrequency': {
|
|
1212
|
+
type: 'string',
|
|
1213
|
+
enum: ['instant', 'daily', 'weekly', 'never'],
|
|
1214
|
+
default: 'daily',
|
|
1215
|
+
description: nls.localize('projectDigestFrequency', 'Notification digest frequency'),
|
|
1216
|
+
scope: ConfigurationScope.RESOURCE,
|
|
1217
|
+
},
|
|
1218
|
+
'project.defaults.template': {
|
|
1219
|
+
type: 'string',
|
|
1220
|
+
default: '',
|
|
1221
|
+
description: nls.localize('projectDefaultTemplate', 'Default project template'),
|
|
1222
|
+
scope: ConfigurationScope.APPLICATION,
|
|
1223
|
+
},
|
|
1224
|
+
};
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
**GraphQL:**
|
|
1228
|
+
|
|
1229
|
+
```graphql
|
|
1230
|
+
# project.graphql
|
|
1231
|
+
|
|
1232
|
+
enum ProjectVisibility {
|
|
1233
|
+
PRIVATE
|
|
1234
|
+
PUBLIC
|
|
1235
|
+
TEAM
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
enum DigestFrequency {
|
|
1239
|
+
INSTANT
|
|
1240
|
+
DAILY
|
|
1241
|
+
WEEKLY
|
|
1242
|
+
NEVER
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
type Preference_Project_Settings_Access {
|
|
1246
|
+
"""
|
|
1247
|
+
Maps to: project.settings.access.defaultVisibility
|
|
1248
|
+
"""
|
|
1249
|
+
defaultVisibility: ProjectVisibility
|
|
1250
|
+
|
|
1251
|
+
"""
|
|
1252
|
+
Maps to: project.settings.access.allowExternalCollaborators
|
|
1253
|
+
"""
|
|
1254
|
+
allowExternalCollaborators: Boolean
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
type Preference_Project_Settings_Notifications {
|
|
1258
|
+
"""
|
|
1259
|
+
Maps to: project.settings.notifications.emailOnUpdate
|
|
1260
|
+
"""
|
|
1261
|
+
emailOnUpdate: Boolean
|
|
1262
|
+
|
|
1263
|
+
"""
|
|
1264
|
+
Maps to: project.settings.notifications.digestFrequency
|
|
1265
|
+
"""
|
|
1266
|
+
digestFrequency: DigestFrequency
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
type Preference_Project_Settings {
|
|
1270
|
+
access: Preference_Project_Settings_Access
|
|
1271
|
+
notifications: Preference_Project_Settings_Notifications
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
type Preference_Project_Defaults {
|
|
1275
|
+
"""
|
|
1276
|
+
Maps to: project.defaults.template
|
|
1277
|
+
"""
|
|
1278
|
+
template: String
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
type Preference_Project {
|
|
1282
|
+
settings: Preference_Project_Settings
|
|
1283
|
+
defaults: Preference_Project_Defaults
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
extend type Preferences {
|
|
1287
|
+
project: Preference_Project
|
|
1288
|
+
}
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
### Example 3: Multiple Contributions in One Module
|
|
1292
|
+
|
|
1293
|
+
**Index File:**
|
|
1294
|
+
|
|
1295
|
+
```typescript
|
|
1296
|
+
// preferences/settings/index.ts
|
|
1297
|
+
import { AccountsContribution } from './accounts-contribution';
|
|
1298
|
+
import { OrganizationContribution } from './organization-contribution';
|
|
1299
|
+
import { TeamsContribution } from './teams-contribution';
|
|
1300
|
+
import { ProfileContribution } from './profile-contribution';
|
|
1301
|
+
|
|
1302
|
+
export const SettingsContributions = [
|
|
1303
|
+
AccountsContribution,
|
|
1304
|
+
OrganizationContribution,
|
|
1305
|
+
TeamsContribution,
|
|
1306
|
+
ProfileContribution,
|
|
1307
|
+
];
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
**Migration:**
|
|
1311
|
+
|
|
1312
|
+
```typescript
|
|
1313
|
+
// Automatically merges all contributions
|
|
1314
|
+
get settings() {
|
|
1315
|
+
return SettingsContributions.reduce(
|
|
1316
|
+
(acc, curr) => ({
|
|
1317
|
+
...acc,
|
|
1318
|
+
...curr,
|
|
1319
|
+
}),
|
|
1320
|
+
{},
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
---
|
|
1326
|
+
|
|
1327
|
+
## Frontend Consumption - Practical UI Usage
|
|
1328
|
+
|
|
1329
|
+
### Overview
|
|
1330
|
+
|
|
1331
|
+
The preferences system provides two hooks for consuming configurations in React components:
|
|
1332
|
+
|
|
1333
|
+
1. **`useSettingsLoader`** - Uses data pre-loaded via Remix loader (faster, no GraphQL call)
|
|
1334
|
+
2. **`useSettings`** - Makes a GraphQL call to fetch settings (flexible, not pre-loaded)
|
|
1335
|
+
|
|
1336
|
+
### Hook 1: useSettingsLoader (Recommended for Performance)
|
|
1337
|
+
|
|
1338
|
+
**Purpose:** Access pre-loaded configurations that were fetched during Remix route loading.
|
|
1339
|
+
|
|
1340
|
+
**Requirements:**
|
|
1341
|
+
|
|
1342
|
+
- Must configure `configurations` in the route definition (compute.ts)
|
|
1343
|
+
- Data is available immediately without GraphQL calls
|
|
1344
|
+
- Best for frequently accessed preferences
|
|
1345
|
+
|
|
1346
|
+
#### Configuration in Route (Required)
|
|
1347
|
+
|
|
1348
|
+
**File:** `packages-modules/{module}/browser/src/modules/{feature}/compute.ts`
|
|
1349
|
+
|
|
1350
|
+
```typescript
|
|
1351
|
+
import { IMenuPosition, IRouteModule } from '@common-stack/core';
|
|
1352
|
+
import { PreDefineAccountPermissions } from '@adminide-stack/core/lib/modules/account-api/enums/index.js';
|
|
1353
|
+
import { ConfigCollectionName, ConfigFragmentName } from 'common';
|
|
1354
|
+
|
|
1355
|
+
export const accountPageStore: IRouteModule[] = [
|
|
1356
|
+
{
|
|
1357
|
+
name: 'menu.account_settings',
|
|
1358
|
+
key: 'account-settings',
|
|
1359
|
+
path: `${ORG_STD_ROUTES.USER_BASE_PATH}/account-settings`,
|
|
1360
|
+
tab: 'Settings',
|
|
1361
|
+
auth: true,
|
|
1362
|
+
authority: [PreDefineAccountPermissions.viewSettings],
|
|
1363
|
+
extraPermissions: [PreDefineAccountPermissions.editSettings],
|
|
1364
|
+
component: () => import('./components/Settings/AccountsSettings'),
|
|
1365
|
+
|
|
1366
|
+
// ============================================
|
|
1367
|
+
// CONFIGURE WHICH SETTINGS TO PRE-LOAD
|
|
1368
|
+
// ============================================
|
|
1369
|
+
|
|
1370
|
+
// Option 1: Load ALL configurations
|
|
1371
|
+
configurations: ['*'],
|
|
1372
|
+
|
|
1373
|
+
// Option 2: Load specific top-level keys only
|
|
1374
|
+
// configurations: ['account', 'organization', 'billing'],
|
|
1375
|
+
|
|
1376
|
+
extraParams: {
|
|
1377
|
+
resourceParams: {
|
|
1378
|
+
resourceType: ConfigCollectionName.Accounts,
|
|
1379
|
+
resourceId: '$params.orgName',
|
|
1380
|
+
organization: '$params.orgName',
|
|
1381
|
+
fragment: ConfigFragmentName.Settings,
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
];
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
**Configuration Options:**
|
|
1389
|
+
|
|
1390
|
+
| Value | Behavior | Use Case |
|
|
1391
|
+
| ----------------------------- | ---------------------------- | ------------------------------------ |
|
|
1392
|
+
| `['*']` | Load all configurations | Settings pages with many preferences |
|
|
1393
|
+
| `['account']` | Load only `account.*` keys | Page only uses account preferences |
|
|
1394
|
+
| `['account', 'organization']` | Load multiple top-level keys | Page uses multiple modules |
|
|
1395
|
+
| Not set or `[]` | No pre-loading | Use `useSettings` instead |
|
|
1396
|
+
|
|
1397
|
+
#### Usage in Components
|
|
1398
|
+
|
|
1399
|
+
**Complete Key - Get Final Value:**
|
|
1400
|
+
|
|
1401
|
+
```typescript
|
|
1402
|
+
import React from 'react';
|
|
1403
|
+
import { useSettingsLoader } from '@adminide-stack/platform-client';
|
|
1404
|
+
|
|
1405
|
+
export const NotificationSettings = () => {
|
|
1406
|
+
// Get a specific string value with full key path
|
|
1407
|
+
const { data: primaryEmail, loading, error, updateConfiguration } = useSettingsLoader({
|
|
1408
|
+
configKey: 'account.notification.primaryEmail',
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
// data type: string (inferred from flattened key)
|
|
1412
|
+
console.log(primaryEmail); // "user@example.com"
|
|
1413
|
+
|
|
1414
|
+
const handleUpdateEmail = async (newEmail: string) => {
|
|
1415
|
+
await updateConfiguration({
|
|
1416
|
+
value: { 'account.notification.primaryEmail': newEmail },
|
|
1417
|
+
target: 'user',
|
|
1418
|
+
});
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
if (loading) return <div>Loading...</div>;
|
|
1422
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1423
|
+
|
|
1424
|
+
return (
|
|
1425
|
+
<div>
|
|
1426
|
+
<h3>Primary Email</h3>
|
|
1427
|
+
<input
|
|
1428
|
+
type="email"
|
|
1429
|
+
value={primaryEmail || ''}
|
|
1430
|
+
onChange={(e) => handleUpdateEmail(e.target.value)}
|
|
1431
|
+
/>
|
|
1432
|
+
</div>
|
|
1433
|
+
);
|
|
1434
|
+
};
|
|
1435
|
+
```
|
|
1436
|
+
|
|
1437
|
+
**Higher-Level Key - Get Object:**
|
|
1438
|
+
|
|
1439
|
+
```typescript
|
|
1440
|
+
import React from 'react';
|
|
1441
|
+
import { useSettingsLoader } from '@adminide-stack/platform-client';
|
|
1442
|
+
|
|
1443
|
+
export const InvitationPage = ({ decoded }) => {
|
|
1444
|
+
// Get entire notification object with partial key
|
|
1445
|
+
const { data: notification, loading, error } = useSettingsLoader({
|
|
1446
|
+
configKey: 'organization.notification',
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// data type: IOrganizationNotificationValues (inferred)
|
|
1450
|
+
// notification = {
|
|
1451
|
+
// notifyOrgManagersOnUserJoined: true,
|
|
1452
|
+
// notifyOrgOwnerOnUserJoined: false
|
|
1453
|
+
// }
|
|
1454
|
+
|
|
1455
|
+
const acceptInvitation = () => {
|
|
1456
|
+
accept({
|
|
1457
|
+
variables: {
|
|
1458
|
+
id: decoded.invitationId,
|
|
1459
|
+
notification, // Pass entire object to mutation
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
if (loading) return <div>Loading...</div>;
|
|
1465
|
+
|
|
1466
|
+
return (
|
|
1467
|
+
<div>
|
|
1468
|
+
<h3>Invitation for {decoded.orgName}</h3>
|
|
1469
|
+
<p>Manager notifications: {notification?.notifyOrgManagersOnUserJoined ? 'Yes' : 'No'}</p>
|
|
1470
|
+
<p>Owner notifications: {notification?.notifyOrgOwnerOnUserJoined ? 'Yes' : 'No'}</p>
|
|
1471
|
+
<button onClick={acceptInvitation}>Accept</button>
|
|
1472
|
+
</div>
|
|
1473
|
+
);
|
|
1474
|
+
};
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
**Complete Example with All Features:**
|
|
1478
|
+
|
|
1479
|
+
```typescript
|
|
1480
|
+
import React, { useState } from 'react';
|
|
1481
|
+
import { useSettingsLoader } from '@adminide-stack/platform-client';
|
|
1482
|
+
|
|
1483
|
+
export const AccountSettingsPage = () => {
|
|
1484
|
+
// Get entire account preferences object
|
|
1485
|
+
const {
|
|
1486
|
+
data: accountPrefs,
|
|
1487
|
+
loading,
|
|
1488
|
+
error,
|
|
1489
|
+
updateConfiguration,
|
|
1490
|
+
preferencesInput
|
|
1491
|
+
} = useSettingsLoader({
|
|
1492
|
+
configKey: 'account',
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
// accountPrefs type: Preference_Account
|
|
1496
|
+
// accountPrefs = {
|
|
1497
|
+
// notification: {
|
|
1498
|
+
// primaryEmail: "user@example.com",
|
|
1499
|
+
// billing: true,
|
|
1500
|
+
// onChangeAccountSettings: true,
|
|
1501
|
+
// professional: {
|
|
1502
|
+
// companyName: "Acme Corp",
|
|
1503
|
+
// companyEmail: "info@acme.com"
|
|
1504
|
+
// }
|
|
1505
|
+
// },
|
|
1506
|
+
// default: {
|
|
1507
|
+
// organization: "my-org",
|
|
1508
|
+
// language: "en",
|
|
1509
|
+
// timezone: "UTC"
|
|
1510
|
+
// }
|
|
1511
|
+
// }
|
|
1512
|
+
|
|
1513
|
+
const [email, setEmail] = useState(accountPrefs?.notification?.primaryEmail || '');
|
|
1514
|
+
|
|
1515
|
+
const handleSave = async () => {
|
|
1516
|
+
try {
|
|
1517
|
+
await updateConfiguration({
|
|
1518
|
+
value: {
|
|
1519
|
+
'account.notification.primaryEmail': email,
|
|
1520
|
+
'account.notification.billing': true,
|
|
1521
|
+
},
|
|
1522
|
+
target: 'user',
|
|
1523
|
+
});
|
|
1524
|
+
console.log('Settings updated successfully');
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
console.error('Failed to update settings:', err);
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
if (loading) return <div>Loading preferences...</div>;
|
|
1531
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1532
|
+
|
|
1533
|
+
return (
|
|
1534
|
+
<div>
|
|
1535
|
+
<h2>Account Settings</h2>
|
|
1536
|
+
|
|
1537
|
+
{/* Access nested values */}
|
|
1538
|
+
<section>
|
|
1539
|
+
<h3>Notification Settings</h3>
|
|
1540
|
+
<div>
|
|
1541
|
+
<label>Primary Email:</label>
|
|
1542
|
+
<input
|
|
1543
|
+
type="email"
|
|
1544
|
+
value={email}
|
|
1545
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
1546
|
+
/>
|
|
1547
|
+
</div>
|
|
1548
|
+
<div>
|
|
1549
|
+
<label>
|
|
1550
|
+
<input
|
|
1551
|
+
type="checkbox"
|
|
1552
|
+
checked={accountPrefs?.notification?.billing}
|
|
1553
|
+
/>
|
|
1554
|
+
Billing Notifications
|
|
1555
|
+
</label>
|
|
1556
|
+
</div>
|
|
1557
|
+
</section>
|
|
1558
|
+
|
|
1559
|
+
{/* Access professional info */}
|
|
1560
|
+
<section>
|
|
1561
|
+
<h3>Professional Information</h3>
|
|
1562
|
+
<p>Company: {accountPrefs?.notification?.professional?.companyName || 'Not set'}</p>
|
|
1563
|
+
<p>Email: {accountPrefs?.notification?.professional?.companyEmail || 'Not set'}</p>
|
|
1564
|
+
</section>
|
|
1565
|
+
|
|
1566
|
+
{/* Access default settings */}
|
|
1567
|
+
<section>
|
|
1568
|
+
<h3>Default Settings</h3>
|
|
1569
|
+
<p>Organization: {accountPrefs?.default?.organization}</p>
|
|
1570
|
+
<p>Language: {accountPrefs?.default?.language}</p>
|
|
1571
|
+
<p>Timezone: {accountPrefs?.default?.timezone}</p>
|
|
1572
|
+
</section>
|
|
1573
|
+
|
|
1574
|
+
<button onClick={handleSave}>Save Changes</button>
|
|
1575
|
+
</div>
|
|
1576
|
+
);
|
|
1577
|
+
};
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
**Type Safety & Autocomplete:**
|
|
1581
|
+
|
|
1582
|
+
```typescript
|
|
1583
|
+
import { useSettingsLoader } from '@adminide-stack/platform-client';
|
|
1584
|
+
import type { IConfigurationsFlattenedKeys, GetFlattenedValue } from 'common';
|
|
1585
|
+
|
|
1586
|
+
export const TypeSafeComponent = () => {
|
|
1587
|
+
// TypeScript provides autocomplete for all flattened keys
|
|
1588
|
+
const { data } = useSettingsLoader({
|
|
1589
|
+
configKey: 'account.notification.primaryEmail', // Autocomplete available!
|
|
1590
|
+
// ^-- Type: IConfigurationsFlattenedKeys
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
// Return type is automatically inferred
|
|
1594
|
+
// data type: string (from 'account.notification.primaryEmail' configuration)
|
|
1595
|
+
|
|
1596
|
+
// Get partial object with type inference
|
|
1597
|
+
const { data: notification } = useSettingsLoader({
|
|
1598
|
+
configKey: 'organization.notification',
|
|
1599
|
+
// ^-- Autocomplete shows: organization.notification.notifyOrgManagersOnUserJoined
|
|
1600
|
+
// organization.notification.notifyOrgOwnerOnUserJoined
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
// notification type: { notifyOrgManagersOnUserJoined?: boolean, notifyOrgOwnerOnUserJoined?: boolean }
|
|
1604
|
+
};
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
### Hook 2: useSettings (For Non-Pre-loaded Settings)
|
|
1608
|
+
|
|
1609
|
+
**Purpose:** Fetch settings via GraphQL when they're not pre-loaded in the route.
|
|
1610
|
+
|
|
1611
|
+
**Use Cases:**
|
|
1612
|
+
|
|
1613
|
+
- Dynamic components not tied to specific routes
|
|
1614
|
+
- Modal dialogs or popups
|
|
1615
|
+
- Components that need settings on-demand
|
|
1616
|
+
- When you don't want to pre-load all settings
|
|
1617
|
+
|
|
1618
|
+
#### Usage
|
|
1619
|
+
|
|
1620
|
+
```typescript
|
|
1621
|
+
import React from 'react';
|
|
1622
|
+
import { useSettings } from '@adminide-stack/platform-client';
|
|
1623
|
+
import { URI } from '@adminide-stack/core';
|
|
1624
|
+
|
|
1625
|
+
export const DynamicSettingsComponent = ({ resourceUri }: { resourceUri: string }) => {
|
|
1626
|
+
const {
|
|
1627
|
+
data, // The specific value from configKey
|
|
1628
|
+
settings, // All settings object
|
|
1629
|
+
pageSettings, // Complete page settings with metadata
|
|
1630
|
+
loading,
|
|
1631
|
+
error,
|
|
1632
|
+
updateConfiguration
|
|
1633
|
+
} = useSettings({
|
|
1634
|
+
resourceUri: resourceUri as URI,
|
|
1635
|
+
options: {
|
|
1636
|
+
configKey: 'organization.notification.notifyOrgOwnerOnUserJoined',
|
|
1637
|
+
},
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// data type: boolean (the value of the specific key)
|
|
1641
|
+
console.log(data); // true or false
|
|
1642
|
+
|
|
1643
|
+
// settings: complete settings object
|
|
1644
|
+
console.log(settings); // { account: {...}, organization: {...}, ... }
|
|
1645
|
+
|
|
1646
|
+
const handleToggle = async () => {
|
|
1647
|
+
await updateConfiguration({
|
|
1648
|
+
value: {
|
|
1649
|
+
'organization.notification.notifyOrgOwnerOnUserJoined': !data
|
|
1650
|
+
},
|
|
1651
|
+
target: 'organization',
|
|
1652
|
+
});
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
if (loading) return <div>Loading...</div>;
|
|
1656
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1657
|
+
|
|
1658
|
+
return (
|
|
1659
|
+
<div>
|
|
1660
|
+
<label>
|
|
1661
|
+
<input
|
|
1662
|
+
type="checkbox"
|
|
1663
|
+
checked={data || false}
|
|
1664
|
+
onChange={handleToggle}
|
|
1665
|
+
/>
|
|
1666
|
+
Notify Owner on User Joined
|
|
1667
|
+
</label>
|
|
1668
|
+
</div>
|
|
1669
|
+
);
|
|
1670
|
+
};
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
**Get Complete Settings Object:**
|
|
1674
|
+
|
|
1675
|
+
```typescript
|
|
1676
|
+
export const CompleteSettingsView = ({ resourceUri }: { resourceUri: string }) => {
|
|
1677
|
+
// Don't provide configKey to get all settings
|
|
1678
|
+
const { settings, loading } = useSettings({
|
|
1679
|
+
resourceUri: resourceUri as URI,
|
|
1680
|
+
options: {
|
|
1681
|
+
// No configKey = get all settings
|
|
1682
|
+
},
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
// settings contains all configuration data
|
|
1686
|
+
console.log(settings?.account?.notification?.primaryEmail);
|
|
1687
|
+
console.log(settings?.organization?.teams?.visibility);
|
|
1688
|
+
|
|
1689
|
+
return (
|
|
1690
|
+
<div>
|
|
1691
|
+
<h2>All Settings</h2>
|
|
1692
|
+
<pre>{JSON.stringify(settings, null, 2)}</pre>
|
|
1693
|
+
</div>
|
|
1694
|
+
);
|
|
1695
|
+
};
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
### Comparison: useSettingsLoader vs useSettings
|
|
1699
|
+
|
|
1700
|
+
| Feature | useSettingsLoader | useSettings |
|
|
1701
|
+
| -------------------------- | ---------------------------------- | -------------------------- |
|
|
1702
|
+
| **Data Source** | Remix loader (pre-loaded) | GraphQL query (on-demand) |
|
|
1703
|
+
| **Performance** | ⚡ Instant (no network call) | 🔄 Network call required |
|
|
1704
|
+
| **Configuration Required** | ✅ Yes (`configurations` in route) | ❌ No route config needed |
|
|
1705
|
+
| **Use Case** | Route-based pages | Dynamic components, modals |
|
|
1706
|
+
| **Type Safety** | ✅ Full autocomplete | ✅ Full autocomplete |
|
|
1707
|
+
| **Cache** | Loader cache | Apollo cache |
|
|
1708
|
+
| **Updates** | Via updateConfiguration | Via updateConfiguration |
|
|
1709
|
+
|
|
1710
|
+
### Update Configuration Pattern
|
|
1711
|
+
|
|
1712
|
+
Both hooks provide `updateConfiguration` method with the same API:
|
|
1713
|
+
|
|
1714
|
+
```typescript
|
|
1715
|
+
const { updateConfiguration } = useSettingsLoader({ configKey: 'account' });
|
|
1716
|
+
// or
|
|
1717
|
+
const { updateConfiguration } = useSettings({ resourceUri, options: {} });
|
|
1718
|
+
|
|
1719
|
+
// Update single value
|
|
1720
|
+
await updateConfiguration({
|
|
1721
|
+
value: {
|
|
1722
|
+
'account.notification.primaryEmail': 'new@email.com',
|
|
1723
|
+
},
|
|
1724
|
+
target: 'user', // or 'organization', 'team', etc.
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
// Update multiple values
|
|
1728
|
+
await updateConfiguration({
|
|
1729
|
+
value: {
|
|
1730
|
+
'account.notification.primaryEmail': 'new@email.com',
|
|
1731
|
+
'account.notification.billing': true,
|
|
1732
|
+
'account.default.timezone': 'America/New_York',
|
|
1733
|
+
},
|
|
1734
|
+
target: 'user',
|
|
1735
|
+
updateOverrides: {
|
|
1736
|
+
resource: 'org://my-org/settings',
|
|
1737
|
+
},
|
|
1738
|
+
});
|
|
1739
|
+
```
|
|
1740
|
+
|
|
1741
|
+
### Real-World Examples
|
|
1742
|
+
|
|
1743
|
+
#### Example 1: Settings Page with Pre-loaded Data
|
|
1744
|
+
|
|
1745
|
+
**Route Configuration:**
|
|
1746
|
+
|
|
1747
|
+
```typescript
|
|
1748
|
+
// compute.ts
|
|
1749
|
+
{
|
|
1750
|
+
key: 'account-settings',
|
|
1751
|
+
path: '/settings/account',
|
|
1752
|
+
component: () => import('./AccountSettings'),
|
|
1753
|
+
configurations: ['account', 'organization'], // Pre-load these keys
|
|
1754
|
+
}
|
|
1755
|
+
```
|
|
1756
|
+
|
|
1757
|
+
**Component:**
|
|
1758
|
+
|
|
1759
|
+
```typescript
|
|
1760
|
+
// AccountSettings.tsx
|
|
1761
|
+
import { useSettingsLoader } from '@adminide-stack/platform-client';
|
|
1762
|
+
|
|
1763
|
+
export const AccountSettings = () => {
|
|
1764
|
+
// All data is pre-loaded, no GraphQL call
|
|
1765
|
+
const { data: account } = useSettingsLoader({ configKey: 'account' });
|
|
1766
|
+
const { data: org } = useSettingsLoader({ configKey: 'organization' });
|
|
1767
|
+
|
|
1768
|
+
return (
|
|
1769
|
+
<div>
|
|
1770
|
+
<h2>Account Settings</h2>
|
|
1771
|
+
<p>Email: {account?.notification?.primaryEmail}</p>
|
|
1772
|
+
<p>Org Notifications: {org?.notification?.notifyOrgOwnerOnUserJoined ? 'On' : 'Off'}</p>
|
|
1773
|
+
</div>
|
|
1774
|
+
);
|
|
1775
|
+
};
|
|
1776
|
+
```
|
|
1777
|
+
|
|
1778
|
+
#### Example 2: Modal with Dynamic Settings
|
|
1779
|
+
|
|
1780
|
+
**No route configuration needed:**
|
|
1781
|
+
|
|
1782
|
+
```typescript
|
|
1783
|
+
// NotificationModal.tsx
|
|
1784
|
+
import { useSettings } from '@adminide-stack/platform-client';
|
|
1785
|
+
|
|
1786
|
+
export const NotificationModal = ({ isOpen, orgUri }) => {
|
|
1787
|
+
// Makes GraphQL call when modal opens
|
|
1788
|
+
const { data: notifyOwner, updateConfiguration } = useSettings({
|
|
1789
|
+
resourceUri: orgUri,
|
|
1790
|
+
options: {
|
|
1791
|
+
configKey: 'organization.notification.notifyOrgOwnerOnUserJoined',
|
|
1792
|
+
},
|
|
1793
|
+
skip: !isOpen, // Don't fetch until modal opens
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
if (!isOpen) return null;
|
|
1797
|
+
|
|
1798
|
+
return (
|
|
1799
|
+
<div className="modal">
|
|
1800
|
+
<h3>Notification Settings</h3>
|
|
1801
|
+
<label>
|
|
1802
|
+
<input
|
|
1803
|
+
type="checkbox"
|
|
1804
|
+
checked={notifyOwner || false}
|
|
1805
|
+
onChange={async (e) => {
|
|
1806
|
+
await updateConfiguration({
|
|
1807
|
+
value: {
|
|
1808
|
+
'organization.notification.notifyOrgOwnerOnUserJoined': e.target.checked
|
|
1809
|
+
},
|
|
1810
|
+
target: 'organization',
|
|
1811
|
+
});
|
|
1812
|
+
}}
|
|
1813
|
+
/>
|
|
1814
|
+
Notify owner when users join
|
|
1815
|
+
</label>
|
|
1816
|
+
</div>
|
|
1817
|
+
);
|
|
1818
|
+
};
|
|
1819
|
+
```
|
|
1820
|
+
|
|
1821
|
+
#### Example 3: Invitation Page (From Your Codebase)
|
|
1822
|
+
|
|
1823
|
+
```typescript
|
|
1824
|
+
import React from 'react';
|
|
1825
|
+
import { useSettingsLoader } from '@adminide-stack/platform-client';
|
|
1826
|
+
import { useAcceptOrganizationInvitationMutation } from 'common/graphql';
|
|
1827
|
+
|
|
1828
|
+
export const InvitationPage = ({ decoded }) => {
|
|
1829
|
+
// Pre-loaded via route configuration
|
|
1830
|
+
const { data: notification } = useSettingsLoader({
|
|
1831
|
+
configKey: 'organization.notification',
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
const [accept] = useAcceptOrganizationInvitationMutation();
|
|
1835
|
+
|
|
1836
|
+
const acceptInvitation = () => {
|
|
1837
|
+
// Pass entire notification object to mutation
|
|
1838
|
+
accept({
|
|
1839
|
+
variables: {
|
|
1840
|
+
id: decoded.invitationId,
|
|
1841
|
+
notification, // { notifyOrgManagersOnUserJoined, notifyOrgOwnerOnUserJoined }
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
return (
|
|
1847
|
+
<div>
|
|
1848
|
+
<h2>Join {decoded.orgName}?</h2>
|
|
1849
|
+
<p>Invited by: {decoded.invitedBy}</p>
|
|
1850
|
+
<button onClick={acceptInvitation}>Accept Invitation</button>
|
|
1851
|
+
</div>
|
|
1852
|
+
);
|
|
1853
|
+
};
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
### Best Practices for Frontend
|
|
1857
|
+
|
|
1858
|
+
1. **Use `useSettingsLoader` for route-based pages:**
|
|
1859
|
+
|
|
1860
|
+
```typescript
|
|
1861
|
+
// ✅ Good: Pre-load in route
|
|
1862
|
+
configurations: ['account', 'organization'];
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
2. **Use `useSettings` for dynamic components:**
|
|
1866
|
+
|
|
1867
|
+
```typescript
|
|
1868
|
+
// ✅ Good: Modal that appears conditionally
|
|
1869
|
+
const { data } = useSettings({ resourceUri, options: { configKey: '...' }, skip: !isOpen });
|
|
1870
|
+
```
|
|
1871
|
+
|
|
1872
|
+
3. **Get objects for related settings:**
|
|
1873
|
+
|
|
1874
|
+
```typescript
|
|
1875
|
+
// ✅ Good: Get object when you need multiple related values
|
|
1876
|
+
const { data: notification } = useSettingsLoader({
|
|
1877
|
+
configKey: 'organization.notification',
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
// ❌ Bad: Multiple calls for related values
|
|
1881
|
+
const { data: notify1 } = useSettingsLoader({
|
|
1882
|
+
configKey: 'organization.notification.notifyOrgOwnerOnUserJoined',
|
|
1883
|
+
});
|
|
1884
|
+
const { data: notify2 } = useSettingsLoader({
|
|
1885
|
+
configKey: 'organization.notification.notifyOrgManagersOnUserJoined',
|
|
1886
|
+
});
|
|
1887
|
+
```
|
|
1888
|
+
|
|
1889
|
+
4. **Configure route with minimal keys:**
|
|
1890
|
+
|
|
1891
|
+
```typescript
|
|
1892
|
+
// ✅ Good: Only load what you need
|
|
1893
|
+
configurations: ['account']; // Page only uses account settings
|
|
1894
|
+
|
|
1895
|
+
// ❌ Bad: Load everything when you only need one module
|
|
1896
|
+
configurations: ['*']; // Loads all settings unnecessarily
|
|
1897
|
+
```
|
|
1898
|
+
|
|
1899
|
+
5. **Use skip option to defer loading:**
|
|
1900
|
+
```typescript
|
|
1901
|
+
// ✅ Good: Don't fetch until needed
|
|
1902
|
+
const { data } = useSettings({
|
|
1903
|
+
resourceUri,
|
|
1904
|
+
options: { configKey: '...' },
|
|
1905
|
+
skip: !shouldFetch,
|
|
1906
|
+
});
|
|
1907
|
+
```
|
|
1908
|
+
|
|
1909
|
+
### Type Definitions Reference
|
|
1910
|
+
|
|
1911
|
+
```typescript
|
|
1912
|
+
// Hook signatures
|
|
1913
|
+
function useSettingsLoader<K extends IConfigurationsFlattenedKeys, R = IPreferences>(
|
|
1914
|
+
settingsVariable: ISettingsLoaderVariable<K>,
|
|
1915
|
+
): ISettingsLoaderResponse<K, GetFlattenedValue<R, K>>;
|
|
1916
|
+
|
|
1917
|
+
function useSettings<T = any>(
|
|
1918
|
+
variables: IPageSettingsConfigKeyVariable,
|
|
1919
|
+
): {
|
|
1920
|
+
data: T;
|
|
1921
|
+
settings: any;
|
|
1922
|
+
pageSettings: any;
|
|
1923
|
+
loading: boolean;
|
|
1924
|
+
error: any;
|
|
1925
|
+
updateConfiguration: (params) => Promise<any>;
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
// Variable types
|
|
1929
|
+
interface ISettingsLoaderVariable<K> {
|
|
1930
|
+
configKey: K; // Autocomplete available
|
|
1931
|
+
skip?: boolean;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
interface IPageSettingsConfigKeyVariable {
|
|
1935
|
+
resourceUri: URI | string;
|
|
1936
|
+
options?: {
|
|
1937
|
+
configKey?: string;
|
|
1938
|
+
};
|
|
1939
|
+
skip?: boolean;
|
|
1940
|
+
}
|
|
1941
|
+
```
|
|
1942
|
+
|
|
1943
|
+
---
|
|
1944
|
+
|
|
1945
|
+
## Testing & Validation
|
|
1946
|
+
|
|
1947
|
+
### Unit Tests for Migration
|
|
1948
|
+
|
|
1949
|
+
```typescript
|
|
1950
|
+
// AddAccountsConfigurationsMigration.test.ts
|
|
1951
|
+
import { Container } from 'inversify';
|
|
1952
|
+
import { AddAccountsConfigurationsMigration } from './AddAccountsConfigurationsMigration';
|
|
1953
|
+
|
|
1954
|
+
describe('AddAccountsConfigurationsMigration', () => {
|
|
1955
|
+
let migration: AddAccountsConfigurationsMigration;
|
|
1956
|
+
let container: Container;
|
|
1957
|
+
|
|
1958
|
+
beforeEach(() => {
|
|
1959
|
+
container = new Container();
|
|
1960
|
+
// Mock dependencies
|
|
1961
|
+
container.bind('MongoDBConnection').toConstantValue(mockConnection);
|
|
1962
|
+
container.bind(SERVER_TYPES.IConfigurationRegistry).toConstantValue(mockRegistry);
|
|
1963
|
+
container.bind('Logger').toConstantValue(mockLogger);
|
|
1964
|
+
container.bind(SERVER_TYPES.IRedisCacheManager).toConstantValue(mockCache);
|
|
1965
|
+
|
|
1966
|
+
migration = container.resolve(AddAccountsConfigurationsMigration);
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
it('should have correct migration id with date', () => {
|
|
1970
|
+
expect(migration.id).toBe('AddAccountsConfigurationsMigration_20250719');
|
|
1971
|
+
expect(migration.id).toMatch(/_\d{8}$/);
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
it('should have module name', () => {
|
|
1975
|
+
expect(migration.name).toBe('account');
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
it('should merge all settings contributions', () => {
|
|
1979
|
+
const settings = migration.settings;
|
|
1980
|
+
|
|
1981
|
+
// Check AccountsContribution keys
|
|
1982
|
+
expect(settings).toHaveProperty('account.notification.primaryEmail');
|
|
1983
|
+
expect(settings).toHaveProperty('account.default.organization');
|
|
1984
|
+
|
|
1985
|
+
// Check OrganizationContribution keys
|
|
1986
|
+
expect(settings).toHaveProperty('organization.notification.notifyOrgOwnerOnUserJoined');
|
|
1987
|
+
|
|
1988
|
+
// Check TeamsContribution keys
|
|
1989
|
+
expect(settings).toHaveProperty('organization.teams.visibility');
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
it('should have valid configuration schema', () => {
|
|
1993
|
+
const settings = migration.settings;
|
|
1994
|
+
|
|
1995
|
+
Object.entries(settings).forEach(([key, config]) => {
|
|
1996
|
+
expect(config).toHaveProperty('type');
|
|
1997
|
+
expect(config).toHaveProperty('default');
|
|
1998
|
+
expect(config).toHaveProperty('description');
|
|
1999
|
+
expect(config).toHaveProperty('scope');
|
|
2000
|
+
});
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
it('should register configurations successfully', async () => {
|
|
2004
|
+
const spy = jest.spyOn(mockRegistry, 'registerConfiguration');
|
|
2005
|
+
|
|
2006
|
+
await migration.up();
|
|
2007
|
+
|
|
2008
|
+
expect(spy).toHaveBeenCalled();
|
|
2009
|
+
expect(spy.mock.calls.length).toBeGreaterThan(0);
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
it('should deregister configurations on down', async () => {
|
|
2013
|
+
const spy = jest.spyOn(mockRegistry, 'deregisterConfigurations');
|
|
2014
|
+
|
|
2015
|
+
await migration.down();
|
|
2016
|
+
|
|
2017
|
+
expect(spy).toHaveBeenCalled();
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
it('should clear cache on down', async () => {
|
|
2021
|
+
const spy = jest.spyOn(mockCache, 'del');
|
|
2022
|
+
|
|
2023
|
+
await migration.down();
|
|
2024
|
+
|
|
2025
|
+
expect(spy).toHaveBeenCalled();
|
|
2026
|
+
});
|
|
2027
|
+
});
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
### GraphQL Schema Tests
|
|
2031
|
+
|
|
2032
|
+
```graphql
|
|
2033
|
+
# test-preferences.graphql
|
|
2034
|
+
|
|
2035
|
+
# Test query to verify all nested structures
|
|
2036
|
+
query TestCompletePreferences {
|
|
2037
|
+
preferences {
|
|
2038
|
+
account {
|
|
2039
|
+
notification {
|
|
2040
|
+
primaryEmail
|
|
2041
|
+
onChangeAccountSettings
|
|
2042
|
+
billing
|
|
2043
|
+
professional {
|
|
2044
|
+
companyName
|
|
2045
|
+
companyEmail
|
|
2046
|
+
companyPhone
|
|
2047
|
+
companyWebsite
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
default {
|
|
2051
|
+
organization
|
|
2052
|
+
language
|
|
2053
|
+
timezone
|
|
2054
|
+
}
|
|
2055
|
+
roles
|
|
2056
|
+
isDeleted
|
|
2057
|
+
isVerified
|
|
2058
|
+
}
|
|
2059
|
+
organization {
|
|
2060
|
+
notification {
|
|
2061
|
+
notifyOrgOwnerOnUserJoined
|
|
2062
|
+
notifyOrgManagersOnUserJoined
|
|
2063
|
+
}
|
|
2064
|
+
teams {
|
|
2065
|
+
visibility
|
|
2066
|
+
joinMethod
|
|
2067
|
+
inviteCode
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
# Test mutation
|
|
2074
|
+
mutation UpdateAccountPreferences($input: Preference_AccountInput!) {
|
|
2075
|
+
updateAccountPreferences(input: $input) {
|
|
2076
|
+
account {
|
|
2077
|
+
notification {
|
|
2078
|
+
primaryEmail
|
|
2079
|
+
billing
|
|
2080
|
+
}
|
|
2081
|
+
default {
|
|
2082
|
+
organization
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
```
|
|
2088
|
+
|
|
2089
|
+
### Integration Tests
|
|
2090
|
+
|
|
2091
|
+
```typescript
|
|
2092
|
+
// test-preferences-integration.ts
|
|
2093
|
+
import { gql } from 'apollo-server-express';
|
|
2094
|
+
import { createTestClient } from 'apollo-server-testing';
|
|
2095
|
+
|
|
2096
|
+
describe('Preferences Integration', () => {
|
|
2097
|
+
let testClient;
|
|
2098
|
+
|
|
2099
|
+
beforeAll(async () => {
|
|
2100
|
+
// Setup test server
|
|
2101
|
+
testClient = createTestClient(server);
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
it('should query nested preferences', async () => {
|
|
2105
|
+
const query = gql`
|
|
2106
|
+
query {
|
|
2107
|
+
preferences {
|
|
2108
|
+
account {
|
|
2109
|
+
notification {
|
|
2110
|
+
primaryEmail
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
`;
|
|
2116
|
+
|
|
2117
|
+
const { data } = await testClient.query({ query });
|
|
2118
|
+
|
|
2119
|
+
expect(data.preferences.account.notification).toBeDefined();
|
|
2120
|
+
expect(data.preferences.account.notification.primaryEmail).toBeDefined();
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
it('should update preferences', async () => {
|
|
2124
|
+
const mutation = gql`
|
|
2125
|
+
mutation ($input: Preference_AccountInput!) {
|
|
2126
|
+
updateAccountPreferences(input: $input) {
|
|
2127
|
+
account {
|
|
2128
|
+
notification {
|
|
2129
|
+
primaryEmail
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
`;
|
|
2135
|
+
|
|
2136
|
+
const { data } = await testClient.mutate({
|
|
2137
|
+
mutation,
|
|
2138
|
+
variables: {
|
|
2139
|
+
input: {
|
|
2140
|
+
notification: {
|
|
2141
|
+
primaryEmail: 'test@example.com',
|
|
2142
|
+
},
|
|
2143
|
+
},
|
|
2144
|
+
},
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
expect(data.updateAccountPreferences.account.notification.primaryEmail).toBe('test@example.com');
|
|
2148
|
+
});
|
|
2149
|
+
});
|
|
2150
|
+
```
|
|
2151
|
+
|
|
2152
|
+
### Manual Testing Commands
|
|
2153
|
+
|
|
2154
|
+
```bash
|
|
2155
|
+
# Run migrations
|
|
2156
|
+
npm run migrate:up
|
|
2157
|
+
|
|
2158
|
+
# Rollback migrations
|
|
2159
|
+
npm run migrate:down
|
|
2160
|
+
|
|
2161
|
+
# Test GraphQL schema
|
|
2162
|
+
npm run test:graphql
|
|
2163
|
+
|
|
2164
|
+
# Run integration tests
|
|
2165
|
+
npm run test:integration
|
|
2166
|
+
|
|
2167
|
+
# Check MongoDB for registered configurations
|
|
2168
|
+
mongo
|
|
2169
|
+
> use your_database
|
|
2170
|
+
> db.configuration_registry.find({ 'context.extensionId': 'account' })
|
|
2171
|
+
```
|
|
2172
|
+
|
|
2173
|
+
---
|
|
2174
|
+
|
|
2175
|
+
## Best Practices
|
|
2176
|
+
|
|
2177
|
+
### 1. Naming Conventions
|
|
2178
|
+
|
|
2179
|
+
**Contributions:**
|
|
2180
|
+
|
|
2181
|
+
- Format: `{Feature}Contribution`
|
|
2182
|
+
- Examples: `AccountsContribution`, `BillingContribution`, `ProjectContribution`
|
|
2183
|
+
|
|
2184
|
+
**Migrations:**
|
|
2185
|
+
|
|
2186
|
+
- Format: `Add{Module}ConfigurationsMigration`
|
|
2187
|
+
- Examples: `AddAccountsConfigurationsMigration`, `AddBillingConfigurationsMigration`
|
|
2188
|
+
|
|
2189
|
+
**GraphQL Types:**
|
|
2190
|
+
|
|
2191
|
+
- Format: `Preference_{Module}_{Section}_{Subsection}`
|
|
2192
|
+
- Examples: `Preference_Account_Notification`, `Preference_Billing_Payment`
|
|
2193
|
+
|
|
2194
|
+
**Flattened Keys:**
|
|
2195
|
+
|
|
2196
|
+
- Format: `{module}.{section}.{property}`
|
|
2197
|
+
- Examples: `account.notification.email`, `billing.payment.method`
|
|
2198
|
+
- Max depth: 4 levels (recommended)
|
|
2199
|
+
|
|
2200
|
+
### 2. Configuration Schema Best Practices
|
|
2201
|
+
|
|
2202
|
+
```typescript
|
|
2203
|
+
// ✅ GOOD: Clear, specific, with localization
|
|
2204
|
+
'account.notification.primaryEmail': {
|
|
2205
|
+
type: 'string',
|
|
2206
|
+
default: '',
|
|
2207
|
+
description: nls.localize('accountNotificationPrimaryEmail', 'Notification Primary Email'),
|
|
2208
|
+
scope: ConfigurationScope.APPLICATION,
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// ❌ BAD: No localization, vague description
|
|
2212
|
+
'account.email': {
|
|
2213
|
+
type: 'string',
|
|
2214
|
+
default: '',
|
|
2215
|
+
description: 'Email',
|
|
2216
|
+
scope: ConfigurationScope.APPLICATION,
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// ✅ GOOD: Enum with descriptions
|
|
2220
|
+
'billing.payment.method': {
|
|
2221
|
+
type: 'string',
|
|
2222
|
+
enum: ['credit_card', 'paypal', 'bank_transfer'],
|
|
2223
|
+
enumDescriptions: [
|
|
2224
|
+
nls.localize('paymentCreditCard', 'Pay with credit or debit card'),
|
|
2225
|
+
nls.localize('paymentPaypal', 'Pay with PayPal account'),
|
|
2226
|
+
nls.localize('paymentBank', 'Direct bank transfer'),
|
|
2227
|
+
],
|
|
2228
|
+
default: 'credit_card',
|
|
2229
|
+
description: nls.localize('billingPaymentMethod', 'Preferred payment method'),
|
|
2230
|
+
scope: ConfigurationScope.APPLICATION,
|
|
2231
|
+
}
|
|
2232
|
+
```
|
|
2233
|
+
|
|
2234
|
+
### 3. GraphQL Schema Best Practices
|
|
2235
|
+
|
|
2236
|
+
```graphql
|
|
2237
|
+
# ✅ GOOD: Clear documentation with mapping
|
|
2238
|
+
"""
|
|
2239
|
+
Account notification preferences
|
|
2240
|
+
Maps to flattened keys: account.notification.*
|
|
2241
|
+
"""
|
|
2242
|
+
type Preference_Account_Notification {
|
|
2243
|
+
"""
|
|
2244
|
+
Primary email for notifications
|
|
2245
|
+
Flattened key: account.notification.primaryEmail
|
|
2246
|
+
"""
|
|
2247
|
+
primaryEmail: String
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
# ❌ BAD: No documentation
|
|
2251
|
+
type Preference_Account_Notification {
|
|
2252
|
+
primaryEmail: String
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
# ✅ GOOD: Use enums for restricted values
|
|
2256
|
+
enum PaymentMethod {
|
|
2257
|
+
CREDIT_CARD
|
|
2258
|
+
BANK_TRANSFER
|
|
2259
|
+
PAYPAL
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
type Preference_Billing_Payment {
|
|
2263
|
+
defaultMethod: PaymentMethod
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
# ❌ BAD: Use String without restriction
|
|
2267
|
+
type Preference_Billing_Payment {
|
|
2268
|
+
defaultMethod: String
|
|
2269
|
+
}
|
|
2270
|
+
```
|
|
2271
|
+
|
|
2272
|
+
### 4. Migration Best Practices
|
|
2273
|
+
|
|
2274
|
+
```typescript
|
|
2275
|
+
// ✅ GOOD: Date-based versioning
|
|
2276
|
+
get id() {
|
|
2277
|
+
return `${AddAccountsConfigurationsMigration.name}_20250119`;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// ❌ BAD: No versioning
|
|
2281
|
+
get id() {
|
|
2282
|
+
return AddAccountsConfigurationsMigration.name;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// ✅ GOOD: Merge multiple contributions
|
|
2286
|
+
get settings() {
|
|
2287
|
+
return SettingsContributions.reduce(
|
|
2288
|
+
(acc, curr) => ({
|
|
2289
|
+
...acc,
|
|
2290
|
+
...curr,
|
|
2291
|
+
}),
|
|
2292
|
+
{},
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// ❌ BAD: Return single contribution
|
|
2297
|
+
get settings() {
|
|
2298
|
+
return AccountsContribution; // Missing other contributions
|
|
2299
|
+
}
|
|
2300
|
+
```
|
|
2301
|
+
|
|
2302
|
+
### 5. Scope Selection Guidelines
|
|
2303
|
+
|
|
2304
|
+
| Setting Type | Recommended Scope | Example |
|
|
2305
|
+
| ------------------ | ---------------------- | ---------------------------------- |
|
|
2306
|
+
| User preferences | `APPLICATION` | Theme, language, email |
|
|
2307
|
+
| Workspace settings | `WINDOW` | Org notifications, team defaults |
|
|
2308
|
+
| Resource settings | `RESOURCE` | Project visibility, access control |
|
|
2309
|
+
| Format overrides | `LANGUAGE_OVERRIDABLE` | Per-language formatting |
|
|
2310
|
+
| System config | `MACHINE` | Installation paths, system proxy |
|
|
2311
|
+
|
|
2312
|
+
### 6. Key Hierarchy Guidelines
|
|
2313
|
+
|
|
2314
|
+
```typescript
|
|
2315
|
+
// ✅ GOOD: Logical hierarchy (max 4 levels)
|
|
2316
|
+
'account.notification.email.frequency';
|
|
2317
|
+
'organization.teams.access.defaultVisibility';
|
|
2318
|
+
'project.settings.editor.formatting';
|
|
2319
|
+
|
|
2320
|
+
// ❌ BAD: Too deep (5+ levels)
|
|
2321
|
+
'app.user.account.settings.notification.email.delivery.frequency';
|
|
2322
|
+
|
|
2323
|
+
// ❌ BAD: Inconsistent structure
|
|
2324
|
+
'account.notificationEmail'; // Should be: account.notification.email
|
|
2325
|
+
'accountNotificationFrequency'; // Should be: account.notification.frequency
|
|
2326
|
+
```
|
|
2327
|
+
|
|
2328
|
+
### 7. Documentation Best Practices
|
|
2329
|
+
|
|
2330
|
+
```typescript
|
|
2331
|
+
/**
|
|
2332
|
+
* Account preferences contribution
|
|
2333
|
+
*
|
|
2334
|
+
* Defines user-level account settings including:
|
|
2335
|
+
* - Notification preferences
|
|
2336
|
+
* - Default organization
|
|
2337
|
+
* - Professional information
|
|
2338
|
+
*
|
|
2339
|
+
* All settings use ConfigurationScope.APPLICATION as they are user-specific.
|
|
2340
|
+
*
|
|
2341
|
+
* @example
|
|
2342
|
+
* // Accessing in code:
|
|
2343
|
+
* const email = configService.getValue('account.notification.primaryEmail');
|
|
2344
|
+
*
|
|
2345
|
+
* // In GraphQL:
|
|
2346
|
+
* query {
|
|
2347
|
+
* preferences {
|
|
2348
|
+
* account {
|
|
2349
|
+
* notification {
|
|
2350
|
+
* primaryEmail
|
|
2351
|
+
* }
|
|
2352
|
+
* }
|
|
2353
|
+
* }
|
|
2354
|
+
* }
|
|
2355
|
+
*/
|
|
2356
|
+
export const AccountsContribution: IConfigurationSchemaMap = {
|
|
2357
|
+
// ... settings
|
|
2358
|
+
};
|
|
2359
|
+
```
|
|
2360
|
+
|
|
2361
|
+
---
|
|
2362
|
+
|
|
2363
|
+
## Troubleshooting
|
|
2364
|
+
|
|
2365
|
+
### Common Issues and Solutions
|
|
2366
|
+
|
|
2367
|
+
#### Issue 1: Settings not appearing in database
|
|
2368
|
+
|
|
2369
|
+
**Symptoms:**
|
|
2370
|
+
|
|
2371
|
+
- Migration runs successfully but settings don't appear in configuration registry
|
|
2372
|
+
- GraphQL queries return null for preferences
|
|
2373
|
+
|
|
2374
|
+
**Solutions:**
|
|
2375
|
+
|
|
2376
|
+
1. Check migration binding:
|
|
2377
|
+
|
|
2378
|
+
```typescript
|
|
2379
|
+
// Verify in module.ts
|
|
2380
|
+
bind('MongodbMigration')
|
|
2381
|
+
.to(AddAccountsConfigurationsMigration)
|
|
2382
|
+
.whenTargetNamed(AddAccountsConfigurationsMigration.name);
|
|
2383
|
+
```
|
|
2384
|
+
|
|
2385
|
+
2. Verify migration execution:
|
|
2386
|
+
|
|
2387
|
+
```bash
|
|
2388
|
+
# Check logs for migration execution
|
|
2389
|
+
npm run migrate:up -- --verbose
|
|
2390
|
+
```
|
|
2391
|
+
|
|
2392
|
+
3. Check MongoDB:
|
|
2393
|
+
|
|
2394
|
+
```javascript
|
|
2395
|
+
// Connect to MongoDB
|
|
2396
|
+
db.configuration_registry.find({ 'context.extensionId': 'account' });
|
|
2397
|
+
```
|
|
2398
|
+
|
|
2399
|
+
4. Verify contribution export:
|
|
2400
|
+
```typescript
|
|
2401
|
+
// In settings/index.ts
|
|
2402
|
+
export const SettingsContributions = [
|
|
2403
|
+
AccountsContribution, // Must be included
|
|
2404
|
+
// ...
|
|
2405
|
+
];
|
|
2406
|
+
```
|
|
2407
|
+
|
|
2408
|
+
#### Issue 2: GraphQL type doesn't match flattened keys
|
|
2409
|
+
|
|
2410
|
+
**Symptoms:**
|
|
2411
|
+
|
|
2412
|
+
- GraphQL queries fail with "Cannot query field"
|
|
2413
|
+
- Type structure doesn't align with configuration keys
|
|
2414
|
+
|
|
2415
|
+
**Solution:**
|
|
2416
|
+
Review the mapping systematically:
|
|
2417
|
+
|
|
2418
|
+
```typescript
|
|
2419
|
+
// Flattened key
|
|
2420
|
+
'account.notification.professional.companyName'
|
|
2421
|
+
|
|
2422
|
+
// Should map to GraphQL (split on dots):
|
|
2423
|
+
// 1. account → Preference_Account
|
|
2424
|
+
// 2. notification → Preference_Account_Notification
|
|
2425
|
+
// 3. professional → Preference_Account_Professional
|
|
2426
|
+
// 4. companyName → companyName: String
|
|
2427
|
+
|
|
2428
|
+
type Preference_Account_Professional {
|
|
2429
|
+
companyName: String // ✅ Correct
|
|
2430
|
+
}
|
|
2431
|
+
```
|
|
2432
|
+
|
|
2433
|
+
#### Issue 3: Cache not clearing after migration
|
|
2434
|
+
|
|
2435
|
+
**Symptoms:**
|
|
2436
|
+
|
|
2437
|
+
- Old preference values persist after migration
|
|
2438
|
+
- Changes don't reflect in queries
|
|
2439
|
+
|
|
2440
|
+
**Solutions:**
|
|
2441
|
+
|
|
2442
|
+
1. Verify Redis connection:
|
|
2443
|
+
|
|
2444
|
+
```typescript
|
|
2445
|
+
// Check redisCacheManager injection
|
|
2446
|
+
@inject(SERVER_TYPES.IRedisCacheManager)
|
|
2447
|
+
redisCacheManager: IRedisCacheManager,
|
|
2448
|
+
```
|
|
2449
|
+
|
|
2450
|
+
2. Manually clear cache:
|
|
2451
|
+
|
|
2452
|
+
```bash
|
|
2453
|
+
# Connect to Redis
|
|
2454
|
+
redis-cli
|
|
2455
|
+
> FLUSHDB
|
|
2456
|
+
```
|
|
2457
|
+
|
|
2458
|
+
3. Check cache clearing in down():
|
|
2459
|
+
```typescript
|
|
2460
|
+
async down() {
|
|
2461
|
+
await this.dropCache(); // Must be called
|
|
2462
|
+
// ... deregister configurations
|
|
2463
|
+
}
|
|
2464
|
+
```
|
|
2465
|
+
|
|
2466
|
+
#### Issue 4: Migration fails with "No matching bindings"
|
|
2467
|
+
|
|
2468
|
+
**Symptoms:**
|
|
2469
|
+
|
|
2470
|
+
```
|
|
2471
|
+
Error: No matching bindings found for serviceIdentifier: Symbol(IConfigurationRegistry)
|
|
2472
|
+
```
|
|
2473
|
+
|
|
2474
|
+
**Solution:**
|
|
2475
|
+
Ensure all dependencies are properly injected:
|
|
2476
|
+
|
|
2477
|
+
```typescript
|
|
2478
|
+
constructor(
|
|
2479
|
+
@inject('MongoDBConnection') db: Connection,
|
|
2480
|
+
@inject(SERVER_TYPES.IConfigurationRegistry) configurationRegistry: IConfigurationRegistry,
|
|
2481
|
+
@inject('Logger') logger: CdmLogger.ILogger,
|
|
2482
|
+
@inject(SERVER_TYPES.IRedisCacheManager) redisCacheManager: IRedisCacheManager,
|
|
2483
|
+
) {
|
|
2484
|
+
super(db, configurationRegistry, redisCacheManager, logger);
|
|
2485
|
+
}
|
|
2486
|
+
```
|
|
2487
|
+
|
|
2488
|
+
#### Issue 5: Enum values not matching
|
|
2489
|
+
|
|
2490
|
+
**Symptoms:**
|
|
2491
|
+
|
|
2492
|
+
- GraphQL returns enum values in wrong case
|
|
2493
|
+
- Enum validation fails
|
|
2494
|
+
|
|
2495
|
+
**Solution:**
|
|
2496
|
+
Ensure consistency between TypeScript and GraphQL:
|
|
2497
|
+
|
|
2498
|
+
```typescript
|
|
2499
|
+
// TypeScript contribution
|
|
2500
|
+
enum: ['private', 'public'] // lowercase
|
|
2501
|
+
|
|
2502
|
+
// GraphQL enum - convert to UPPERCASE
|
|
2503
|
+
enum TeamVisibility {
|
|
2504
|
+
PRIVATE # Maps to 'private'
|
|
2505
|
+
PUBLIC # Maps to 'public'
|
|
2506
|
+
}
|
|
2507
|
+
```
|
|
2508
|
+
|
|
2509
|
+
#### Issue 6: Nested objects not saving
|
|
2510
|
+
|
|
2511
|
+
**Symptoms:**
|
|
2512
|
+
|
|
2513
|
+
- Top-level preferences save, but nested objects don't persist
|
|
2514
|
+
- Mutations succeed but queries return null for nested fields
|
|
2515
|
+
|
|
2516
|
+
**Solution:**
|
|
2517
|
+
Ensure complete input type hierarchy:
|
|
2518
|
+
|
|
2519
|
+
```graphql
|
|
2520
|
+
# Need complete chain of input types
|
|
2521
|
+
input Preference_Account_ProfessionalInput {
|
|
2522
|
+
companyName: String
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
input Preference_Account_NotificationInput {
|
|
2526
|
+
professional: Preference_Account_ProfessionalInput # Must include nested
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
input Preference_AccountInput {
|
|
2530
|
+
notification: Preference_Account_NotificationInput # Must include parent
|
|
2531
|
+
}
|
|
2532
|
+
```
|
|
2533
|
+
|
|
2534
|
+
---
|
|
2535
|
+
|
|
2536
|
+
## Reference
|
|
2537
|
+
|
|
2538
|
+
### File Structure
|
|
2539
|
+
|
|
2540
|
+
```
|
|
2541
|
+
packages-modules/{module-name}/
|
|
2542
|
+
├── server/
|
|
2543
|
+
│ ├── src/
|
|
2544
|
+
│ │ ├── preferences/
|
|
2545
|
+
│ │ │ ├── index.ts # Export all preferences
|
|
2546
|
+
│ │ │ ├── settings/
|
|
2547
|
+
│ │ │ │ ├── index.ts # Export settings contributions
|
|
2548
|
+
│ │ │ │ ├── {feature}-contribution.ts # Flattened keys
|
|
2549
|
+
│ │ │ │ └── ...
|
|
2550
|
+
│ │ │ ├── roles/
|
|
2551
|
+
│ │ │ │ └── index.ts # Role definitions
|
|
2552
|
+
│ │ │ └── policies/
|
|
2553
|
+
│ │ │ └── index.ts # Policy definitions
|
|
2554
|
+
│ │ ├── migrations/
|
|
2555
|
+
│ │ │ └── dbMigrations/
|
|
2556
|
+
│ │ │ └── Add{Module}ConfigurationsMigration.ts
|
|
2557
|
+
│ │ ├── graphql/
|
|
2558
|
+
│ │ │ └── schema/
|
|
2559
|
+
│ │ │ ├── preferences.graphql # Nested types
|
|
2560
|
+
│ │ │ └── ...
|
|
2561
|
+
│ │ └── containers/
|
|
2562
|
+
│ │ └── module.ts # DI binding
|
|
2563
|
+
```
|
|
2564
|
+
|
|
2565
|
+
### Quick Reference Table
|
|
2566
|
+
|
|
2567
|
+
| Component | File Location | Key Pattern | Export Format |
|
|
2568
|
+
| ------------------ | ---------------------------------------- | --------------------------- | ------------------------- |
|
|
2569
|
+
| **Contribution** | `preferences/settings/*-contribution.ts` | `module.section.property` | `IConfigurationSchemaMap` |
|
|
2570
|
+
| **Settings Index** | `preferences/settings/index.ts` | N/A | `SettingsContributions[]` |
|
|
2571
|
+
| **Migration** | `migrations/dbMigrations/Add*.ts` | N/A | `@injectable() class` |
|
|
2572
|
+
| **GraphQL Types** | `graphql/schema/preferences.graphql` | `Preference_Module_Section` | GraphQL SDL |
|
|
2573
|
+
| **DI Binding** | `containers/module.ts` | N/A | InversifyJS binding |
|
|
2574
|
+
|
|
2575
|
+
### Configuration Scope Reference
|
|
2576
|
+
|
|
2577
|
+
```typescript
|
|
2578
|
+
export enum ConfigurationScope {
|
|
2579
|
+
APPLICATION = 1, // User-specific settings
|
|
2580
|
+
WINDOW = 2, // Workspace/Organization settings
|
|
2581
|
+
RESOURCE = 3, // Project/Team/Resource settings
|
|
2582
|
+
LANGUAGE_OVERRIDABLE = 4, // Can be overridden per language
|
|
2583
|
+
MACHINE = 5, // System-wide settings
|
|
2584
|
+
MACHINE_OVERRIDABLE = 6, // System settings with user override
|
|
2585
|
+
}
|
|
2586
|
+
```
|
|
2587
|
+
|
|
2588
|
+
### Common Imports
|
|
2589
|
+
|
|
2590
|
+
```typescript
|
|
2591
|
+
// Contributions
|
|
2592
|
+
import { IConfigurationSchemaMap } from '@adminide-stack/core';
|
|
2593
|
+
import * as nls from '@vscode-alt/monaco-editor/esm/vs/nls';
|
|
2594
|
+
import { ConfigurationScope } from 'common/server';
|
|
2595
|
+
|
|
2596
|
+
// Migrations
|
|
2597
|
+
import { IConfigurationRegistry } from '@adminide-stack/core';
|
|
2598
|
+
import { inject, injectable } from 'inversify';
|
|
2599
|
+
import type { Connection } from 'mongoose';
|
|
2600
|
+
import { CdmLogger } from '@cdm-logger/core';
|
|
2601
|
+
import { SERVER_TYPES, IRedisCacheManager, ContributionSchemaId, ContributionFragmentName } from 'common/server';
|
|
2602
|
+
import { BaseConfigurationsMigration, ISchemaConfiguration, ISchemaOverride } from '@adminide-stack/platform-server';
|
|
2603
|
+
|
|
2604
|
+
// DI Container
|
|
2605
|
+
import { ContainerModule, interfaces } from 'inversify';
|
|
2606
|
+
```
|
|
2607
|
+
|
|
2608
|
+
### Useful Commands
|
|
2609
|
+
|
|
2610
|
+
```bash
|
|
2611
|
+
# Development
|
|
2612
|
+
npm run dev # Start development server
|
|
2613
|
+
npm run build # Build the project
|
|
2614
|
+
|
|
2615
|
+
# Migrations
|
|
2616
|
+
npm run migrate:up # Run all pending migrations
|
|
2617
|
+
npm run migrate:down # Rollback last migration
|
|
2618
|
+
npm run migrate:status # Check migration status
|
|
2619
|
+
|
|
2620
|
+
# Testing
|
|
2621
|
+
npm run test # Run all tests
|
|
2622
|
+
npm run test:unit # Run unit tests
|
|
2623
|
+
npm run test:integration # Run integration tests
|
|
2624
|
+
npm run test:graphql # Test GraphQL schema
|
|
2625
|
+
|
|
2626
|
+
# Database
|
|
2627
|
+
npm run db:seed # Seed database
|
|
2628
|
+
npm run db:reset # Reset database
|
|
2629
|
+
|
|
2630
|
+
# Debugging
|
|
2631
|
+
npm run migrate:up -- --verbose # Verbose migration output
|
|
2632
|
+
npm run test -- --watch # Watch mode for tests
|
|
2633
|
+
```
|
|
2634
|
+
|
|
2635
|
+
### GraphQL Query Examples
|
|
2636
|
+
|
|
2637
|
+
```graphql
|
|
2638
|
+
# Get all preferences
|
|
2639
|
+
query GetAllPreferences {
|
|
2640
|
+
preferences {
|
|
2641
|
+
account {
|
|
2642
|
+
notification {
|
|
2643
|
+
primaryEmail
|
|
2644
|
+
billing
|
|
2645
|
+
}
|
|
2646
|
+
default {
|
|
2647
|
+
organization
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
organization {
|
|
2651
|
+
notification {
|
|
2652
|
+
notifyOrgOwnerOnUserJoined
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
# Get specific preference section
|
|
2659
|
+
query GetAccountNotifications {
|
|
2660
|
+
preferences {
|
|
2661
|
+
account {
|
|
2662
|
+
notification {
|
|
2663
|
+
primaryEmail
|
|
2664
|
+
onChangeAccountSettings
|
|
2665
|
+
billing
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
# Update preferences
|
|
2672
|
+
mutation UpdatePreferences($input: Preference_AccountInput!) {
|
|
2673
|
+
updateAccountPreferences(input: $input) {
|
|
2674
|
+
account {
|
|
2675
|
+
notification {
|
|
2676
|
+
primaryEmail
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
```
|
|
2682
|
+
|
|
2683
|
+
### MongoDB Queries
|
|
2684
|
+
|
|
2685
|
+
```javascript
|
|
2686
|
+
// Find all configurations for a module
|
|
2687
|
+
db.configuration_registry.find({
|
|
2688
|
+
'context.extensionId': 'account',
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
// Find specific configuration
|
|
2692
|
+
db.configuration_registry.findOne({
|
|
2693
|
+
'node.id': 'account.notification.primaryEmail',
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
// Check migration status
|
|
2697
|
+
db.migrations.find().sort({ executedAt: -1 });
|
|
2698
|
+
|
|
2699
|
+
// Clear configuration cache
|
|
2700
|
+
db.configuration_cache.deleteMany({});
|
|
2701
|
+
```
|
|
2702
|
+
|
|
2703
|
+
---
|
|
2704
|
+
|
|
2705
|
+
## Summary Checklist
|
|
2706
|
+
|
|
2707
|
+
When creating new preferences, ensure you:
|
|
2708
|
+
|
|
2709
|
+
### Configuration Phase
|
|
2710
|
+
|
|
2711
|
+
- [ ] Create `{feature}-contribution.ts` with flattened keys
|
|
2712
|
+
- [ ] Use proper naming: `module.section.property`
|
|
2713
|
+
- [ ] Add type, default, description, and scope for each key
|
|
2714
|
+
- [ ] Use `nls.localize()` for all descriptions
|
|
2715
|
+
- [ ] Export in `settings/index.ts`
|
|
2716
|
+
- [ ] Add to `SettingsContributions` array
|
|
2717
|
+
|
|
2718
|
+
### Migration Phase
|
|
2719
|
+
|
|
2720
|
+
- [ ] Create migration extending `BaseConfigurationsMigration`
|
|
2721
|
+
- [ ] Set unique migration ID with date: `{ClassName}_YYYYMMDD`
|
|
2722
|
+
- [ ] Implement all required getters
|
|
2723
|
+
- [ ] Merge contributions in `settings` getter
|
|
2724
|
+
- [ ] Add `@injectable()` decorator
|
|
2725
|
+
|
|
2726
|
+
### GraphQL Phase
|
|
2727
|
+
|
|
2728
|
+
- [ ] Create nested types matching flattened structure
|
|
2729
|
+
- [ ] Follow naming: `Preference_{Module}_{Section}`
|
|
2730
|
+
- [ ] Add GraphQL descriptions with flattened key mappings
|
|
2731
|
+
- [ ] Create corresponding input types for mutations
|
|
2732
|
+
- [ ] Extend base `Preferences` type
|
|
2733
|
+
- [ ] Define necessary enums
|
|
2734
|
+
|
|
2735
|
+
### Integration Phase
|
|
2736
|
+
|
|
2737
|
+
- [ ] Bind migration in `module.ts` with `whenTargetNamed()`
|
|
2738
|
+
- [ ] Ensure all dependencies are injected
|
|
2739
|
+
- [ ] Add to module exports if needed
|
|
2740
|
+
|
|
2741
|
+
### Testing Phase
|
|
2742
|
+
|
|
2743
|
+
- [ ] Write unit tests for migration
|
|
2744
|
+
- [ ] Test GraphQL schema with queries
|
|
2745
|
+
- [ ] Verify database registration
|
|
2746
|
+
- [ ] Test cache clearing
|
|
2747
|
+
- [ ] Run integration tests
|
|
2748
|
+
|
|
2749
|
+
### Frontend Integration Phase
|
|
2750
|
+
|
|
2751
|
+
- [ ] Configure `configurations` in route (compute.ts)
|
|
2752
|
+
- [ ] Choose between `useSettingsLoader` (pre-loaded) or `useSettings` (on-demand)
|
|
2753
|
+
- [ ] Test with complete keys for specific values
|
|
2754
|
+
- [ ] Test with partial keys for nested objects
|
|
2755
|
+
- [ ] Verify type autocomplete works
|
|
2756
|
+
- [ ] Test `updateConfiguration` mutation
|
|
2757
|
+
- [ ] Handle loading and error states
|
|
2758
|
+
|
|
2759
|
+
### Documentation Phase
|
|
2760
|
+
|
|
2761
|
+
- [ ] Document flattened keys with examples
|
|
2762
|
+
- [ ] Add JSDoc to contribution
|
|
2763
|
+
- [ ] Document GraphQL type mappings
|
|
2764
|
+
- [ ] Document frontend usage patterns
|
|
2765
|
+
- [ ] Update module README if needed
|
|
2766
|
+
|
|
2767
|
+
---
|
|
2768
|
+
|
|
2769
|
+
## Additional Resources
|
|
2770
|
+
|
|
2771
|
+
- **TypeScript Configuration**: `@adminide-stack/core` package
|
|
2772
|
+
- **Base Migration**: `packages/adminide-platform/server/src/migrations/BaseConfigurationService.ts`
|
|
2773
|
+
- **GraphQL Base Types**: Search for base `Preferences` type in schema files
|
|
2774
|
+
- **InversifyJS Docs**: https://inversify.io/
|
|
2775
|
+
- **MongoDB Docs**: https://docs.mongodb.com/
|
|
2776
|
+
|
|
2777
|
+
---
|
|
2778
|
+
|
|
2779
|
+
**Document Version:** 1.0.0
|
|
2780
|
+
**Last Updated:** January 19, 2025
|
|
2781
|
+
**Maintained By:** AdminIDE Stack Team
|