create-windy 0.2.34 → 0.3.1

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 (261) hide show
  1. package/README.md +26 -3
  2. package/dist/cli.js +8121 -3746
  3. package/package.json +3 -3
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +5 -0
  6. package/template/README.md +28 -4
  7. package/template/_gitignore +4 -0
  8. package/template/apps/agent-server/Dockerfile +1 -0
  9. package/template/apps/agent-server/README.md +18 -5
  10. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  12. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  13. package/template/apps/agent-server/pyproject.toml +1 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  18. package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
  19. package/template/apps/agent-server/tests/__init__.py +1 -0
  20. package/template/apps/agent-server/tests/openai_provider_test_support.py +131 -0
  21. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  22. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  23. package/template/apps/agent-server/tests/test_openai_provider.py +7 -123
  24. package/template/apps/server/package.json +4 -1
  25. package/template/apps/server/src/agent/run-facade.ts +2 -7
  26. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  27. package/template/apps/server/src/application-services.ts +50 -0
  28. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  29. package/template/apps/server/src/audit/search-summary.ts +2 -6
  30. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  31. package/template/apps/server/src/auth/password.ts +30 -21
  32. package/template/apps/server/src/auth/routes.test-support.ts +94 -0
  33. package/template/apps/server/src/auth/routes.test.ts +51 -78
  34. package/template/apps/server/src/auth/routes.ts +6 -1
  35. package/template/apps/server/src/auth/runtime.ts +4 -1
  36. package/template/apps/server/src/auth/service.ts +25 -7
  37. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  38. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  39. package/template/apps/server/src/configuration/definition.ts +242 -12
  40. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  41. package/template/apps/server/src/configuration/repository.ts +27 -2
  42. package/template/apps/server/src/configuration/routes.ts +4 -1
  43. package/template/apps/server/src/configuration/service.test.ts +100 -0
  44. package/template/apps/server/src/configuration/service.ts +50 -10
  45. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  46. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  47. package/template/apps/server/src/example-modules.ts +4 -1
  48. package/template/apps/server/src/guards.ts +18 -5
  49. package/template/apps/server/src/http/request-source.test.ts +85 -0
  50. package/template/apps/server/src/http/request-source.ts +90 -0
  51. package/template/apps/server/src/index.ts +40 -40
  52. package/template/apps/server/src/license/routes.ts +2 -0
  53. package/template/apps/server/src/license/runtime-service.ts +10 -11
  54. package/template/apps/server/src/mcp/adapter.test.ts +3 -2
  55. package/template/apps/server/src/mcp/adapter.ts +9 -9
  56. package/template/apps/server/src/mcp/routes.ts +4 -3
  57. package/template/apps/server/src/module-bootstrap.ts +2 -2
  58. package/template/apps/server/src/module-host/request-context.ts +6 -6
  59. package/template/apps/server/src/module-host/storage-scope.ts +2 -9
  60. package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
  61. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  62. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  63. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  64. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  65. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  66. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  67. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  68. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  69. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  70. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  71. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  72. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  73. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  74. package/template/apps/server/src/notification/memory-support.ts +72 -0
  75. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  76. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  77. package/template/apps/server/src/notification/repository.test.ts +171 -0
  78. package/template/apps/server/src/notification/repository.ts +179 -32
  79. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  80. package/template/apps/server/src/notification/route-support.ts +225 -0
  81. package/template/apps/server/src/notification/routes.test.ts +182 -0
  82. package/template/apps/server/src/notification/routes.ts +225 -132
  83. package/template/apps/server/src/notification/service.test.ts +104 -0
  84. package/template/apps/server/src/notification/service.ts +140 -0
  85. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  86. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  87. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  88. package/template/apps/server/src/platform-http-runtime.ts +87 -0
  89. package/template/apps/server/src/route-guards.ts +1 -0
  90. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  91. package/template/apps/server/src/runtime-development.ts +1 -0
  92. package/template/apps/server/src/runtime.test.ts +23 -9
  93. package/template/apps/server/src/runtime.ts +39 -2
  94. package/template/apps/server/src/scheduler/audit.ts +2 -11
  95. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  96. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  97. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  98. package/template/apps/server/src/scheduler/routes.ts +2 -7
  99. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  100. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  101. package/template/apps/server/src/search/service.ts +2 -6
  102. package/template/apps/server/src/settings/routes.ts +15 -3
  103. package/template/apps/server/src/settings/runtime.ts +10 -1
  104. package/template/apps/server/src/storage/runtime.ts +13 -0
  105. package/template/apps/server/src/system/audit-query.ts +33 -4
  106. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  107. package/template/apps/server/src/system/audit-routes.ts +6 -5
  108. package/template/apps/server/src/system/department-organization.ts +320 -0
  109. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  110. package/template/apps/server/src/system/department-routes.ts +114 -33
  111. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  112. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  113. package/template/apps/server/src/system/drizzle-system-configs.ts +319 -0
  114. package/template/apps/server/src/system/drizzle-system.ts +59 -311
  115. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  116. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  117. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  118. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  119. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  120. package/template/apps/server/src/system/resource-routes.ts +270 -0
  121. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  122. package/template/apps/server/src/system/route-helpers.ts +2 -5
  123. package/template/apps/server/src/system/route-types.ts +12 -3
  124. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  125. package/template/apps/server/src/system/routes.test.ts +10 -2
  126. package/template/apps/server/src/system/routes.ts +35 -267
  127. package/template/apps/server/src/system/seed.ts +7 -0
  128. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  129. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  130. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  131. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  132. package/template/apps/server/src/work-order/service.ts +42 -10
  133. package/template/apps/web/src/app/error-pages.ts +9 -0
  134. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  135. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  136. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  137. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  138. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  139. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  140. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  141. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  142. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  143. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  144. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  145. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  146. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  147. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  148. package/template/apps/web/src/lib/date-time.ts +22 -10
  149. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  150. package/template/apps/web/src/main.ts +14 -1
  151. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  152. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  153. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  154. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  155. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  156. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  157. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  158. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  159. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  160. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  161. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  162. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  163. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  164. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  165. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  166. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  167. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  168. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  169. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  170. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  171. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  172. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  173. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  174. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  175. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.vue +21 -0
  176. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.webtest.ts +17 -0
  177. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +96 -108
  178. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  179. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  180. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  181. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  182. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  183. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  184. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  185. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  186. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  187. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  188. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  189. package/template/apps/web/src/router/error-navigation.ts +50 -0
  190. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  191. package/template/apps/web/src/router/index.ts +71 -0
  192. package/template/apps/web/src/router/index.webtest.ts +29 -0
  193. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  194. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  195. package/template/apps/web/src/services/auth-api.ts +5 -0
  196. package/template/apps/web/src/services/http.ts +43 -6
  197. package/template/apps/web/src/services/http.webtest.ts +70 -0
  198. package/template/apps/web/src/services/notification-api.ts +85 -2
  199. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  200. package/template/apps/web/src/services/search-api.ts +39 -0
  201. package/template/apps/web/src/services/system-api.ts +18 -0
  202. package/template/apps/web/vitest.config.ts +1 -0
  203. package/template/docker-compose.yml +66 -0
  204. package/template/docs/architecture/ai-runtime.md +14 -5
  205. package/template/docs/architecture/mcp-business-capabilities.md +107 -0
  206. package/template/docs/architecture/search-provider-protocol.md +38 -2
  207. package/template/docs/development/custom-error-pages.md +78 -0
  208. package/template/docs/development/custom-login-page.md +3 -3
  209. package/template/docs/platform/agent-runtime.md +32 -6
  210. package/template/docs/platform/agent-tools.md +4 -0
  211. package/template/docs/platform/audit-reliability.md +46 -2
  212. package/template/docs/platform/module-configuration.md +13 -4
  213. package/template/docs/platform/notifications.md +67 -11
  214. package/template/docs/platform/organization-membership.md +17 -3
  215. package/template/docs/platform/system-crud-api.md +16 -0
  216. package/template/docs/platform/web-system-crud.md +6 -2
  217. package/template/package.json +4 -0
  218. package/template/packages/config/index.test.ts +45 -0
  219. package/template/packages/config/package.json +1 -1
  220. package/template/packages/config/src/platform.ts +169 -48
  221. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  222. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  223. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  224. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  225. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  226. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  227. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  228. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  229. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  230. package/template/packages/database/package.json +1 -1
  231. package/template/packages/database/src/schema/identity.ts +7 -0
  232. package/template/packages/database/src/schema/notifications.ts +135 -0
  233. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  234. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  235. package/template/packages/modules/package.json +1 -1
  236. package/template/packages/modules/src/composition-fixtures.test-support.ts +164 -0
  237. package/template/packages/modules/src/composition.test.ts +5 -158
  238. package/template/packages/modules/src/system-api-operational.ts +234 -0
  239. package/template/packages/modules/src/system-api-permissions.ts +14 -181
  240. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  241. package/template/packages/modules/src/system-features.ts +1 -1
  242. package/template/packages/modules/src/system-module-catalog.ts +2 -1
  243. package/template/packages/modules/src/system-module-tasks.ts +86 -0
  244. package/template/packages/modules/src/system-modules-access.test.ts +60 -0
  245. package/template/packages/modules/src/system-modules.test.ts +13 -54
  246. package/template/packages/modules/src/system-modules.ts +2 -72
  247. package/template/packages/modules/src/system-permissions.ts +9 -5
  248. package/template/packages/server-sdk/package.json +1 -1
  249. package/template/packages/shared/index.ts +1 -0
  250. package/template/packages/shared/package.json +1 -1
  251. package/template/packages/shared/src/audit.ts +25 -0
  252. package/template/packages/shared/src/mcp.test.ts +12 -0
  253. package/template/packages/shared/src/mcp.ts +7 -0
  254. package/template/packages/shared/src/module-configuration.ts +5 -0
  255. package/template/packages/shared/src/notification.ts +92 -1
  256. package/template/packages/shared/src/password.ts +7 -2
  257. package/template/packages/shared/src/platform-settings.ts +2 -0
  258. package/template/packages/storage/package.json +1 -1
  259. package/template/packages/storage/src/upload-service.test.ts +16 -0
  260. package/template/packages/storage/src/upload-service.ts +5 -1
  261. 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))
@@ -0,0 +1,319 @@
1
+ import {
2
+ departments,
3
+ dictionaries,
4
+ featureFlags,
5
+ // @windy-module system.license begin
6
+ legacyLicenses,
7
+ // @windy-module system.license end
8
+ menus,
9
+ roles,
10
+ users,
11
+ } from "@southwind-ai/database";
12
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
13
+ import type {
14
+ ManagedDepartment,
15
+ ManagedDictionary,
16
+ ManagedFeature,
17
+ // @windy-module system.license begin
18
+ ManagedLicense,
19
+ // @windy-module system.license end
20
+ ManagedMenu,
21
+ ManagedRole,
22
+ ManagedUser,
23
+ } from "./entities.js";
24
+ import {
25
+ array,
26
+ auditEntity,
27
+ auditRow,
28
+ // @windy-module system.license begin
29
+ date,
30
+ iso,
31
+ optionalIso,
32
+ // @windy-module system.license end
33
+ pick,
34
+ } from "./drizzle-mapping.js";
35
+ import {
36
+ DrizzleEntityRepository,
37
+ type DrizzleEntityRepositoryConfig,
38
+ } from "./drizzle-repository.js";
39
+ import type { DrizzleScopedEntityRepositoryConfig } from "./drizzle-scoped-repository.js";
40
+ import type { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
41
+ import { userDataScopeBinding } from "./user-data-scope.js";
42
+
43
+ type SystemEntity =
44
+ | ManagedUser
45
+ | ManagedRole
46
+ | ManagedDepartment
47
+ | ManagedMenu
48
+ | ManagedDictionary
49
+ | ManagedFeature
50
+ // @windy-module system.license begin
51
+ | ManagedLicense;
52
+ // @windy-module system.license end
53
+
54
+ export function createSystemEntityRepository<T extends SystemEntity>(
55
+ db: NodePgDatabase,
56
+ config: DrizzleEntityRepositoryConfig<T>,
57
+ transactionContext: DrizzleTransactionContext,
58
+ ) {
59
+ return new DrizzleEntityRepository<T>(db, config, transactionContext);
60
+ }
61
+
62
+ export function userConfig(
63
+ seedItems: ManagedUser[],
64
+ ): DrizzleScopedEntityRepositoryConfig<ManagedUser> {
65
+ return {
66
+ table: users,
67
+ idColumn: "id",
68
+ seedItems,
69
+ dataScope: {
70
+ drizzle: {
71
+ ownerId: users.id,
72
+ tenant: { mode: "fixed", tenantId: "single-tenant" },
73
+ dimensions: { departmentId: users.departmentId },
74
+ },
75
+ entity: userDataScopeBinding,
76
+ },
77
+ toRow: (item) => ({
78
+ ...auditRow(item),
79
+ ...pick(item, [
80
+ "id",
81
+ "username",
82
+ "displayName",
83
+ "email",
84
+ "phone",
85
+ "departmentId",
86
+ "roleIds",
87
+ "postIds",
88
+ "status",
89
+ ]),
90
+ }),
91
+ toEntity: (row) =>
92
+ ({
93
+ ...auditEntity(row),
94
+ ...pick(row, [
95
+ "id",
96
+ "username",
97
+ "displayName",
98
+ "email",
99
+ "phone",
100
+ "departmentId",
101
+ "postIds",
102
+ "status",
103
+ ]),
104
+ roleIds: array(row.roleIds),
105
+ }) as ManagedUser,
106
+ };
107
+ }
108
+
109
+ export function roleConfig(
110
+ seedItems: ManagedRole[],
111
+ ): DrizzleEntityRepositoryConfig<ManagedRole> {
112
+ return {
113
+ table: roles,
114
+ idColumn: "id",
115
+ seedItems,
116
+ toRow: (item) => ({
117
+ ...auditRow(item),
118
+ ...pick(item, [
119
+ "id",
120
+ "code",
121
+ "name",
122
+ "dataScope",
123
+ "permissionKeys",
124
+ "menuKeys",
125
+ "departmentIds",
126
+ "status",
127
+ ]),
128
+ }),
129
+ toEntity: (row) =>
130
+ ({
131
+ ...auditEntity(row),
132
+ ...pick(row, ["id", "code", "name", "dataScope", "status"]),
133
+ permissionKeys: array(row.permissionKeys),
134
+ menuKeys: array(row.menuKeys),
135
+ departmentIds: array(row.departmentIds),
136
+ }) as ManagedRole,
137
+ };
138
+ }
139
+
140
+ export function departmentConfig(
141
+ seedItems: ManagedDepartment[],
142
+ ): DrizzleEntityRepositoryConfig<ManagedDepartment> {
143
+ return {
144
+ table: departments,
145
+ idColumn: "id",
146
+ seedItems,
147
+ toRow: (item) => ({
148
+ ...auditRow(item),
149
+ ...pick(item, ["id", "parentId", "name", "code", "order", "status"]),
150
+ }),
151
+ toEntity: (row) =>
152
+ ({
153
+ ...auditEntity(row),
154
+ ...pick(row, ["id", "parentId", "name", "code", "order", "status"]),
155
+ }) as ManagedDepartment,
156
+ };
157
+ }
158
+
159
+ export function menuConfig(
160
+ seedItems: ManagedMenu[],
161
+ ): DrizzleEntityRepositoryConfig<ManagedMenu> {
162
+ return {
163
+ table: menus,
164
+ idColumn: "id",
165
+ seedItems,
166
+ toRow: (item) => ({
167
+ ...auditRow(item),
168
+ ...pick(item, [
169
+ "id",
170
+ "key",
171
+ "title",
172
+ "type",
173
+ "order",
174
+ "path",
175
+ "component",
176
+ "icon",
177
+ "permissionKey",
178
+ "featureKey",
179
+ "parentKey",
180
+ "status",
181
+ ]),
182
+ visible: item.visible ? "visible" : "hidden",
183
+ }),
184
+ toEntity: (row) =>
185
+ ({
186
+ ...auditEntity(row),
187
+ ...pick(row, [
188
+ "id",
189
+ "key",
190
+ "title",
191
+ "type",
192
+ "order",
193
+ "path",
194
+ "component",
195
+ "icon",
196
+ "permissionKey",
197
+ "featureKey",
198
+ "parentKey",
199
+ "status",
200
+ ]),
201
+ visible: row.visible !== "hidden",
202
+ }) as ManagedMenu,
203
+ };
204
+ }
205
+
206
+ export function dictionaryConfig(
207
+ seedItems: ManagedDictionary[],
208
+ ): DrizzleEntityRepositoryConfig<ManagedDictionary> {
209
+ return {
210
+ table: dictionaries,
211
+ idColumn: "id",
212
+ seedItems,
213
+ toRow: (item) => ({
214
+ ...auditRow(item),
215
+ ...pick(item, ["id", "code", "name", "description", "status"]),
216
+ items: item.items || [],
217
+ }),
218
+ toEntity: (row) =>
219
+ ({
220
+ ...auditEntity(row),
221
+ ...pick(row, ["id", "code", "name", "description", "status"]),
222
+ items: array(row.items),
223
+ }) as ManagedDictionary,
224
+ };
225
+ }
226
+
227
+ export function featureConfig(
228
+ seedItems: ManagedFeature[],
229
+ ): DrizzleEntityRepositoryConfig<ManagedFeature> {
230
+ return {
231
+ table: featureFlags,
232
+ idColumn: "key",
233
+ seedItems,
234
+ toRow: (item) => ({
235
+ ...auditRow(item),
236
+ key: item.key || item.id,
237
+ module: item.module,
238
+ label: item.label,
239
+ description: item.description,
240
+ enabled: item.enabled,
241
+ visible: item.visible,
242
+ stage: item.stage,
243
+ licenseRequired: false,
244
+ licenseKey: item.requiredEntitlement,
245
+ allowedLicenseVersions: item.licenseVersionOverride,
246
+ dependencies: item.dependencies || [],
247
+ status: item.status,
248
+ }),
249
+ toEntity: (row) =>
250
+ ({
251
+ ...auditEntity(row),
252
+ id: String(row.key),
253
+ key: String(row.key),
254
+ module: String(row.module),
255
+ label: String(row.label),
256
+ description: row.description as string | undefined,
257
+ enabled: Boolean(row.enabled),
258
+ visible: row.visible === "hidden" ? "hidden" : "visible",
259
+ stage: String(row.stage || "stable") as ManagedFeature["stage"],
260
+ requiredEntitlement: row.licenseKey
261
+ ? (row.licenseKey as NonNullable<
262
+ ManagedFeature["requiredEntitlement"]
263
+ >)
264
+ : undefined,
265
+ licenseVersionOverride: Array.isArray(row.allowedLicenseVersions)
266
+ ? (row.allowedLicenseVersions as NonNullable<
267
+ ManagedFeature["licenseVersionOverride"]
268
+ >)
269
+ : undefined,
270
+ dependencies: array(row.dependencies),
271
+ status: String(row.status || "active") as ManagedFeature["status"],
272
+ }) as ManagedFeature,
273
+ };
274
+ }
275
+
276
+ // @windy-module system.license begin
277
+ export function licenseConfig(
278
+ seedItems: ManagedLicense[],
279
+ ): DrizzleEntityRepositoryConfig<ManagedLicense> {
280
+ return {
281
+ table: legacyLicenses,
282
+ idColumn: "id",
283
+ seedItems,
284
+ toRow: (item) => ({
285
+ ...auditRow(item),
286
+ ...pick(item, [
287
+ "id",
288
+ "licenseKey",
289
+ "subject",
290
+ "machineCode",
291
+ "publicKeyId",
292
+ "edition",
293
+ "modules",
294
+ "status",
295
+ ]),
296
+ issuedAt: date(item.issuedAt),
297
+ expiresAt: item.expiresAt ? date(item.expiresAt) : undefined,
298
+ payload: { modules: item.modules },
299
+ signature: "",
300
+ }),
301
+ toEntity: (row) =>
302
+ ({
303
+ ...auditEntity(row),
304
+ ...pick(row, [
305
+ "id",
306
+ "licenseKey",
307
+ "subject",
308
+ "machineCode",
309
+ "publicKeyId",
310
+ "edition",
311
+ "status",
312
+ ]),
313
+ modules: array(row.modules),
314
+ issuedAt: iso(row.issuedAt),
315
+ expiresAt: optionalIso(row.expiresAt),
316
+ }) as ManagedLicense,
317
+ };
318
+ }
319
+ // @windy-module system.license end