@oj-module/cli 0.2.64 → 0.2.67

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 (369) hide show
  1. package/bin/.oj-version +1 -0
  2. package/bin/oj +4 -0
  3. package/devkit/README.md +18 -4
  4. package/devkit/SKILL.md +35 -3
  5. package/devkit/framework-dev.md +22 -11
  6. package/devkit/manual.md +161 -68
  7. package/devkit/menu-rules.md +4 -1
  8. package/devkit/publish-walkthrough.md +110 -37
  9. package/devkit/wecom-internal-app.md +7 -2
  10. package/devkit/wecom-multi-corp-links.md +163 -0
  11. package/package.json +3 -2
  12. package/shell-dist/assets/ahooks.2l850ymmh0.js +18 -0
  13. package/shell-dist/assets/antd-es-locale-zh_CN.1gjjsvxbf0.js +1 -0
  14. package/shell-dist/assets/antd-img-crop.196rkty480.js +99 -0
  15. package/shell-dist/assets/antd-locale-en_US.561tozx860.js +1 -0
  16. package/shell-dist/assets/antd-locale-zh_CN.1cmpmgzh70.js +1 -0
  17. package/shell-dist/assets/antd.uw1coq15l0.js +348 -0
  18. package/shell-dist/assets/clsx.jotilrt2e0.js +1 -0
  19. package/shell-dist/assets/contract-errors.2o17vz9u50.js +1 -0
  20. package/shell-dist/assets/contract.3h327o3mj0.js +57 -0
  21. package/shell-dist/assets/cssinjs.atgmhan7m0.js +3 -0
  22. package/shell-dist/assets/dayjs-plugin-advancedFormat.3vsyd2eis0.js +1 -0
  23. package/shell-dist/assets/dayjs-plugin-customParseFormat.13xhghv1d0.js +1 -0
  24. package/shell-dist/assets/dayjs-plugin-isoWeek.1fn1tw82p0.js +1 -0
  25. package/shell-dist/assets/dayjs-plugin-localeData.2rwbjp9nk0.js +1 -0
  26. package/shell-dist/assets/dayjs-plugin-quarterOfYear.2oqo1lcx80.js +1 -0
  27. package/shell-dist/assets/dayjs-plugin-relativeTime.2pqs3et4y0.js +1 -0
  28. package/shell-dist/assets/dayjs-plugin-weekOfYear.gvn9iph080.js +1 -0
  29. package/shell-dist/assets/dayjs-plugin-weekYear.2u78l3ex10.js +1 -0
  30. package/shell-dist/assets/dayjs-plugin-weekday.2d56a4tml0.js +1 -0
  31. package/shell-dist/assets/dayjs.pa60l6wjo0.js +1 -0
  32. package/shell-dist/assets/dnd-kit-core.1xyreig6r0.js +5 -0
  33. package/shell-dist/assets/dnd-kit-sortable.s94qciuft0.js +1 -0
  34. package/shell-dist/assets/dnd-kit-utilities.1umw4xphb0.js +1 -0
  35. package/shell-dist/assets/i18next.33zqofapj0.js +1 -0
  36. package/shell-dist/assets/icons-es-components-Context.365qmflwg0.js +1 -0
  37. package/shell-dist/assets/icons.159wjwals0.js +57 -0
  38. package/shell-dist/assets/{index-xZhmujjM.js → index.6alqyx6ae0.js} +21 -1
  39. package/shell-dist/assets/jsx-dev-runtime.3uibuc82f0.js +14 -0
  40. package/shell-dist/assets/jsx-runtime.h40xu4o5b0.js +14 -0
  41. package/shell-dist/assets/keepalive-for-react.xenu5vam70.js +1 -0
  42. package/shell-dist/assets/kit.37kr8ui390.js +1 -0
  43. package/shell-dist/assets/ky.1cgch7gu60.js +6 -0
  44. package/shell-dist/assets/motion-react.pjsxytvjs0.js +12 -0
  45. package/shell-dist/assets/motion.21tciqutg0.js +4 -0
  46. package/shell-dist/assets/nprogress.2nz76n9ny0.js +7 -0
  47. package/shell-dist/assets/ojm-dayjs-locale-zh-cn.3vr3beiyp0.js +9 -0
  48. package/shell-dist/assets/pinyin-pro.3v0mnxczn0.js +1 -0
  49. package/shell-dist/assets/pro-components.2kyot1vcl0.js +83 -0
  50. package/shell-dist/assets/react-countup.2x8idnr8y0.js +10 -0
  51. package/shell-dist/assets/react-dom-client.ick0v05ob0.js +43 -0
  52. package/shell-dist/assets/react-dom.2so9uxg3f0.js +22 -0
  53. package/shell-dist/assets/react-error-boundary.2dncdfj3b0.js +1 -0
  54. package/shell-dist/assets/react-i18next.auhvjhqv00.js +1 -0
  55. package/shell-dist/assets/react-jss.1ei6dr9e00.js +29 -0
  56. package/shell-dist/assets/react-query.3rln45ow00.js +1 -0
  57. package/shell-dist/assets/react-router-dom.3c6zld8h40.js +44 -0
  58. package/shell-dist/assets/react-router.2h9rebcv50.js +56 -0
  59. package/shell-dist/assets/react.1ew63mwts0.js +14 -0
  60. package/shell-dist/assets/runtime.2dgstpjsu0.js +21702 -0
  61. package/shell-dist/assets/simplebar-react.2g739uvai0.js +1 -0
  62. package/shell-dist/assets/spin-delay.yjeyj6iwp0.js +9 -0
  63. package/shell-dist/assets/tailwind-merge.125ehiqjw0.js +1 -0
  64. package/shell-dist/assets/use-sync-external-store-shim.1nz1pi9bb0.js +22 -0
  65. package/shell-dist/assets/zustand-middleware.1xaxjueme0.js +6 -0
  66. package/shell-dist/assets/zustand-react-shallow.pybayv1hx0.js +1 -0
  67. package/shell-dist/assets/zustand-react.9fsl9brxy0.js +1 -0
  68. package/shell-dist/assets/zustand-shallow.6cz0yyv9x0.js +1 -0
  69. package/shell-dist/assets/zustand-vanilla-shallow.3424monv50.js +1 -0
  70. package/shell-dist/assets/zustand-vanilla.1mo3ka3g30.js +1 -0
  71. package/shell-dist/assets/zustand.3gjgzlnqo0.js +1 -0
  72. package/shell-dist/index.html +67 -118
  73. package/shell-dist/versions.json +3 -3
  74. package/src/__tests__/publish-keygen.test.ts +157 -0
  75. package/src/__tests__/templates-migration-schema-parity.test.ts +540 -0
  76. package/src/__tests__/templates-schema-columns.test.ts +15 -2
  77. package/src/__tests__/templates-tenant-scope.test.ts +6 -2
  78. package/src/bundle.ts +8 -3
  79. package/src/cli.ts +17 -5
  80. package/src/dev.ts +15 -10
  81. package/src/esm-exports.ts +152 -70
  82. package/src/install.ts +11 -4
  83. package/src/keygen.ts +54 -0
  84. package/src/lifecycle/__tests__/bundle.test.ts +21 -3
  85. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +1 -1
  86. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +2 -2
  87. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +5 -2
  88. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +1 -1
  89. package/src/lifecycle/__tests__/manifest-contract.test.ts +75 -3
  90. package/src/lifecycle/__tests__/smoke.test.ts +2 -2
  91. package/src/manifest-contract.ts +91 -8
  92. package/src/market/bootstrap.ts +26 -2
  93. package/src/market/command.ts +1 -0
  94. package/src/market/publish.ts +5 -1
  95. package/src/market/walkthrough.ts +24 -8
  96. package/src/shared-deps.ts +69 -19
  97. package/src/site-base.ts +4 -0
  98. package/src/site.ts +2 -0
  99. package/src/usage.ts +8 -4
  100. package/src/vendor.ts +5 -1
  101. package/templates/config.yaml +4 -2
  102. package/templates/init.include.yaml +8 -3
  103. package/templates/openapi.json +460 -1
  104. package/templates/src/app/api/_platform/_lifecycle/compensate.ts +136 -0
  105. package/templates/src/app/api/_platform/_lifecycle/installer.ts +71 -22
  106. package/templates/src/app/api/_platform/_lifecycle/lock.ts +23 -0
  107. package/templates/src/app/api/_platform/_lifecycle/steps/deploy-web.ts +28 -0
  108. package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +46 -0
  109. package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +100 -36
  110. package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +7 -15
  111. package/templates/src/app/api/_platform/_lifecycle/text-edit.ts +71 -19
  112. package/templates/src/app/api/_platform/_lifecycle/types.ts +26 -2
  113. package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +93 -14
  114. package/templates/src/app/api/_platform/_shared/service/domain/settings.ts +8 -4
  115. package/templates/src/app/api/_platform/manifest.yaml +1 -0
  116. package/templates/src/app/api/_platform/market/api.ts +235 -10
  117. package/templates/src/app/api/_platform/migrations/0001__baseline.mysql.sql +178 -0
  118. package/templates/src/app/api/_platform/migrations/0001__baseline.pg.sql +173 -0
  119. package/templates/src/app/api/_platform/migrations/0001__baseline.sql +188 -0
  120. package/templates/src/app/api/_platform/migrations/0002__market_release.mysql.sql +17 -0
  121. package/templates/src/app/api/_platform/migrations/0002__market_release.pg.sql +16 -0
  122. package/templates/src/app/api/_platform/migrations/0002__market_release.sql +33 -0
  123. package/templates/src/app/api/_platform/schema.yaml +18 -0
  124. package/templates/src/app/api/_platform/seed.mysql.sql +10 -0
  125. package/templates/src/app/api/_platform/seed.pg.sql +10 -0
  126. package/templates/src/app/api/auth/_shared/wecom/client.ts +24 -4
  127. package/templates/src/app/api/home/migrations/0001__create_home_tables.mysql.sql +14 -0
  128. package/templates/src/app/api/home/migrations/0001__create_home_tables.pg.sql +14 -0
  129. package/templates/src/app/api/home/migrations/0002__tenant.pg.sql +4 -0
  130. package/templates/src/app/api/home/seed.mysql.sql +71 -0
  131. package/templates/src/app/api/home/seed.pg.sql +71 -0
  132. package/templates/src/app/api/notification/_shared/notify.ts +6 -6
  133. package/templates/src/app/api/notification/migrations/0001__create_notifications.mysql.sql +11 -0
  134. package/templates/src/app/api/notification/migrations/0001__create_notifications.pg.sql +11 -0
  135. package/templates/src/app/api/notification/migrations/0002__notifications_user.mysql.sql +8 -0
  136. package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.mysql.sql +21 -0
  137. package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.pg.sql +22 -0
  138. package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.mysql.sql +10 -0
  139. package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.pg.sql +11 -0
  140. package/templates/src/app/api/notification/migrations/0005__tenant.pg.sql +6 -0
  141. package/templates/src/app/api/notification/seed.mysql.sql +8 -0
  142. package/templates/src/app/api/notification/seed.pg.sql +11 -0
  143. package/templates/src/app/api/publish/_shared/identity.ts +69 -0
  144. package/templates/src/app/api/publish/api.ts +349 -0
  145. package/templates/src/app/api/publish/manifest.yaml +8 -0
  146. package/templates/src/app/api/publish/migrations/0001__publish_credential.mysql.sql +23 -0
  147. package/templates/src/app/api/publish/migrations/0001__publish_credential.pg.sql +23 -0
  148. package/templates/src/app/api/publish/migrations/0001__publish_credential.sql +39 -0
  149. package/templates/src/app/api/publish/schema.yaml +33 -0
  150. package/templates/src/app/api/system/login-methods/api.ts +2 -1
  151. package/templates/src/app/web/_platform/__client/api.schemas.ts +15 -0
  152. package/templates/src/app/web/_platform/__client/index.ts +25 -1
  153. package/templates/src/app/web/entry.ts +9 -5
  154. package/templates/src/app/web/home/pages/components/bar-chart.tsx +5 -3
  155. package/templates/src/app/web/home/pages/components/echarts-setup.ts +30 -0
  156. package/templates/src/app/web/home/pages/components/line-chart.tsx +5 -3
  157. package/templates/src/app/web/home/pages/components/pie-chart.tsx +6 -3
  158. package/templates/src/app/web/home/pages/index.tsx +3 -2
  159. package/templates/src/app/web/home/routes.ts +8 -2
  160. package/templates/src/app/web/login/locales/en-US.json +7 -1
  161. package/templates/src/app/web/login/locales/zh-CN.json +7 -1
  162. package/templates/src/app/web/login/pages/login.tsx +79 -86
  163. package/templates/src/app/web/login/routes.ts +54 -1
  164. package/templates/src/app/web/personal-center/locales/en-US.json +1 -0
  165. package/templates/src/app/web/personal-center/locales/zh-CN.json +1 -0
  166. package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +5 -0
  167. package/templates/src/app/web/system/__client/api.schemas.ts +2 -1
  168. package/templates/src/app/web/system/locales/en-US.json +1 -8
  169. package/templates/src/app/web/system/locales/zh-CN.json +1 -8
  170. package/templates/src/app/web/system/pages/settings/index.tsx +17 -4
  171. package/templates/src/app/web/system/routes.ts +0 -6
  172. package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +33 -0
  173. package/templates/src/oj-market/web/__tests__/DetailDrawer.test.tsx +69 -0
  174. package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +7 -1
  175. package/templates/src/oj-market/web/api/client.ts +3 -0
  176. package/templates/src/oj-market/web/api/defaultClient.ts +5 -0
  177. package/templates/src/oj-market/web/components/CardItem.tsx +44 -1
  178. package/templates/src/oj-market/web/components/DetailDrawer.tsx +17 -1
  179. package/templates/src/oj-market/web/entry.ts +31 -2
  180. package/templates/src/oj-market/web/locales/en-US.json +5 -1
  181. package/templates/src/oj-market/web/locales/zh-CN.json +5 -1
  182. package/templates/src/oj-market/web/publish/__client/api.schemas.ts +61 -0
  183. package/templates/src/oj-market/web/publish/__client/index.ts +164 -0
  184. package/templates/src/oj-market/web/publish/locales/en-US.json +80 -0
  185. package/templates/src/oj-market/web/publish/locales/zh-CN.json +80 -0
  186. package/templates/src/oj-market/web/publish/pages/history.tsx +112 -0
  187. package/templates/src/oj-market/web/publish/pages/index.tsx +196 -0
  188. package/templates/src/oj-market/web/publish/pages/model.ts +52 -0
  189. package/templates/src/oj-market/web/publish/pages/token-modal.tsx +82 -0
  190. package/templates/src/oj-market/web/publish/routes.ts +41 -0
  191. package/templates/src/oj-market/web/types.ts +30 -0
  192. package/templates/tests/lifecycle/compensate.spec.ts +207 -0
  193. package/templates/tests/lifecycle/deploy-smoke.spec.ts +38 -14
  194. package/templates/tests/lifecycle/installer.spec.ts +28 -9
  195. package/templates/tests/lifecycle/market-api.spec.ts +164 -12
  196. package/templates/tests/lifecycle/preflight.spec.ts +68 -0
  197. package/templates/tests/lifecycle/role-title-translate.spec.ts +2 -32
  198. package/templates/tests/lifecycle/text-edit.spec.ts +45 -27
  199. package/templates/tests/lifecycle/uninstall.spec.ts +36 -0
  200. package/templates/tests/publish-credential.spec.ts +354 -0
  201. package/templates/tests/publish-identity.spec.ts +44 -0
  202. package/templates/tests/wecom-client-timeout.spec.ts +33 -3
  203. package/templates/tests/wecom-landing.spec.ts +63 -1
  204. package/templates/tsconfig.paths.json +1 -0
  205. package/templates/web.config.ts +6 -4
  206. package/vendor/host-versions.json +3 -3
  207. package/shell-dist/assets/ahooks.js +0 -6909
  208. package/shell-dist/assets/antd-es-locale-zh_CN.js +0 -257
  209. package/shell-dist/assets/antd-img-crop.js +0 -1495
  210. package/shell-dist/assets/antd-locale-en_US.js +0 -374
  211. package/shell-dist/assets/antd-locale-zh_CN.js +0 -379
  212. package/shell-dist/assets/antd.js +0 -106874
  213. package/shell-dist/assets/clsx.js +0 -33
  214. package/shell-dist/assets/contract-errors.js +0 -30
  215. package/shell-dist/assets/contract.js +0 -18900
  216. package/shell-dist/assets/cssinjs.js +0 -2595
  217. package/shell-dist/assets/dayjs-plugin-advancedFormat.js +0 -98
  218. package/shell-dist/assets/dayjs-plugin-customParseFormat.js +0 -173
  219. package/shell-dist/assets/dayjs-plugin-isoWeek.js +0 -77
  220. package/shell-dist/assets/dayjs-plugin-localeData.js +0 -126
  221. package/shell-dist/assets/dayjs-plugin-quarterOfYear.js +0 -77
  222. package/shell-dist/assets/dayjs-plugin-relativeTime.js +0 -92
  223. package/shell-dist/assets/dayjs-plugin-weekOfYear.js +0 -73
  224. package/shell-dist/assets/dayjs-plugin-weekYear.js +0 -63
  225. package/shell-dist/assets/dayjs-plugin-weekday.js +0 -63
  226. package/shell-dist/assets/dayjs.js +0 -326
  227. package/shell-dist/assets/dnd-kit-core.js +0 -3601
  228. package/shell-dist/assets/dnd-kit-sortable.js +0 -717
  229. package/shell-dist/assets/dnd-kit-utilities.js +0 -340
  230. package/shell-dist/assets/i18next.js +0 -2321
  231. package/shell-dist/assets/icons-es-components-Context.js +0 -20
  232. package/shell-dist/assets/icons.js +0 -34961
  233. package/shell-dist/assets/jsx-dev-runtime.js +0 -75
  234. package/shell-dist/assets/jsx-runtime.js +0 -97
  235. package/shell-dist/assets/keepalive-for-react.js +0 -283
  236. package/shell-dist/assets/kit.js +0 -151
  237. package/shell-dist/assets/ky.js +0 -1427
  238. package/shell-dist/assets/motion-react.js +0 -13589
  239. package/shell-dist/assets/motion.js +0 -9959
  240. package/shell-dist/assets/nprogress.js +0 -314
  241. package/shell-dist/assets/ojm--ant-design-icons-es-icons-BarsOutlined.js +0 -7
  242. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CalendarOutlined.js +0 -7
  243. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CaretDownFilled.js +0 -7
  244. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CaretDownOutlined.js +0 -7
  245. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CaretUpOutlined.js +0 -7
  246. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CheckCircleFilled.js +0 -7
  247. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CheckOutlined.js +0 -7
  248. package/shell-dist/assets/ojm--ant-design-icons-es-icons-ClockCircleOutlined.js +0 -7
  249. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CloseCircleFilled.js +0 -7
  250. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CloseOutlined.js +0 -7
  251. package/shell-dist/assets/ojm--ant-design-icons-es-icons-CopyOutlined.js +0 -7
  252. package/shell-dist/assets/ojm--ant-design-icons-es-icons-DeleteOutlined.js +0 -7
  253. package/shell-dist/assets/ojm--ant-design-icons-es-icons-DoubleLeftOutlined.js +0 -7
  254. package/shell-dist/assets/ojm--ant-design-icons-es-icons-DoubleRightOutlined.js +0 -7
  255. package/shell-dist/assets/ojm--ant-design-icons-es-icons-DownOutlined.js +0 -7
  256. package/shell-dist/assets/ojm--ant-design-icons-es-icons-DownloadOutlined.js +0 -7
  257. package/shell-dist/assets/ojm--ant-design-icons-es-icons-EditOutlined.js +0 -7
  258. package/shell-dist/assets/ojm--ant-design-icons-es-icons-EllipsisOutlined.js +0 -7
  259. package/shell-dist/assets/ojm--ant-design-icons-es-icons-EnterOutlined.js +0 -7
  260. package/shell-dist/assets/ojm--ant-design-icons-es-icons-ExclamationCircleFilled.js +0 -7
  261. package/shell-dist/assets/ojm--ant-design-icons-es-icons-EyeInvisibleOutlined.js +0 -7
  262. package/shell-dist/assets/ojm--ant-design-icons-es-icons-EyeOutlined.js +0 -7
  263. package/shell-dist/assets/ojm--ant-design-icons-es-icons-FileOutlined.js +0 -7
  264. package/shell-dist/assets/ojm--ant-design-icons-es-icons-FileTextOutlined.js +0 -7
  265. package/shell-dist/assets/ojm--ant-design-icons-es-icons-FilterFilled.js +0 -7
  266. package/shell-dist/assets/ojm--ant-design-icons-es-icons-FolderOpenOutlined.js +0 -7
  267. package/shell-dist/assets/ojm--ant-design-icons-es-icons-FolderOutlined.js +0 -7
  268. package/shell-dist/assets/ojm--ant-design-icons-es-icons-HolderOutlined.js +0 -7
  269. package/shell-dist/assets/ojm--ant-design-icons-es-icons-InfoCircleFilled.js +0 -7
  270. package/shell-dist/assets/ojm--ant-design-icons-es-icons-LeftOutlined.js +0 -7
  271. package/shell-dist/assets/ojm--ant-design-icons-es-icons-LoadingOutlined.js +0 -7
  272. package/shell-dist/assets/ojm--ant-design-icons-es-icons-MinusOutlined.js +0 -7
  273. package/shell-dist/assets/ojm--ant-design-icons-es-icons-MinusSquareOutlined.js +0 -7
  274. package/shell-dist/assets/ojm--ant-design-icons-es-icons-PaperClipOutlined.js +0 -7
  275. package/shell-dist/assets/ojm--ant-design-icons-es-icons-PictureOutlined.js +0 -7
  276. package/shell-dist/assets/ojm--ant-design-icons-es-icons-PlusOutlined.js +0 -7
  277. package/shell-dist/assets/ojm--ant-design-icons-es-icons-PlusSquareOutlined.js +0 -7
  278. package/shell-dist/assets/ojm--ant-design-icons-es-icons-QuestionCircleOutlined.js +0 -7
  279. package/shell-dist/assets/ojm--ant-design-icons-es-icons-ReloadOutlined.js +0 -7
  280. package/shell-dist/assets/ojm--ant-design-icons-es-icons-RightOutlined.js +0 -7
  281. package/shell-dist/assets/ojm--ant-design-icons-es-icons-RotateLeftOutlined.js +0 -7
  282. package/shell-dist/assets/ojm--ant-design-icons-es-icons-RotateRightOutlined.js +0 -7
  283. package/shell-dist/assets/ojm--ant-design-icons-es-icons-SearchOutlined.js +0 -7
  284. package/shell-dist/assets/ojm--ant-design-icons-es-icons-StarFilled.js +0 -7
  285. package/shell-dist/assets/ojm--ant-design-icons-es-icons-SwapOutlined.js +0 -7
  286. package/shell-dist/assets/ojm--ant-design-icons-es-icons-SwapRightOutlined.js +0 -7
  287. package/shell-dist/assets/ojm--ant-design-icons-es-icons-UpOutlined.js +0 -7
  288. package/shell-dist/assets/ojm--ant-design-icons-es-icons-VerticalAlignTopOutlined.js +0 -7
  289. package/shell-dist/assets/ojm--ant-design-icons-es-icons-WarningFilled.js +0 -7
  290. package/shell-dist/assets/ojm--ant-design-icons-es-icons-ZoomInOutlined.js +0 -7
  291. package/shell-dist/assets/ojm--ant-design-icons-es-icons-ZoomOutOutlined.js +0 -7
  292. package/shell-dist/assets/ojm-dayjs-locale-zh-cn.js +0 -81
  293. package/shell-dist/assets/pinyin-pro.js +0 -25175
  294. package/shell-dist/assets/pro-components.js +0 -44627
  295. package/shell-dist/assets/react-countup.js +0 -525
  296. package/shell-dist/assets/react-dom-client.js +0 -12134
  297. package/shell-dist/assets/react-dom.js +0 -266
  298. package/shell-dist/assets/react-error-boundary.js +0 -141
  299. package/shell-dist/assets/react-i18next.js +0 -1773
  300. package/shell-dist/assets/react-jss.js +0 -4089
  301. package/shell-dist/assets/react-query.js +0 -3072
  302. package/shell-dist/assets/react-router-dom.js +0 -9927
  303. package/shell-dist/assets/react-router.js +0 -14584
  304. package/shell-dist/assets/react.js +0 -590
  305. package/shell-dist/assets/runtime.js +0 -28311
  306. package/shell-dist/assets/simplebar-react.js +0 -1119
  307. package/shell-dist/assets/spin-delay.js +0 -111
  308. package/shell-dist/assets/tailwind-merge.js +0 -3364
  309. package/shell-dist/assets/use-sync-external-store-shim.js +0 -130
  310. package/shell-dist/assets/zustand-middleware.js +0 -513
  311. package/shell-dist/assets/zustand-react-shallow.js +0 -27
  312. package/shell-dist/assets/zustand-react.js +0 -39
  313. package/shell-dist/assets/zustand-shallow.js +0 -22
  314. package/shell-dist/assets/zustand-vanilla-shallow.js +0 -71
  315. package/shell-dist/assets/zustand-vanilla.js +0 -40
  316. package/shell-dist/assets/zustand.js +0 -32
  317. package/templates/seed.mysql.sql +0 -6
  318. package/templates/seed.pg.sql +0 -6
  319. package/templates/src/app/api/_platform/migrations/0001__create_users.mysql.sql +0 -9
  320. package/templates/src/app/api/_platform/migrations/0001__create_users.pg.sql +0 -9
  321. package/templates/src/app/api/_platform/migrations/0001__create_users.sql +0 -7
  322. package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +0 -3
  323. package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.sql +0 -2
  324. package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.mysql.sql +0 -5
  325. package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.sql +0 -5
  326. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.mysql.sql +0 -70
  327. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.pg.sql +0 -67
  328. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.sql +0 -67
  329. package/templates/src/app/api/_platform/migrations/0005__users_must_change.mysql.sql +0 -3
  330. package/templates/src/app/api/_platform/migrations/0005__users_must_change.sql +0 -2
  331. package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.mysql.sql +0 -3
  332. package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.sql +0 -3
  333. package/templates/src/app/api/_platform/migrations/0007__users_email.sql +0 -3
  334. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.mysql.sql +0 -10
  335. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.pg.sql +0 -10
  336. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.sql +0 -11
  337. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.mysql.sql +0 -12
  338. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.pg.sql +0 -12
  339. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.sql +0 -13
  340. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.mysql.sql +0 -17
  341. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.pg.sql +0 -16
  342. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.sql +0 -18
  343. package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.mysql.sql +0 -2
  344. package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.sql +0 -4
  345. package/templates/src/app/api/_platform/migrations/0012__market_history.mysql.sql +0 -13
  346. package/templates/src/app/api/_platform/migrations/0012__market_history.pg.sql +0 -12
  347. package/templates/src/app/api/_platform/migrations/0012__market_history.sql +0 -14
  348. package/templates/src/app/api/_platform/migrations/0013__users_profile.sql +0 -4
  349. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.mysql.sql +0 -9
  350. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.pg.sql +0 -7
  351. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.sql +0 -8
  352. package/templates/src/app/api/_platform/migrations/0015__tenant.mysql.sql +0 -12
  353. package/templates/src/app/api/_platform/migrations/0015__tenant.pg.sql +0 -11
  354. package/templates/src/app/api/_platform/migrations/0015__tenant.sql +0 -44
  355. package/templates/src/app/api/_platform/migrations/0016__org.mysql.sql +0 -41
  356. package/templates/src/app/api/_platform/migrations/0016__org.pg.sql +0 -40
  357. package/templates/src/app/api/_platform/migrations/0016__org.sql +0 -62
  358. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +0 -7
  359. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +0 -4
  360. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +0 -8
  361. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +0 -3
  362. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +0 -3
  363. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +0 -8
  364. package/templates/src/app/web/system/pages/login-methods/index.tsx +0 -30
  365. /package/shell-dist/assets/{nprogress-css.js → nprogress-css.1cq2pzkre0.js} +0 -0
  366. /package/shell-dist/assets/{ojm-antd-es-button.js → ojm-antd-es-button.2uriphio50.js} +0 -0
  367. /package/shell-dist/assets/{ojm-antd-es-modal.js → ojm-antd-es-modal.bf3tgdh8v0.js} +0 -0
  368. /package/shell-dist/assets/{ojm-antd-es-slider.js → ojm-antd-es-slider.xyvptuhid0.js} +0 -0
  369. /package/shell-dist/assets/{ojm-antd-es-upload.js → ojm-antd-es-upload.13gx45e0g0.js} +0 -0
@@ -0,0 +1,157 @@
1
+ // BDD:`ojm keygen --credential`(凭证通道,P5)。
2
+ //
3
+ // 三条不可退让的行为:
4
+ // · 身份只来自凭证——服务端下发什么 key_id 就落什么目录,CLI 不推导;
5
+ // · 兑换失败不落任何密钥文件(先兑换、后生成,顺序即保证);
6
+ // · 不给凭证时逐字节走原离线路径(环境变量残留不得把离线用例带进网络分支)。
7
+ import fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
+ import { buildCli } from "../cli";
12
+ import { exchangePublishCredential } from "../keygen";
13
+
14
+ const TOKEN = "a".repeat(64);
15
+ const KEY_ID = "demo-solution.wecom-everpan";
16
+
17
+ let tmp: string;
18
+ let logs: string[];
19
+ let exitSpy: ReturnType<typeof vi.fn>;
20
+ let savedCredential: string | undefined;
21
+ let savedSite: string | undefined;
22
+
23
+ beforeEach(() => {
24
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ojm-publish-keygen-"));
25
+ logs = [];
26
+ exitSpy = vi.fn();
27
+ vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => logs.push(args.join(" ")));
28
+ vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => logs.push(args.join(" ")));
29
+ // E-4:环境变量残留会把离线用例带进网络分支(先例 registry-cli.test.ts)
30
+ savedCredential = process.env.OJM_PUBLISH_CREDENTIAL;
31
+ savedSite = process.env.OJM_SITE_URL;
32
+ delete process.env.OJM_PUBLISH_CREDENTIAL;
33
+ delete process.env.OJM_SITE_URL;
34
+ });
35
+
36
+ afterEach(() => {
37
+ vi.restoreAllMocks();
38
+ vi.unstubAllGlobals();
39
+ if (savedCredential === undefined)
40
+ delete process.env.OJM_PUBLISH_CREDENTIAL;
41
+ else
42
+ process.env.OJM_PUBLISH_CREDENTIAL = savedCredential;
43
+ if (savedSite === undefined)
44
+ delete process.env.OJM_SITE_URL;
45
+ else
46
+ process.env.OJM_SITE_URL = savedSite;
47
+ fs.rmSync(tmp, { recursive: true, force: true });
48
+ });
49
+
50
+ /** 站点信封应答(oj 统一 {code,msg,data}) */
51
+ function stubExchange(env: { code: number, msg?: string, data?: unknown }): ReturnType<typeof vi.fn> {
52
+ const fn = vi.fn(async () => new Response(JSON.stringify(env), { status: 200, headers: { "content-type": "application/json" } }));
53
+ vi.stubGlobal("fetch", fn);
54
+ return fn;
55
+ }
56
+
57
+ /** 跑一次 CLI 并等动作链结束(动态 import 需数百毫秒,同 cli.test.ts 的轮询口径) */
58
+ async function run(argv: string[]): Promise<{ out: string, exitCode: number | undefined }> {
59
+ const cli = buildCli(tmp, "zh-CN", { exit: exitSpy as unknown as (code: number) => never });
60
+ cli.parse(["node", "ojm", ...argv], { run: true });
61
+ try {
62
+ await vi.waitUntil(() => logs.length > 0 || exitSpy.mock.calls.length > 0, { timeout: 2500, interval: 10 });
63
+ }
64
+ catch {
65
+ // 无输出型动作:兜底继续
66
+ }
67
+ await new Promise(r => setTimeout(r, 50));
68
+ return { out: logs.join("\n"), exitCode: exitSpy.mock.calls[0]?.[0] as number | undefined };
69
+ }
70
+
71
+ describe("ojm keygen --credential", () => {
72
+ it("given 有效凭证 When keygen --credential Then 落盘目录名等于服务端下发 key_id", async () => {
73
+ const fetchFn = stubExchange({ code: 0, data: { key_id: KEY_ID, product: "demo-solution", username: "wecom:everpan", display_name: "张三" } });
74
+
75
+ const r = await run(["keygen", "--credential", TOKEN, "--out", path.join(tmp, "keys"), "--yes"]);
76
+
77
+ expect(r.exitCode).toBeUndefined();
78
+ expect(fetchFn).toHaveBeenCalledTimes(1);
79
+ const [, init] = fetchFn.mock.calls[0] as unknown as [string, RequestInit];
80
+ expect(JSON.parse(String(init.body))).toEqual({ token: TOKEN });
81
+ expect(fs.existsSync(path.join(tmp, "keys", "private.pem"))).toBe(true);
82
+ expect(r.out).toContain(KEY_ID);
83
+ // 申请人展示带回人话(产品 + 人名),而不是只有一串 key_id
84
+ expect(r.out).toContain("demo-solution");
85
+ expect(r.out).toContain("张三");
86
+ });
87
+
88
+ it("given 已用/过期凭证 When keygen --credential Then 非零退出且不落任何密钥", async () => {
89
+ stubExchange({ code: 400, msg: "发布凭证无效或已失效(可能已兑换、已过期或已撤销)。请到「发布管理」页重新申请" });
90
+
91
+ const r = await run(["keygen", "--credential", TOKEN, "--out", path.join(tmp, "keys"), "--yes"]);
92
+
93
+ expect(r.exitCode).toBe(1);
94
+ expect(r.out).toContain("发布管理");
95
+ expect(fs.existsSync(path.join(tmp, "keys"))).toBe(false);
96
+ });
97
+
98
+ it("given 站点连不上 When keygen --credential Then 报站点地址且不落盘", async () => {
99
+ vi.stubGlobal("fetch", vi.fn(async () => {
100
+ throw new Error("ECONNREFUSED");
101
+ }));
102
+
103
+ const r = await run(["keygen", "--credential", TOKEN, "--site-url", "http://10.0.0.1:9778", "--out", path.join(tmp, "keys"), "--yes"]);
104
+
105
+ expect(r.exitCode).toBe(1);
106
+ expect(r.out).toContain("http://10.0.0.1:9778");
107
+ expect(fs.existsSync(path.join(tmp, "keys"))).toBe(false);
108
+ });
109
+
110
+ it("given 同时给 --credential 与 --id Then exit 2 且文案说明二选一", async () => {
111
+ const fetchFn = stubExchange({ code: 0, data: { key_id: KEY_ID } });
112
+
113
+ const r = await run(["keygen", "--credential", TOKEN, "--id", "acme.market"]);
114
+
115
+ expect(r.exitCode).toBe(2);
116
+ expect(r.out).toContain("不能同时使用");
117
+ expect(fetchFn).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it("given 未设任何凭证 When keygen Then 走离线路径(既有行为零回归)", async () => {
121
+ const fetchFn = stubExchange({ code: 0, data: { key_id: KEY_ID } });
122
+
123
+ const r = await run(["keygen", "--out", path.join(tmp, "keys"), "--yes"]);
124
+
125
+ expect(r.exitCode).toBeUndefined();
126
+ expect(fetchFn).not.toHaveBeenCalled();
127
+ expect(r.out).toContain("local.dev");
128
+ expect(fs.existsSync(path.join(tmp, "keys", "private.pem"))).toBe(true);
129
+ });
130
+
131
+ it("given 环境变量 OJM_PUBLISH_CREDENTIAL When keygen 不带 --credential Then 仍走兑换通道", async () => {
132
+ process.env.OJM_PUBLISH_CREDENTIAL = TOKEN;
133
+ const fetchFn = stubExchange({ code: 0, data: { key_id: KEY_ID, product: "demo-solution", username: "u1" } });
134
+
135
+ const r = await run(["keygen", "--out", path.join(tmp, "keys"), "--yes"]);
136
+
137
+ expect(r.exitCode).toBeUndefined();
138
+ expect(fetchFn).toHaveBeenCalledTimes(1);
139
+ expect(r.out).toContain(KEY_ID);
140
+ });
141
+
142
+ it("given 服务端返回越界 key_id When 兑换 Then 拒绝且不落盘(key_id 会拼进密钥目录)", async () => {
143
+ stubExchange({ code: 0, data: { key_id: "../../etc", product: "x", username: "u" } });
144
+
145
+ const r = await run(["keygen", "--credential", TOKEN, "--out", path.join(tmp, "keys"), "--yes"]);
146
+
147
+ expect(r.exitCode).toBe(1);
148
+ expect(r.out).toContain("key_id 非法");
149
+ expect(fs.existsSync(path.join(tmp, "keys"))).toBe(false);
150
+ });
151
+
152
+ it("given 凭证不是 64 位 hex When 兑换 Then 本地即拒,不发请求", () => {
153
+ const fetchFn = stubExchange({ code: 0, data: { key_id: KEY_ID } });
154
+
155
+ return expect(exchangePublishCredential("short", { fetchFn: fetchFn as unknown as typeof fetch })).rejects.toThrow(/64 位十六进制/).then(() => expect(fetchFn).not.toHaveBeenCalled());
156
+ });
157
+ });
@@ -0,0 +1,540 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, join, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import { parse } from "yaml";
6
+
7
+ /**
8
+ * BDD:迁移里的建表语句与 schema.yaml / manifest.yaml 三处一致。
9
+ *
10
+ * 为什么要这条:oj 的 SQL 构造器在发 SQL 前先按 schema.yaml 校验列名,多一列少一列都是
11
+ * 端点 500 或「列不存在」的假象;而 manifest.yaml 的 tables 才是模块装载时的表清单。
12
+ * 历史演进靠迁移叠加,结构漂移往往就藏在最后一条 ALTER 里,属于最难靠人眼发现的一类。
13
+ *
14
+ * 2026-11 把 _platform 的 0001–0018(45 个文件)压平成一条基线后,原先靠
15
+ * templates-schema-columns 的 `ALTER ADD COLUMN` 校验对 _platform 直接失效(基线里没有
16
+ * ALTER)——本用例补的就是这个缺口,且覆盖面从「只查新增列」扩大到「整表逐列 + 三方言」。
17
+ *
18
+ * S006 纪律:结构只进 migrations。故本用例不解析 seed.sql。
19
+ */
20
+
21
+ const templatesRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../templates");
22
+ const apiRoot = join(templatesRoot, "src", "app", "api");
23
+
24
+ /**
25
+ * 表级约束行。注意不能简单地按首词判 KEY——`key TEXT PRIMARY KEY` 里的 key 是**列名**
26
+ * (sys_setting 的历史遗留,mysql 侧写作 `key`),按首词判会把它当约束整行丢掉。
27
+ */
28
+ const CONSTRAINT_PATTERNS = [
29
+ /^UNIQUE\b/i,
30
+ /^PRIMARY\s+KEY\b/i,
31
+ /^CONSTRAINT\b/i,
32
+ /^FOREIGN\s+KEY\b/i,
33
+ /^CHECK\s*\(/i,
34
+ /^INDEX\s/i,
35
+ /^KEY\s+[`"\w]+\s*\(/i,
36
+ ];
37
+
38
+ function isConstraintLine(segment: string): boolean {
39
+ return CONSTRAINT_PATTERNS.some(re => re.test(segment));
40
+ }
41
+
42
+ /** 由 oj 自建自管的 journal,结构不归任何模块的迁移管 */
43
+ const OJ_OWNED_TABLES = new Set(["_oj_migrations"]);
44
+
45
+ interface TableDecl {
46
+ tenant?: boolean
47
+ columns?: Record<string, unknown>
48
+ }
49
+
50
+ function walk(dir: string, out: string[] = []): string[] {
51
+ if (!existsSync(dir))
52
+ return out;
53
+ for (const entry of readdirSync(dir)) {
54
+ const full = join(dir, entry);
55
+ if (statSync(full).isDirectory())
56
+ walk(full, out);
57
+ else
58
+ out.push(full);
59
+ }
60
+ return out;
61
+ }
62
+
63
+ function modulesWithApi(): string[] {
64
+ return readdirSync(apiRoot).filter((name) => {
65
+ const full = join(apiRoot, name);
66
+ return statSync(full).isDirectory() && existsSync(join(full, "manifest.yaml"));
67
+ });
68
+ }
69
+
70
+ /**
71
+ * 按「方言覆盖文件必胜、通用文件兜底」的装配规则取某方言实际执行的迁移序列。
72
+ * 覆盖是**按版本号**替换,不是追加:0002__tenant.mysql.sql 存在时,0002__tenant.sql 不执行。
73
+ */
74
+ function migrationsFor(module: string, dialect: "base" | "mysql" | "pg"): string[] {
75
+ const dir = join(apiRoot, module, "migrations");
76
+ if (!existsSync(dir))
77
+ return [];
78
+
79
+ const base = new Map<string, string>();
80
+ const override = new Map<string, string>();
81
+
82
+ for (const f of readdirSync(dir).filter(name => name.endsWith(".sql"))) {
83
+ const seq = f.slice(0, 4);
84
+ if (f.endsWith(".mysql.sql")) {
85
+ if (dialect === "mysql")
86
+ override.set(seq, join(dir, f));
87
+ }
88
+ else if (f.endsWith(".pg.sql")) {
89
+ if (dialect === "pg")
90
+ override.set(seq, join(dir, f));
91
+ }
92
+ else {
93
+ base.set(seq, join(dir, f));
94
+ }
95
+ }
96
+
97
+ return [...new Set([...base.keys(), ...override.keys()])]
98
+ .sort()
99
+ .map(seq => override.get(seq) ?? base.get(seq)!);
100
+ }
101
+
102
+ function hasDialectOverrides(module: string, dialect: "mysql" | "pg"): boolean {
103
+ const dir = join(apiRoot, module, "migrations");
104
+ return existsSync(dir) && readdirSync(dir).some(f => f.endsWith(`.${dialect}.sql`));
105
+ }
106
+
107
+ /** 只留可执行语句:`--` 行注释里的示例语法不算方言违规 */
108
+ function executableSql(sql: string): string {
109
+ return sql.split("\n").filter(line => !line.trim().startsWith("--")).join("\n");
110
+ }
111
+
112
+ /**
113
+ * 各方言必须拒绝的 SQLite 专有写法。命中任意一条,该方言在真库上必然语法错
114
+ * (2026-11-05 演练实踩:home 0001 只写 sqlite 版,MySQL 报 1064 AUTOINCREMENT)。
115
+ * `CREATE INDEX IF NOT EXISTS` 在 pg 合法,故只列 mysql。
116
+ */
117
+ const DIALECT_ONLY_SQLITE: Record<"mysql" | "pg", RegExp[]> = {
118
+ mysql: [/\bAUTOINCREMENT\b/i, /\bINSERT\s+OR\s+IGNORE\b/i, /\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\b/i],
119
+ pg: [/\bAUTOINCREMENT\b/i, /\bINSERT\s+OR\s+IGNORE\b/i],
120
+ };
121
+
122
+ /** `INSERT [OR IGNORE|IGNORE] INTO <表>` 按表计数(剥注释,避免注释里的示例语句算数) */
123
+ function insertCountsByTable(sql: string): Map<string, number> {
124
+ const out = new Map<string, number>();
125
+ const re = /\bINSERT\s+(?:OR\s+IGNORE\s+|IGNORE\s+)?INTO\s+([`"\w]+)/gi;
126
+ for (const m of executableSql(sql).matchAll(re)) {
127
+ const table = m[1].replace(/[`"]/g, "");
128
+ out.set(table, (out.get(table) ?? 0) + 1);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /** 模板内所有 `seed*.sql`(跳过依赖与产物目录),返回相对模板根的路径 */
134
+ function seedSqlFiles(): string[] {
135
+ const out: string[] = [];
136
+ const skip = new Set(["node_modules", "dist", "compress-cache", "logs", ".git"]);
137
+ const visit = (dir: string) => {
138
+ for (const entry of readdirSync(dir)) {
139
+ if (skip.has(entry))
140
+ continue;
141
+ const full = join(dir, entry);
142
+ if (statSync(full).isDirectory())
143
+ visit(full);
144
+ else if (/^seed.*\.sql$/i.test(entry))
145
+ out.push(relative(templatesRoot, full));
146
+ }
147
+ };
148
+ visit(templatesRoot);
149
+ return out;
150
+ }
151
+
152
+ /**
153
+ * mysql 保留字里最容易被当成列名的一批。不求穷举——目标是把「裸 key」这类**必然 1064**
154
+ * 的写法挡在模板里,而不是当 SQL 解析器(真语法错误由真库演练兜底)。
155
+ */
156
+ const MYSQL_RESERVED = new Set([
157
+ "key",
158
+ "group",
159
+ "order",
160
+ "desc",
161
+ "index",
162
+ "rank",
163
+ "range",
164
+ "system",
165
+ "interval",
166
+ "condition",
167
+ "references",
168
+ "usage",
169
+ "values",
170
+ "table",
171
+ ]);
172
+
173
+ /** `INSERT ... (列清单)` 里裸写的保留字(反引号 / 双引号包起来的合法,跳过) */
174
+ function bareReservedColumns(sql: string): string[] {
175
+ const hits: string[] = [];
176
+ const re = /\bINSERT\s+(?:OR\s+IGNORE\s+|IGNORE\s+)?INTO\s+[`"\w]+\s*\(([^)]*)\)/gi;
177
+ for (const m of executableSql(sql).matchAll(re)) {
178
+ for (const raw of m[1].split(",")) {
179
+ const col = raw.trim();
180
+ if (!col || col.startsWith("`") || col.startsWith("\""))
181
+ continue;
182
+ const name = col.split(".").pop()!.replace(/\W/g, "").toLowerCase();
183
+ if (MYSQL_RESERVED.has(name))
184
+ hits.push(col);
185
+ }
186
+ }
187
+ return hits;
188
+ }
189
+
190
+ /** 解析单文件里的 `CREATE TABLE` → 表 → 列 → 首词类型(去掉引号与长度参数) */
191
+ function columnTypes(file: string): Map<string, Map<string, string>> {
192
+ const out = new Map<string, Map<string, string>>();
193
+ const sql = readFileSync(file, "utf-8");
194
+ for (const m of sql.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"\w]+)\s*\(([\s\S]*?)\)\s*(?:ENGINE|;)/gi)) {
195
+ const table = m[1].replace(/[`"]/g, "");
196
+ const cols = new Map<string, string>();
197
+ for (const raw of m[2].split("\n")) {
198
+ const line = raw.trim().replace(/,$/, "");
199
+ if (!line || line.startsWith("--") || isConstraintLine(line))
200
+ continue;
201
+ const [, name, type] = line.match(/^([`"\w]+)\s+([a-z]+)/i) ?? [];
202
+ if (name && type)
203
+ cols.set(name.replace(/[`"]/g, "").toLowerCase(), type.toUpperCase());
204
+ }
205
+ out.set(table, cols);
206
+ }
207
+ return out;
208
+ }
209
+
210
+ /** seed 装配规则:`seed.<dialect>.sql` 存在则必胜,否则回落 `seed.sql` */function seedFor(module: string, dialect: "base" | "mysql" | "pg"): string | null {
211
+ const dir = join(apiRoot, module);
212
+ if (dialect !== "base") {
213
+ const override = join(dir, `seed.${dialect}.sql`);
214
+ if (existsSync(override))
215
+ return override;
216
+ }
217
+ const base = join(dir, "seed.sql");
218
+ return existsSync(base) ? base : null;
219
+ }
220
+
221
+ /** 取出 `CREATE TABLE <name> ( ... )` 的 (表名, 列名集合),能处理引号与嵌套括号 */
222
+ function parseCreateTables(sql: string): Map<string, Set<string>> {
223
+ const out = new Map<string, Set<string>>();
224
+ const re = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"\w]+)\s*\(/gi;
225
+ for (let m = re.exec(sql); m !== null; m = re.exec(sql)) {
226
+ const rawName = m[1].replace(/[`"]/g, "");
227
+ const start = re.lastIndex;
228
+ let depth = 1;
229
+ let i = start;
230
+ while (i < sql.length && depth > 0) {
231
+ const ch = sql[i];
232
+ if (ch === "(")
233
+ depth++;
234
+ else if (ch === ")")
235
+ depth--;
236
+ i++;
237
+ }
238
+ const body = sql.slice(start, i - 1);
239
+ out.set(rawName, parseColumns(body));
240
+ }
241
+ return out;
242
+ }
243
+
244
+ /** 顶层逗号切分(忽略括号内逗号),取每段的首个标识符为列名 */
245
+ function parseColumns(body: string): Set<string> {
246
+ const cols = new Set<string>();
247
+ let depth = 0;
248
+ let start = 0;
249
+ const segments: string[] = [];
250
+ for (let i = 0; i < body.length; i++) {
251
+ const ch = body[i];
252
+ if (ch === "(") {
253
+ depth++;
254
+ }
255
+ else if (ch === ")") {
256
+ depth--;
257
+ }
258
+ else if (ch === "," && depth === 0) {
259
+ segments.push(body.slice(start, i));
260
+ start = i + 1;
261
+ }
262
+ }
263
+ segments.push(body.slice(start));
264
+
265
+ for (const raw of segments) {
266
+ // 去掉行注释再取首词
267
+ const seg = raw
268
+ .split("\n")
269
+ .filter(line => !line.trim().startsWith("--"))
270
+ .join(" ")
271
+ .trim();
272
+ if (seg === "" || isConstraintLine(seg))
273
+ continue;
274
+ cols.add(seg.split(/\s+/)[0].replace(/[`"]/g, "").toLowerCase());
275
+ }
276
+ return cols;
277
+ }
278
+
279
+ const DROP_TABLE_RE = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?([`"\w]+)\s*;/gi;
280
+ const ADD_COLUMN_RE = /ALTER\s+TABLE\s+([`"\w]+)\s+ADD\s+(?:COLUMN\s+)?([`"\w]+)/gi;
281
+
282
+ /**
283
+ * 按文件序累积出「跑完这些迁移后的表 → 列集」。
284
+ * 覆盖三种演进形态:整表 CREATE、DROP 后重建、ALTER ADD COLUMN(历史模块的常态)。
285
+ * 压平后的 _platform 只有第一种,正是本文件要守的那一类。
286
+ */
287
+ function effectiveSchema(files: string[]): Map<string, Set<string>> {
288
+ const tables = new Map<string, Set<string>>();
289
+
290
+ for (const file of files) {
291
+ const sql = readFileSync(file, "utf-8");
292
+
293
+ for (const m of sql.matchAll(DROP_TABLE_RE))
294
+ tables.delete(m[1].replace(/[`"]/g, ""));
295
+
296
+ for (const [table, cols] of parseCreateTables(sql))
297
+ tables.set(table, cols);
298
+
299
+ for (const m of sql.matchAll(ADD_COLUMN_RE)) {
300
+ const table = m[1].replace(/[`"]/g, "");
301
+ const column = m[2].replace(/[`"]/g, "").toLowerCase();
302
+ if (!tables.has(table))
303
+ tables.set(table, new Set());
304
+ tables.get(table)!.add(column);
305
+ }
306
+ }
307
+
308
+ return tables;
309
+ }
310
+
311
+ function loadSchemaTables(): Map<string, TableDecl> {
312
+ const tables = new Map<string, TableDecl>();
313
+ for (const file of walk(join(templatesRoot, "src")).filter(f => f.endsWith("schema.yaml"))) {
314
+ const doc = parse(readFileSync(file, "utf-8")) as { tables?: Record<string, TableDecl> } | null;
315
+ for (const [name, decl] of Object.entries(doc?.tables ?? {}))
316
+ tables.set(name, decl);
317
+ }
318
+ return tables;
319
+ }
320
+
321
+ function loadManifestTables(module: string): string[] {
322
+ const doc = parse(readFileSync(join(apiRoot, module, "manifest.yaml"), "utf-8")) as { tables?: string[] } | null;
323
+ return doc?.tables ?? [];
324
+ }
325
+
326
+ describe("templates 迁移 ↔ schema.yaml / manifest.yaml 结构一致", () => {
327
+ it("m-1 迁移落地后的列集与 schema.yaml 双向一致(多列 = 构造器拒查,少列 = 运行时缺列)", () => {
328
+ const declared = loadSchemaTables();
329
+ const violations: string[] = [];
330
+
331
+ for (const module of modulesWithApi()) {
332
+ const effective = effectiveSchema(migrationsFor(module, "base"));
333
+ for (const [table, cols] of effective) {
334
+ const where = `${module}/migrations`;
335
+ const decl = declared.get(table);
336
+ if (!decl) {
337
+ violations.push(`${where}: 表 ${table} 未在任何 schema.yaml 声明`);
338
+ continue;
339
+ }
340
+ const declaredCols = new Set(Object.keys(decl.columns ?? {}).map(c => c.toLowerCase()));
341
+ for (const c of cols) {
342
+ if (!declaredCols.has(c))
343
+ violations.push(`${where}: ${table}.${c} 建了列但 schema.yaml 未声明`);
344
+ }
345
+ for (const c of declaredCols) {
346
+ if (!cols.has(c))
347
+ violations.push(`${where}: ${table}.${c} 在 schema.yaml 声明但迁移未建列`);
348
+ }
349
+ }
350
+ }
351
+
352
+ expect(violations).toEqual([]);
353
+ });
354
+
355
+ it("m-2 每个 manifest.yaml 声明的表都有建表语句(_oj_migrations 由 oj 自建自有,豁免)", () => {
356
+ const violations: string[] = [];
357
+
358
+ for (const module of modulesWithApi()) {
359
+ const created = new Set(effectiveSchema(migrationsFor(module, "base")).keys());
360
+ for (const table of loadManifestTables(module)) {
361
+ if (OJ_OWNED_TABLES.has(table))
362
+ continue;
363
+ if (!created.has(table))
364
+ violations.push(`${module}: manifest.yaml 声明了 ${table},但该模块 migrations 里没有建表语句`);
365
+ }
366
+ }
367
+
368
+ expect(violations).toEqual([]);
369
+ });
370
+
371
+ it("m-3 方言覆盖不得缺表(有方言文件时,基础迁移的每张表都要有方言版)", () => {
372
+ const violations: string[] = [];
373
+
374
+ for (const module of modulesWithApi()) {
375
+ const baseTables = new Set(effectiveSchema(migrationsFor(module, "base")).keys());
376
+ if (baseTables.size === 0)
377
+ continue;
378
+
379
+ for (const dialect of ["mysql", "pg"] as const) {
380
+ const files = migrationsFor(module, dialect);
381
+ // 整个模块没有方言覆盖是合法的:oj 用基础迁移兜底(home / notification 现状)
382
+ if (!hasDialectOverrides(module, dialect))
383
+ continue;
384
+
385
+ const dialectTables = new Set(effectiveSchema(files).keys());
386
+ for (const table of baseTables) {
387
+ if (!dialectTables.has(table))
388
+ violations.push(`${module}: 表 ${table} 缺 ${dialect} 方言建表语句`);
389
+ }
390
+ for (const table of dialectTables) {
391
+ if (!baseTables.has(table))
392
+ violations.push(`${module}: ${dialect} 方言建了基础迁移里不存在的表 ${table}`);
393
+ }
394
+ }
395
+ }
396
+
397
+ expect(violations).toEqual([]);
398
+ });
399
+
400
+ it("m-4 方言覆盖的列集与 schema.yaml 一致(类型可方言化,列名不行)", () => {
401
+ const declared = loadSchemaTables();
402
+ const violations: string[] = [];
403
+
404
+ for (const module of modulesWithApi()) {
405
+ for (const dialect of ["mysql", "pg"] as const) {
406
+ const files = migrationsFor(module, dialect);
407
+ if (!hasDialectOverrides(module, dialect))
408
+ continue;
409
+
410
+ for (const [table, cols] of effectiveSchema(files)) {
411
+ const decl = declared.get(table);
412
+ if (!decl) {
413
+ violations.push(`${module}/${dialect}: 表 ${table} 未在任何 schema.yaml 声明`);
414
+ continue;
415
+ }
416
+ const declaredCols = new Set(Object.keys(decl.columns ?? {}).map(c => c.toLowerCase()));
417
+ for (const c of cols) {
418
+ if (!declaredCols.has(c))
419
+ violations.push(`${module}/${dialect}: ${table}.${c} 建了列但 schema.yaml 未声明`);
420
+ }
421
+ for (const c of declaredCols) {
422
+ if (!cols.has(c))
423
+ violations.push(`${module}/${dialect}: ${table}.${c} 在 schema.yaml 声明但方言迁移未建列`);
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ expect(violations).toEqual([]);
430
+ });
431
+
432
+ it("m-5 任一方言实际执行的迁移与 seed 不含该方言不支持的语法(方言覆盖须整条链路可执行)", () => {
433
+ const violations: string[] = [];
434
+
435
+ for (const module of modulesWithApi()) {
436
+ for (const dialect of ["mysql", "pg"] as const) {
437
+ const files = [...migrationsFor(module, dialect)];
438
+ const seed = seedFor(module, dialect);
439
+ if (seed)
440
+ files.push(seed);
441
+
442
+ for (const file of files) {
443
+ const sql = executableSql(readFileSync(file, "utf-8"));
444
+ for (const re of DIALECT_ONLY_SQLITE[dialect]) {
445
+ if (re.test(sql))
446
+ violations.push(`${module}/${dialect}: ${relative(apiRoot, file)} 含该方言不支持的语法 ${re}`);
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ expect(violations).toEqual([]);
453
+ });
454
+
455
+ it("m-6 方言 seed 与基础 seed 覆盖同一批表、同样条数(漏段 = 部署面静默缺数据)", () => {
456
+ const violations: string[] = [];
457
+
458
+ for (const module of modulesWithApi()) {
459
+ const baseSeed = seedFor(module, "base");
460
+ if (!baseSeed)
461
+ continue;
462
+ const expected = insertCountsByTable(readFileSync(baseSeed, "utf-8"));
463
+
464
+ for (const dialect of ["mysql", "pg"] as const) {
465
+ const seed = seedFor(module, dialect);
466
+ if (!seed || seed === baseSeed)
467
+ continue;
468
+
469
+ // seed 按 `;` 朴素切分:一行出现第二个终止符会切出孤立语句(如 `; ON CONFLICT DO NOTHING`)
470
+ for (const [i, line] of readFileSync(seed, "utf-8").split("\n").entries()) {
471
+ if (line.trim().startsWith("--"))
472
+ continue;
473
+ if ((line.match(/;/g) ?? []).length > 1)
474
+ violations.push(`${module}/${dialect} seed: 第 ${i + 1} 行有多个语句终止符(拼接错会切出孤立语句)`);
475
+ }
476
+
477
+ const actual = insertCountsByTable(readFileSync(seed, "utf-8"));
478
+ for (const table of new Set([...expected.keys(), ...actual.keys()])) {
479
+ if ((expected.get(table) ?? 0) !== (actual.get(table) ?? 0)) {
480
+ violations.push(
481
+ `${module}/${dialect} seed: ${table} 基础 ${expected.get(table) ?? 0} 条 / 方言 ${actual.get(table) ?? 0} 条`,
482
+ );
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ expect(violations).toEqual([]);
489
+ });
490
+
491
+ it("m-7 _platform 基线的 BIGINT 时间列在 pg 不得收窄为 INTEGER(毫秒 epoch 超出 int32)", () => {
492
+ const dir = join(apiRoot, "_platform", "migrations");
493
+ const mysql = columnTypes(join(dir, "0001__baseline.mysql.sql"));
494
+ const pg = columnTypes(join(dir, "0001__baseline.pg.sql"));
495
+ // 只盯时间语义列:*_at / *_time / *_start / *_until(秒级与毫秒级 epoch 都要求 64 位)
496
+ const TIME_COLUMN = /(?:^|_)(?:at|time|start|until)$/;
497
+ const violations: string[] = [];
498
+
499
+ for (const [table, cols] of mysql) {
500
+ for (const [col, type] of cols) {
501
+ if (type !== "BIGINT" || !TIME_COLUMN.test(col))
502
+ continue;
503
+ const pgType = pg.get(table)?.get(col);
504
+ if (pgType && pgType !== "BIGINT")
505
+ violations.push(`${table}.${col}: mysql=BIGINT / pg=${pgType}`);
506
+ }
507
+ }
508
+
509
+ expect(violations).toEqual([]);
510
+ });
511
+
512
+ /**
513
+ * 2026-11-05 实踩:模板根曾有 `seed.mysql.sql` / `seed.pg.sql` 的旧副本——只有 4 条
514
+ * sys_setting(模块内真件 22 条),且 mysql 那份把保留字 `key` 裸写,照文档部署必
515
+ * `1064 near 'key, module, ...'`。它既不在 m-5/m-6 的扫描范围(只扫 `src/<模块>/`),
516
+ * 也没有任何执行路径天天跑它,坏了大半年没人发现。这条守卫钉两件事:
517
+ * 1) `seed*.sql` 只允许活在模块目录(根级/别处出现即违规,从源头禁掉第二份真源);
518
+ * 2) mysql 方言件的 INSERT 列名里,保留字必须反引号(裸写 = 该方言必然语法错)。
519
+ */
520
+ it("m-8 seed*.sql 只活在模块目录,且 mysql 保留字列必须反引号(根级副本会漂移)", () => {
521
+ const misplaced = seedSqlFiles().filter((rel) => {
522
+ return !rel.startsWith(join("src", "app", "api") + sep);
523
+ });
524
+ expect(misplaced).toEqual([]);
525
+
526
+ const unescaped: string[] = [];
527
+ for (const module of modulesWithApi()) {
528
+ const file = join(apiRoot, module, "seed.mysql.sql");
529
+ if (!existsSync(file))
530
+ continue;
531
+ for (const col of bareReservedColumns(readFileSync(file, "utf-8")))
532
+ unescaped.push(`${module}/seed.mysql.sql: 列 ${col} 未转义`);
533
+ }
534
+ expect(unescaped).toEqual([]);
535
+
536
+ // 反证:检测器对坏样本必须命中(否则这条守卫只是恒绿)
537
+ expect(bareReservedColumns("INSERT IGNORE INTO t (key, module) VALUES ('a','b')")).toEqual(["key"]);
538
+ expect(bareReservedColumns("INSERT IGNORE INTO t (`key`, module) VALUES ('a','b')")).toEqual([]);
539
+ });
540
+ });