@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.
Files changed (415) hide show
  1. package/.cursor/agents/generator-engineer.md +31 -0
  2. package/.cursor/agents/keycloak-engineer.md +33 -0
  3. package/.cursor/agents/qa-engineer.md +32 -0
  4. package/.cursor/agents/security-reviewer.md +30 -0
  5. package/.cursor/mcp.json +13 -0
  6. package/.cursor/rules/crudforge-precautions.mdc +16 -0
  7. package/.cursor/rules/playwright-browser.mdc +30 -0
  8. package/.cursor/rules/readme-todo-sync.mdc +13 -0
  9. package/.env.qa.example +19 -0
  10. package/.github/workflows/qa-live.yml +32 -0
  11. package/.github/workflows/qa.yml +27 -0
  12. package/.vscode/settings.json +32 -0
  13. package/QA/IAM-PLAN.md +25 -0
  14. package/QA/IAM-RESULTS.md +39 -0
  15. package/QA/IAM-TESTCASES.md +36 -0
  16. package/QA/IAM-TODO.md +7 -0
  17. package/QA/KEYCLOAK-BOOTSTRAP.md +105 -0
  18. package/QA/PLAN.md +43 -0
  19. package/QA/RESULTS.md +59 -0
  20. package/QA/TESTCASES.md +65 -0
  21. package/QA/TODO.md +14 -0
  22. package/QA/pmb-dev/README.md +49 -0
  23. package/QA/pmb-dev/config/config.yml +152 -0
  24. package/QA/pmb-dev/gdl/i.gdl +17 -0
  25. package/README.md +851 -2
  26. package/Todo.md +207 -0
  27. package/assets/branding/crudforge-logo.png +0 -0
  28. package/assets/branding/crudforge-mark.png +0 -0
  29. package/branding/poster-crudforge.jpg +0 -0
  30. package/chat/last-chat/Investigating Mock Data Configuration.md +428 -0
  31. package/cli.js +167 -0
  32. package/config/default-config.yml +245 -0
  33. package/docs/performance/README.md +217 -0
  34. package/docs/performance/after.json +111 -0
  35. package/docs/performance/before.json +111 -0
  36. package/docs/quality/README.md +276 -0
  37. package/e2e/crudforge.e2e.yml +47 -0
  38. package/e2e/crudforge.field-profile.yml +33 -0
  39. package/e2e/crudforge.iam-live.yml +124 -0
  40. package/e2e/crudforge.iam.yml +62 -0
  41. package/e2e/tests/field-profile.spec.js +152 -0
  42. package/e2e/tests/generated-crud.spec.js +251 -0
  43. package/e2e/tests/iam-live.spec.js +149 -0
  44. package/e2e/tests/iam-pack.spec.js +151 -0
  45. package/e2e/tests/list-state.spec.js +73 -0
  46. package/features.csv +27 -0
  47. package/fuse-starter-pack/branding/crudforge-mark-source.jpg +0 -0
  48. package/fuse-starter-pack/branding/crudforge-mark-source.png +0 -0
  49. package/fuse-starter-pack/branding/crudforge-mark.png +0 -0
  50. package/fuse-starter-pack/branding/crudforge-wordmark-source.jpg +0 -0
  51. package/fuse-starter-pack/branding/crudforge-wordmark-source.png +0 -0
  52. package/fuse-starter-pack/branding/crudforge-wordmark.png +0 -0
  53. package/fuse-starter-pack/fuse-v21.1/Dockerfile +14 -0
  54. package/fuse-starter-pack/fuse-v21.1/LICENSE.md +6 -0
  55. package/fuse-starter-pack/fuse-v21.1/README.md +59 -0
  56. package/fuse-starter-pack/fuse-v21.1/angular.json +113 -0
  57. package/fuse-starter-pack/fuse-v21.1/eslint.config.ts +150 -0
  58. package/fuse-starter-pack/fuse-v21.1/nginx.conf +23 -0
  59. package/fuse-starter-pack/fuse-v21.1/nginx.dev.conf +18 -0
  60. package/fuse-starter-pack/fuse-v21.1/package-lock.json +11513 -0
  61. package/fuse-starter-pack/fuse-v21.1/package.json +69 -0
  62. package/fuse-starter-pack/fuse-v21.1/postcss.config.json +5 -0
  63. package/fuse-starter-pack/fuse-v21.1/prettier.config.ts +15 -0
  64. package/fuse-starter-pack/fuse-v21.1/proxy.conf.json +8 -0
  65. package/fuse-starter-pack/fuse-v21.1/proxy.dev.conf.json +8 -0
  66. package/fuse-starter-pack/fuse-v21.1/public/favicon.png +0 -0
  67. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist-Italic.woff2 +0 -0
  68. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist.woff2 +0 -0
  69. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono-Italic.woff2 +0 -0
  70. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono.woff2 +0 -0
  71. package/fuse-starter-pack/fuse-v21.1/public/i18n/en.json +8 -0
  72. package/fuse-starter-pack/fuse-v21.1/public/i18n/es.json +8 -0
  73. package/fuse-starter-pack/fuse-v21.1/public/images/flags/ES.svg +79 -0
  74. package/fuse-starter-pack/fuse-v21.1/public/images/flags/US.svg +14 -0
  75. package/fuse-starter-pack/fuse-v21.1/public/images/flags/where-to-find-other-flags.txt +5 -0
  76. package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo-mark.png +0 -0
  77. package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo.png +0 -0
  78. package/fuse-starter-pack/fuse-v21.1/push.sh +27 -0
  79. package/fuse-starter-pack/fuse-v21.1/src/app/app.config.server.ts +10 -0
  80. package/fuse-starter-pack/fuse-v21.1/src/app/app.config.ts +64 -0
  81. package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.server.ts +8 -0
  82. package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.ts +39 -0
  83. package/fuse-starter-pack/fuse-v21.1/src/app/app.ts +22 -0
  84. package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/index.ts +1 -0
  85. package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/lucide.ts +1 -0
  86. package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/provider.ts +23 -0
  87. package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/index.ts +2 -0
  88. package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/local-storage.ts +109 -0
  89. package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/provider.ts +15 -0
  90. package/fuse-starter-pack/fuse-v21.1/src/app/core/media/index.ts +2 -0
  91. package/fuse-starter-pack/fuse-v21.1/src/app/core/media/media.ts +64 -0
  92. package/fuse-starter-pack/fuse-v21.1/src/app/core/media/provider.ts +15 -0
  93. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/index.ts +3 -0
  94. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/models/theming.ts +13 -0
  95. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/palette.ts +209 -0
  96. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/provider.ts +24 -0
  97. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/theming.ts +129 -0
  98. package/fuse-starter-pack/fuse-v21.1/src/app/core/transloco/transloco-http-loader.ts +12 -0
  99. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/data/navigation.ts +176 -0
  100. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/layout.ts +77 -0
  101. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/language-switcher.ts +91 -0
  102. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/navigation.ts +238 -0
  103. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/notifications.ts +252 -0
  104. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/scheme-switcher.ts +58 -0
  105. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/sidebar.ts +31 -0
  106. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/user.ts +132 -0
  107. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/academy.ts +753 -0
  108. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/model.ts +27 -0
  109. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/course.ts +318 -0
  110. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/courses.ts +228 -0
  111. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/routes.ts +14 -0
  112. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/contacts.ts +4372 -0
  113. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/model.ts +34 -0
  114. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contact.ts +239 -0
  115. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contacts.ts +185 -0
  116. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/routes.ts +16 -0
  117. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/file-manager.ts +261 -0
  118. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/model.ts +12 -0
  119. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/features/file-manager.ts +131 -0
  120. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/routes.ts +15 -0
  121. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/ui/list.ts +164 -0
  122. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/faq.ts +288 -0
  123. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/guide.ts +134 -0
  124. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/home.ts +219 -0
  125. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/support.ts +161 -0
  126. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/routes.ts +33 -0
  127. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/model.ts +18 -0
  128. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/notes.ts +324 -0
  129. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/note.ts +152 -0
  130. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/notes.ts +118 -0
  131. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/routes.ts +16 -0
  132. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/model.ts +17 -0
  133. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/tasks.ts +562 -0
  134. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/task.ts +195 -0
  135. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/tasks.ts +271 -0
  136. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/routes.ts +16 -0
  137. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/analytics.ts +1620 -0
  138. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/finance.ts +1065 -0
  139. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/project.ts +376 -0
  140. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/analytics/analytics.ts +1096 -0
  141. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/finance/finance.ts +658 -0
  142. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/budget-table.ts +188 -0
  143. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/project.ts +565 -0
  144. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/routes.ts +23 -0
  145. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/changelog/changelog.ts +932 -0
  146. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/building.ts +33 -0
  147. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/development.ts +42 -0
  148. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/installation.ts +47 -0
  149. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/layout.ts +9 -0
  150. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/routes.ts +45 -0
  151. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/features/error-page.ts +70 -0
  152. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/routes.ts +21 -0
  153. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/features/notifications.ts +262 -0
  154. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/routes.ts +10 -0
  155. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/account.ts +238 -0
  156. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/notifications.ts +134 -0
  157. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/plan-and-billing.ts +238 -0
  158. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/security.ts +211 -0
  159. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/team.ts +188 -0
  160. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/layout.ts +109 -0
  161. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/routes.ts +37 -0
  162. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/welcome/welcome.component.ts +22 -0
  163. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/routes.ts +95 -0
  164. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.html +54 -0
  165. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.ts +51 -0
  166. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.html +82 -0
  167. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.ts +69 -0
  168. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.html +132 -0
  169. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.ts +54 -0
  170. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.html +183 -0
  171. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.ts +56 -0
  172. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/layout.ts +9 -0
  173. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/routes.ts +34 -0
  174. package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/features/coming-soon.ts +156 -0
  175. package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/layout.ts +9 -0
  176. package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/routes.ts +16 -0
  177. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/layout/layout.ts +9 -0
  178. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/features/home.ts +23 -0
  179. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/routes.ts +10 -0
  180. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/routes.ts +30 -0
  181. package/fuse-starter-pack/fuse-v21.1/src/environments/environment.mock.ts +5 -0
  182. package/fuse-starter-pack/fuse-v21.1/src/environments/environment.prod.ts +5 -0
  183. package/fuse-starter-pack/fuse-v21.1/src/environments/environment.ts +5 -0
  184. package/fuse-starter-pack/fuse-v21.1/src/index.html +66 -0
  185. package/fuse-starter-pack/fuse-v21.1/src/main.server.ts +11 -0
  186. package/fuse-starter-pack/fuse-v21.1/src/main.ts +5 -0
  187. package/fuse-starter-pack/fuse-v21.1/src/server.ts +70 -0
  188. package/fuse-starter-pack/fuse-v21.1/src/styles/base/base.css +63 -0
  189. package/fuse-starter-pack/fuse-v21.1/src/styles/base/typography.css +54 -0
  190. package/fuse-starter-pack/fuse-v21.1/src/styles/components/a.css +18 -0
  191. package/fuse-starter-pack/fuse-v21.1/src/styles/components/input.css +22 -0
  192. package/fuse-starter-pack/fuse-v21.1/src/styles/components/kbd.css +55 -0
  193. package/fuse-starter-pack/fuse-v21.1/src/styles/components/material.css +1149 -0
  194. package/fuse-starter-pack/fuse-v21.1/src/styles/styles.css +31 -0
  195. package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/theme.css +147 -0
  196. package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/utilities.css +563 -0
  197. package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/variants.css +4 -0
  198. package/fuse-starter-pack/fuse-v21.1/src/styles/third-party/lucide-icons.css +12 -0
  199. package/fuse-starter-pack/fuse-v21.1/tsconfig.app.json +16 -0
  200. package/fuse-starter-pack/fuse-v21.1/tsconfig.json +37 -0
  201. package/fuse-starter-pack/fuse-v21.1/tsconfig.spec.json +10 -0
  202. package/generator/README.md +12 -0
  203. package/generator/issues/.gitkeep +0 -0
  204. package/package.json +56 -4
  205. package/playwright.config.js +29 -0
  206. package/playwright.field-profile.config.js +6 -0
  207. package/playwright.iam-live.config.js +31 -0
  208. package/playwright.iam.config.js +29 -0
  209. package/playwright.list-state.config.js +6 -0
  210. package/playwright.performance.config.js +1 -0
  211. package/run-config-tests.sh +5 -0
  212. package/scripts/e2e-paths.js +89 -0
  213. package/scripts/e2e-prepare-field-profile.js +17 -0
  214. package/scripts/e2e-prepare-iam-live.js +135 -0
  215. package/scripts/e2e-prepare-iam.js +65 -0
  216. package/scripts/e2e-prepare.js +72 -0
  217. package/scripts/e2e-serve-iam-live.js +28 -0
  218. package/scripts/e2e-serve-iam.js +28 -0
  219. package/scripts/e2e-serve.js +28 -0
  220. package/scripts/performance/generate.js +17 -0
  221. package/scripts/performance/measure.js +55 -0
  222. package/scripts/postinstall.js +9 -0
  223. package/scripts/prepare-quality-apps.js +34 -0
  224. package/scripts/run-config-tests.js +63 -0
  225. package/src/assets/logo-with-lable.jpeg +0 -0
  226. package/src/assets/logo.jpeg +0 -0
  227. package/src/commands/bootstrap.js +92 -0
  228. package/src/commands/doctor.js +403 -0
  229. package/src/commands/generate.js +301 -0
  230. package/src/commands/init-config.js +310 -0
  231. package/src/commands/keycloak-options.js +182 -0
  232. package/src/commands/validate.js +52 -0
  233. package/src/config/auth-config.js +183 -0
  234. package/src/config/default-values.js +235 -0
  235. package/src/config/env-refs.js +71 -0
  236. package/src/config/field-preferences.js +99 -0
  237. package/src/config/input-sources.js +104 -0
  238. package/src/config/load-config.js +166 -0
  239. package/src/config/proxy-routes.js +44 -0
  240. package/src/generator/access-control-tools.js +334 -0
  241. package/src/generator/ai-docs.js +691 -0
  242. package/src/generator/application-tests.js +252 -0
  243. package/src/generator/auth-pack.js +965 -0
  244. package/src/generator/embed-config.js +431 -0
  245. package/src/generator/entity-layout.js +30 -0
  246. package/src/generator/fuse-build.js +57 -0
  247. package/src/generator/fuse-runtime.js +344 -0
  248. package/src/generator/generated-banner.js +137 -0
  249. package/src/generator/index.js +874 -0
  250. package/src/generator/project-output.js +126 -0
  251. package/src/generator/team-pack.js +220 -0
  252. package/src/index.js +68 -0
  253. package/src/parser/gdl-parser.js +169 -0
  254. package/src/parser/jdl-parser.js +171 -0
  255. package/src/presets/pmb-test.js +139 -0
  256. package/src/presets/static-source.js +73 -0
  257. package/src/presets/store.js +96 -0
  258. package/src/provision/admin-service.js +148 -0
  259. package/src/provision/bootstrap.js +147 -0
  260. package/src/provision/http.js +123 -0
  261. package/src/provision/keycloak-admin-rest.js +335 -0
  262. package/src/provision/state.js +51 -0
  263. package/src/provision/token.js +52 -0
  264. package/src/templates/angular/auth/access-control.service.ts.ejs +179 -0
  265. package/src/templates/angular/auth/access-ctrl.pipe.ts.ejs +20 -0
  266. package/src/templates/angular/auth/auth.guard.ts.ejs +27 -0
  267. package/src/templates/angular/auth/auth.interceptor.ts.ejs +53 -0
  268. package/src/templates/angular/auth/auth.service.ts.ejs +594 -0
  269. package/src/templates/angular/auth/auth.utils.ts.ejs +28 -0
  270. package/src/templates/angular/auth/current-user.ts.ejs +24 -0
  271. package/src/templates/angular/auth/disabled-account-overlay.component.css.ejs +95 -0
  272. package/src/templates/angular/auth/disabled-account-overlay.component.html.ejs +33 -0
  273. package/src/templates/angular/auth/disabled-account-overlay.component.ts.ejs +23 -0
  274. package/src/templates/angular/auth/fuse-account.ts.ejs +234 -0
  275. package/src/templates/angular/auth/fuse-user.ts.ejs +264 -0
  276. package/src/templates/angular/auth/has-permission.directive.ts.ejs +51 -0
  277. package/src/templates/angular/auth/index.ts.ejs +24 -0
  278. package/src/templates/angular/auth/keycloak-admin.service.ts.ejs +670 -0
  279. package/src/templates/angular/auth/landing.guard.ts.ejs +61 -0
  280. package/src/templates/angular/auth/no-auth.guard.ts.ejs +19 -0
  281. package/src/templates/angular/auth/permission-attribute.util.ts.ejs +182 -0
  282. package/src/templates/angular/auth/sign-in.ts.ejs +60 -0
  283. package/src/templates/angular/entity/_entity_.model.ts.ejs +64 -0
  284. package/src/templates/angular/entity/_entity_.routes.ts.ejs +9 -0
  285. package/src/templates/angular/entity/components/forms/form/_entity_-form.component.html.ejs +168 -0
  286. package/src/templates/angular/entity/components/forms/form/_entity_-form.component.ts.ejs +175 -0
  287. package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.html.ejs +10 -0
  288. package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.ts.ejs +86 -0
  289. package/src/templates/angular/entity/components/forms/update/_entity_-form.service.ts.ejs +136 -0
  290. package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.html.ejs +68 -0
  291. package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.ts.ejs +69 -0
  292. package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.html.ejs +111 -0
  293. package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.ts.ejs +102 -0
  294. package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.css.ejs +33 -0
  295. package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.html.ejs +29 -0
  296. package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.ts.ejs +93 -0
  297. package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.css.ejs +54 -0
  298. package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.html.ejs +58 -0
  299. package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.ts.ejs +47 -0
  300. package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs +419 -0
  301. package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.scss.ejs +48 -0
  302. package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs +592 -0
  303. package/src/templates/angular/entity/service/_entity_.service.ts.ejs +153 -0
  304. package/src/templates/angular/entity/settings/_entity_.settings.ts.ejs +49 -0
  305. package/src/templates/angular/enums/_enum_.model.ts.ejs +11 -0
  306. package/src/templates/angular/iam/change-password.component.html.ejs +39 -0
  307. package/src/templates/angular/iam/change-password.component.ts.ejs +44 -0
  308. package/src/templates/angular/iam/no-access.component.ts.ejs +17 -0
  309. package/src/templates/angular/iam/role-list.component.html.ejs +331 -0
  310. package/src/templates/angular/iam/role-list.component.scss.ejs +107 -0
  311. package/src/templates/angular/iam/role-list.component.ts.ejs +597 -0
  312. package/src/templates/angular/iam/role-management/role-header.component.html.ejs +51 -0
  313. package/src/templates/angular/iam/role-management/role-header.component.ts.ejs +45 -0
  314. package/src/templates/angular/iam/role-management/role-inheritance.component.html.ejs +15 -0
  315. package/src/templates/angular/iam/role-management/role-inheritance.component.ts.ejs +19 -0
  316. package/src/templates/angular/iam/role-management/role-permissions-panel.component.css.ejs +24 -0
  317. package/src/templates/angular/iam/role-management/role-permissions-panel.component.html.ejs +108 -0
  318. package/src/templates/angular/iam/role-management/role-permissions-panel.component.ts.ejs +122 -0
  319. package/src/templates/angular/iam/role-management/role-users.component.html.ejs +104 -0
  320. package/src/templates/angular/iam/role-management/role-users.component.ts.ejs +53 -0
  321. package/src/templates/angular/iam/role-management/user-select-dialog.component.html.ejs +113 -0
  322. package/src/templates/angular/iam/role-management/user-select-dialog.component.ts.ejs +157 -0
  323. package/src/templates/angular/iam/role-permission-matrix.component.html.ejs +355 -0
  324. package/src/templates/angular/iam/role-permission-matrix.component.scss.ejs +217 -0
  325. package/src/templates/angular/iam/role-permission-matrix.component.ts.ejs +800 -0
  326. package/src/templates/angular/iam/screen-catalog.ts.ejs +147 -0
  327. package/src/templates/angular/iam/shared/confirm-dialog.component.ts.ejs +83 -0
  328. package/src/templates/angular/iam/shared/matrix-hierarchy.util.ts.ejs +196 -0
  329. package/src/templates/angular/iam/shared/permission-matrix-sound.ts.ejs +59 -0
  330. package/src/templates/angular/iam/shared/role-display.util.ts.ejs +43 -0
  331. package/src/templates/angular/iam/shared/role-permission-skeleton.component.ts.ejs +54 -0
  332. package/src/templates/angular/iam/shared/screen-matrix.util.ts.ejs +554 -0
  333. package/src/templates/angular/iam/shared/screen-navigation-map.ts.ejs +187 -0
  334. package/src/templates/angular/iam/shared/screen-navigation.registry.ts.ejs +64 -0
  335. package/src/templates/angular/iam/shared/text-highlight.pipe.ts.ejs +39 -0
  336. package/src/templates/angular/iam/user-access-matrix.component.html.ejs +212 -0
  337. package/src/templates/angular/iam/user-access-matrix.component.scss.ejs +34 -0
  338. package/src/templates/angular/iam/user-access-matrix.component.ts.ejs +239 -0
  339. package/src/templates/angular/iam/user-detail.component.html.ejs +143 -0
  340. package/src/templates/angular/iam/user-detail.component.ts.ejs +108 -0
  341. package/src/templates/angular/iam/user-form.component.html.ejs +63 -0
  342. package/src/templates/angular/iam/user-form.component.ts.ejs +100 -0
  343. package/src/templates/angular/iam/user-list.component.html.ejs +369 -0
  344. package/src/templates/angular/iam/user-list.component.scss.ejs +242 -0
  345. package/src/templates/angular/iam/user-list.component.ts.ejs +446 -0
  346. package/src/templates/angular/iam/user-management.routes.ts.ejs +47 -0
  347. package/src/templates/angular/iam/user-permission-matrix.component.html.ejs +231 -0
  348. package/src/templates/angular/iam/user-permission-matrix.component.scss.ejs +216 -0
  349. package/src/templates/angular/iam/user-permission-matrix.component.ts.ejs +516 -0
  350. package/src/templates/angular/shared/base/audit-user-name.pipe.ts.ejs +69 -0
  351. package/src/templates/angular/shared/base/base-filter.component.ts.ejs +245 -0
  352. package/src/templates/angular/shared/base/base-form.component.ts.ejs +109 -0
  353. package/src/templates/angular/shared/base/base-list.component.ts.ejs +507 -0
  354. package/src/templates/angular/shared/base/filter-types.ts.ejs +55 -0
  355. package/src/templates/angular/shared/base/input-format.directive.ts.ejs +47 -0
  356. package/src/templates/angular/shared/base/input-format.ts.ejs +15 -0
  357. package/src/templates/angular/shared/confirm-with-input-dialog.component.ts.ejs +91 -0
  358. package/src/templates/angular/shared/confirmation-dialog.component.ts.ejs +80 -0
  359. package/src/templates/angular/shared/list-styles.css.ejs +103 -0
  360. package/src/templates/angular/shared/loading-overlay.component.ts.ejs +24 -0
  361. package/src/templates/angular/shared/resizable.directive.ts.ejs +115 -0
  362. package/src/templates/angular/shared/snackbar-styles.css.ejs +11 -0
  363. package/src/templates/fuse-packs/access-control-tools/access-control-sync.js +819 -0
  364. package/src/templates/fuse-packs/access-control-tools/build-rename-map.js +67 -0
  365. package/src/templates/fuse-packs/access-control-tools/inject-qa-ids.js +14 -0
  366. package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.js +628 -0
  367. package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.test.js +323 -0
  368. package/src/templates/fuse-packs/access-control-tools/lib/html-scanner.js +362 -0
  369. package/src/templates/fuse-packs/access-control-tools/lib/id-rules.duplicate.test.js +136 -0
  370. package/src/templates/fuse-packs/access-control-tools/lib/id-rules.js +1095 -0
  371. package/src/templates/fuse-packs/access-control-tools/lib/ts-component-index.js +482 -0
  372. package/src/templates/fuse-packs/access-control-tools/output/.gitkeep +0 -0
  373. package/src/templates/fuse-packs/access-control-tools/run.sh +223 -0
  374. package/src/templates/fuse-packs/access-control-tools/scan-access-control.js +14 -0
  375. package/src/templates/fuse-packs/iam-assets/sound/apple-play.mp3 +0 -0
  376. package/src/templates/fuse-packs/iam-assets/sound/remove.mp3 +0 -0
  377. package/src/templates/generator/README.md +12 -0
  378. package/src/templates/testing/ci.cjs +45 -0
  379. package/src/templates/testing/mock.cjs +149 -0
  380. package/src/templates/testing/playwright.config.cjs +25 -0
  381. package/src/templates/testing/serve.cjs +22 -0
  382. package/src/templates/testing/smoke.spec.cjs +180 -0
  383. package/src/utils/branding.js +39 -0
  384. package/src/utils/colors.js +18 -0
  385. package/src/utils/diff.js +109 -0
  386. package/src/utils/merger.js +115 -0
  387. package/src/validator/backend-diagnostics.js +30 -0
  388. package/src/validator/blueprint-validator.js +276 -0
  389. package/src/validator/diagnostics.js +608 -0
  390. package/src/validator/pattern.js +28 -0
  391. package/test/auth-interceptor.js +84 -0
  392. package/test/auth-provision-checks.js +297 -0
  393. package/test/field-preferences.js +145 -0
  394. package/test/fixtures/field-profile.gdl +7 -0
  395. package/test/fixtures/mock.jdl +17 -0
  396. package/test/fixtures/mock.png +0 -0
  397. package/test/fixtures/performance/config.yml +33 -0
  398. package/test/fixtures/performance/large.gdl +199 -0
  399. package/test/fixtures/performance/small.gdl +7 -0
  400. package/test/fixtures/quality/custom.yml +22 -0
  401. package/test/fixtures/quality/go-duck.yml +22 -0
  402. package/test/fixtures/quality/jhipster.yml +30 -0
  403. package/test/fixtures/quality/representative.gdl +14 -0
  404. package/test/list-state.js +138 -0
  405. package/test/micro-functions-layout.js +95 -0
  406. package/test/performance-generation.js +32 -0
  407. package/test/pmb-preset.js +127 -0
  408. package/test/project-workflow.js +111 -0
  409. package/test/quality-features.js +250 -0
  410. package/test/verify-commands.js +83 -0
  411. package/test/verify.js +959 -0
  412. package/test-files/GDL/example.gdl +15 -0
  413. package/test-files/JDL/example.jdl +15 -0
  414. package/test-files/config/crudforge.config.yml +127 -0
  415. package/test-run.sh +272 -0
@@ -0,0 +1,171 @@
1
+ const _ = require('lodash');
2
+
3
+ /**
4
+ * Standard default audit fields matching JHipster/TW-Hipster conventions.
5
+ */
6
+ const DEFAULT_AUDIT_FIELDS = [
7
+ { fieldName: 'createdBy', fieldType: 'String', fieldTypeIsEnum: false, fieldValidateRules: [], comment: null },
8
+ { fieldName: 'createdDate', fieldType: 'Instant', fieldTypeIsEnum: false, fieldValidateRules: [], comment: null },
9
+ { fieldName: 'lastModifiedBy', fieldType: 'String', fieldTypeIsEnum: false, fieldValidateRules: [], comment: null },
10
+ { fieldName: 'lastModifiedDate', fieldType: 'Instant', fieldTypeIsEnum: false, fieldValidateRules: [], comment: null }
11
+ ];
12
+
13
+ /** Injected when `@ActiveStatus` is present and the entity does not already declare `isActive`. */
14
+ const ACTIVE_STATUS_FIELD = {
15
+ fieldName: 'isActive',
16
+ fieldType: 'Boolean',
17
+ fieldTypeIsEnum: false,
18
+ fieldValidateRules: [],
19
+ comment: null
20
+ };
21
+
22
+ /** GDL entity annotations with implemented generator behavior. */
23
+ const SUPPORTED_GDL_ANNOTATIONS = new Set(['Audited', 'Searchable', 'ActiveStatus']);
24
+
25
+ /** JDL entity annotations with implemented generator behavior. */
26
+ const SUPPORTED_JDL_ANNOTATIONS = new Set(['EnableAudit', 'Searchable', 'ActiveStatus']);
27
+
28
+ function parseEntityAnnotations(annotationsStr = '') {
29
+ return [...annotationsStr.matchAll(/@(\w+)/g)].map((m) => m[1]);
30
+ }
31
+
32
+ function annotationSet(annotations) {
33
+ return new Set(annotations);
34
+ }
35
+
36
+ /**
37
+ * Parses JDL file contents.
38
+ * @param {string} jdlContent - Raw JDL content string.
39
+ * @param {object} preferences - Developer configuration settings.
40
+ * @returns {{entities: Array, enums: Array, relationships: Array}}
41
+ */
42
+ function parseJdl(jdlContent, preferences = {}) {
43
+ const entities = {};
44
+ const enums = {};
45
+ const relationships = [];
46
+
47
+ // Remove block comments
48
+ const cleanContent = jdlContent.replace(/\/\*[\s\S]*?\*\//g, '');
49
+
50
+ // Helper check for line comment index
51
+ function isCommented(content, index) {
52
+ const lastNewline = content.lastIndexOf('\n', index);
53
+ const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
54
+ const line = content.substring(lineStart, index);
55
+ return line.trim().startsWith('//');
56
+ }
57
+
58
+ // 1. Parse Enums
59
+ const enumRegex = /enum\s+(\w+)\s*\{([^}]+)\}/g;
60
+ let enumMatch;
61
+ while ((enumMatch = enumRegex.exec(cleanContent)) !== null) {
62
+ if (isCommented(cleanContent, enumMatch.index)) continue;
63
+ const enumName = enumMatch[1];
64
+ const values = enumMatch[2]
65
+ .trim()
66
+ .split(',')
67
+ .map(v => v.trim().split('(')[0].trim())
68
+ .filter(Boolean);
69
+ enums[enumName] = { name: enumName, values: values.map(v => ({ name: v })) };
70
+ }
71
+
72
+ // Determine audit fields based on config preferences
73
+ let auditFieldsToUse = DEFAULT_AUDIT_FIELDS;
74
+ if (preferences.fields && preferences.fields.audit && Array.isArray(preferences.fields.audit.names)) {
75
+ auditFieldsToUse = preferences.fields.audit.names.map(name => {
76
+ const type = (name.toLowerCase().endsWith('date') || /(?:At|_at)$/.test(name)) ? 'Instant' : 'String';
77
+ return { fieldName: name, fieldType: type, fieldTypeIsEnum: false, fieldValidateRules: [], comment: null };
78
+ });
79
+ }
80
+
81
+ // 2. Parse Entities and Fields
82
+ const entityRegex = /((?:@\w+\s+)*)entity\s+(\w+)\s*\{([^}]+)\}/g;
83
+ let entityMatch;
84
+ while ((entityMatch = entityRegex.exec(cleanContent)) !== null) {
85
+ if (isCommented(cleanContent, entityMatch.index)) continue;
86
+
87
+ const annotations = parseEntityAnnotations(entityMatch[1] || '');
88
+ const flags = annotationSet(annotations);
89
+ const hasAuditAnnotation = flags.has('EnableAudit');
90
+ const hasActiveStatus = flags.has('ActiveStatus');
91
+ const hasSearchable = flags.has('Searchable');
92
+ const entityName = entityMatch[2];
93
+ const fieldsContent = entityMatch[3];
94
+
95
+ const fields = [];
96
+ const fieldLines = fieldsContent
97
+ .split('\n')
98
+ .map(l => l.trim())
99
+ .filter(line => line && !line.startsWith('//'));
100
+
101
+ fieldLines.forEach(line => {
102
+ const fieldMatch = line.match(/^(\w+)\s+([\w<>]+)(.*)/);
103
+ if (fieldMatch) {
104
+ const fieldName = fieldMatch[1];
105
+ const fieldType = fieldMatch[2];
106
+ const restOfLine = fieldMatch[3] || '';
107
+
108
+ let validations = restOfLine;
109
+ let comment = null;
110
+
111
+ const commentIndex = restOfLine.indexOf('//');
112
+ if (commentIndex !== -1) {
113
+ validations = restOfLine.substring(0, commentIndex).trim();
114
+ comment = restOfLine.substring(commentIndex + 2).trim();
115
+ }
116
+
117
+ fields.push({
118
+ fieldName,
119
+ fieldType,
120
+ fieldTypeIsEnum: !!enums[fieldType],
121
+ fieldValidateRules: validations.split(/\s+/).filter(Boolean),
122
+ comment
123
+ });
124
+ }
125
+ });
126
+
127
+ if (hasActiveStatus && !fields.some((f) => f.fieldName === 'isActive')) {
128
+ fields.push({ ...ACTIVE_STATUS_FIELD });
129
+ }
130
+
131
+ if (hasAuditAnnotation && preferences.fields?.audit?.enabled !== false) {
132
+ fields.push(...auditFieldsToUse);
133
+ }
134
+
135
+ entities[entityName] = {
136
+ name: entityName,
137
+ fields,
138
+ annotations,
139
+ searchable: hasSearchable,
140
+ activeStatus: hasActiveStatus,
141
+ audited: hasAuditAnnotation
142
+ };
143
+ }
144
+
145
+ // 3. Parse Relationships
146
+ const relationshipRegex = /relationship\s+(OneToOne|ManyToOne|OneToMany|ManyToMany)\s*\{\s*(\w+)(?:[({](\w+)[)}])?\s+to\s+(\w+)(?:[({](\w+)[)}])?\s*\}/g;
147
+ let relMatch;
148
+ while ((relMatch = relationshipRegex.exec(cleanContent)) !== null) {
149
+ if (isCommented(cleanContent, relMatch.index)) continue;
150
+ relationships.push({
151
+ type: relMatch[1],
152
+ from: { name: relMatch[2], fieldName: relMatch[3] },
153
+ to: { name: relMatch[4], fieldName: relMatch[5] }
154
+ });
155
+ }
156
+
157
+ return {
158
+ entities: Object.values(entities),
159
+ enums: Object.values(enums),
160
+ relationships
161
+ };
162
+ }
163
+
164
+ module.exports = {
165
+ parseJdl,
166
+ DEFAULT_AUDIT_FIELDS,
167
+ ACTIVE_STATUS_FIELD,
168
+ SUPPORTED_GDL_ANNOTATIONS,
169
+ SUPPORTED_JDL_ANNOTATIONS,
170
+ parseEntityAnnotations
171
+ };
@@ -0,0 +1,139 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { literalObject, stringLiteral, numberLiteral, withoutComments } = require('./static-source');
5
+ const { sanitizeProxyRoutes } = require('../config/proxy-routes');
6
+
7
+ function extractPmbPreset(source) {
8
+ const root = fs.realpathSync(path.resolve(source));
9
+ const fingerprints = {};
10
+ function read(relative) {
11
+ const file = path.resolve(root, relative);
12
+ if (!file.startsWith(root + path.sep) || !fs.existsSync(file)) throw new Error(`Missing PMB source file: ${file}`);
13
+ if (!fs.realpathSync(file).startsWith(root + path.sep)) throw new Error(`PMB source file points outside source project: ${relative}`);
14
+ const content = fs.readFileSync(file, 'utf8');
15
+ fingerprints[relative] = crypto.createHash('sha256').update(content).digest('hex');
16
+ return content;
17
+ }
18
+ const angular = JSON.parse(read('angular.json'));
19
+ const candidates = Object.entries(angular.projects || {}).filter(([, p]) => (p.architect || p.targets)?.build);
20
+ const chosen = candidates.find(([name]) => name === angular.defaultProject) || (candidates.length === 1 ? candidates[0] : null);
21
+ if (!chosen) throw new Error('PMB angular.json: cannot determine the application project.');
22
+ const targets = chosen[1].architect || chosen[1].targets;
23
+ const development = targets.build.configurations?.development;
24
+ const environmentFile = development?.fileReplacements?.find(item => item.replace.endsWith('/environment.ts'))?.with;
25
+ if (!environmentFile) throw new Error('PMB angular.json: development environment file replacement is missing.');
26
+ const scripts = JSON.parse(read('package.json')).scripts || {};
27
+ if (!/--configuration(?:=|\s+)development\b/.test(scripts.dev || '')) {
28
+ throw new Error('PMB package.json: dev script must explicitly select the development configuration.');
29
+ }
30
+ const proxyFlag = /--proxy-config(?:=|\s+)([\w./-]+)(?=\s|$)/.exec(scripts.dev);
31
+ if (scripts.dev.includes('--proxy-config') && !proxyFlag) throw new Error('PMB dev script has a dynamic or unsupported proxy path; use a literal project-relative path.');
32
+ const proxyFile = proxyFlag?.[1] || targets.serve?.configurations?.development?.proxyConfig || targets.serve?.options?.proxyConfig;
33
+ if (!proxyFile) throw new Error('PMB development proxy configuration is missing.');
34
+ const portFlag = /--port(?:=|\s+)(\d+)(?=\s|$)/.exec(scripts.dev);
35
+ if (scripts.dev.includes('--port') && !portFlag) throw new Error('PMB dev script has a dynamic or unsupported port; use a literal port number.');
36
+ const port = Number(portFlag?.[1] || targets.serve?.configurations?.development?.port || targets.serve?.options?.port || 4200);
37
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Invalid PMB development port.');
38
+ const values = literalObject(read(environmentFile), 'environment', environmentFile);
39
+ const string = key => stringLiteral(values[key], `${environmentFile}: ${key}`);
40
+ if (values.useMockAuth !== 'false') throw new Error(`${environmentFile}: useMockAuth must be false for pmb-test.`);
41
+ if (string('kcClientSecret') !== '') throw new Error('PMB frontend client requires a secret. Cannot generate a public-client preset; no secret was imported.');
42
+ const realm = string('kcRealm');
43
+ const clientId = string('kcClientId');
44
+ if (!/^[\w.-]+$/.test(realm) || !/^[\w.-]+$/.test(clientId)) throw new Error('PMB realm and client ID must be nonempty identifiers.');
45
+ const tokenPath = string('sso');
46
+ if (tokenPath !== `/realms/${realm}/protocol/openid-connect/token` || string('kcUrl') !== tokenPath.replace(/\/token$/, '')) {
47
+ throw new Error('PMB OIDC paths no longer match the supported public-client password flow.');
48
+ }
49
+ if (string('apiBaseUrl') !== '/' || string('keycloakBaseUrl') !== '') throw new Error('PMB development API bases are no longer relative; review the importer before refreshing.');
50
+ const routes = sanitizeProxyRoutes(JSON.parse(read(proxyFile)), proxyFile);
51
+ if (!routes['/realms'] || !routes['/api/v1']) throw new Error(`${proxyFile}: /realms and /api/v1 routes are required.`);
52
+ if (routes['/realms'].pathRewrite || routes['/api/v1'].pathRewrite) throw new Error('Rewritten PMB OIDC/admin base paths need an explicit importer update.');
53
+ const authSource = withoutComments(read('src/app/core/auth/auth.service.ts'));
54
+ const probeExpression = /const ACCESS_TOKEN_EXPIRY_PROBE_INTERVAL_MS\s*=\s*([^;]+);/.exec(authSource)?.[1].trim();
55
+ const probeInterval = numberLiteral(probeExpression, 'ACCESS_TOKEN_EXPIRY_PROBE_INTERVAL_MS');
56
+ for (const pattern of [/set\(['"]grant_type['"],\s*['"]password['"]\)/, /set\(['"]grant_type['"],\s*['"]refresh_token['"]\)/,
57
+ /SIGN_IN_OIDC_SCOPE\s*=\s*['"]openid['"]/, /set\(['"]client_id['"],\s*config.clientId\)/, /['"]\/logout['"]/]) {
58
+ if (!pattern.test(authSource)) throw new Error('PMB auth.service.ts has an unsupported authentication flow. Review it before refreshing.');
59
+ }
60
+ const interceptor = withoutComments(read('src/app/core/auth/auth.interceptor.ts'));
61
+ if (!interceptor.includes("delete('X-Tenant-ID')") || !interceptor.includes('Bearer ${')) throw new Error('PMB bearer/tenant header behavior changed; review the importer.');
62
+ const access = withoutComments(read('src/app/core/auth/access-control.service.ts'));
63
+ const adminCheck = /readonly isUserAdmin\s*=\s*computed(?:<boolean>)?\([\s\S]*?\n\s*\}\);/.exec(access)?.[0];
64
+ const adminRoles = [...(adminCheck || '').matchAll(/role\s*===\s*['"]([^'"]+)['"]/g)].map(match => match[1]);
65
+ if (!adminRoles.length) throw new Error('Cannot determine PMB admin roles from access-control.service.ts.');
66
+ const base = 'src/app/domains/admin/modules/apps/micro-functions/referenceModule/entities/province';
67
+ const model = withoutComments(read(`${base}/province.model.ts`));
68
+ const service = withoutComments(read(`${base}/service/province.service.ts`));
69
+ const resource = /resourceUrl\s*=\s*(['"])([^'"]+)\1/.exec(service)?.[2];
70
+ if (!resource?.endsWith('/api/provinces')) throw new Error('Province client no longer exposes the expected /api/provinces resource.');
71
+ const route = Object.keys(routes).filter(key => resource === key || resource.startsWith(key + '/')).sort((a, b) => b.length - a.length)[0];
72
+ if (!route) throw new Error(`No development proxy route for Province resource ${resource}.`);
73
+ let backendPath = resource;
74
+ for (const [pattern, replacement] of Object.entries(routes[route].pathRewrite || {})) backendPath = backendPath.replace(new RegExp(pattern), replacement);
75
+ const microservice = /^\/([\w-]+)\/api\/provinces$/.exec(backendPath)?.[1];
76
+ if (!microservice) throw new Error(`Unsupported Province backend path: ${backendPath}`);
77
+ const iface = /interface IProvince\s*\{([\s\S]*?)\}/.exec(model)?.[1];
78
+ if (!iface) throw new Error('Province model interface is missing.');
79
+ const fields = [...iface.matchAll(/(\w+)\??:\s*([^;]+);/g)];
80
+ const audits = ['createdBy', 'createdDate', 'lastModifiedBy', 'lastModifiedDate'];
81
+ const types = { String: 'string', string: 'string', boolean: 'bool', 'dayjs.Dayjs': 'datetime' };
82
+ const declarations = fields.filter(([, name]) => name !== 'id').map(([, name, rawType]) => {
83
+ const type = rawType.replace(/\s*\|\s*null/g, '').trim();
84
+ if (!types[type]) throw new Error(`Unsupported Province field type for ${name}; update the sample mapping.`);
85
+ return { name, type: types[type] };
86
+ });
87
+ if (!declarations.some(field => field.name === 'provinceName')) throw new Error('Province sample fields changed.');
88
+ const audited = audits.every(name => declarations.some(field => field.name === name));
89
+ const gdl = '// PMB reference-data sample derived from its Province model/client.\n' +
90
+ '// Backend reads/writes still depend on your account permissions.\n' +
91
+ `${audited ? '@Audited\n' : ''}entity Province {\n` +
92
+ declarations.filter(field => !audited || !audits.includes(field.name)).map(field => ` ${field.type} ${field.name}`).join('\n') + '\n}\n';
93
+ const services = {};
94
+ for (const [key, entry] of Object.entries(routes)) {
95
+ if (!key.startsWith('/services/')) continue;
96
+ const rewrite = entry.pathRewrite?.['^' + key];
97
+ services[key.slice('/services/'.length)] = { target: entry.target, ...(rewrite !== undefined ? { pathRewrite: rewrite } : {}) };
98
+ }
99
+ services[microservice] = { target: routes[route].target, pathRewrite: '/' + microservice };
100
+ // Use the route actually used by the source client if historical aliases disagree.
101
+ routes['/services/' + microservice] = {
102
+ ...routes[route], pathRewrite: { ['^/services/' + microservice]: '/' + microservice },
103
+ };
104
+ // The generated admin client uses its own prefix; strip it before PMB's /api/v1.
105
+ routes['/services/keycloak-admin'] = { ...routes['/api/v1'], pathRewrite: { '^/services/keycloak-admin': '' } };
106
+ // This secondary realm requires credentials and is not part of the imported login.
107
+ delete routes['/pmb-dad-mock-module'];
108
+ const config = {
109
+ project: { name: 'PMB Development Test' },
110
+ input: { file: './gdl/example.gdl', type: 'gdl' },
111
+ output: { directory: '.', overwritePolicy: 'fail' },
112
+ validation: true,
113
+ fuse: { enabled: true, cleanLayout: true, includePrebuilt: false },
114
+ api: { baseUrl: '/api', microservice, services },
115
+ proxy: { write: 'both', routes },
116
+ devServer: { port },
117
+ mock: { enabled: false },
118
+ auth: {
119
+ provider: 'keycloak', serverUrl: routes['/realms'].target, realm, clientId,
120
+ useMockAuth: false, adminRoles,
121
+ session: { idleTimeoutMs: numberLiteral(values.sessionIdleTimeoutMs, 'sessionIdleTimeoutMs'), statusProbeIntervalMs: probeInterval },
122
+ provision: { enabled: false, adapter: 'admin-service', adminServiceUrl: routes['/api/v1'].target },
123
+ },
124
+ iam: { enabled: false },
125
+ };
126
+ return { config, gdl, metadata: {
127
+ preset: 'pmb-test', version: 1, importedAt: new Date().toISOString(), source: root,
128
+ sourceFiles: fingerprints, environmentFile, proxyFile,
129
+ sample: { entity: 'Province', sourceResource: resource, backendUrl: routes[route].target + backendPath },
130
+ limitations: [
131
+ 'Login and backend API availability have not been established by source extraction.',
132
+ 'Province fields come from the frontend model; backend write constraints are not verified.',
133
+ 'DAD secondary login, confidential internal client, and privileged IAM management are not imported.',
134
+ 'Non-admin screen permissions and PMB-specific profile/business context require separate verification.',
135
+ ],
136
+ } };
137
+ }
138
+
139
+ module.exports = { extractPmbPreset };
@@ -0,0 +1,73 @@
1
+ // Read a restricted literal object from TypeScript without evaluating source code.
2
+ // Dynamic expressions are deliberately rejected for the settings we import.
3
+ function withoutComments(source) {
4
+ let out = '';
5
+ let quote = null;
6
+ for (let i = 0; i < source.length; i++) {
7
+ const char = source[i];
8
+ if (quote) {
9
+ out += char;
10
+ if (char === '\\') out += source[++i] || '';
11
+ else if (char === quote) quote = null;
12
+ } else if (['"', "'", '`'].includes(char)) {
13
+ quote = char; out += char;
14
+ } else if (source.slice(i, i + 2) === '//') {
15
+ while (i < source.length && source[i] !== '\n') i++;
16
+ out += '\n';
17
+ } else if (source.slice(i, i + 2) === '/*') {
18
+ i += 2;
19
+ while (i < source.length && source.slice(i, i + 2) !== '*/') {
20
+ out += source[i] === '\n' ? '\n' : ' '; i++;
21
+ }
22
+ i++; out += ' ';
23
+ } else out += char;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function literalObject(source, name, file) {
29
+ const text = withoutComments(source);
30
+ const match = new RegExp(`\\b(?:const|let)\\s+${name}\\s*=\\s*\\{`).exec(text);
31
+ if (!match) throw new Error(`Cannot statically read ${name} in ${file}; expected a literal object.`);
32
+ const values = Object.create(null);
33
+ let start = match.index + match[0].length;
34
+ let depth = 0;
35
+ let quote = null;
36
+ function add(end) {
37
+ const entry = text.slice(start, end).trim();
38
+ if (!entry) return;
39
+ const property = /^(\w+)\s*:\s*([\s\S]+)$/.exec(entry);
40
+ if (!property || Object.hasOwn(values, property[1])) throw new Error(`Unsupported or duplicate property in ${file}. Use explicit environment settings.`);
41
+ values[property[1]] = property[2].trim();
42
+ }
43
+ for (let i = start; i < text.length; i++) {
44
+ const char = text[i];
45
+ if (quote) {
46
+ if (char === '\\') i++;
47
+ else if (char === quote) quote = null;
48
+ } else if (['"', "'", '`'].includes(char)) quote = char;
49
+ else if (char === '}' && depth === 0) { add(i); return values; }
50
+ else if ('{[('.includes(char)) depth++;
51
+ else if ('}])'.includes(char)) depth--;
52
+ else if (char === ',' && depth === 0) { add(i); start = i + 1; }
53
+ }
54
+ throw new Error(`Unterminated ${name} object in ${file}.`);
55
+ }
56
+
57
+ function stringLiteral(expression, label) {
58
+ // No interpolation, escapes, or executable expressions; URLs and identifiers need none.
59
+ const match = /^(['"])([^'"\\\r\n]*)\1$/.exec(expression || '');
60
+ if (!match) throw new Error(`Cannot statically read ${label}; expected a quoted string.`);
61
+ return match[2];
62
+ }
63
+
64
+ function numberLiteral(expression, label) {
65
+ if (!/^\d+(?:\s*\*\s*\d+)*$/.test(expression || '')) {
66
+ throw new Error(`Cannot statically read ${label}; expected a number or multiplication of numbers.`);
67
+ }
68
+ const number = expression.split('*').reduce((total, part) => total * Number(part.trim()), 1);
69
+ if (!Number.isSafeInteger(number)) throw new Error(`Invalid number for ${label}.`);
70
+ return number;
71
+ }
72
+
73
+ module.exports = { withoutComments, literalObject, stringLiteral, numberLiteral };
@@ -0,0 +1,96 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const YAML = require('yaml');
5
+ const { extractPmbPreset } = require('./pmb-test');
6
+ const { validateConfig } = require('../config/load-config');
7
+ const { validateBlueprint } = require('../validator/blueprint-validator');
8
+ const { parseGdl } = require('../parser/gdl-parser');
9
+
10
+ function presetDirectory(name) {
11
+ if (name !== 'pmb-test') throw new Error(`Unknown preset "${name}". Supported optional preset: pmb-test.`);
12
+ const home = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
13
+ if (!path.isAbsolute(home)) throw new Error('XDG_CONFIG_HOME must be an absolute directory.');
14
+ return path.join(home, 'crudforge/presets', name);
15
+ }
16
+
17
+ function assertSecretFree(value, prefix = '') {
18
+ if (!value || typeof value !== 'object') return;
19
+ for (const [key, item] of Object.entries(value)) {
20
+ if (!key.endsWith('Env') && /secret|password|access.?token|refresh.?token|authorization|cookie/i.test(key) &&
21
+ item !== '' && item !== null && item !== false) {
22
+ throw new Error(`Preset contains a credential field: ${prefix}${key}. Remove it; credentials are never persisted in presets.`);
23
+ }
24
+ assertSecretFree(item, prefix + key + '.');
25
+ }
26
+ }
27
+
28
+ function validatePreset(preset, label) {
29
+ validateConfig(preset.config, label);
30
+ assertSecretFree(preset.config);
31
+ if (preset.config.input?.file !== './gdl/example.gdl' || preset.config.output?.directory !== '.') {
32
+ throw new Error(`${label}: preset paths must be ./gdl/example.gdl and output.directory: .`);
33
+ }
34
+ if (preset.config.auth?.provision?.enabled !== false || preset.config.auth?.useMockAuth !== false) {
35
+ throw new Error(`${label}: pmb-test requires real authentication with provisioning disabled.`);
36
+ }
37
+ const errors = validateBlueprint(preset.gdl, true);
38
+ if (errors.length || !parseGdl(preset.gdl, preset.config).entities.length) {
39
+ throw new Error(`${label}: invalid preset GDL. ${errors.join('\n')}`);
40
+ }
41
+ }
42
+
43
+ async function loadPreset(name) {
44
+ const directory = presetDirectory(name);
45
+ if (!await fs.pathExists(directory)) throw new Error(`Preset not found: ${directory}. Initialize with --preset ${name} --source <PMB-project> first.`);
46
+ const result = {
47
+ config: YAML.parse(await fs.readFile(path.join(directory, 'config.yml'), 'utf8')),
48
+ gdl: await fs.readFile(path.join(directory, 'example.gdl'), 'utf8'),
49
+ metadata: await fs.readJson(path.join(directory, 'metadata.json')),
50
+ };
51
+ validatePreset(result, directory);
52
+ return result;
53
+ }
54
+
55
+ async function importPreset(name, source, { refresh = false } = {}) {
56
+ const directory = presetDirectory(name);
57
+ if (!source) throw new Error('Specify --source <PMB-project> to import or refresh a preset.');
58
+ // Extract and validate without executing PMB code, and before touching the registry.
59
+ const preset = extractPmbPreset(source);
60
+ validatePreset(preset, directory);
61
+ const parent = path.dirname(directory);
62
+ await fs.ensureDir(parent);
63
+ const lock = directory + '.lock';
64
+ try { await fs.mkdir(lock); } catch (error) {
65
+ if (error.code === 'EEXIST') throw new Error(`Preset import already in progress: ${lock}. Remove the lock only if that process has stopped.`);
66
+ throw error;
67
+ }
68
+ let stage;
69
+ try {
70
+ const exists = await fs.pathExists(directory);
71
+ if (exists && !refresh) throw new Error(`Preset already exists: ${directory}. Reuse it without --source, or run "crudforge preset refresh ${name} --source <PMB-project>".`);
72
+ if (!exists && refresh) throw new Error(`Preset not found: ${directory}. Use "crudforge init <project> --preset ${name} --source <PMB-project>" first.`);
73
+ stage = await fs.mkdtemp(path.join(parent, `.${name}-import-`));
74
+ await fs.writeFile(path.join(stage, 'config.yml'), '# Reusable PMB development preset. No credentials.\n' + YAML.stringify(preset.config));
75
+ await fs.writeFile(path.join(stage, 'example.gdl'), preset.gdl);
76
+ await fs.writeJson(path.join(stage, 'metadata.json'), preset.metadata, { spaces: 2 });
77
+ let backup;
78
+ if (exists) {
79
+ backup = directory + '.backup-' + new Date().toISOString().replace(/[:.]/g, '-') + '-' + path.basename(stage).slice(-6);
80
+ await fs.rename(directory, backup);
81
+ }
82
+ try { await fs.rename(stage, directory); } catch (error) {
83
+ if (backup) await fs.rename(backup, directory);
84
+ throw error;
85
+ }
86
+ console.log(`Saved preset: ${directory}`);
87
+ if (backup) console.log(`Previous preset retained: ${backup}`);
88
+ for (const limitation of preset.metadata.limitations) console.log(`PMB preset: ${limitation}`);
89
+ return preset;
90
+ } finally {
91
+ if (stage) await fs.remove(stage);
92
+ await fs.remove(lock);
93
+ }
94
+ }
95
+
96
+ module.exports = { presetDirectory, loadPreset, importPreset, validatePreset };
@@ -0,0 +1,148 @@
1
+ const { request, ProvisionHttpError } = require('./http');
2
+
3
+ const OWNED_ATTRIBUTE = 'crudforge.owned';
4
+
5
+ function baseUrl(adminServiceUrl) {
6
+ return String(adminServiceUrl || '').replace(/\/+$/, '');
7
+ }
8
+
9
+ function authHeaders(token) {
10
+ return { Authorization: `Bearer ${token}` };
11
+ }
12
+
13
+ async function adminServiceRequest({ adminServiceUrl, token, method, path, json }) {
14
+ const url = `${baseUrl(adminServiceUrl)}${path}`;
15
+ const response = await request({
16
+ method,
17
+ url,
18
+ headers: authHeaders(token),
19
+ json,
20
+ });
21
+ if (response.status === 401) {
22
+ throw new Error(
23
+ `Admin service at ${baseUrl(adminServiceUrl)} rejected the token (HTTP 401). Authorization header is required; use a Keycloak bearer token.`
24
+ );
25
+ }
26
+ if (response.status === 403) {
27
+ const detail =
28
+ response.body && typeof response.body === 'object' ? response.body.error || JSON.stringify(response.body) : '';
29
+ throw new Error(
30
+ `Admin service at ${baseUrl(adminServiceUrl)} denied access (HTTP 403)${
31
+ detail ? `: ${detail}` : ''
32
+ }. This wrapper requires realm role KEYCLOAK_ADMIN_ACCESS (not only Keycloak realm-admin). Native Keycloak Admin REST can still be used with auth.provision.adapter: keycloak-admin-rest.`
33
+ );
34
+ }
35
+ return response;
36
+ }
37
+
38
+ async function listClients(ctx) {
39
+ const response = await adminServiceRequest({ ...ctx, method: 'GET', path: '/api/v1/clients' });
40
+ if (response.status >= 400) {
41
+ throw new ProvisionHttpError(`GET /api/v1/clients failed (HTTP ${response.status})`, response.status, response.body);
42
+ }
43
+ return Array.isArray(response.body) ? response.body : response.body?.clients || [];
44
+ }
45
+
46
+ async function listRoles(ctx) {
47
+ const response = await adminServiceRequest({ ...ctx, method: 'GET', path: '/api/v1/roles' });
48
+ if (response.status >= 400) {
49
+ throw new ProvisionHttpError(`GET /api/v1/roles failed (HTTP ${response.status})`, response.status, response.body);
50
+ }
51
+ return Array.isArray(response.body) ? response.body : response.body?.roles || [];
52
+ }
53
+
54
+ async function listUsers(ctx) {
55
+ const response = await adminServiceRequest({
56
+ ...ctx,
57
+ method: 'GET',
58
+ path: '/api/v1/users?page=0&size=100',
59
+ });
60
+ if (response.status >= 400) {
61
+ throw new ProvisionHttpError(`GET /api/v1/users failed (HTTP ${response.status})`, response.status, response.body);
62
+ }
63
+ if (Array.isArray(response.body)) {
64
+ return response.body;
65
+ }
66
+ return response.body?.users || response.body?.content || [];
67
+ }
68
+
69
+ function createUnsupported(kind, name) {
70
+ return new Error(
71
+ `Cannot create ${kind} "${name}" through the admin-service adapter. Published contract at GET / only documents GET /api/v1/clients, GET /api/v1/roles, GET /api/v1/users, user update/attributes/password, and GET user roles. Set auth.provision.adapter to keycloak-admin-rest and auth.serverUrl to the Keycloak base URL to create clients, realm roles, users, and role assignments.`
72
+ );
73
+ }
74
+
75
+ async function runAdminServiceBootstrap({
76
+ adminServiceUrl,
77
+ token,
78
+ desired,
79
+ dryRun,
80
+ resetExistingPasswords,
81
+ resolveUserPassword,
82
+ }) {
83
+ const ctx = { adminServiceUrl, token };
84
+ const plan = [];
85
+ const result = { adapter: 'admin-service', created: [], skipped: [], errors: [] };
86
+
87
+ const [clients, roles, users] = await Promise.all([listClients(ctx), listRoles(ctx), listUsers(ctx)]);
88
+ plan.push(`Found ${clients.length} clients, ${roles.length} roles, ${users.length} users via admin service.`);
89
+
90
+ const clientId = desired.clientId;
91
+ const clientMatch = clients.find((client) => (client.clientId || client.client_id || client.id) === clientId);
92
+ if (!clientMatch) {
93
+ throw createUnsupported('client', clientId);
94
+ }
95
+ plan.push(`Client "${clientId}" already exists on admin service; create is not in the published contract.`);
96
+ result.skipped.push({ type: 'client', id: clientId });
97
+
98
+ for (const roleName of desired.roles) {
99
+ const exists = roles.some((role) => (role.name || role.roleName) === roleName);
100
+ if (!exists) {
101
+ throw createUnsupported('role', roleName);
102
+ }
103
+ result.skipped.push({ type: 'role', id: roleName });
104
+ }
105
+
106
+ for (const user of desired.users) {
107
+ const existing = users.find((entry) => (entry.username || entry.preferred_username) === user.username);
108
+ if (!existing) {
109
+ throw createUnsupported('user', user.username);
110
+ }
111
+ result.skipped.push({ type: 'user', id: user.username });
112
+ if (resetExistingPasswords) {
113
+ const userId = existing.id || existing.userId;
114
+ if (!userId) {
115
+ throw new Error(`User "${user.username}" exists but has no id; cannot reset password.`);
116
+ }
117
+ if (dryRun) {
118
+ plan.push(`Dry run: would POST /api/v1/users/${userId}/password (resetExistingPasswords).`);
119
+ } else {
120
+ const password = resolveUserPassword(user);
121
+ const response = await adminServiceRequest({
122
+ ...ctx,
123
+ method: 'POST',
124
+ path: `/api/v1/users/${userId}/password`,
125
+ json: { value: password, temporary: !!user.temporaryPassword },
126
+ });
127
+ if (response.status >= 400) {
128
+ throw new ProvisionHttpError(
129
+ `Reset password for "${user.username}" failed (HTTP ${response.status})`,
130
+ response.status,
131
+ response.body
132
+ );
133
+ }
134
+ plan.push(`Reset password for existing user "${user.username}" via admin service.`);
135
+ }
136
+ }
137
+ }
138
+
139
+ return { plan, result, ownedAttribute: OWNED_ATTRIBUTE };
140
+ }
141
+
142
+ module.exports = {
143
+ runAdminServiceBootstrap,
144
+ listClients,
145
+ listRoles,
146
+ listUsers,
147
+ OWNED_ATTRIBUTE,
148
+ };