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
@@ -1,14 +1,3 @@
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
1
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
13
2
  import { ManifestFeatureRepository } from "../feature/manifest-feature-repository.js";
14
3
  import type {
@@ -22,28 +11,26 @@ import type {
22
11
  ManagedRole,
23
12
  ManagedUser,
24
13
  } from "./entities.js";
14
+ import { DrizzleScopedEntityRepository } from "./drizzle-scoped-repository.js";
15
+ import type { EntityRepository } from "./repository.js";
16
+ import type { ScopedEntityRepository } from "../data-access/scoped-repository.js";
17
+ import { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
18
+ import {
19
+ PostgresAdvisoryMutationRunner,
20
+ type MutationRunner,
21
+ } from "./mutation-runner.js";
25
22
  import {
26
- array,
27
- auditEntity,
28
- auditRow,
23
+ createSystemEntityRepository,
24
+ departmentConfig,
25
+ dictionaryConfig,
26
+ featureConfig,
29
27
  // @windy-module system.license begin
30
- date,
31
- iso,
32
- optionalIso,
28
+ licenseConfig,
33
29
  // @windy-module system.license end
34
- pick,
35
- } from "./drizzle-mapping.js";
36
- import {
37
- DrizzleEntityRepository,
38
- type DrizzleEntityRepositoryConfig,
39
- } from "./drizzle-repository.js";
40
- import {
41
- DrizzleScopedEntityRepository,
42
- type DrizzleScopedEntityRepositoryConfig,
43
- } from "./drizzle-scoped-repository.js";
44
- import type { EntityRepository } from "./repository.js";
45
- import type { ScopedEntityRepository } from "../data-access/scoped-repository.js";
46
- import { userDataScopeBinding } from "./user-data-scope.js";
30
+ menuConfig,
31
+ roleConfig,
32
+ userConfig,
33
+ } from "./drizzle-system-configs.js";
47
34
 
48
35
  export interface DrizzleSystemRepositorySeeds {
49
36
  users: ManagedUser[];
@@ -67,297 +54,58 @@ export interface DrizzleSystemRepositories {
67
54
  // @windy-module system.license begin
68
55
  licenses: EntityRepository<ManagedLicense>;
69
56
  // @windy-module system.license end
57
+ organizationMutationRunner: MutationRunner;
70
58
  }
71
59
 
72
60
  export function createDrizzleSystemRepositories(
73
61
  db: NodePgDatabase,
74
62
  seeds: DrizzleSystemRepositorySeeds,
75
63
  ): DrizzleSystemRepositories {
64
+ const transactionContext = new DrizzleTransactionContext();
76
65
  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)),
66
+ users: new DrizzleScopedEntityRepository(
67
+ db,
68
+ userConfig(seeds.users),
69
+ transactionContext,
70
+ ),
71
+ roles: createSystemEntityRepository(
72
+ db,
73
+ roleConfig(seeds.roles),
74
+ transactionContext,
75
+ ),
76
+ departments: createSystemEntityRepository(
77
+ db,
78
+ departmentConfig(seeds.departments),
79
+ transactionContext,
80
+ ),
81
+ menus: createSystemEntityRepository(
82
+ db,
83
+ menuConfig(seeds.menus),
84
+ transactionContext,
85
+ ),
86
+ dictionaries: createSystemEntityRepository(
87
+ db,
88
+ dictionaryConfig(seeds.dictionaries),
89
+ transactionContext,
90
+ ),
82
91
  features: new ManifestFeatureRepository(
83
- repo(db, featureConfig(seeds.features)),
92
+ createSystemEntityRepository(
93
+ db,
94
+ featureConfig(seeds.features),
95
+ transactionContext,
96
+ ),
84
97
  ),
85
98
  // @windy-module system.license begin
86
- licenses: repo(db, licenseConfig(seeds.licenses)),
99
+ licenses: createSystemEntityRepository(
100
+ db,
101
+ licenseConfig(seeds.licenses),
102
+ transactionContext,
103
+ ),
87
104
  // @windy-module system.license end
105
+ organizationMutationRunner: new PostgresAdvisoryMutationRunner(
106
+ db,
107
+ "windy:system:organization",
108
+ transactionContext,
109
+ ),
88
110
  };
89
111
  }
90
-
91
- function repo<
92
- T extends
93
- | ManagedUser
94
- | ManagedRole
95
- | ManagedDepartment
96
- | ManagedMenu
97
- | ManagedDictionary
98
- | ManagedFeature
99
- // @windy-module system.license begin
100
- | ManagedLicense,
101
- // @windy-module system.license end
102
- >(db: NodePgDatabase, config: DrizzleEntityRepositoryConfig<T>) {
103
- return new DrizzleEntityRepository<T>(db, config);
104
- }
105
-
106
- function userConfig(
107
- seedItems: ManagedUser[],
108
- ): DrizzleScopedEntityRepositoryConfig<ManagedUser> {
109
- return {
110
- table: users,
111
- idColumn: "id",
112
- seedItems,
113
- dataScope: {
114
- drizzle: {
115
- ownerId: users.id,
116
- tenant: { mode: "fixed", tenantId: "single-tenant" },
117
- dimensions: { departmentId: users.departmentId },
118
- },
119
- entity: userDataScopeBinding,
120
- },
121
- toRow: (item) => ({
122
- ...auditRow(item),
123
- ...pick(item, [
124
- "id",
125
- "username",
126
- "displayName",
127
- "email",
128
- "phone",
129
- "departmentId",
130
- "roleIds",
131
- "postIds",
132
- "status",
133
- ]),
134
- }),
135
- toEntity: (row) =>
136
- ({
137
- ...auditEntity(row),
138
- ...pick(row, [
139
- "id",
140
- "username",
141
- "displayName",
142
- "email",
143
- "phone",
144
- "departmentId",
145
- "postIds",
146
- "status",
147
- ]),
148
- roleIds: array(row.roleIds),
149
- }) as ManagedUser,
150
- };
151
- }
152
-
153
- function roleConfig(
154
- seedItems: ManagedRole[],
155
- ): DrizzleEntityRepositoryConfig<ManagedRole> {
156
- return {
157
- table: roles,
158
- idColumn: "id",
159
- seedItems,
160
- toRow: (item) => ({
161
- ...auditRow(item),
162
- ...pick(item, [
163
- "id",
164
- "code",
165
- "name",
166
- "dataScope",
167
- "permissionKeys",
168
- "menuKeys",
169
- "departmentIds",
170
- "status",
171
- ]),
172
- }),
173
- toEntity: (row) =>
174
- ({
175
- ...auditEntity(row),
176
- ...pick(row, ["id", "code", "name", "dataScope", "status"]),
177
- permissionKeys: array(row.permissionKeys),
178
- menuKeys: array(row.menuKeys),
179
- departmentIds: array(row.departmentIds),
180
- }) as ManagedRole,
181
- };
182
- }
183
-
184
- function departmentConfig(
185
- seedItems: ManagedDepartment[],
186
- ): DrizzleEntityRepositoryConfig<ManagedDepartment> {
187
- return {
188
- table: departments,
189
- idColumn: "id",
190
- seedItems,
191
- toRow: (item) => ({
192
- ...auditRow(item),
193
- ...pick(item, ["id", "parentId", "name", "code", "order", "status"]),
194
- }),
195
- toEntity: (row) =>
196
- ({
197
- ...auditEntity(row),
198
- ...pick(row, ["id", "parentId", "name", "code", "order", "status"]),
199
- }) as ManagedDepartment,
200
- };
201
- }
202
-
203
- function menuConfig(
204
- seedItems: ManagedMenu[],
205
- ): DrizzleEntityRepositoryConfig<ManagedMenu> {
206
- return {
207
- table: menus,
208
- idColumn: "id",
209
- seedItems,
210
- toRow: (item) => ({
211
- ...auditRow(item),
212
- ...pick(item, [
213
- "id",
214
- "key",
215
- "title",
216
- "type",
217
- "order",
218
- "path",
219
- "component",
220
- "icon",
221
- "permissionKey",
222
- "featureKey",
223
- "parentKey",
224
- "status",
225
- ]),
226
- visible: item.visible ? "visible" : "hidden",
227
- }),
228
- toEntity: (row) =>
229
- ({
230
- ...auditEntity(row),
231
- ...pick(row, [
232
- "id",
233
- "key",
234
- "title",
235
- "type",
236
- "order",
237
- "path",
238
- "component",
239
- "icon",
240
- "permissionKey",
241
- "featureKey",
242
- "parentKey",
243
- "status",
244
- ]),
245
- visible: row.visible !== "hidden",
246
- }) as ManagedMenu,
247
- };
248
- }
249
-
250
- function dictionaryConfig(
251
- seedItems: ManagedDictionary[],
252
- ): DrizzleEntityRepositoryConfig<ManagedDictionary> {
253
- return {
254
- table: dictionaries,
255
- idColumn: "id",
256
- seedItems,
257
- toRow: (item) => ({
258
- ...auditRow(item),
259
- ...pick(item, ["id", "code", "name", "description", "status"]),
260
- items: item.items || [],
261
- }),
262
- toEntity: (row) =>
263
- ({
264
- ...auditEntity(row),
265
- ...pick(row, ["id", "code", "name", "description", "status"]),
266
- items: array(row.items),
267
- }) as ManagedDictionary,
268
- };
269
- }
270
-
271
- function featureConfig(
272
- seedItems: ManagedFeature[],
273
- ): DrizzleEntityRepositoryConfig<ManagedFeature> {
274
- return {
275
- table: featureFlags,
276
- idColumn: "key",
277
- seedItems,
278
- toRow: (item) => ({
279
- ...auditRow(item),
280
- key: item.key || item.id,
281
- module: item.module,
282
- label: item.label,
283
- description: item.description,
284
- enabled: item.enabled,
285
- visible: item.visible,
286
- stage: item.stage,
287
- licenseRequired: false,
288
- licenseKey: item.requiredEntitlement,
289
- allowedLicenseVersions: item.licenseVersionOverride,
290
- dependencies: item.dependencies || [],
291
- status: item.status,
292
- }),
293
- toEntity: (row) =>
294
- ({
295
- ...auditEntity(row),
296
- id: String(row.key),
297
- key: String(row.key),
298
- module: String(row.module),
299
- label: String(row.label),
300
- description: row.description as string | undefined,
301
- enabled: Boolean(row.enabled),
302
- visible: row.visible === "hidden" ? "hidden" : "visible",
303
- stage: String(row.stage || "stable") as ManagedFeature["stage"],
304
- requiredEntitlement: row.licenseKey
305
- ? (row.licenseKey as NonNullable<
306
- ManagedFeature["requiredEntitlement"]
307
- >)
308
- : undefined,
309
- licenseVersionOverride: Array.isArray(row.allowedLicenseVersions)
310
- ? (row.allowedLicenseVersions as NonNullable<
311
- ManagedFeature["licenseVersionOverride"]
312
- >)
313
- : undefined,
314
- dependencies: array(row.dependencies),
315
- status: String(row.status || "active") as ManagedFeature["status"],
316
- }) as ManagedFeature,
317
- };
318
- }
319
-
320
- // @windy-module system.license begin
321
- function licenseConfig(
322
- seedItems: ManagedLicense[],
323
- ): DrizzleEntityRepositoryConfig<ManagedLicense> {
324
- return {
325
- table: legacyLicenses,
326
- idColumn: "id",
327
- seedItems,
328
- toRow: (item) => ({
329
- ...auditRow(item),
330
- ...pick(item, [
331
- "id",
332
- "licenseKey",
333
- "subject",
334
- "machineCode",
335
- "publicKeyId",
336
- "edition",
337
- "modules",
338
- "status",
339
- ]),
340
- issuedAt: date(item.issuedAt),
341
- expiresAt: item.expiresAt ? date(item.expiresAt) : undefined,
342
- payload: { modules: item.modules },
343
- signature: "",
344
- }),
345
- toEntity: (row) =>
346
- ({
347
- ...auditEntity(row),
348
- ...pick(row, [
349
- "id",
350
- "licenseKey",
351
- "subject",
352
- "machineCode",
353
- "publicKeyId",
354
- "edition",
355
- "status",
356
- ]),
357
- modules: array(row.modules),
358
- issuedAt: iso(row.issuedAt),
359
- expiresAt: optionalIso(row.expiresAt),
360
- }) as ManagedLicense,
361
- };
362
- }
363
- // @windy-module system.license end
@@ -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() {