create-windy 0.2.33 → 0.3.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 (284) hide show
  1. package/dist/cli.js +173 -17
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +9 -0
  6. package/template/README.md +3 -1
  7. package/template/apps/agent-server/Dockerfile +1 -0
  8. package/template/apps/agent-server/README.md +28 -5
  9. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  12. package/template/apps/agent-server/pyproject.toml +1 -1
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
  18. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  21. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  22. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  25. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  26. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  27. package/template/apps/agent-server/tests/test_api.py +16 -1
  28. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  29. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  30. package/template/apps/server/package.json +4 -1
  31. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  32. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  33. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  34. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  35. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  36. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  37. package/template/apps/server/src/agent/run-facade.ts +38 -9
  38. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  39. package/template/apps/server/src/application-services.ts +50 -0
  40. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  41. package/template/apps/server/src/audit/search-summary.ts +2 -6
  42. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  43. package/template/apps/server/src/auth/password.ts +30 -21
  44. package/template/apps/server/src/auth/routes.test.ts +44 -3
  45. package/template/apps/server/src/auth/routes.ts +6 -1
  46. package/template/apps/server/src/auth/runtime.ts +4 -1
  47. package/template/apps/server/src/auth/service.ts +25 -7
  48. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  49. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  50. package/template/apps/server/src/configuration/definition.ts +242 -12
  51. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  52. package/template/apps/server/src/configuration/repository.ts +27 -2
  53. package/template/apps/server/src/configuration/routes.ts +4 -1
  54. package/template/apps/server/src/configuration/service.test.ts +100 -0
  55. package/template/apps/server/src/configuration/service.ts +50 -10
  56. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  57. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  58. package/template/apps/server/src/example-modules.ts +4 -1
  59. package/template/apps/server/src/guards.ts +18 -5
  60. package/template/apps/server/src/http/request-source.test.ts +85 -0
  61. package/template/apps/server/src/http/request-source.ts +90 -0
  62. package/template/apps/server/src/index.ts +45 -45
  63. package/template/apps/server/src/license/routes.ts +2 -0
  64. package/template/apps/server/src/license/runtime-service.ts +10 -11
  65. package/template/apps/server/src/module-bootstrap.ts +2 -2
  66. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  67. package/template/apps/server/src/module-host/initialize.ts +30 -44
  68. package/template/apps/server/src/module-host/request-context.ts +29 -23
  69. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  70. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  71. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  72. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  73. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  74. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  75. package/template/apps/server/src/module-host/storage-scope.ts +89 -0
  76. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  77. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  78. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  79. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  80. package/template/apps/server/src/module-host/user-directory-port.ts +139 -0
  81. package/template/apps/server/src/module-host.ts +4 -0
  82. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  83. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  84. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  85. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  86. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  87. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  88. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  89. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  90. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  91. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  92. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  93. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  94. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  95. package/template/apps/server/src/notification/memory-support.ts +72 -0
  96. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  97. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  98. package/template/apps/server/src/notification/repository.test.ts +171 -0
  99. package/template/apps/server/src/notification/repository.ts +179 -32
  100. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  101. package/template/apps/server/src/notification/route-support.ts +225 -0
  102. package/template/apps/server/src/notification/routes.test.ts +182 -0
  103. package/template/apps/server/src/notification/routes.ts +225 -132
  104. package/template/apps/server/src/notification/service.test.ts +104 -0
  105. package/template/apps/server/src/notification/service.ts +140 -0
  106. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  107. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  108. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  109. package/template/apps/server/src/route-guards.ts +1 -0
  110. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  111. package/template/apps/server/src/runtime-development.ts +2 -0
  112. package/template/apps/server/src/runtime.test.ts +23 -9
  113. package/template/apps/server/src/runtime.ts +39 -2
  114. package/template/apps/server/src/scheduler/audit.ts +2 -11
  115. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  116. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  117. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  118. package/template/apps/server/src/scheduler/routes.ts +2 -7
  119. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  120. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  121. package/template/apps/server/src/search/service.ts +2 -6
  122. package/template/apps/server/src/settings/routes.ts +15 -3
  123. package/template/apps/server/src/settings/runtime.ts +10 -1
  124. package/template/apps/server/src/storage/runtime.ts +13 -0
  125. package/template/apps/server/src/system/audit-query.ts +33 -4
  126. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  127. package/template/apps/server/src/system/audit-routes.ts +6 -5
  128. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  129. package/template/apps/server/src/system/department-organization.ts +320 -0
  130. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  131. package/template/apps/server/src/system/department-routes.ts +114 -33
  132. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  133. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  134. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  135. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  136. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  137. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  138. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  139. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  140. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  141. package/template/apps/server/src/system/route-helpers.ts +2 -5
  142. package/template/apps/server/src/system/route-types.ts +12 -3
  143. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  144. package/template/apps/server/src/system/routes.test.ts +10 -2
  145. package/template/apps/server/src/system/routes.ts +70 -36
  146. package/template/apps/server/src/system/seed.ts +7 -0
  147. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  148. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  149. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  150. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  151. package/template/apps/server/src/work-order/service.ts +42 -10
  152. package/template/apps/web/src/app/error-pages.ts +9 -0
  153. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  154. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  155. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  156. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  157. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  158. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  159. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  160. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  161. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  162. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  163. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  164. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  165. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  166. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  167. package/template/apps/web/src/lib/date-time.ts +22 -10
  168. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  169. package/template/apps/web/src/main.ts +14 -1
  170. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  171. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  172. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  173. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  174. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  175. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  176. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  177. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  178. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  179. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  180. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  181. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  182. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  183. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  184. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  185. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  186. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  187. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  188. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  189. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  190. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  191. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  192. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  193. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  194. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  195. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  196. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  197. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  198. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  199. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  200. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  201. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  202. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  203. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  204. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  205. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  206. package/template/apps/web/src/router/error-navigation.ts +50 -0
  207. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  208. package/template/apps/web/src/router/index.ts +71 -0
  209. package/template/apps/web/src/router/index.webtest.ts +29 -0
  210. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  211. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  212. package/template/apps/web/src/services/auth-api.ts +5 -0
  213. package/template/apps/web/src/services/http.ts +43 -6
  214. package/template/apps/web/src/services/http.webtest.ts +70 -0
  215. package/template/apps/web/src/services/notification-api.ts +85 -2
  216. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  217. package/template/apps/web/src/services/search-api.ts +39 -0
  218. package/template/apps/web/src/services/system-api.ts +18 -0
  219. package/template/apps/web/vitest.config.ts +1 -0
  220. package/template/docker-compose.yml +68 -0
  221. package/template/docs/architecture/ai-egress.md +49 -7
  222. package/template/docs/architecture/ai-runtime.md +34 -6
  223. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  224. package/template/docs/architecture/object-storage.md +22 -2
  225. package/template/docs/architecture/search-provider-protocol.md +38 -2
  226. package/template/docs/development/custom-error-pages.md +78 -0
  227. package/template/docs/development/custom-login-page.md +3 -3
  228. package/template/docs/platform/agent-runtime.md +76 -7
  229. package/template/docs/platform/audit-reliability.md +46 -2
  230. package/template/docs/platform/business-user-directory.md +82 -0
  231. package/template/docs/platform/module-configuration.md +13 -4
  232. package/template/docs/platform/notifications.md +67 -11
  233. package/template/docs/platform/organization-membership.md +17 -3
  234. package/template/docs/platform/system-crud-api.md +16 -0
  235. package/template/docs/platform/web-system-crud.md +6 -2
  236. package/template/package.json +4 -0
  237. package/template/packages/config/index.test.ts +45 -0
  238. package/template/packages/config/package.json +1 -1
  239. package/template/packages/config/src/platform.ts +169 -48
  240. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  241. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  242. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  243. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  244. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  245. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  246. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  247. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  248. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  249. package/template/packages/database/package.json +1 -1
  250. package/template/packages/database/src/schema/identity.ts +7 -0
  251. package/template/packages/database/src/schema/notifications.ts +135 -0
  252. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  253. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  254. package/template/packages/modules/package.json +1 -1
  255. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  256. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  257. package/template/packages/modules/src/manifest.ts +26 -0
  258. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  259. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  260. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  261. package/template/packages/modules/src/system-features.ts +6 -1
  262. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  263. package/template/packages/modules/src/system-module-catalog.ts +28 -4
  264. package/template/packages/modules/src/system-modules.test.ts +13 -1
  265. package/template/packages/modules/src/system-modules.ts +12 -0
  266. package/template/packages/modules/src/system-permissions.ts +11 -5
  267. package/template/packages/server-sdk/index.ts +11 -0
  268. package/template/packages/server-sdk/package.json +3 -2
  269. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  270. package/template/packages/server-sdk/src/module-host.ts +8 -0
  271. package/template/packages/server-sdk/src/request-context.ts +2 -0
  272. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  273. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  274. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
  275. package/template/packages/shared/package.json +1 -1
  276. package/template/packages/shared/src/audit.ts +25 -0
  277. package/template/packages/shared/src/module-configuration.ts +5 -0
  278. package/template/packages/shared/src/notification.ts +92 -1
  279. package/template/packages/shared/src/password.ts +7 -2
  280. package/template/packages/shared/src/platform-settings.ts +2 -0
  281. package/template/packages/storage/package.json +1 -1
  282. package/template/packages/storage/src/upload-service.test.ts +16 -0
  283. package/template/packages/storage/src/upload-service.ts +5 -1
  284. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -6,6 +6,11 @@ import type { ManagedDepartment } from "./entities.js";
6
6
  import { forbidden, resourceEvent, routeContext } from "./route-helpers.js";
7
7
  import type { EntityRepository } from "./repository.js";
8
8
  import type { RouteApp } from "./route-types.js";
9
+ import type { MutationRunner } from "./mutation-runner.js";
10
+ import {
11
+ DepartmentOrganizationPolicy,
12
+ type DepartmentTransitionAction,
13
+ } from "./department-organization.js";
9
14
 
10
15
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
11
16
 
@@ -14,6 +19,8 @@ export function registerDepartmentQuickCreateRoute(
14
19
  departments: EntityRepository<ManagedDepartment>,
15
20
  runtime: ServerRuntime,
16
21
  feature?: FeatureFlagDefinition,
22
+ policy?: DepartmentOrganizationPolicy,
23
+ mutationRunner?: MutationRunner,
17
24
  ): void {
18
25
  app.post("/system/departments/quick", async (rawContext) => {
19
26
  const { guardContext, body } = routeContext(rawContext);
@@ -28,36 +35,22 @@ export function registerDepartmentQuickCreateRoute(
28
35
  if (!decision.allowed) return forbidden(decision.reason);
29
36
  const parsed = parseDepartment(body);
30
37
  if (parsed instanceof Response) return parsed;
31
- const duplicate = (await departments.all()).find(
32
- (department) => department.code === parsed.code,
33
- );
34
- if (duplicate) {
35
- return Response.json(
36
- { error: "DUPLICATE_DEPARTMENT_CODE", message: "部门编码已存在" },
37
- { status: 409 },
38
- );
39
- }
40
- if (parsed.parentId) {
41
- const parent = await departments.get(parsed.parentId);
42
- if (!parent || parent.status !== "active") {
43
- return Response.json(
44
- {
45
- error: "INVALID_DEPARTMENT_PARENT",
46
- message: "上级部门不存在或已停用",
47
- },
48
- { status: 400 },
49
- );
38
+ const create = async () => {
39
+ const policyError = await policy?.beforeCreate(parsed);
40
+ if (policyError) {
41
+ return Response.json(policyError, { status: 409 });
50
42
  }
51
- }
52
- const item = await departments.create(parsed, guardContext.actor);
53
- await recordMutationAudit(
54
- runtime,
55
- "departments",
56
- resourceEvent(guardContext, "crud.create", "departments", item.id, {
57
- source: "user-form-shortcut",
58
- }),
59
- );
60
- return item;
43
+ const item = await departments.create(parsed, guardContext.actor);
44
+ await recordMutationAudit(
45
+ runtime,
46
+ "departments",
47
+ resourceEvent(guardContext, "crud.create", "departments", item.id, {
48
+ source: "user-form-shortcut",
49
+ }),
50
+ );
51
+ return item;
52
+ };
53
+ return mutationRunner ? mutationRunner.run(create) : create();
61
54
  });
62
55
  }
63
56
 
@@ -75,16 +68,104 @@ function parseDepartment(body: unknown): Partial<ManagedDepartment> | Response {
75
68
  { status: 400 },
76
69
  );
77
70
  }
71
+ const parentId =
72
+ typeof value.parentId === "string" && value.parentId
73
+ ? value.parentId
74
+ : undefined;
75
+ if (!parentId) {
76
+ return Response.json(
77
+ {
78
+ error: "DEPARTMENT_PARENT_REQUIRED",
79
+ message: "平台只允许一个根组织,新部门必须选择上级部门",
80
+ },
81
+ { status: 400 },
82
+ );
83
+ }
78
84
  return {
79
85
  code,
80
86
  name,
81
- parentId:
82
- typeof value.parentId === "string" && value.parentId
83
- ? value.parentId
84
- : undefined,
87
+ parentId,
85
88
  order:
86
89
  typeof value.order === "number" && Number.isFinite(value.order)
87
90
  ? value.order
88
91
  : 100,
89
92
  };
90
93
  }
94
+
95
+ export function registerDepartmentTransitionRoute(
96
+ app: RouteApp,
97
+ policy: DepartmentOrganizationPolicy,
98
+ mutationRunner: MutationRunner,
99
+ runtime: ServerRuntime,
100
+ feature?: FeatureFlagDefinition,
101
+ ): void {
102
+ app.post("/system/departments/:id/transition", async (rawContext) => {
103
+ const { guardContext, params, body } = routeContext(rawContext);
104
+ const decision = await evaluateRouteGuards(runtime, guardContext, {
105
+ permission: {
106
+ method: "POST",
107
+ path: "/api/system/departments/:id/transition",
108
+ permissionKey: "system.department.manage",
109
+ },
110
+ feature,
111
+ });
112
+ if (!decision.allowed) return forbidden(decision.reason);
113
+ const parsed = parseTransition(body);
114
+ if (parsed instanceof Response) return parsed;
115
+ return mutationRunner.run(async () => {
116
+ const result = await policy.transition(
117
+ String(params.id || ""),
118
+ parsed.action,
119
+ parsed.targetDepartmentId,
120
+ guardContext.actor,
121
+ );
122
+ if ("error" in result) {
123
+ return Response.json(result, { status: 409 });
124
+ }
125
+ await recordMutationAudit(
126
+ runtime,
127
+ "departments",
128
+ resourceEvent(
129
+ guardContext,
130
+ "organization.department.transition",
131
+ "departments",
132
+ result.department.id,
133
+ {
134
+ action: parsed.action,
135
+ targetDepartmentId: parsed.targetDepartmentId,
136
+ movedChildren: result.movedChildren,
137
+ movedUsers: result.movedUsers,
138
+ updatedRoles: result.updatedRoles,
139
+ },
140
+ ),
141
+ );
142
+ return result;
143
+ });
144
+ });
145
+ }
146
+
147
+ function parseTransition(body: unknown):
148
+ | {
149
+ action: DepartmentTransitionAction;
150
+ targetDepartmentId?: string;
151
+ }
152
+ | Response {
153
+ const value =
154
+ body && typeof body === "object" ? (body as Record<string, unknown>) : {};
155
+ if (value.action !== "disable" && value.action !== "delete") {
156
+ return Response.json(
157
+ {
158
+ error: "INVALID_DEPARTMENT_TRANSITION",
159
+ message: "部门处置动作只能是 disable 或 delete",
160
+ },
161
+ { status: 400 },
162
+ );
163
+ }
164
+ return {
165
+ action: value.action,
166
+ targetDepartmentId:
167
+ typeof value.targetDepartmentId === "string" && value.targetDepartmentId
168
+ ? value.targetDepartmentId
169
+ : undefined,
170
+ };
171
+ }
@@ -21,6 +21,7 @@ import type {
21
21
  EntityListQuery,
22
22
  EntityRepository,
23
23
  } from "./repository.js";
24
+ import { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
24
25
 
25
26
  type DbTable = unknown;
26
27
 
@@ -40,6 +41,7 @@ export class DrizzleEntityRepository<
40
41
  constructor(
41
42
  protected readonly db: NodePgDatabase,
42
43
  protected readonly config: DrizzleEntityRepositoryConfig<T>,
44
+ private readonly transactionContext?: DrizzleTransactionContext,
43
45
  ) {}
44
46
 
45
47
  async list(
@@ -99,7 +101,9 @@ export class DrizzleEntityRepository<
99
101
  updatedAt: now,
100
102
  updatedBy: actor.id,
101
103
  } as T;
102
- await this.db.insert(this.table()).values(this.config.toRow(entity));
104
+ await this.database()
105
+ .insert(this.table())
106
+ .values(this.config.toRow(entity));
103
107
  return entity;
104
108
  }
105
109
 
@@ -119,7 +123,7 @@ export class DrizzleEntityRepository<
119
123
  updatedAt: new Date().toISOString(),
120
124
  updatedBy: actor.id,
121
125
  } as T;
122
- await this.db
126
+ await this.database()
123
127
  .update(this.table())
124
128
  .set(this.config.toRow(entity))
125
129
  .where(eq(this.idColumn(), id));
@@ -133,7 +137,7 @@ export class DrizzleEntityRepository<
133
137
  if (updates.length === 0) return [];
134
138
  await this.ensureSeeded();
135
139
  const ids = updates.map(({ id }) => id);
136
- const rows = await this.db
140
+ const rows = await this.database()
137
141
  .select()
138
142
  .from(this.table())
139
143
  .where(inArray(this.idColumn(), ids));
@@ -154,7 +158,7 @@ export class DrizzleEntityRepository<
154
158
  updatedAt: now,
155
159
  updatedBy: actor.id,
156
160
  })) as T[];
157
- await this.db.transaction(async (transaction) => {
161
+ await this.database().transaction(async (transaction) => {
158
162
  for (const entity of entities) {
159
163
  await transaction
160
164
  .update(this.table())
@@ -221,7 +225,7 @@ export class DrizzleEntityRepository<
221
225
  if (items.length === 0) {
222
226
  return;
223
227
  }
224
- await this.db
228
+ await this.database()
225
229
  .insert(this.table())
226
230
  .values(items.map((item) => this.config.toRow(item)))
227
231
  .onConflictDoNothing();
@@ -255,7 +259,7 @@ export class DrizzleEntityRepository<
255
259
  offset?: number,
256
260
  ) {
257
261
  const orderBy = this.orderBy(sort);
258
- let query = this.db.select().from(this.table()).$dynamic();
262
+ let query = this.database().select().from(this.table()).$dynamic();
259
263
  if (where) query = query.where(where);
260
264
  if (orderBy.length) query = query.orderBy(...orderBy);
261
265
  if (limit) query = query.limit(limit);
@@ -266,7 +270,7 @@ export class DrizzleEntityRepository<
266
270
  protected countRows(
267
271
  where: SQL | undefined,
268
272
  ): Promise<Array<{ count: number }>> {
269
- let query = this.db
273
+ let query = this.database()
270
274
  .select({ count: sql<number>`count(*)::int` })
271
275
  .from(this.table())
272
276
  .$dynamic();
@@ -295,4 +299,8 @@ export class DrizzleEntityRepository<
295
299
  protected table() {
296
300
  return this.config.table as never;
297
301
  }
302
+
303
+ protected database(): NodePgDatabase {
304
+ return this.transactionContext?.current(this.db) || this.db;
305
+ }
298
306
  }
@@ -19,6 +19,7 @@ import {
19
19
  type DrizzleEntityRepositoryConfig,
20
20
  } from "./drizzle-repository.js";
21
21
  import type { EntityImportResult, EntityListQuery } from "./repository.js";
22
+ import type { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
22
23
 
23
24
  export interface DrizzleScopedEntityRepositoryConfig<
24
25
  T extends ManagedEntity,
@@ -38,8 +39,9 @@ export class DrizzleScopedEntityRepository<T extends ManagedEntity>
38
39
  constructor(
39
40
  db: NodePgDatabase,
40
41
  config: DrizzleScopedEntityRepositoryConfig<T>,
42
+ transactionContext?: DrizzleTransactionContext,
41
43
  ) {
42
- super(db, config);
44
+ super(db, config, transactionContext);
43
45
  }
44
46
 
45
47
  async listScoped(
@@ -98,7 +100,7 @@ export class DrizzleScopedEntityRepository<T extends ManagedEntity>
98
100
  ): Promise<T[] | undefined> {
99
101
  if (ids.length === 0) return [];
100
102
  await this.ensureSeeded();
101
- return this.db.transaction(
103
+ return this.database().transaction(
102
104
  async (transaction) => {
103
105
  await transaction.execute(sql`select 1`);
104
106
  await this.afterBatchSnapshotEstablished();
@@ -157,7 +159,7 @@ export class DrizzleScopedEntityRepository<T extends ManagedEntity>
157
159
  updatedBy: actor.id,
158
160
  } as T;
159
161
  assertEntityInDataScope(entity, scope, this.config.dataScope.entity);
160
- const rows = (await this.db
162
+ const rows = (await this.database()
161
163
  .update(this.table())
162
164
  .set(this.config.toRow(entity))
163
165
  .where(this.idWhere(id, scope))
@@ -44,6 +44,11 @@ import {
44
44
  import type { EntityRepository } from "./repository.js";
45
45
  import type { ScopedEntityRepository } from "../data-access/scoped-repository.js";
46
46
  import { userDataScopeBinding } from "./user-data-scope.js";
47
+ import { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
48
+ import {
49
+ PostgresAdvisoryMutationRunner,
50
+ type MutationRunner,
51
+ } from "./mutation-runner.js";
47
52
 
48
53
  export interface DrizzleSystemRepositorySeeds {
49
54
  users: ManagedUser[];
@@ -67,24 +72,43 @@ export interface DrizzleSystemRepositories {
67
72
  // @windy-module system.license begin
68
73
  licenses: EntityRepository<ManagedLicense>;
69
74
  // @windy-module system.license end
75
+ organizationMutationRunner: MutationRunner;
70
76
  }
71
77
 
72
78
  export function createDrizzleSystemRepositories(
73
79
  db: NodePgDatabase,
74
80
  seeds: DrizzleSystemRepositorySeeds,
75
81
  ): DrizzleSystemRepositories {
82
+ const transactionContext = new DrizzleTransactionContext();
76
83
  return {
77
- users: new DrizzleScopedEntityRepository(db, userConfig(seeds.users)),
78
- roles: repo(db, roleConfig(seeds.roles)),
79
- departments: repo(db, departmentConfig(seeds.departments)),
80
- menus: repo(db, menuConfig(seeds.menus)),
81
- dictionaries: repo(db, dictionaryConfig(seeds.dictionaries)),
84
+ users: new DrizzleScopedEntityRepository(
85
+ db,
86
+ userConfig(seeds.users),
87
+ transactionContext,
88
+ ),
89
+ roles: repo(db, roleConfig(seeds.roles), transactionContext),
90
+ departments: repo(
91
+ db,
92
+ departmentConfig(seeds.departments),
93
+ transactionContext,
94
+ ),
95
+ menus: repo(db, menuConfig(seeds.menus), transactionContext),
96
+ dictionaries: repo(
97
+ db,
98
+ dictionaryConfig(seeds.dictionaries),
99
+ transactionContext,
100
+ ),
82
101
  features: new ManifestFeatureRepository(
83
- repo(db, featureConfig(seeds.features)),
102
+ repo(db, featureConfig(seeds.features), transactionContext),
84
103
  ),
85
104
  // @windy-module system.license begin
86
- licenses: repo(db, licenseConfig(seeds.licenses)),
105
+ licenses: repo(db, licenseConfig(seeds.licenses), transactionContext),
87
106
  // @windy-module system.license end
107
+ organizationMutationRunner: new PostgresAdvisoryMutationRunner(
108
+ db,
109
+ "windy:system:organization",
110
+ transactionContext,
111
+ ),
88
112
  };
89
113
  }
90
114
 
@@ -99,8 +123,12 @@ function repo<
99
123
  // @windy-module system.license begin
100
124
  | ManagedLicense,
101
125
  // @windy-module system.license end
102
- >(db: NodePgDatabase, config: DrizzleEntityRepositoryConfig<T>) {
103
- return new DrizzleEntityRepository<T>(db, config);
126
+ >(
127
+ db: NodePgDatabase,
128
+ config: DrizzleEntityRepositoryConfig<T>,
129
+ transactionContext: DrizzleTransactionContext,
130
+ ) {
131
+ return new DrizzleEntityRepository<T>(db, config, transactionContext);
104
132
  }
105
133
 
106
134
  function userConfig(
@@ -0,0 +1,14 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+
4
+ export class DrizzleTransactionContext {
5
+ private readonly storage = new AsyncLocalStorage<NodePgDatabase>();
6
+
7
+ current(fallback: NodePgDatabase): NodePgDatabase {
8
+ return this.storage.getStore() || fallback;
9
+ }
10
+
11
+ run<T>(database: NodePgDatabase, mutation: () => Promise<T>): Promise<T> {
12
+ return this.storage.run(database, mutation);
13
+ }
14
+ }
@@ -8,8 +8,8 @@ import type { ResourceRouteConfig } from "./route-types.js";
8
8
  import type { SystemRepositories } from "./seed.js";
9
9
  import { UserAccountPolicy } from "./user-account-policy.js";
10
10
  import { userDepartmentValidator } from "./user-department-validation.js";
11
- import { SerialMutationRunner } from "./mutation-runner.js";
12
11
  import { projectMaskedUser } from "./user-projection.js";
12
+ import { RoleDepartmentScopePolicy } from "./role-department-scope-policy.js";
13
13
 
14
14
  export function identityRouteConfigs(
15
15
  repositories: SystemRepositories,
@@ -24,7 +24,10 @@ export function identityRouteConfigs(
24
24
  repositories.roles,
25
25
  authRepository,
26
26
  );
27
- const mutationRunner = new SerialMutationRunner();
27
+ const roleDepartmentScope = new RoleDepartmentScopePolicy(
28
+ repositories.departments,
29
+ );
30
+ const mutationRunner = repositories.organizationMutationRunner;
28
31
  return {
29
32
  users: {
30
33
  ...resource(
@@ -65,6 +68,8 @@ export function identityRouteConfigs(
65
68
  ),
66
69
  beforeUpdate: (current, input) =>
67
70
  policy.protectRoleUpdate(current, input),
71
+ validateInput: (input, current) =>
72
+ roleDepartmentScope.validate(input, current),
68
73
  beforeDelete: (current) => policy.protectRoleDelete(current),
69
74
  importable: false,
70
75
  mutationRunner,
@@ -1,5 +1,8 @@
1
+ import { sql } from "drizzle-orm";
2
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
1
3
  import type { ManagedEntity } from "./entities.js";
2
4
  import type { ResourceRouteConfig } from "./route-types.js";
5
+ import { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
3
6
 
4
7
  export interface MutationRunner {
5
8
  run<T>(mutation: () => Promise<T>): Promise<T>;
@@ -23,6 +26,26 @@ export class SerialMutationRunner implements MutationRunner {
23
26
  }
24
27
  }
25
28
 
29
+ export class PostgresAdvisoryMutationRunner implements MutationRunner {
30
+ constructor(
31
+ private readonly db: NodePgDatabase,
32
+ private readonly lockKey: string,
33
+ private readonly transactionContext: DrizzleTransactionContext,
34
+ ) {}
35
+
36
+ run<T>(mutation: () => Promise<T>): Promise<T> {
37
+ return this.db.transaction(async (transaction) => {
38
+ await transaction.execute(
39
+ sql`select pg_advisory_xact_lock(hashtextextended(${this.lockKey}, 0))`,
40
+ );
41
+ return this.transactionContext.run(
42
+ transaction as unknown as NodePgDatabase,
43
+ mutation,
44
+ );
45
+ });
46
+ }
47
+ }
48
+
26
49
  export function runResourceMutation<T extends ManagedEntity, R>(
27
50
  config: ResourceRouteConfig<T>,
28
51
  mutation: () => Promise<R>,
@@ -0,0 +1,176 @@
1
+ import {
2
+ afterAll,
3
+ beforeAll,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ test,
8
+ } from "bun:test";
9
+ import { resolve } from "node:path";
10
+ import { drizzle } from "drizzle-orm/node-postgres";
11
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
12
+ import { Pool } from "pg";
13
+ import { createFoundationSnapshot } from "../foundation.js";
14
+ import type { RequestActor } from "../guards.js";
15
+ import { DepartmentOrganizationPolicy } from "./department-organization.js";
16
+ import { createSystemRepositories } from "./seed.js";
17
+ import { userDepartmentValidator } from "./user-department-validation.js";
18
+
19
+ const databaseUrl = process.env.ORGANIZATION_TEST_DATABASE_URL;
20
+ const describePostgres = databaseUrl ? describe : describe.skip;
21
+
22
+ describePostgres("PostgreSQL organization invariants", () => {
23
+ let pool: Pool;
24
+
25
+ beforeAll(async () => {
26
+ pool = new Pool({ connectionString: databaseUrl });
27
+ await migrate(drizzle(pool), {
28
+ migrationsFolder: resolve(
29
+ import.meta.dir,
30
+ "../../../../packages/database/drizzle",
31
+ ),
32
+ migrationsSchema: "public",
33
+ migrationsTable: "__organization_test_migrations",
34
+ });
35
+ });
36
+
37
+ beforeEach(async () => {
38
+ await pool.query("truncate table users, roles, departments cascade");
39
+ });
40
+
41
+ afterAll(async () => {
42
+ await pool?.end();
43
+ });
44
+
45
+ test("数据库约束拒绝并发创建第二个根组织", async () => {
46
+ const results = await Promise.allSettled([
47
+ insertRoot(pool, "root_a"),
48
+ insertRoot(pool, "root_b"),
49
+ ]);
50
+
51
+ expect(results.filter(({ status }) => status === "fulfilled")).toHaveLength(
52
+ 1,
53
+ );
54
+ expect(
55
+ await pool.query(
56
+ "select count(*)::int as count from departments where parent_id is null and deleted_at is null",
57
+ ),
58
+ ).toMatchObject({ rows: [{ count: 1 }] });
59
+ });
60
+
61
+ test("跨 Server advisory lock 让并发互相移动只成功一次", async () => {
62
+ const left = repositories(pool);
63
+ const right = repositories(pool);
64
+ await left.departments.all();
65
+ await left.departments.create(department("dept_a", "dept_platform"), actor);
66
+ await left.departments.create(department("dept_b", "dept_platform"), actor);
67
+
68
+ const results = await Promise.all([
69
+ move(left, "dept_a", "dept_b"),
70
+ move(right, "dept_b", "dept_a"),
71
+ ]);
72
+ expect(results.filter((result) => result === "moved")).toHaveLength(1);
73
+ expect(
74
+ results.filter((result) => result === "DEPARTMENT_CYCLE"),
75
+ ).toHaveLength(1);
76
+ });
77
+
78
+ test("用户归属写入与部门停用并发后不会产生启用用户挂在停用部门", async () => {
79
+ const left = repositories(pool);
80
+ const right = repositories(pool);
81
+ await left.departments.all();
82
+ await left.departments.create(department("source", "dept_platform"), actor);
83
+ const policy = new DepartmentOrganizationPolicy(left);
84
+
85
+ const [transition, assignment] = await Promise.all([
86
+ left.organizationMutationRunner.run(() =>
87
+ policy.transition("source", "disable", undefined, actor),
88
+ ),
89
+ right.organizationMutationRunner.run(async () => {
90
+ const error = await userDepartmentValidator(right.departments)({
91
+ departmentId: "source",
92
+ });
93
+ if (error) return "rejected";
94
+ await right.users.create(
95
+ {
96
+ id: "user_source",
97
+ username: "source-user",
98
+ displayName: "来源用户",
99
+ departmentId: "source",
100
+ roleIds: [],
101
+ },
102
+ actor,
103
+ );
104
+ return "created";
105
+ }),
106
+ ]);
107
+
108
+ const source = await left.departments.get("source");
109
+ const user = await left.users.get("user_source");
110
+ if (source?.status === "disabled") {
111
+ expect(assignment).toBe("rejected");
112
+ expect(user).toBeUndefined();
113
+ } else {
114
+ expect(assignment).toBe("created");
115
+ expect(transition).toMatchObject({
116
+ error: "DEPARTMENT_MIGRATION_REQUIRED",
117
+ });
118
+ expect(user?.departmentId).toBe("source");
119
+ }
120
+ });
121
+ });
122
+
123
+ function repositories(pool: Pool) {
124
+ return createSystemRepositories(createFoundationSnapshot(), {
125
+ db: drizzle(pool),
126
+ });
127
+ }
128
+
129
+ async function move(
130
+ repositories: ReturnType<typeof createSystemRepositories>,
131
+ id: string,
132
+ parentId: string,
133
+ ) {
134
+ const policy = new DepartmentOrganizationPolicy(repositories);
135
+ return repositories.organizationMutationRunner.run(async () => {
136
+ const current = await repositories.departments.get(id);
137
+ if (!current) throw new Error(`missing ${id}`);
138
+ const conflict = await policy.beforeUpdate(current, { parentId });
139
+ if (conflict && typeof conflict !== "string") return conflict.error;
140
+ await repositories.departments.update(id, { parentId }, actor);
141
+ return "moved";
142
+ });
143
+ }
144
+
145
+ function department(id: string, parentId?: string) {
146
+ return {
147
+ id,
148
+ parentId,
149
+ code: id,
150
+ name: id,
151
+ order: 10,
152
+ status: "active" as const,
153
+ };
154
+ }
155
+
156
+ function insertRoot(pool: Pool, id: string) {
157
+ return pool.query(
158
+ `insert into departments (
159
+ id, name, code, "order", status,
160
+ created_at, created_by, updated_at, updated_by
161
+ ) values ($1, $1, $1, 10, 'active', now(), 'test', now(), 'test')`,
162
+ [id],
163
+ );
164
+ }
165
+
166
+ const actor: RequestActor = {
167
+ id: "organization-test",
168
+ type: "service",
169
+ name: "组织回归",
170
+ roleCodes: ["system"],
171
+ permissionKeys: [
172
+ "system.department.manage",
173
+ "system.user.manage",
174
+ "system.role.manage",
175
+ ],
176
+ };
@@ -2,13 +2,19 @@ import { DataAccessDeniedError } from "../data-access/scoped-resource.js";
2
2
  import { forbidden } from "./route-helpers.js";
3
3
  import type { RequestActor } from "../guards.js";
4
4
  import type { ManagedEntity } from "./entities.js";
5
- import type { ResourceRouteConfig } from "./route-types.js";
5
+ import type {
6
+ ResourcePolicyResult,
7
+ ResourceRouteConfig,
8
+ } from "./route-types.js";
6
9
 
7
- export function resourceConflict(message: string) {
8
- return Response.json(
9
- { error: "RESOURCE_POLICY_CONFLICT", message },
10
- { status: 409 },
11
- );
10
+ export function resourceConflict(
11
+ conflict: Exclude<ResourcePolicyResult, undefined>,
12
+ ) {
13
+ const payload =
14
+ typeof conflict === "string"
15
+ ? { error: "RESOURCE_POLICY_CONFLICT", message: conflict }
16
+ : conflict;
17
+ return Response.json(payload, { status: 409 });
12
18
  }
13
19
 
14
20
  export function importNotSupported() {