@team-codeme/crud-forge 0.0.0-stage → 1.0.0
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/.cursor/agents/generator-engineer.md +31 -0
- package/.cursor/agents/keycloak-engineer.md +33 -0
- package/.cursor/agents/qa-engineer.md +32 -0
- package/.cursor/agents/security-reviewer.md +30 -0
- package/.cursor/mcp.json +13 -0
- package/.cursor/rules/crudforge-precautions.mdc +16 -0
- package/.cursor/rules/playwright-browser.mdc +30 -0
- package/.cursor/rules/readme-todo-sync.mdc +13 -0
- package/.env.qa.example +19 -0
- package/.github/workflows/qa-live.yml +32 -0
- package/.github/workflows/qa.yml +27 -0
- package/.vscode/settings.json +32 -0
- package/QA/IAM-PLAN.md +25 -0
- package/QA/IAM-RESULTS.md +39 -0
- package/QA/IAM-TESTCASES.md +36 -0
- package/QA/IAM-TODO.md +7 -0
- package/QA/KEYCLOAK-BOOTSTRAP.md +105 -0
- package/QA/PLAN.md +43 -0
- package/QA/RESULTS.md +59 -0
- package/QA/TESTCASES.md +65 -0
- package/QA/TODO.md +14 -0
- package/QA/pmb-dev/README.md +49 -0
- package/QA/pmb-dev/config/config.yml +152 -0
- package/QA/pmb-dev/gdl/i.gdl +17 -0
- package/README.md +851 -2
- package/Todo.md +207 -0
- package/assets/branding/crudforge-logo.png +0 -0
- package/assets/branding/crudforge-mark.png +0 -0
- package/branding/poster-crudforge.jpg +0 -0
- package/chat/last-chat/Investigating Mock Data Configuration.md +428 -0
- package/cli.js +167 -0
- package/config/default-config.yml +245 -0
- package/docs/performance/README.md +217 -0
- package/docs/performance/after.json +111 -0
- package/docs/performance/before.json +111 -0
- package/docs/quality/README.md +276 -0
- package/e2e/crudforge.e2e.yml +47 -0
- package/e2e/crudforge.field-profile.yml +33 -0
- package/e2e/crudforge.iam-live.yml +124 -0
- package/e2e/crudforge.iam.yml +62 -0
- package/e2e/tests/field-profile.spec.js +152 -0
- package/e2e/tests/generated-crud.spec.js +251 -0
- package/e2e/tests/iam-live.spec.js +149 -0
- package/e2e/tests/iam-pack.spec.js +151 -0
- package/e2e/tests/list-state.spec.js +73 -0
- package/features.csv +27 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-mark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-mark.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.jpg +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark-source.png +0 -0
- package/fuse-starter-pack/branding/crudforge-wordmark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/Dockerfile +14 -0
- package/fuse-starter-pack/fuse-v21.1/LICENSE.md +6 -0
- package/fuse-starter-pack/fuse-v21.1/README.md +59 -0
- package/fuse-starter-pack/fuse-v21.1/angular.json +113 -0
- package/fuse-starter-pack/fuse-v21.1/eslint.config.ts +150 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.conf +23 -0
- package/fuse-starter-pack/fuse-v21.1/nginx.dev.conf +18 -0
- package/fuse-starter-pack/fuse-v21.1/package-lock.json +11513 -0
- package/fuse-starter-pack/fuse-v21.1/package.json +69 -0
- package/fuse-starter-pack/fuse-v21.1/postcss.config.json +5 -0
- package/fuse-starter-pack/fuse-v21.1/prettier.config.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/proxy.dev.conf.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/favicon.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono-Italic.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono.woff2 +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/en.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/i18n/es.json +8 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/ES.svg +79 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/US.svg +14 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/flags/where-to-find-other-flags.txt +5 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo-mark.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo.png +0 -0
- package/fuse-starter-pack/fuse-v21.1/push.sh +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.server.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.config.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.server.ts +8 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.ts +39 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/app.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/index.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/lucide.ts +1 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/provider.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/local-storage.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/index.ts +2 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/media.ts +64 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/media/provider.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/index.ts +3 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/models/theming.ts +13 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/palette.ts +209 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/provider.ts +24 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/theming.ts +129 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/core/transloco/transloco-http-loader.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/data/navigation.ts +176 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/layout.ts +77 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/language-switcher.ts +91 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/navigation.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/notifications.ts +252 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/scheme-switcher.ts +58 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/sidebar.ts +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/user.ts +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/academy.ts +753 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/model.ts +27 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/course.ts +318 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/courses.ts +228 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/routes.ts +14 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/contacts.ts +4372 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/model.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contact.ts +239 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contacts.ts +185 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/file-manager.ts +261 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/model.ts +12 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/features/file-manager.ts +131 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/routes.ts +15 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/ui/list.ts +164 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/faq.ts +288 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/guide.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/home.ts +219 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/support.ts +161 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/routes.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/model.ts +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/notes.ts +324 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/note.ts +152 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/notes.ts +118 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/model.ts +17 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/tasks.ts +562 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/task.ts +195 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/tasks.ts +271 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/analytics.ts +1620 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/finance.ts +1065 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/project.ts +376 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/analytics/analytics.ts +1096 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/finance/finance.ts +658 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/budget-table.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/project.ts +565 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/routes.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/changelog/changelog.ts +932 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/building.ts +33 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/development.ts +42 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/installation.ts +47 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/routes.ts +45 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/features/error-page.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/routes.ts +21 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/features/notifications.ts +262 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/account.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/notifications.ts +134 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/plan-and-billing.ts +238 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/security.ts +211 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/team.ts +188 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/layout.ts +109 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/routes.ts +37 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/welcome/welcome.component.ts +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/routes.ts +95 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.html +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.ts +51 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.html +82 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.ts +69 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.html +132 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.ts +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.html +183 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.ts +56 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/routes.ts +34 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/features/coming-soon.ts +156 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/routes.ts +16 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/layout/layout.ts +9 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/features/home.ts +23 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/routes.ts +10 -0
- package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/routes.ts +30 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.mock.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.prod.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/environments/environment.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/index.html +66 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.server.ts +11 -0
- package/fuse-starter-pack/fuse-v21.1/src/main.ts +5 -0
- package/fuse-starter-pack/fuse-v21.1/src/server.ts +70 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/base.css +63 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/base/typography.css +54 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/a.css +18 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/input.css +22 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/kbd.css +55 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/components/material.css +1149 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/styles.css +31 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/theme.css +147 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/utilities.css +563 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/variants.css +4 -0
- package/fuse-starter-pack/fuse-v21.1/src/styles/third-party/lucide-icons.css +12 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.app.json +16 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.json +37 -0
- package/fuse-starter-pack/fuse-v21.1/tsconfig.spec.json +10 -0
- package/generator/README.md +12 -0
- package/generator/issues/.gitkeep +0 -0
- package/package.json +56 -4
- package/playwright.config.js +29 -0
- package/playwright.field-profile.config.js +6 -0
- package/playwright.iam-live.config.js +31 -0
- package/playwright.iam.config.js +29 -0
- package/playwright.list-state.config.js +6 -0
- package/playwright.performance.config.js +1 -0
- package/run-config-tests.sh +5 -0
- package/scripts/e2e-paths.js +89 -0
- package/scripts/e2e-prepare-field-profile.js +17 -0
- package/scripts/e2e-prepare-iam-live.js +135 -0
- package/scripts/e2e-prepare-iam.js +65 -0
- package/scripts/e2e-prepare.js +72 -0
- package/scripts/e2e-serve-iam-live.js +28 -0
- package/scripts/e2e-serve-iam.js +28 -0
- package/scripts/e2e-serve.js +28 -0
- package/scripts/performance/generate.js +17 -0
- package/scripts/performance/measure.js +55 -0
- package/scripts/postinstall.js +9 -0
- package/scripts/prepare-quality-apps.js +34 -0
- package/scripts/run-config-tests.js +63 -0
- package/src/assets/logo-with-lable.jpeg +0 -0
- package/src/assets/logo.jpeg +0 -0
- package/src/commands/bootstrap.js +92 -0
- package/src/commands/doctor.js +403 -0
- package/src/commands/generate.js +301 -0
- package/src/commands/init-config.js +310 -0
- package/src/commands/keycloak-options.js +182 -0
- package/src/commands/validate.js +52 -0
- package/src/config/auth-config.js +183 -0
- package/src/config/default-values.js +235 -0
- package/src/config/env-refs.js +71 -0
- package/src/config/field-preferences.js +99 -0
- package/src/config/input-sources.js +104 -0
- package/src/config/load-config.js +166 -0
- package/src/config/proxy-routes.js +44 -0
- package/src/generator/access-control-tools.js +334 -0
- package/src/generator/ai-docs.js +691 -0
- package/src/generator/application-tests.js +252 -0
- package/src/generator/auth-pack.js +965 -0
- package/src/generator/embed-config.js +431 -0
- package/src/generator/entity-layout.js +30 -0
- package/src/generator/fuse-build.js +57 -0
- package/src/generator/fuse-runtime.js +344 -0
- package/src/generator/generated-banner.js +137 -0
- package/src/generator/index.js +874 -0
- package/src/generator/project-output.js +126 -0
- package/src/generator/team-pack.js +220 -0
- package/src/index.js +68 -0
- package/src/parser/gdl-parser.js +169 -0
- package/src/parser/jdl-parser.js +171 -0
- package/src/presets/pmb-test.js +139 -0
- package/src/presets/static-source.js +73 -0
- package/src/presets/store.js +96 -0
- package/src/provision/admin-service.js +148 -0
- package/src/provision/bootstrap.js +147 -0
- package/src/provision/http.js +123 -0
- package/src/provision/keycloak-admin-rest.js +335 -0
- package/src/provision/state.js +51 -0
- package/src/provision/token.js +52 -0
- package/src/templates/angular/auth/access-control.service.ts.ejs +179 -0
- package/src/templates/angular/auth/access-ctrl.pipe.ts.ejs +20 -0
- package/src/templates/angular/auth/auth.guard.ts.ejs +27 -0
- package/src/templates/angular/auth/auth.interceptor.ts.ejs +53 -0
- package/src/templates/angular/auth/auth.service.ts.ejs +594 -0
- package/src/templates/angular/auth/auth.utils.ts.ejs +28 -0
- package/src/templates/angular/auth/current-user.ts.ejs +24 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.css.ejs +95 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.html.ejs +33 -0
- package/src/templates/angular/auth/disabled-account-overlay.component.ts.ejs +23 -0
- package/src/templates/angular/auth/fuse-account.ts.ejs +234 -0
- package/src/templates/angular/auth/fuse-user.ts.ejs +264 -0
- package/src/templates/angular/auth/has-permission.directive.ts.ejs +51 -0
- package/src/templates/angular/auth/index.ts.ejs +24 -0
- package/src/templates/angular/auth/keycloak-admin.service.ts.ejs +670 -0
- package/src/templates/angular/auth/landing.guard.ts.ejs +61 -0
- package/src/templates/angular/auth/no-auth.guard.ts.ejs +19 -0
- package/src/templates/angular/auth/permission-attribute.util.ts.ejs +182 -0
- package/src/templates/angular/auth/sign-in.ts.ejs +60 -0
- package/src/templates/angular/entity/_entity_.model.ts.ejs +64 -0
- package/src/templates/angular/entity/_entity_.routes.ts.ejs +9 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.html.ejs +168 -0
- package/src/templates/angular/entity/components/forms/form/_entity_-form.component.ts.ejs +175 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.html.ejs +10 -0
- package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.ts.ejs +86 -0
- package/src/templates/angular/entity/components/forms/update/_entity_-form.service.ts.ejs +136 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.html.ejs +68 -0
- package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.ts.ejs +69 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.html.ejs +111 -0
- package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.ts.ejs +102 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.css.ejs +33 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.html.ejs +29 -0
- package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.ts.ejs +93 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.css.ejs +54 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.html.ejs +58 -0
- package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.ts.ejs +47 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs +419 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.scss.ejs +48 -0
- package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs +592 -0
- package/src/templates/angular/entity/service/_entity_.service.ts.ejs +153 -0
- package/src/templates/angular/entity/settings/_entity_.settings.ts.ejs +49 -0
- package/src/templates/angular/enums/_enum_.model.ts.ejs +11 -0
- package/src/templates/angular/iam/change-password.component.html.ejs +39 -0
- package/src/templates/angular/iam/change-password.component.ts.ejs +44 -0
- package/src/templates/angular/iam/no-access.component.ts.ejs +17 -0
- package/src/templates/angular/iam/role-list.component.html.ejs +331 -0
- package/src/templates/angular/iam/role-list.component.scss.ejs +107 -0
- package/src/templates/angular/iam/role-list.component.ts.ejs +597 -0
- package/src/templates/angular/iam/role-management/role-header.component.html.ejs +51 -0
- package/src/templates/angular/iam/role-management/role-header.component.ts.ejs +45 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.html.ejs +15 -0
- package/src/templates/angular/iam/role-management/role-inheritance.component.ts.ejs +19 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.css.ejs +24 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.html.ejs +108 -0
- package/src/templates/angular/iam/role-management/role-permissions-panel.component.ts.ejs +122 -0
- package/src/templates/angular/iam/role-management/role-users.component.html.ejs +104 -0
- package/src/templates/angular/iam/role-management/role-users.component.ts.ejs +53 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.html.ejs +113 -0
- package/src/templates/angular/iam/role-management/user-select-dialog.component.ts.ejs +157 -0
- package/src/templates/angular/iam/role-permission-matrix.component.html.ejs +355 -0
- package/src/templates/angular/iam/role-permission-matrix.component.scss.ejs +217 -0
- package/src/templates/angular/iam/role-permission-matrix.component.ts.ejs +800 -0
- package/src/templates/angular/iam/screen-catalog.ts.ejs +147 -0
- package/src/templates/angular/iam/shared/confirm-dialog.component.ts.ejs +83 -0
- package/src/templates/angular/iam/shared/matrix-hierarchy.util.ts.ejs +196 -0
- package/src/templates/angular/iam/shared/permission-matrix-sound.ts.ejs +59 -0
- package/src/templates/angular/iam/shared/role-display.util.ts.ejs +43 -0
- package/src/templates/angular/iam/shared/role-permission-skeleton.component.ts.ejs +54 -0
- package/src/templates/angular/iam/shared/screen-matrix.util.ts.ejs +554 -0
- package/src/templates/angular/iam/shared/screen-navigation-map.ts.ejs +187 -0
- package/src/templates/angular/iam/shared/screen-navigation.registry.ts.ejs +64 -0
- package/src/templates/angular/iam/shared/text-highlight.pipe.ts.ejs +39 -0
- package/src/templates/angular/iam/user-access-matrix.component.html.ejs +212 -0
- package/src/templates/angular/iam/user-access-matrix.component.scss.ejs +34 -0
- package/src/templates/angular/iam/user-access-matrix.component.ts.ejs +239 -0
- package/src/templates/angular/iam/user-detail.component.html.ejs +143 -0
- package/src/templates/angular/iam/user-detail.component.ts.ejs +108 -0
- package/src/templates/angular/iam/user-form.component.html.ejs +63 -0
- package/src/templates/angular/iam/user-form.component.ts.ejs +100 -0
- package/src/templates/angular/iam/user-list.component.html.ejs +369 -0
- package/src/templates/angular/iam/user-list.component.scss.ejs +242 -0
- package/src/templates/angular/iam/user-list.component.ts.ejs +446 -0
- package/src/templates/angular/iam/user-management.routes.ts.ejs +47 -0
- package/src/templates/angular/iam/user-permission-matrix.component.html.ejs +231 -0
- package/src/templates/angular/iam/user-permission-matrix.component.scss.ejs +216 -0
- package/src/templates/angular/iam/user-permission-matrix.component.ts.ejs +516 -0
- package/src/templates/angular/shared/base/audit-user-name.pipe.ts.ejs +69 -0
- package/src/templates/angular/shared/base/base-filter.component.ts.ejs +245 -0
- package/src/templates/angular/shared/base/base-form.component.ts.ejs +109 -0
- package/src/templates/angular/shared/base/base-list.component.ts.ejs +507 -0
- package/src/templates/angular/shared/base/filter-types.ts.ejs +55 -0
- package/src/templates/angular/shared/base/input-format.directive.ts.ejs +47 -0
- package/src/templates/angular/shared/base/input-format.ts.ejs +15 -0
- package/src/templates/angular/shared/confirm-with-input-dialog.component.ts.ejs +91 -0
- package/src/templates/angular/shared/confirmation-dialog.component.ts.ejs +80 -0
- package/src/templates/angular/shared/list-styles.css.ejs +103 -0
- package/src/templates/angular/shared/loading-overlay.component.ts.ejs +24 -0
- package/src/templates/angular/shared/resizable.directive.ts.ejs +115 -0
- package/src/templates/angular/shared/snackbar-styles.css.ejs +11 -0
- package/src/templates/fuse-packs/access-control-tools/access-control-sync.js +819 -0
- package/src/templates/fuse-packs/access-control-tools/build-rename-map.js +67 -0
- package/src/templates/fuse-packs/access-control-tools/inject-qa-ids.js +14 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.js +628 -0
- package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.test.js +323 -0
- package/src/templates/fuse-packs/access-control-tools/lib/html-scanner.js +362 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.duplicate.test.js +136 -0
- package/src/templates/fuse-packs/access-control-tools/lib/id-rules.js +1095 -0
- package/src/templates/fuse-packs/access-control-tools/lib/ts-component-index.js +482 -0
- package/src/templates/fuse-packs/access-control-tools/output/.gitkeep +0 -0
- package/src/templates/fuse-packs/access-control-tools/run.sh +223 -0
- package/src/templates/fuse-packs/access-control-tools/scan-access-control.js +14 -0
- package/src/templates/fuse-packs/iam-assets/sound/apple-play.mp3 +0 -0
- package/src/templates/fuse-packs/iam-assets/sound/remove.mp3 +0 -0
- package/src/templates/generator/README.md +12 -0
- package/src/templates/testing/ci.cjs +45 -0
- package/src/templates/testing/mock.cjs +149 -0
- package/src/templates/testing/playwright.config.cjs +25 -0
- package/src/templates/testing/serve.cjs +22 -0
- package/src/templates/testing/smoke.spec.cjs +180 -0
- package/src/utils/branding.js +39 -0
- package/src/utils/colors.js +18 -0
- package/src/utils/diff.js +109 -0
- package/src/utils/merger.js +115 -0
- package/src/validator/backend-diagnostics.js +30 -0
- package/src/validator/blueprint-validator.js +276 -0
- package/src/validator/diagnostics.js +608 -0
- package/src/validator/pattern.js +28 -0
- package/test/auth-interceptor.js +84 -0
- package/test/auth-provision-checks.js +297 -0
- package/test/field-preferences.js +145 -0
- package/test/fixtures/field-profile.gdl +7 -0
- package/test/fixtures/mock.jdl +17 -0
- package/test/fixtures/mock.png +0 -0
- package/test/fixtures/performance/config.yml +33 -0
- package/test/fixtures/performance/large.gdl +199 -0
- package/test/fixtures/performance/small.gdl +7 -0
- package/test/fixtures/quality/custom.yml +22 -0
- package/test/fixtures/quality/go-duck.yml +22 -0
- package/test/fixtures/quality/jhipster.yml +30 -0
- package/test/fixtures/quality/representative.gdl +14 -0
- package/test/list-state.js +138 -0
- package/test/micro-functions-layout.js +95 -0
- package/test/performance-generation.js +32 -0
- package/test/pmb-preset.js +127 -0
- package/test/project-workflow.js +111 -0
- package/test/quality-features.js +250 -0
- package/test/verify-commands.js +83 -0
- package/test/verify.js +959 -0
- package/test-files/GDL/example.gdl +15 -0
- package/test-files/JDL/example.jdl +15 -0
- package/test-files/config/crudforge.config.yml +127 -0
- package/test-run.sh +272 -0
package/test/verify.js
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { generateFromSource } = require('../src/index');
|
|
4
|
+
|
|
5
|
+
async function verify() {
|
|
6
|
+
console.log('🧪 Starting CRUDForge Verification Tests...');
|
|
7
|
+
|
|
8
|
+
const fixturePath = path.join(__dirname, 'fixtures', 'mock.jdl');
|
|
9
|
+
const outputDir = path.join(__dirname, 'output');
|
|
10
|
+
|
|
11
|
+
// Clean previous output
|
|
12
|
+
await fs.remove(outputDir);
|
|
13
|
+
|
|
14
|
+
const jdlContent = await fs.readFile(fixturePath, 'utf8');
|
|
15
|
+
|
|
16
|
+
console.log('🧪 Running generation with standard JDL fixture...');
|
|
17
|
+
const result = await generateFromSource(jdlContent, {
|
|
18
|
+
filePath: fixturePath,
|
|
19
|
+
outputDirectory: outputDir,
|
|
20
|
+
microservice: 'book-service'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
console.log('🧪 Checking generated files...');
|
|
24
|
+
|
|
25
|
+
// Define expected files
|
|
26
|
+
const expectedFiles = [
|
|
27
|
+
'generator/README.md',
|
|
28
|
+
'generator/issues/.gitkeep',
|
|
29
|
+
'entities/book/book.model.ts',
|
|
30
|
+
'entities/book/book.routes.ts',
|
|
31
|
+
'entities/book/service/book.service.ts',
|
|
32
|
+
'entities/book/components/forms/update/book-form.service.ts',
|
|
33
|
+
'entities/book/components/forms/form-page/book-form-page.component.ts',
|
|
34
|
+
'entities/book/components/forms/form-page/book-form-page.component.html',
|
|
35
|
+
'entities/book/components/presentation/detail/book-detail.component.ts',
|
|
36
|
+
'entities/book/components/presentation/detail/book-detail.component.html',
|
|
37
|
+
'entities/book/components/presentation/detail/book-detail.component.css',
|
|
38
|
+
'entities/book/components/presentation/list/book-list.component.ts',
|
|
39
|
+
'entities/book/components/presentation/list/book-list.component.html',
|
|
40
|
+
|
|
41
|
+
// Author files (no dates)
|
|
42
|
+
'entities/author/author.model.ts',
|
|
43
|
+
'entities/author/service/author.service.ts',
|
|
44
|
+
'entities/author/components/forms/update/author-form.service.ts'
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const relPath of expectedFiles) {
|
|
48
|
+
const fullPath = path.join(outputDir, relPath);
|
|
49
|
+
const exists = await fs.pathExists(fullPath);
|
|
50
|
+
if (!exists) {
|
|
51
|
+
throw new Error(`❌ File verification failed: Missing ${relPath}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
console.log('✅ All expected component files generated successfully.');
|
|
55
|
+
|
|
56
|
+
// Verify dates conversion bug fix
|
|
57
|
+
const authorServiceContent = await fs.readFile(path.join(outputDir, 'entities/author/service/author.service.ts'), 'utf8');
|
|
58
|
+
if (authorServiceContent.includes('Omit<T, >')) {
|
|
59
|
+
throw new Error('❌ Date type error validation failed: Generated author.service.ts contains invalid "Omit<T, >" type syntax.');
|
|
60
|
+
}
|
|
61
|
+
console.log('✅ author.service.ts Omit syntax type is correct (Omit<T, never>).');
|
|
62
|
+
|
|
63
|
+
// Real-API builds must not import mock files that are never generated.
|
|
64
|
+
if (authorServiceContent.includes('../mock/data') || authorServiceContent.includes('mockData')) {
|
|
65
|
+
throw new Error('Non-mock service references missing mock data.');
|
|
66
|
+
}
|
|
67
|
+
const mockOutput = await fs.mkdtemp(path.join(require('os').tmpdir(), 'crudforge-mock-service-'));
|
|
68
|
+
try {
|
|
69
|
+
await generateFromSource(jdlContent, {
|
|
70
|
+
filePath: fixturePath,
|
|
71
|
+
outputDirectory: mockOutput,
|
|
72
|
+
microservice: 'book-service',
|
|
73
|
+
mock: { enabled: true, recordsPerEntity: 2 },
|
|
74
|
+
});
|
|
75
|
+
const service = await fs.readFile(path.join(mockOutput, 'entities/author/service/author.service.ts'), 'utf8');
|
|
76
|
+
if (!service.includes("from '../mock/data'") ||
|
|
77
|
+
!await fs.pathExists(path.join(mockOutput, 'entities/author/mock/data.ts'))) {
|
|
78
|
+
throw new Error('Mock services must retain their generated mock data dependency.');
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
await fs.remove(mockOutput);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Verify text highlights XSS bug fix
|
|
85
|
+
const bookListContent = await fs.readFile(path.join(outputDir, 'entities/book/components/presentation/list/book-list.component.ts'), 'utf8');
|
|
86
|
+
const bookBaseListContent = await fs.readFile(path.join(outputDir, 'shared/base/base-list.component.ts'), 'utf8');
|
|
87
|
+
if (!bookBaseListContent.includes('.replace(/&/g, \'&\')')) {
|
|
88
|
+
throw new Error('❌ Security check failed: Book list component does not escape HTML text before highlighting.');
|
|
89
|
+
}
|
|
90
|
+
console.log('✅ highlightText security HTML escape logic is present.');
|
|
91
|
+
|
|
92
|
+
const bookSettingsContent = await fs.readFile(path.join(outputDir, 'entities/book/settings/settings.config.ts'), 'utf8');
|
|
93
|
+
if (!bookSettingsContent.includes("formMode: 'sidebar'") || !bookSettingsContent.includes("searchMode: 'field'")) {
|
|
94
|
+
throw new Error('❌ Settings check failed: book settings.config.ts is missing formMode sidebar or searchMode field.');
|
|
95
|
+
}
|
|
96
|
+
if (!bookListContent.includes('openFormFromQueryParams') || !bookListContent.includes('mountSidebarForm') || !bookListContent.includes('skipConfirm')) {
|
|
97
|
+
throw new Error('❌ Form mode check failed: book list does not keep edit and new on the same sidebar path.');
|
|
98
|
+
}
|
|
99
|
+
const bookFormPageContent = await fs.readFile(path.join(outputDir, 'entities/book/components/forms/form-page/book-form-page.component.ts'), 'utf8');
|
|
100
|
+
if (!bookFormPageContent.includes("Settings.formMode !== 'page'") || !bookFormPageContent.includes("form: 'edit'")) {
|
|
101
|
+
throw new Error('❌ Form mode check failed: form-page does not redirect sidebar-mode edits back to the list drawer.');
|
|
102
|
+
}
|
|
103
|
+
const bookActionsHtml = await fs.readFile(path.join(outputDir, 'entities/book/components/interaction/actions/book-actions.component.html'), 'utf8');
|
|
104
|
+
if (!bookActionsHtml.includes("searchMode === 'field'") || !bookActionsHtml.includes('Search all fields')) {
|
|
105
|
+
throw new Error('❌ Search mode check failed: actions toolbar is missing field/global search switch.');
|
|
106
|
+
}
|
|
107
|
+
console.log('✅ Form mode and search mode generation checks passed.');
|
|
108
|
+
|
|
109
|
+
// Verify JDL String Capitalization fix
|
|
110
|
+
const bookModelContent = await fs.readFile(path.join(outputDir, 'entities/book/book.model.ts'), 'utf8');
|
|
111
|
+
if (bookModelContent.includes('title?: String')) {
|
|
112
|
+
throw new Error('❌ Type capitalization check failed: Model interface maps JDL String to capitalized String class.');
|
|
113
|
+
}
|
|
114
|
+
console.log('✅ Model interface correctly maps String to primitive lowercase string.');
|
|
115
|
+
|
|
116
|
+
// Verify generator statistics
|
|
117
|
+
if (!result.stats || result.stats.entityCount !== 2 || typeof result.stats.fileCount !== 'number') {
|
|
118
|
+
throw new Error('❌ Stats verification failed: generateFromSource did not return expected execution stats.');
|
|
119
|
+
}
|
|
120
|
+
console.log(`✅ Generation statistics verified successfully. Processed: ${result.stats.entityCount} entities, ${result.stats.fileCount} files.`);
|
|
121
|
+
|
|
122
|
+
// Verify Colors utility under NO_COLOR override
|
|
123
|
+
delete require.cache[require.resolve('../src/utils/colors')];
|
|
124
|
+
process.env.NO_COLOR = '1';
|
|
125
|
+
const colorsNoColor = require('../src/utils/colors');
|
|
126
|
+
if (colorsNoColor.green('hello') !== 'hello') {
|
|
127
|
+
throw new Error('❌ Colors check failed: green() did not fall back to plain text when NO_COLOR is set.');
|
|
128
|
+
}
|
|
129
|
+
console.log('✅ Colors utility correctly respects process.env.NO_COLOR.');
|
|
130
|
+
|
|
131
|
+
delete require.cache[require.resolve('../src/utils/colors')];
|
|
132
|
+
delete process.env.NO_COLOR;
|
|
133
|
+
|
|
134
|
+
// Verify Branding configuration properties
|
|
135
|
+
const branding = require('../src/utils/branding');
|
|
136
|
+
const colors = require('../src/utils/colors');
|
|
137
|
+
if (!branding.BANNER.includes('CRUDForge') && !branding.BANNER.includes('___')) {
|
|
138
|
+
throw new Error('❌ Branding check failed: ASCII banner is missing.');
|
|
139
|
+
}
|
|
140
|
+
if (branding.DEVELOPER_INFO !== 'Developed by TeamCodeMe') {
|
|
141
|
+
throw new Error('❌ Branding check failed: Developer attribution is incorrect.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Verify Branding configuration options
|
|
145
|
+
console.log('🧪 Testing branding configuration settings...');
|
|
146
|
+
let loggedOutput = [];
|
|
147
|
+
const originalLog = console.log;
|
|
148
|
+
console.log = (...args) => {
|
|
149
|
+
loggedOutput.push(args.join(' '));
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
// 1. Test banner: false disables banner print
|
|
153
|
+
loggedOutput = [];
|
|
154
|
+
branding.printBanner({ branding: { banner: false } });
|
|
155
|
+
if (loggedOutput.length > 0) {
|
|
156
|
+
throw new Error('Banner was printed despite banner: false config preference.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Test bannerColor: 'red' uses red color formatting
|
|
160
|
+
loggedOutput = [];
|
|
161
|
+
branding.printBanner({ branding: { banner: true, bannerColor: 'red' } });
|
|
162
|
+
const expectedColor = colors.colorsEnabled ? '\x1b[31m' : '____';
|
|
163
|
+
if (loggedOutput.length === 0 || !loggedOutput[0].includes(expectedColor)) {
|
|
164
|
+
throw new Error('Banner was not printed in red color format as requested.');
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
console.log = originalLog;
|
|
168
|
+
}
|
|
169
|
+
console.log('✅ Branding banner configuration options validated successfully.');
|
|
170
|
+
console.log('✅ ASCII banner and developer info validated successfully.');
|
|
171
|
+
|
|
172
|
+
// Verify Blueprint Validator
|
|
173
|
+
const { validateBlueprint } = require('../src/validator/blueprint-validator');
|
|
174
|
+
|
|
175
|
+
// Test case 1: Valid JDL
|
|
176
|
+
const validJdl = `
|
|
177
|
+
entity Book {
|
|
178
|
+
title String required
|
|
179
|
+
pages Integer min(5)
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
const errors1 = validateBlueprint(validJdl, false);
|
|
183
|
+
if (errors1.length !== 0) {
|
|
184
|
+
throw new Error('❌ Validator check failed: valid JDL was reported as invalid.');
|
|
185
|
+
}
|
|
186
|
+
console.log('✅ Validator correctly marked valid JDL as clean.');
|
|
187
|
+
|
|
188
|
+
// Test case 2: Braces mismatch
|
|
189
|
+
const mismatchedBracesJdl = `
|
|
190
|
+
entity Book {
|
|
191
|
+
title String
|
|
192
|
+
`;
|
|
193
|
+
const errors2 = validateBlueprint(mismatchedBracesJdl, false);
|
|
194
|
+
if (errors2.length !== 1 || !errors2[0].includes('Mismatched curly braces')) {
|
|
195
|
+
throw new Error('❌ Validator check failed: mismatched curly braces error was not detected.');
|
|
196
|
+
}
|
|
197
|
+
console.log('✅ Validator correctly detected mismatched curly braces.');
|
|
198
|
+
|
|
199
|
+
// Test case 3: Invalid validation type constraints (Semantic error)
|
|
200
|
+
const invalidConstraintsJdl = `
|
|
201
|
+
entity Book {
|
|
202
|
+
title String min(5)
|
|
203
|
+
pages Integer minlength(3)
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
const errors3 = validateBlueprint(invalidConstraintsJdl, false);
|
|
207
|
+
if (errors3.length !== 2) {
|
|
208
|
+
throw new Error(`❌ Validator check failed: expected 2 semantic validation errors, got ${errors3.length}.`);
|
|
209
|
+
}
|
|
210
|
+
if (!errors3[0].includes('min') || !errors3[0].includes('column 20')) {
|
|
211
|
+
throw new Error('❌ Validator check failed: did not report correct error column for String min constraint.');
|
|
212
|
+
}
|
|
213
|
+
if (!errors3[1].includes('minlength') || !errors3[1].includes('column 21')) {
|
|
214
|
+
throw new Error('❌ Validator check failed: did not report correct error column for Integer minlength constraint.');
|
|
215
|
+
}
|
|
216
|
+
console.log('✅ Validator correctly flagged invalid validations and returned line & column caret details.');
|
|
217
|
+
|
|
218
|
+
// Test case 4: Undefined relationship entities (Semantic error)
|
|
219
|
+
const undefinedRelJdl = `
|
|
220
|
+
entity Book {
|
|
221
|
+
title String
|
|
222
|
+
}
|
|
223
|
+
relationship OneToOne {
|
|
224
|
+
Book to Library
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
const errors4 = validateBlueprint(undefinedRelJdl, false);
|
|
228
|
+
if (errors4.length !== 1 || !errors4[0].includes('Library') || !errors4[0].includes('is not defined')) {
|
|
229
|
+
throw new Error('❌ Validator check failed: undefined relationship target entity error was not detected.');
|
|
230
|
+
}
|
|
231
|
+
console.log('✅ Validator correctly caught relationship pointing to undefined entities.');
|
|
232
|
+
|
|
233
|
+
// Verify Dry-Run and Diff Preview
|
|
234
|
+
console.log('🧪 Testing dry-run and diff preview logic...');
|
|
235
|
+
const dryRunOutputDir = path.join(__dirname, 'output-dryrun');
|
|
236
|
+
await fs.remove(dryRunOutputDir);
|
|
237
|
+
|
|
238
|
+
// Load loadConfig and programmatic parser wrapper to run generation in dryRun
|
|
239
|
+
const { loadConfig } = require('../src/config/load-config');
|
|
240
|
+
const { parseJdl } = require('../src/parser/jdl-parser');
|
|
241
|
+
const { generate } = require('../src/generator');
|
|
242
|
+
|
|
243
|
+
const { config } = await loadConfig(process.cwd(), null);
|
|
244
|
+
config.dryRun = true;
|
|
245
|
+
config.output.directory = dryRunOutputDir;
|
|
246
|
+
config.api.microservice = 'book-service';
|
|
247
|
+
|
|
248
|
+
const parsed = parseJdl(jdlContent, config);
|
|
249
|
+
const dryRunStats = await generate(parsed, config);
|
|
250
|
+
|
|
251
|
+
// Ensure output directory is empty or does not exist
|
|
252
|
+
const folderExists = await fs.pathExists(dryRunOutputDir);
|
|
253
|
+
if (folderExists) {
|
|
254
|
+
const files = await fs.readdir(dryRunOutputDir);
|
|
255
|
+
if (files.length > 0) {
|
|
256
|
+
throw new Error('❌ Dry-run check failed: Files were written to disk when dryRun was enabled.');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
console.log('✅ Dry-run mode correctly bypassed filesystem writes.');
|
|
260
|
+
|
|
261
|
+
// Validate dryRunReport properties
|
|
262
|
+
if (!dryRunStats || !dryRunStats.dryRun || !dryRunStats.dryRunReport) {
|
|
263
|
+
throw new Error('❌ Dry-run check failed: stats.dryRunReport was not returned.');
|
|
264
|
+
}
|
|
265
|
+
const report = dryRunStats.dryRunReport;
|
|
266
|
+
if (report.newFiles.length === 0 || report.modifiedFiles.length !== 0) {
|
|
267
|
+
throw new Error('❌ Dry-run check failed: newFiles or modifiedFiles count in report is incorrect.');
|
|
268
|
+
}
|
|
269
|
+
console.log('✅ Dry-run report returned correct metrics.');
|
|
270
|
+
|
|
271
|
+
// Verify Diff formatter utility
|
|
272
|
+
const { formatDiff } = require('../src/utils/diff');
|
|
273
|
+
const oldText = 'line1\nline2\nline3';
|
|
274
|
+
const newText = 'line1\nlinechanged\nline3';
|
|
275
|
+
const diff = formatDiff(oldText, newText);
|
|
276
|
+
|
|
277
|
+
const hasAdded = diff.some(line => line.type === 'added' && line.text.includes('linechanged'));
|
|
278
|
+
const hasRemoved = diff.some(line => line.type === 'removed' && line.text.includes('line2'));
|
|
279
|
+
if (!hasAdded || !hasRemoved) {
|
|
280
|
+
throw new Error('❌ Diff utility check failed: line-level differences were not calculated correctly.');
|
|
281
|
+
}
|
|
282
|
+
console.log('✅ Diff utility computed and formatted unified diff successfully.');
|
|
283
|
+
|
|
284
|
+
// Verify Relationship Generation on Author components
|
|
285
|
+
console.log('🧪 Testing relationship generation logic...');
|
|
286
|
+
const authorFormContent = await fs.readFile(path.join(outputDir, 'entities/author/components/forms/form/author-form.component.ts'), 'utf8');
|
|
287
|
+
if (!authorFormContent.includes('BookService') || !authorFormContent.includes('booksSharedCollection') || !authorFormContent.includes('compareBook')) {
|
|
288
|
+
throw new Error('❌ Relationship check failed: AuthorForm component is missing Book relationship bindings.');
|
|
289
|
+
}
|
|
290
|
+
console.log('✅ AuthorForm component typescript correctly loads related Book collections.');
|
|
291
|
+
|
|
292
|
+
const authorFormHtml = await fs.readFile(path.join(outputDir, 'entities/author/components/forms/form/author-form.component.html'), 'utf8');
|
|
293
|
+
if (!authorFormHtml.includes('formControlName="book"') && !authorFormHtml.includes('booksSharedCollection')) {
|
|
294
|
+
throw new Error('❌ Relationship check failed: AuthorForm HTML is missing relationship select dropdown picker.');
|
|
295
|
+
}
|
|
296
|
+
console.log('✅ AuthorForm component HTML correctly renders relationship dropdown picker.');
|
|
297
|
+
|
|
298
|
+
const authorDetailHtml = await fs.readFile(path.join(outputDir, 'entities/author/components/presentation/detail/author-detail.component.html'), 'utf8');
|
|
299
|
+
if (!authorDetailHtml.includes('[routerLink]="[\'/book\'') || !authorDetailHtml.includes('item.id')) {
|
|
300
|
+
throw new Error('❌ Relationship check failed: AuthorDetail HTML is missing relationship detail links.');
|
|
301
|
+
}
|
|
302
|
+
console.log('✅ AuthorDetail component HTML correctly renders relationship router links.');
|
|
303
|
+
|
|
304
|
+
// Verify Feature Custom Config Preferences overrides
|
|
305
|
+
console.log('🧪 Testing custom configurability overrides...');
|
|
306
|
+
const configOutputDir = path.join(__dirname, 'output-config-test');
|
|
307
|
+
await fs.remove(configOutputDir);
|
|
308
|
+
await fs.ensureDir(configOutputDir);
|
|
309
|
+
|
|
310
|
+
// Invalid schemas must never bypass the shared fail-fast gate.
|
|
311
|
+
await require('node:assert/strict').rejects(generateFromSource('entity InvalidBook {', {
|
|
312
|
+
outputDirectory: configOutputDir, validation: false
|
|
313
|
+
}), /Unclosed/);
|
|
314
|
+
|
|
315
|
+
// 2. Test generate.relationships: false disables relationships rendering
|
|
316
|
+
const cleanRelationOutputDir = path.join(__dirname, 'output-no-relations');
|
|
317
|
+
await fs.remove(cleanRelationOutputDir);
|
|
318
|
+
await fs.ensureDir(cleanRelationOutputDir);
|
|
319
|
+
await generateFromSource(jdlContent, {
|
|
320
|
+
outputDirectory: cleanRelationOutputDir,
|
|
321
|
+
generate: {
|
|
322
|
+
relationships: false // disable relations rendering!
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const noRelAuthorFormContent = await fs.readFile(path.join(cleanRelationOutputDir, 'entities/author/components/forms/form/author-form.component.ts'), 'utf8');
|
|
327
|
+
if (noRelAuthorFormContent.includes('BookService') || noRelAuthorFormContent.includes('booksSharedCollection')) {
|
|
328
|
+
throw new Error('❌ Config preference failed: relationships: false did not disable TS imports/loaders.');
|
|
329
|
+
}
|
|
330
|
+
const noRelAuthorFormHtml = await fs.readFile(path.join(cleanRelationOutputDir, 'entities/author/components/forms/form/author-form.component.html'), 'utf8');
|
|
331
|
+
if (noRelAuthorFormHtml.includes('booksSharedCollection')) {
|
|
332
|
+
throw new Error('❌ Config preference failed: relationships: false did not disable select input picker HTML.');
|
|
333
|
+
}
|
|
334
|
+
const noRelAuthorDetailHtml = await fs.readFile(path.join(cleanRelationOutputDir, 'entities/author/components/presentation/detail/author-detail.component.html'), 'utf8');
|
|
335
|
+
if (noRelAuthorDetailHtml.includes('[routerLink]="[\'/book\'')) {
|
|
336
|
+
throw new Error('❌ Config preference failed: relationships: false did not disable detail view links.');
|
|
337
|
+
}
|
|
338
|
+
console.log('✅ Bypassed relationships generation successfully using generate.relationships: false preference.');
|
|
339
|
+
|
|
340
|
+
// Clean up config test folders
|
|
341
|
+
await fs.remove(configOutputDir);
|
|
342
|
+
await fs.remove(cleanRelationOutputDir);
|
|
343
|
+
|
|
344
|
+
// 3. Test Fuse Project Scaffolding & Generation
|
|
345
|
+
console.log('🧪 Testing Fuse Angular project generation...');
|
|
346
|
+
const fuseOutputDir = path.join(__dirname, 'output-fuse-test');
|
|
347
|
+
await fs.remove(fuseOutputDir);
|
|
348
|
+
await fs.ensureDir(fuseOutputDir);
|
|
349
|
+
|
|
350
|
+
const starterPackPath = path.resolve(__dirname, '../fuse-starter-pack/fuse-v21.1');
|
|
351
|
+
|
|
352
|
+
await generateFromSource(jdlContent, {
|
|
353
|
+
outputDirectory: fuseOutputDir,
|
|
354
|
+
fuse: {
|
|
355
|
+
enabled: true,
|
|
356
|
+
starterPackPath: starterPackPath,
|
|
357
|
+
cleanLayout: true,
|
|
358
|
+
theme: {
|
|
359
|
+
scheme: 'light',
|
|
360
|
+
primary: '#0f766e',
|
|
361
|
+
error: '#b91c1c'
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Verify that files were copied
|
|
367
|
+
if (!fs.existsSync(path.join(fuseOutputDir, 'tsconfig.json'))) {
|
|
368
|
+
throw new Error('❌ Fuse check failed: tsconfig.json was not copied.');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Verify that new assets (environments, Nginx, Docker, push.sh, proxy files) were copied
|
|
372
|
+
if (!fs.existsSync(path.join(fuseOutputDir, 'src/environments/environment.ts')) ||
|
|
373
|
+
!fs.existsSync(path.join(fuseOutputDir, 'src/environments/environment.prod.ts')) ||
|
|
374
|
+
!fs.existsSync(path.join(fuseOutputDir, 'src/environments/environment.mock.ts'))) {
|
|
375
|
+
throw new Error('❌ Fuse check failed: Environment files were not copied.');
|
|
376
|
+
}
|
|
377
|
+
if (!fs.existsSync(path.join(fuseOutputDir, 'proxy.dev.conf.json')) ||
|
|
378
|
+
!fs.existsSync(path.join(fuseOutputDir, 'proxy.conf.json'))) {
|
|
379
|
+
throw new Error('❌ Fuse check failed: Proxy configurations were not copied.');
|
|
380
|
+
}
|
|
381
|
+
if (!fs.existsSync(path.join(fuseOutputDir, 'nginx.conf')) ||
|
|
382
|
+
!fs.existsSync(path.join(fuseOutputDir, 'nginx.dev.conf'))) {
|
|
383
|
+
throw new Error('❌ Fuse check failed: Nginx configuration files were not copied.');
|
|
384
|
+
}
|
|
385
|
+
if (!fs.existsSync(path.join(fuseOutputDir, 'Dockerfile')) ||
|
|
386
|
+
!fs.existsSync(path.join(fuseOutputDir, 'push.sh'))) {
|
|
387
|
+
throw new Error('❌ Fuse check failed: Deployment Dockerfile or push.sh script was not copied.');
|
|
388
|
+
}
|
|
389
|
+
const appJson = require(path.join(fuseOutputDir, 'angular.json'));
|
|
390
|
+
if (!appJson.projects.fuse.architect.build.configurations.production.fileReplacements ||
|
|
391
|
+
!appJson.projects.fuse.architect.serve.configurations.production.proxyConfig) {
|
|
392
|
+
throw new Error('❌ Fuse check failed: angular.json was not updated with environment and proxy configurations.');
|
|
393
|
+
}
|
|
394
|
+
const pkgJson = require(path.join(fuseOutputDir, 'package.json'));
|
|
395
|
+
if (!pkgJson.scripts['dev'] || !pkgJson.scripts['start']) {
|
|
396
|
+
throw new Error('❌ Fuse check failed: package.json is missing environment scripts.');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Verify generic error page fallback routing
|
|
400
|
+
const appRoutesPath = path.join(fuseOutputDir, 'src/app/app.routes.ts');
|
|
401
|
+
const appRoutesContent = fs.readFileSync(appRoutesPath, 'utf8');
|
|
402
|
+
if (!appRoutesContent.includes('path: \'**\'') || !appRoutesContent.includes('error-page')) {
|
|
403
|
+
throw new Error('❌ Fuse routing check failed: Global wildcard fallback route is missing from app.routes.ts');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const adminRoutesPath = path.join(fuseOutputDir, 'src/app/domains/admin/routes.ts');
|
|
407
|
+
const adminRoutesContent = fs.readFileSync(adminRoutesPath, 'utf8');
|
|
408
|
+
if (!adminRoutesContent.includes('path: \'**\'') || !adminRoutesContent.includes('error-page')) {
|
|
409
|
+
throw new Error('❌ Fuse routing check failed: Scoped wildcard fallback route is missing from admin/routes.ts');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Verify that entity folder was created inside the correct Fuse modules path
|
|
413
|
+
const bookRoutesPath = path.join(fuseOutputDir, 'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/book.routes.ts');
|
|
414
|
+
if (!fs.existsSync(bookRoutesPath)) {
|
|
415
|
+
throw new Error('❌ Fuse check failed: entity book.routes.ts was not generated in microservice path.');
|
|
416
|
+
}
|
|
417
|
+
const fuseSharedConfirm = path.join(
|
|
418
|
+
fuseOutputDir,
|
|
419
|
+
'src/app/domains/admin/modules/apps/shared/components/confirmation-dialog.component.ts'
|
|
420
|
+
);
|
|
421
|
+
if (!fs.existsSync(fuseSharedConfirm)) {
|
|
422
|
+
throw new Error('❌ Fuse check failed: shared confirmation dialog was not hoisted to apps/shared.');
|
|
423
|
+
}
|
|
424
|
+
const fuseBookListHtml = await fs.readFile(
|
|
425
|
+
path.join(fuseOutputDir, 'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/components/presentation/list/book-list.component.html'),
|
|
426
|
+
'utf8'
|
|
427
|
+
);
|
|
428
|
+
if (!fuseBookListHtml.includes('data-screen-id="microservice/book"')) {
|
|
429
|
+
throw new Error('❌ Fuse check failed: book list is missing data-screen-id stamps.');
|
|
430
|
+
}
|
|
431
|
+
if (fuseBookListHtml.includes('accessCtrl')) {
|
|
432
|
+
throw new Error('❌ Fuse check failed: book list hides actions with accessCtrl when auth is off.');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Verify that routes.ts contains the generated routing entry
|
|
436
|
+
const mainRoutesPath = path.join(fuseOutputDir, 'src/app/domains/admin/routes.ts');
|
|
437
|
+
const mainRoutesContent = await fs.readFile(mainRoutesPath, 'utf8');
|
|
438
|
+
if (!mainRoutesContent.includes('loadChildren: () => import(\'./modules/apps/micro-functions/microservice/entities/book/book.routes\')')) {
|
|
439
|
+
throw new Error('❌ Fuse check failed: routes.ts is missing book lazy loading entry.');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Verify clean routes layout: other route paths (like dashboards, academy) should be removed
|
|
443
|
+
if (mainRoutesContent.includes('loadChildren: () => import(\'./modules/dashboards/routes\')') || mainRoutesContent.includes('path: \'academy\'')) {
|
|
444
|
+
throw new Error('❌ Fuse clean layout failed: routes.ts still contains pre-built dashboards or academy routes.');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Verify that routes backup file was created
|
|
448
|
+
if (!fs.existsSync(mainRoutesPath + '.bak')) {
|
|
449
|
+
throw new Error('❌ Fuse check failed: routes backup file was not created.');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Verify that navigation.ts contains the navigation items
|
|
453
|
+
const navPath = path.join(fuseOutputDir, 'src/app/domains/admin/layout/data/navigation.ts');
|
|
454
|
+
const navContent = await fs.readFile(navPath, 'utf8');
|
|
455
|
+
if (!navContent.includes('id: \'microservice-group\'') || !navContent.includes('route: \'/admin/book\'')) {
|
|
456
|
+
throw new Error('❌ Fuse check failed: navigation.ts is missing microservice menu bindings.');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Verify clean navigation layout: other default category entries (like dashboards) should be removed
|
|
460
|
+
if (navContent.includes('id: \'dashboards\'') || navContent.includes('id: \'documentation\'')) {
|
|
461
|
+
throw new Error('❌ Fuse clean layout failed: navigation.ts still contains pre-built dashboards or documentation options.');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Verify that navigation backup file was created
|
|
465
|
+
if (!fs.existsSync(navPath + '.bak')) {
|
|
466
|
+
throw new Error('❌ Fuse check failed: navigation backup file was not created.');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Verify sign-in page branding updates
|
|
470
|
+
const signInPath = path.join(fuseOutputDir, 'src/app/domains/auth/features/sign-in/sign-in.html');
|
|
471
|
+
const signInHtml = await fs.readFile(signInPath, 'utf8');
|
|
472
|
+
if (!signInHtml.includes('Welcome to CRUDForge') || !signInHtml.includes('CRUDForge logo')) {
|
|
473
|
+
throw new Error('❌ Fuse sign-in branding failed: sign-in.html was not updated with CRUDForge assets.');
|
|
474
|
+
}
|
|
475
|
+
if (
|
|
476
|
+
signInHtml.includes('Google') ||
|
|
477
|
+
signInHtml.includes('Or continue with') ||
|
|
478
|
+
signInHtml.includes('/images/photos/') ||
|
|
479
|
+
signInHtml.includes('You are browsing')
|
|
480
|
+
) {
|
|
481
|
+
throw new Error('❌ Fuse sign-in branding failed: demo chrome / stock photos still present on sign-in.');
|
|
482
|
+
}
|
|
483
|
+
if (!fs.existsSync(path.join(fuseOutputDir, 'public/images/logo/logo.png')) ||
|
|
484
|
+
!fs.existsSync(path.join(fuseOutputDir, 'public/images/logo/logo-mark.png'))) {
|
|
485
|
+
throw new Error('❌ Fuse branding failed: logo.png / logo-mark.png missing from public/images/logo.');
|
|
486
|
+
}
|
|
487
|
+
if (fs.existsSync(path.join(fuseOutputDir, 'public/images/photos')) ||
|
|
488
|
+
fs.existsSync(path.join(fuseOutputDir, 'public/images/cards'))) {
|
|
489
|
+
throw new Error('❌ Fuse branding failed: unused stock image folders (photos/cards) should not ship.');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Verify clean layout visual components
|
|
493
|
+
const layoutFilePath = path.join(fuseOutputDir, 'src/app/domains/admin/layout/layout.ts');
|
|
494
|
+
const layoutContent = await fs.readFile(layoutFilePath, 'utf8');
|
|
495
|
+
if (layoutContent.includes('BuilderKit') || layoutContent.includes('<language-switcher />')) {
|
|
496
|
+
throw new Error('❌ Fuse clean layout failed: layout.ts still contains promo banner or language flag switcher.');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const sidebarFilePath = path.join(fuseOutputDir, 'src/app/domains/admin/layout/ui/sidebar.ts');
|
|
500
|
+
const sidebarContent = await fs.readFile(sidebarFilePath, 'utf8');
|
|
501
|
+
if (sidebarContent.includes('Your trial is expiring soon') || sidebarContent.includes('Upgrade now')) {
|
|
502
|
+
throw new Error('❌ Fuse clean layout failed: sidebar.ts still contains expiring trial notification card.');
|
|
503
|
+
}
|
|
504
|
+
if (!sidebarContent.includes('/images/logo/logo.png') || !sidebarContent.includes('CRUDForge logo')) {
|
|
505
|
+
throw new Error('❌ Fuse clean layout failed: sidebar.ts is missing the CRUDForge wordmark logo.');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const fuseAppTs = await fs.readFile(path.join(fuseOutputDir, 'src/app/app.ts'), 'utf8');
|
|
509
|
+
if (!fuseAppTs.includes('export const fuseTheme') || !fuseAppTs.includes("primary: '#0f766e'") || !fuseAppTs.includes("error: '#b91c1c'")) {
|
|
510
|
+
throw new Error('❌ Fuse theme check failed: app.ts fuseTheme was not written from fuse.theme config.');
|
|
511
|
+
}
|
|
512
|
+
const fuseAppConfig = await fs.readFile(path.join(fuseOutputDir, 'src/app/app.config.ts'), 'utf8');
|
|
513
|
+
if (!fuseAppConfig.includes('provideTheming(fuseTheme)')) {
|
|
514
|
+
throw new Error('❌ Fuse theme check failed: app.config.ts does not use root fuseTheme.');
|
|
515
|
+
}
|
|
516
|
+
console.log('✅ Fuse root theme colors were applied from app.ts.');
|
|
517
|
+
|
|
518
|
+
// Symlink node_modules to run strict compilation
|
|
519
|
+
const outputNodeModules = path.join(fuseOutputDir, 'node_modules');
|
|
520
|
+
const sourceNodeModules = path.join(starterPackPath, 'node_modules');
|
|
521
|
+
if (fs.existsSync(sourceNodeModules)) {
|
|
522
|
+
await fs.symlink(sourceNodeModules, outputNodeModules, 'dir');
|
|
523
|
+
console.log('✅ Symlinked node_modules to output directory.');
|
|
524
|
+
|
|
525
|
+
// Execute tsc compilation
|
|
526
|
+
console.log('🧪 Running strict TypeScript compilation check on Fuse output...');
|
|
527
|
+
const execSync = require('child_process').execSync;
|
|
528
|
+
try {
|
|
529
|
+
execSync('npx tsc -p tsconfig.app.json --noEmit', { cwd: fuseOutputDir, stdio: 'pipe' });
|
|
530
|
+
console.log('✅ Strict TypeScript compilation check PASSED successfully.');
|
|
531
|
+
} catch (err) {
|
|
532
|
+
const errMsg = err.stdout ? err.stdout.toString() : err.message;
|
|
533
|
+
throw new Error('❌ Fuse compilation failed: ' + errMsg);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Clean up
|
|
538
|
+
await fs.remove(fuseOutputDir);
|
|
539
|
+
|
|
540
|
+
console.log('🧪 Testing Keycloak auth + IAM pack...');
|
|
541
|
+
const iamOutputDir = path.join(__dirname, 'output-iam-test');
|
|
542
|
+
await fs.remove(iamOutputDir);
|
|
543
|
+
await generateFromSource(jdlContent, {
|
|
544
|
+
outputDirectory: iamOutputDir,
|
|
545
|
+
fuse: {
|
|
546
|
+
enabled: true,
|
|
547
|
+
starterPackPath,
|
|
548
|
+
cleanLayout: true,
|
|
549
|
+
},
|
|
550
|
+
auth: {
|
|
551
|
+
provider: 'keycloak',
|
|
552
|
+
useMockAuth: true,
|
|
553
|
+
clientSecret: 'MUST-NOT-EMIT-SECRET',
|
|
554
|
+
},
|
|
555
|
+
iam: {
|
|
556
|
+
enabled: true,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const iamFiles = [
|
|
561
|
+
'src/app/core/auth/AGENT_README.md',
|
|
562
|
+
'src/app/core/auth/index.ts',
|
|
563
|
+
'src/app/core/auth/services/auth.service.ts',
|
|
564
|
+
'src/app/core/auth/services/access-control.service.ts',
|
|
565
|
+
'src/app/core/auth/services/keycloak-admin.service.ts',
|
|
566
|
+
'src/app/core/auth/interceptors/auth.interceptor.ts',
|
|
567
|
+
'src/app/core/auth/guards/auth.guard.ts',
|
|
568
|
+
'src/app/core/auth/guards/landing.guard.ts',
|
|
569
|
+
'src/app/core/auth/components/disabled-account-overlay/disabled-account-overlay.component.ts',
|
|
570
|
+
'src/app/core/auth/components/disabled-account-overlay/disabled-account-overlay.component.html',
|
|
571
|
+
'src/app/core/auth/components/disabled-account-overlay/disabled-account-overlay.component.css',
|
|
572
|
+
'src/app/domains/admin/modules/apps/shared/components/confirmation-dialog.component.ts',
|
|
573
|
+
'src/app/domains/admin/modules/apps/shared/components/confirm-with-input-dialog.component.ts',
|
|
574
|
+
'src/app/domains/admin/modules/apps/shared/components/loading-overlay.component.ts',
|
|
575
|
+
'src/app/domains/admin/modules/extras/no-access/no-access.component.ts',
|
|
576
|
+
'src/app/core/user-management/users/user-list/user-list.component.ts',
|
|
577
|
+
'src/app/core/user-management/users/user-list/user-list.component.html',
|
|
578
|
+
'src/app/core/user-management/users/user-list/user-list.component.scss',
|
|
579
|
+
'src/app/core/user-management/roles/role-list/role-list.component.ts',
|
|
580
|
+
'src/app/core/user-management/roles/role-list/role-list.component.html',
|
|
581
|
+
'src/app/core/user-management/roles/role-list/role-list.component.scss',
|
|
582
|
+
'src/app/core/user-management/roles/role-list/components/role-header/role-header.component.ts',
|
|
583
|
+
'src/app/core/user-management/roles/role-list/components/role-users/role-users.component.ts',
|
|
584
|
+
'src/app/core/user-management/roles/role-list/components/role-inheritance/role-inheritance.component.ts',
|
|
585
|
+
'src/app/core/user-management/roles/role-list/components/role-permissions-panel/role-permissions-panel.component.ts',
|
|
586
|
+
'src/app/core/user-management/roles/role-list/components/user-select-dialog/user-select-dialog.component.ts',
|
|
587
|
+
'src/app/core/user-management/shared/confirm-dialog.component.ts',
|
|
588
|
+
'src/app/core/user-management/matrices/role-permission-matrix/role-permission-matrix.component.ts',
|
|
589
|
+
'src/app/core/user-management/matrices/role-permission-matrix/role-permission-matrix.component.html',
|
|
590
|
+
'src/app/core/user-management/matrices/role-permission-matrix/role-permission-matrix.component.scss',
|
|
591
|
+
'src/app/core/user-management/shared/permission-matrix-sound.ts',
|
|
592
|
+
'public/assets/sound/apple-play.mp3',
|
|
593
|
+
'public/assets/sound/remove.mp3',
|
|
594
|
+
'src/app/core/user-management/AGENT_README.md',
|
|
595
|
+
'src/app/core/user-management/user-management.routes.ts',
|
|
596
|
+
'src/app/core/user-management/shared/screen-catalog.ts',
|
|
597
|
+
];
|
|
598
|
+
for (const relPath of iamFiles) {
|
|
599
|
+
if (!fs.existsSync(path.join(iamOutputDir, relPath))) {
|
|
600
|
+
throw new Error(`❌ IAM pack check failed: Missing ${relPath}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const iamEnv = await fs.readFile(path.join(iamOutputDir, 'src/environments/environment.ts'), 'utf8');
|
|
605
|
+
if (iamEnv.includes('kcClientSecret') || iamEnv.includes('MUST-NOT-EMIT-SECRET')) {
|
|
606
|
+
throw new Error('❌ IAM pack check failed: environment.ts must not contain kcClientSecret or provision secrets.');
|
|
607
|
+
}
|
|
608
|
+
const iamBookForm = await fs.readFile(
|
|
609
|
+
path.join(iamOutputDir, 'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/components/forms/form/book-form.component.ts'),
|
|
610
|
+
'utf8'
|
|
611
|
+
);
|
|
612
|
+
if (iamBookForm.includes('MUST-NOT-EMIT-SECRET') || iamBookForm.includes('"clientSecret"')) {
|
|
613
|
+
throw new Error('❌ IAM pack check failed: generated preferences must not embed auth.clientSecret.');
|
|
614
|
+
}
|
|
615
|
+
if (!iamEnv.includes('kcRealm') || !iamEnv.includes('useMockAuth: true')) {
|
|
616
|
+
throw new Error('❌ IAM pack check failed: environment.ts is missing Keycloak settings.');
|
|
617
|
+
}
|
|
618
|
+
if (
|
|
619
|
+
!iamEnv.includes('sessionIdleTimeoutMs: 0') ||
|
|
620
|
+
!iamEnv.includes('sessionRefreshOnFocus:') ||
|
|
621
|
+
!iamEnv.includes('disabledAccountEnabled: false')
|
|
622
|
+
) {
|
|
623
|
+
throw new Error('❌ IAM pack check failed: environment.ts is missing session / disabled-account settings.');
|
|
624
|
+
}
|
|
625
|
+
const iamAuthService = await fs.readFile(
|
|
626
|
+
path.join(iamOutputDir, 'src/app/core/auth/services/auth.service.ts'),
|
|
627
|
+
'utf8'
|
|
628
|
+
);
|
|
629
|
+
if (
|
|
630
|
+
!iamAuthService.includes('tryStartDisabledAccountLockFromAuthError') ||
|
|
631
|
+
!iamAuthService.includes('refreshTokenIfDue') ||
|
|
632
|
+
!iamAuthService.includes('SignOutReason')
|
|
633
|
+
) {
|
|
634
|
+
throw new Error('❌ IAM pack check failed: auth.service.ts missing session / disabled-account pack.');
|
|
635
|
+
}
|
|
636
|
+
const iamAppRoot = await fs.readFile(path.join(iamOutputDir, 'src/app/app.ts'), 'utf8');
|
|
637
|
+
if (
|
|
638
|
+
!iamAppRoot.includes('DisabledAccountOverlayComponent') ||
|
|
639
|
+
!iamAppRoot.includes('disabledAccountOverlayVisible')
|
|
640
|
+
) {
|
|
641
|
+
throw new Error('❌ IAM pack check failed: app.ts missing disabled-account overlay wiring.');
|
|
642
|
+
}
|
|
643
|
+
const iamAppConfig = await fs.readFile(path.join(iamOutputDir, 'src/app/app.config.ts'), 'utf8');
|
|
644
|
+
if (!iamAppConfig.includes('authInterceptor') || !iamAppConfig.includes('withInterceptors')) {
|
|
645
|
+
throw new Error('❌ IAM pack check failed: app.config.ts does not register authInterceptor.');
|
|
646
|
+
}
|
|
647
|
+
const iamSignIn = await fs.readFile(path.join(iamOutputDir, 'src/app/domains/auth/features/sign-in/sign-in.ts'), 'utf8');
|
|
648
|
+
if (!iamSignIn.includes('AuthService') || !iamSignIn.includes('signIn({ username')) {
|
|
649
|
+
throw new Error('❌ IAM pack check failed: sign-in.ts does not call Keycloak AuthService.');
|
|
650
|
+
}
|
|
651
|
+
const iamAdminRoutes = await fs.readFile(path.join(iamOutputDir, 'src/app/domains/admin/routes.ts'), 'utf8');
|
|
652
|
+
if (!iamAdminRoutes.includes('AuthGuard') || !iamAdminRoutes.includes('user-management')) {
|
|
653
|
+
throw new Error('❌ IAM pack check failed: admin routes missing AuthGuard or user-management.');
|
|
654
|
+
}
|
|
655
|
+
if (!iamAdminRoutes.includes('LandingGuard') || !iamAdminRoutes.includes('no-access')) {
|
|
656
|
+
throw new Error('❌ IAM pack check failed: admin routes missing LandingGuard or no-access.');
|
|
657
|
+
}
|
|
658
|
+
const entitySharedCopy = path.join(
|
|
659
|
+
iamOutputDir,
|
|
660
|
+
'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/shared/confirmation-dialog.component.ts'
|
|
661
|
+
);
|
|
662
|
+
if (fs.existsSync(entitySharedCopy)) {
|
|
663
|
+
throw new Error('❌ IAM pack check failed: Fuse still copied confirmation-dialog into the entity folder.');
|
|
664
|
+
}
|
|
665
|
+
const iamBookList = await fs.readFile(
|
|
666
|
+
path.join(iamOutputDir, 'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/components/presentation/list/book-list.component.html'),
|
|
667
|
+
'utf8'
|
|
668
|
+
);
|
|
669
|
+
if (!iamBookList.includes('data-screen-id="microservice/book"') || !iamBookList.includes("accessCtrl: 'edit'")) {
|
|
670
|
+
throw new Error('❌ IAM pack check failed: book list is missing accessCtrl stamps.');
|
|
671
|
+
}
|
|
672
|
+
const iamBookActions = await fs.readFile(
|
|
673
|
+
path.join(
|
|
674
|
+
iamOutputDir,
|
|
675
|
+
'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/components/interaction/actions/book-actions.component.html'
|
|
676
|
+
),
|
|
677
|
+
'utf8'
|
|
678
|
+
);
|
|
679
|
+
if (
|
|
680
|
+
!iamBookActions.includes("accessCtrl: 'create'") ||
|
|
681
|
+
!iamBookActions.includes("accessCtrl: 'open-filter'") ||
|
|
682
|
+
!iamBookActions.includes("accessCtrl: 'toggle-view'") ||
|
|
683
|
+
!iamBookActions.includes("accessCtrl: 'open-columns'")
|
|
684
|
+
) {
|
|
685
|
+
throw new Error('❌ IAM pack check failed: book actions missing semantic ACL ids (create/open-filter/toggle-view/open-columns).');
|
|
686
|
+
}
|
|
687
|
+
const aclToolsRoot = path.join(
|
|
688
|
+
iamOutputDir,
|
|
689
|
+
'src/app/domains/admin/modules/apps/shared/access-control-tools'
|
|
690
|
+
);
|
|
691
|
+
for (const rel of [
|
|
692
|
+
'access-control-sync.js',
|
|
693
|
+
'lib/id-rules.js',
|
|
694
|
+
'lib/ts-component-index.js',
|
|
695
|
+
'lib/hierarchy-scanner.js',
|
|
696
|
+
'run.sh',
|
|
697
|
+
'README.md',
|
|
698
|
+
'AGENT_README.md',
|
|
699
|
+
'AGENT_MEMORY.md',
|
|
700
|
+
'AGENTS.md',
|
|
701
|
+
]) {
|
|
702
|
+
if (!fs.existsSync(path.join(aclToolsRoot, rel))) {
|
|
703
|
+
throw new Error(`❌ Access-control tools pack missing: ${rel}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const iamPkg = JSON.parse(await fs.readFile(path.join(iamOutputDir, 'package.json'), 'utf8'));
|
|
707
|
+
if (!iamPkg.scripts?.['access-control:sync'] || !iamPkg.scripts?.['access-control:apply']) {
|
|
708
|
+
throw new Error('❌ Access-control tools pack missing npm scripts access-control:sync/apply.');
|
|
709
|
+
}
|
|
710
|
+
const roleMatrixTs = await fs.readFile(
|
|
711
|
+
path.join(iamOutputDir, 'src/app/core/user-management/matrices/role-permission-matrix/role-permission-matrix.component.ts'),
|
|
712
|
+
'utf8'
|
|
713
|
+
);
|
|
714
|
+
if (
|
|
715
|
+
!roleMatrixTs.includes('isUiLocked') ||
|
|
716
|
+
!roleMatrixTs.includes('playPermissionGrantSound') ||
|
|
717
|
+
!roleMatrixTs.includes('offline_access') ||
|
|
718
|
+
!roleMatrixTs.includes('setRoleScreenPermission')
|
|
719
|
+
) {
|
|
720
|
+
throw new Error('❌ Role permission matrix missing lock/sync/sound behavior.');
|
|
721
|
+
}
|
|
722
|
+
if (
|
|
723
|
+
!roleMatrixTs.includes('ScreenNavigationRegistry') ||
|
|
724
|
+
!roleMatrixTs.includes('buildMatrixHierarchy') ||
|
|
725
|
+
!roleMatrixTs.includes('grantPageEssentials') ||
|
|
726
|
+
!roleMatrixTs.includes('filterEssentialComponentIds')
|
|
727
|
+
) {
|
|
728
|
+
throw new Error('❌ Role permission matrix missing hierarchy / sensitive-action consumer.');
|
|
729
|
+
}
|
|
730
|
+
const navMapUtil = path.join(
|
|
731
|
+
iamOutputDir,
|
|
732
|
+
'src/app/core/user-management/shared/screen-navigation-map.ts'
|
|
733
|
+
);
|
|
734
|
+
const navRegistry = path.join(
|
|
735
|
+
iamOutputDir,
|
|
736
|
+
'src/app/core/user-management/shared/screen-navigation.registry.ts'
|
|
737
|
+
);
|
|
738
|
+
const matrixHier = path.join(
|
|
739
|
+
iamOutputDir,
|
|
740
|
+
'src/app/core/user-management/shared/matrix-hierarchy.util.ts'
|
|
741
|
+
);
|
|
742
|
+
const navAsset = path.join(iamOutputDir, 'public/assets/screen-navigation-map.json');
|
|
743
|
+
for (const f of [navMapUtil, navRegistry, matrixHier, navAsset]) {
|
|
744
|
+
if (!fs.existsSync(f)) {
|
|
745
|
+
throw new Error(`❌ IAM hierarchy consumer missing: ${path.relative(iamOutputDir, f)}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const acsPath = path.join(iamOutputDir, 'src/app/core/auth/services/access-control.service.ts');
|
|
749
|
+
if (!fs.existsSync(acsPath)) {
|
|
750
|
+
throw new Error('❌ AccessControlService missing at core/auth/services/.');
|
|
751
|
+
}
|
|
752
|
+
const acs = await fs.readFile(acsPath, 'utf8');
|
|
753
|
+
if (!acs.includes('ScreenNavigationRegistry') || !acs.includes('getParentHostScreenIds')) {
|
|
754
|
+
throw new Error('❌ AccessControlService missing ScreenNavigationRegistry host/embed fallbacks.');
|
|
755
|
+
}
|
|
756
|
+
const roleMatrixHtml = await fs.readFile(
|
|
757
|
+
path.join(iamOutputDir, 'src/app/core/user-management/matrices/role-permission-matrix/role-permission-matrix.component.html'),
|
|
758
|
+
'utf8'
|
|
759
|
+
);
|
|
760
|
+
if (!roleMatrixHtml.includes('overflow-auto') || !roleMatrixHtml.includes('permission-toggle-visual')) {
|
|
761
|
+
throw new Error('❌ Role permission matrix missing scrollable grid / PMB toggle visuals.');
|
|
762
|
+
}
|
|
763
|
+
if (!roleMatrixHtml.includes('Syncing Permissions') || !roleMatrixHtml.includes('matrix-enabled-fab')) {
|
|
764
|
+
throw new Error('❌ Role permission matrix missing PMB sync badge / enabled-only FAB.');
|
|
765
|
+
}
|
|
766
|
+
const accessMatrixHtml = await fs.readFile(
|
|
767
|
+
path.join(iamOutputDir, 'src/app/core/user-management/matrices/user-access-matrix/user-access-matrix.component.html'),
|
|
768
|
+
'utf8'
|
|
769
|
+
);
|
|
770
|
+
if (!accessMatrixHtml.includes('Assigned Roles') || !accessMatrixHtml.includes('Available Roles') || !accessMatrixHtml.includes('requestRoleToggle')) {
|
|
771
|
+
throw new Error('❌ User access matrix must follow PMB assigned/available role cards + confirm toggle.');
|
|
772
|
+
}
|
|
773
|
+
const screenCsv = await fs.readFile(path.join(iamOutputDir, 'public/assets/screen-components.csv'), 'utf8');
|
|
774
|
+
if (!/,create,BUTTON,/.test(screenCsv) || /,new,BUTTON,/.test(screenCsv)) {
|
|
775
|
+
throw new Error('❌ screen-components.csv should use semantic create (not legacy new) component ids.');
|
|
776
|
+
}
|
|
777
|
+
if (!/,open-filter,BUTTON,/.test(screenCsv) || !/,toggle-view,BUTTON,/.test(screenCsv)) {
|
|
778
|
+
throw new Error('❌ screen-components.csv missing open-filter / toggle-view semantic component ids.');
|
|
779
|
+
}
|
|
780
|
+
const iamNav = await fs.readFile(path.join(iamOutputDir, 'src/app/domains/admin/layout/data/navigation.ts'), 'utf8');
|
|
781
|
+
if (!iamNav.includes("id: 'identity-group'") || !iamNav.includes("label: 'User Management'") || !iamNav.includes('user-management')) {
|
|
782
|
+
throw new Error('❌ IAM pack check failed: navigation is missing the User Management group.');
|
|
783
|
+
}
|
|
784
|
+
const iamUserMenu = await fs.readFile(
|
|
785
|
+
path.join(iamOutputDir, 'src/app/domains/admin/layout/ui/user.ts'),
|
|
786
|
+
'utf8'
|
|
787
|
+
);
|
|
788
|
+
if (!iamUserMenu.includes('AuthService') || !iamUserMenu.includes("signOut('user')")) {
|
|
789
|
+
throw new Error('❌ IAM pack check failed: user menu Sign out must call AuthService.signOut(\'user\').');
|
|
790
|
+
}
|
|
791
|
+
if (
|
|
792
|
+
!iamUserMenu.includes('power-btn-container') ||
|
|
793
|
+
!iamUserMenu.includes('Super Admin') ||
|
|
794
|
+
!iamUserMenu.includes('confirmLogout') ||
|
|
795
|
+
!iamUserMenu.includes('AccessControlService')
|
|
796
|
+
) {
|
|
797
|
+
throw new Error('❌ IAM pack check failed: sidebar user chip must include PMB power logout + Super Admin badge.');
|
|
798
|
+
}
|
|
799
|
+
const iamSidebar = await fs.readFile(
|
|
800
|
+
path.join(iamOutputDir, 'src/app/domains/admin/layout/ui/sidebar.ts'),
|
|
801
|
+
'utf8'
|
|
802
|
+
);
|
|
803
|
+
if (!iamSidebar.includes('border-t') || !iamSidebar.includes('<user')) {
|
|
804
|
+
throw new Error('❌ IAM pack check failed: sidebar footer must host the bordered user chip.');
|
|
805
|
+
}
|
|
806
|
+
const embeddedConfig = path.join(iamOutputDir, 'config/crudforge.config.yml');
|
|
807
|
+
if (!fs.existsSync(embeddedConfig)) {
|
|
808
|
+
throw new Error('❌ Fuse generate must embed config/crudforge.config.yml.');
|
|
809
|
+
}
|
|
810
|
+
const embeddedText = await fs.readFile(embeddedConfig, 'utf8');
|
|
811
|
+
if (embeddedText.includes('MUST-NOT-EMIT-SECRET') || /clientSecret:\s*['"]?[^'"\n\s]+/.test(embeddedText)) {
|
|
812
|
+
throw new Error('❌ Embedded crudforge.config.yml must not contain client secrets.');
|
|
813
|
+
}
|
|
814
|
+
if (!embeddedText.includes('Generated at') || !embeddedText.includes('meta:') || !embeddedText.includes('generatedAt:')) {
|
|
815
|
+
throw new Error('❌ Embedded crudforge.config.yml must include a readable header and meta.generatedAt.');
|
|
816
|
+
}
|
|
817
|
+
if (!embeddedText.includes('# --- Project') || !embeddedText.includes('# --- Auth')) {
|
|
818
|
+
throw new Error('❌ Embedded crudforge.config.yml must use section comments for readability.');
|
|
819
|
+
}
|
|
820
|
+
const teamFiles = [
|
|
821
|
+
'AGENT_README.md',
|
|
822
|
+
'docs/ai/README.md',
|
|
823
|
+
'docs/ai/ARCHITECTURE.md',
|
|
824
|
+
'docs/ai/ENTITIES.md',
|
|
825
|
+
'docs/ai/ROUTES.md',
|
|
826
|
+
'docs/ai/UI_CONTRACTS.md',
|
|
827
|
+
'docs/ai/PRESERVATION.md',
|
|
828
|
+
'.crudforge/config.yaml',
|
|
829
|
+
'.cursor/agents/generator-engineer.md',
|
|
830
|
+
'.codex/agents/generator-engineer.md',
|
|
831
|
+
'.agents/agents/generator-engineer.md',
|
|
832
|
+
'.agents/rules/generator-engineer.md',
|
|
833
|
+
'.vscode/extensions/README.md',
|
|
834
|
+
'.vscode/extensions.json',
|
|
835
|
+
];
|
|
836
|
+
for (const rel of teamFiles) {
|
|
837
|
+
if (!fs.existsSync(path.join(iamOutputDir, rel))) {
|
|
838
|
+
throw new Error(`❌ GO-DUCK-style / keep-team pack missing: ${rel}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const entitiesDoc = await fs.readFile(path.join(iamOutputDir, 'docs/ai/ENTITIES.md'), 'utf8');
|
|
842
|
+
if (!entitiesDoc.includes('Generated by `@crudforge/cli`') && !entitiesDoc.includes('Generated by **`@crudforge/cli`**')) {
|
|
843
|
+
throw new Error('❌ docs/ai/ENTITIES.md must declare Generated by @crudforge/cli.');
|
|
844
|
+
}
|
|
845
|
+
if (entitiesDoc.includes('`undefined`: undefined') || entitiesDoc.includes('| `undefined` |')) {
|
|
846
|
+
throw new Error('❌ docs/ai/ENTITIES.md must use fieldName/fieldType (got undefined placeholders).');
|
|
847
|
+
}
|
|
848
|
+
if (!entitiesDoc.includes('`title`') || !entitiesDoc.includes('`Book`')) {
|
|
849
|
+
throw new Error('❌ docs/ai/ENTITIES.md must list real entity fields from the blueprint.');
|
|
850
|
+
}
|
|
851
|
+
const bookService = await fs.readFile(
|
|
852
|
+
path.join(iamOutputDir, 'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/service/book.service.ts'),
|
|
853
|
+
'utf8'
|
|
854
|
+
);
|
|
855
|
+
const bookListHtml = await fs.readFile(
|
|
856
|
+
path.join(
|
|
857
|
+
iamOutputDir,
|
|
858
|
+
'src/app/domains/admin/modules/apps/micro-functions/microservice/entities/book/components/presentation/list/book-list.component.html'
|
|
859
|
+
),
|
|
860
|
+
'utf8'
|
|
861
|
+
);
|
|
862
|
+
const sharedUtil = await fs.readFile(
|
|
863
|
+
path.join(iamOutputDir, 'src/app/domains/admin/modules/apps/shared/base/filter-types.ts'),
|
|
864
|
+
'utf8'
|
|
865
|
+
);
|
|
866
|
+
for (const [label, text] of [
|
|
867
|
+
['book.service.ts', bookService],
|
|
868
|
+
['book-list.component.html', bookListHtml],
|
|
869
|
+
['filter-types.ts', sharedUtil],
|
|
870
|
+
]) {
|
|
871
|
+
if (!text.includes('@generated by @crudforge/cli')) {
|
|
872
|
+
throw new Error(`❌ Generated Fuse file missing @crudforge/cli banner: ${label}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
console.log('✅ Keycloak auth + IAM pack generated core files, config/, docs/ai, and team pack.');
|
|
876
|
+
await fs.remove(iamOutputDir);
|
|
877
|
+
|
|
878
|
+
// Verify Overwrite Protection and Safe Merging
|
|
879
|
+
console.log('🧪 Testing overwrite protection and safe merging policies...');
|
|
880
|
+
const policyOutputDir = path.join(__dirname, 'output-policy');
|
|
881
|
+
await fs.remove(policyOutputDir);
|
|
882
|
+
await fs.ensureDir(policyOutputDir);
|
|
883
|
+
|
|
884
|
+
// Pre-seed an existing file with custom code blocks
|
|
885
|
+
const targetFilePath = path.join(policyOutputDir, 'entities/book/book.model.ts');
|
|
886
|
+
await fs.ensureDir(path.dirname(targetFilePath));
|
|
887
|
+
|
|
888
|
+
const existingWithCustomCode = `
|
|
889
|
+
import dayjs from 'dayjs/esm';
|
|
890
|
+
export interface IBook {
|
|
891
|
+
id?: number;
|
|
892
|
+
}
|
|
893
|
+
// cforge-custom-code-start:model-additions
|
|
894
|
+
export const MY_CUSTOM_CONSTANT = 'hello';
|
|
895
|
+
// cforge-custom-code-end
|
|
896
|
+
`.trim();
|
|
897
|
+
await fs.writeFile(targetFilePath, existingWithCustomCode, 'utf8');
|
|
898
|
+
|
|
899
|
+
// Load parser and loadConfig
|
|
900
|
+
const policyConfig = (await loadConfig(process.cwd(), null)).config;
|
|
901
|
+
policyConfig.output.directory = policyOutputDir;
|
|
902
|
+
policyConfig.api.microservice = 'book-service';
|
|
903
|
+
|
|
904
|
+
// Test case 1: 'skip' policy
|
|
905
|
+
policyConfig.output.overwritePolicy = 'skip';
|
|
906
|
+
const parsedForPolicy = parseJdl(jdlContent, policyConfig);
|
|
907
|
+
await generate(parsedForPolicy, policyConfig);
|
|
908
|
+
const skipContent = await fs.readFile(targetFilePath, 'utf8');
|
|
909
|
+
if (skipContent !== existingWithCustomCode) {
|
|
910
|
+
throw new Error('❌ Overwrite policy "skip" failed: File was overwritten.');
|
|
911
|
+
}
|
|
912
|
+
console.log('✅ Overwrite policy "skip" successfully preserved existing file.');
|
|
913
|
+
|
|
914
|
+
// Test case 2: 'fail' policy
|
|
915
|
+
policyConfig.output.overwritePolicy = 'fail';
|
|
916
|
+
try {
|
|
917
|
+
await generate(parsedForPolicy, policyConfig);
|
|
918
|
+
throw new Error('❌ Overwrite policy "fail" failed: Did not throw expected conflict error.');
|
|
919
|
+
} catch (err) {
|
|
920
|
+
if (!err.message.includes('Conflict Error')) {
|
|
921
|
+
throw err;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
console.log('✅ Overwrite policy "fail" correctly aborted and threw conflict error.');
|
|
925
|
+
|
|
926
|
+
// Test case 3: 'merge' policy
|
|
927
|
+
policyConfig.output.overwritePolicy = 'merge';
|
|
928
|
+
await generate(parsedForPolicy, policyConfig);
|
|
929
|
+
const mergedContent = await fs.readFile(targetFilePath, 'utf8');
|
|
930
|
+
if (!mergedContent.includes('export const MY_CUSTOM_CONSTANT = \'hello\';')) {
|
|
931
|
+
throw new Error('❌ Overwrite policy "merge" failed: Developer custom code was not preserved.');
|
|
932
|
+
}
|
|
933
|
+
if (!mergedContent.includes('title?: string | null;')) {
|
|
934
|
+
throw new Error('❌ Overwrite policy "merge" failed: New pristine fields were not generated.');
|
|
935
|
+
}
|
|
936
|
+
console.log('✅ Overwrite policy "merge" correctly preserved custom code block zones and merged new fields.');
|
|
937
|
+
|
|
938
|
+
// Test case 4: 'ask' policy returns conflict objects
|
|
939
|
+
policyConfig.output.overwritePolicy = 'ask';
|
|
940
|
+
// Re-seed file with changes to trigger conflict
|
|
941
|
+
const modifiedSeed = existingWithCustomCode + '\n// trigger diff';
|
|
942
|
+
await fs.writeFile(targetFilePath, modifiedSeed, 'utf8');
|
|
943
|
+
const askStats = await generate(parsedForPolicy, policyConfig);
|
|
944
|
+
const bookConflict = askStats.conflicts && askStats.conflicts.find(c => c.relativePath === 'entities/book/book.model.ts');
|
|
945
|
+
if (!bookConflict) {
|
|
946
|
+
throw new Error('❌ Overwrite policy "ask" failed: Did not report expected conflict for book.model.ts.');
|
|
947
|
+
}
|
|
948
|
+
console.log('✅ Overwrite policy "ask" correctly accumulated and returned conflict stats.');
|
|
949
|
+
|
|
950
|
+
const { runAuthProvisionChecks } = require('./auth-provision-checks');
|
|
951
|
+
await runAuthProvisionChecks();
|
|
952
|
+
|
|
953
|
+
console.log('🎉 CRUDForge Rebuild verification PASSED successfully!');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
verify().catch(err => {
|
|
957
|
+
console.error(err.message);
|
|
958
|
+
process.exit(1);
|
|
959
|
+
});
|