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,45 +1,25 @@
1
1
  import type { FeatureFlagDefinition } from "@southwind-ai/shared";
2
- import { recordMutationAudit } from "../audit/policy.js";
3
2
  import { registerFeatureFlagRoutes } from "../feature/routes.js";
4
3
  import { findSystemFeature } from "../route-guards.js";
5
4
  import type { createServerRuntime } from "../runtime.js";
6
- import type { ManagedEntity } from "./entities.js";
7
5
  import { registerMenuRoutes } from "./menu-routes.js";
8
- import { registerDepartmentQuickCreateRoute } from "./department-routes.js";
9
6
  import {
10
- asPartial,
11
- notFound,
12
- parseImportPayload,
13
- parseListQuery,
14
- requiredId,
15
- resourceEvent,
16
- routeContext,
17
- } from "./route-helpers.js";
18
- import type { ResourceRouteConfig, RouteApp } from "./route-types.js";
7
+ registerDepartmentQuickCreateRoute,
8
+ registerDepartmentTransitionRoute,
9
+ } from "./department-routes.js";
10
+ import type { RouteApp } from "./route-types.js";
19
11
  import type { SystemRepositories } from "./seed.js";
20
- import { resource, withPermission } from "./resource-route-support.js";
12
+ import { resource } from "./resource-route-support.js";
21
13
  import type { AuthRepository } from "../auth/repository.js";
22
14
  import { identityRouteConfigs } from "./identity-route-config.js";
23
15
  // @windy-module system.audit begin
24
16
  import { registerAuditRoutes } from "./audit-routes.js";
25
17
  // @windy-module system.audit end
26
- import {
27
- accessCall,
28
- importNotSupported,
29
- readAccessible,
30
- resourceConflict,
31
- } from "./resource-access-response.js";
32
- import { runResourceMutation } from "./mutation-runner.js";
33
- import { registerResourceExportRoute } from "./resource-export-route.js";
34
- import {
35
- projectEntity,
36
- projectEntities,
37
- projectImportResult,
38
- } from "./response-projection.js";
39
18
  // @windy-module system.data-governance begin
40
19
  import { registerUserPlaintextRoutes } from "../data-governance/user-plaintext-routes.js";
41
20
  // @windy-module system.data-governance end
42
- import { searchAuditEvent } from "../audit/search-summary.js";
21
+ import { DepartmentOrganizationPolicy } from "./department-organization.js";
22
+ import { registerResourceRoutes } from "./resource-routes.js";
43
23
 
44
24
  export type { RouteApp } from "./route-types.js";
45
25
 
@@ -57,6 +37,7 @@ export function registerSystemRoutes(
57
37
  features,
58
38
  options.authRepository,
59
39
  );
40
+ const departmentPolicy = new DepartmentOrganizationPolicy(repositories);
60
41
  // @windy-module system.data-governance begin
61
42
  registerUserPlaintextRoutes(app, identity.users, runtime);
62
43
  // @windy-module system.data-governance end
@@ -66,18 +47,37 @@ export function registerSystemRoutes(
66
47
  repositories.departments,
67
48
  runtime,
68
49
  findSystemFeature(features, "department"),
50
+ departmentPolicy,
51
+ repositories.organizationMutationRunner,
52
+ );
53
+ registerDepartmentTransitionRoute(
54
+ app,
55
+ departmentPolicy,
56
+ repositories.organizationMutationRunner,
57
+ runtime,
58
+ findSystemFeature(features, "department"),
69
59
  );
70
60
  registerResourceRoutes(app, identity.roles, runtime);
71
61
  registerResourceRoutes(
72
62
  app,
73
- resource(
74
- "departments",
75
- "department",
76
- repositories.departments,
77
- features,
78
- ["code", "name"],
79
- { mode: "platform" },
80
- ),
63
+ {
64
+ ...resource(
65
+ "departments",
66
+ "department",
67
+ repositories.departments,
68
+ features,
69
+ ["code", "name"],
70
+ { mode: "platform" },
71
+ ),
72
+ importable: false,
73
+ mutationRunner: repositories.organizationMutationRunner,
74
+ validateInput: (input, current) =>
75
+ departmentPolicy.validateInput(input, current),
76
+ beforeCreate: (input) => departmentPolicy.beforeCreate(input),
77
+ beforeUpdate: (current, input) =>
78
+ departmentPolicy.beforeUpdate(current, input),
79
+ beforeDelete: (current) => departmentPolicy.beforeDelete(current),
80
+ },
81
81
  runtime,
82
82
  );
83
83
  registerMenuRoutes(
@@ -120,235 +120,3 @@ export function registerSystemRoutes(
120
120
 
121
121
  return app;
122
122
  }
123
-
124
- function registerResourceRoutes<T extends ManagedEntity>(
125
- app: RouteApp,
126
- config: ResourceRouteConfig<T>,
127
- runtime: Parameters<typeof registerSystemRoutes>[2],
128
- ) {
129
- app.get(`/system/${config.path}`, (rawContext) => {
130
- const { guardContext, query } = routeContext(rawContext);
131
- return withPermission(
132
- runtime,
133
- guardContext,
134
- "GET",
135
- config.permissionResource,
136
- "read",
137
- config.feature,
138
- async () => {
139
- const listQuery = parseListQuery(query);
140
- if (listQuery instanceof Response) return listQuery;
141
- const result = await accessCall(() =>
142
- config.dataAccess.mode === "request"
143
- ? config.dataAccess.access.list(
144
- listQuery,
145
- config.searchFields,
146
- guardContext.actor,
147
- )
148
- : config.repository.list(listQuery, config.searchFields),
149
- );
150
- if (result instanceof Response) return result;
151
- const search = listQuery.q?.trim();
152
- if (search) {
153
- runtime.auditSink.recordEvent(
154
- searchAuditEvent(guardContext, config.path, search, result.total),
155
- );
156
- }
157
- return { ...result, items: projectEntities(config, result.items) };
158
- },
159
- );
160
- });
161
-
162
- if (config.backupExport !== false) {
163
- registerResourceExportRoute(app, config, runtime);
164
- }
165
-
166
- app.get(`/system/${config.path}/:id`, (rawContext) => {
167
- const { guardContext, params } = routeContext(rawContext);
168
- return withPermission(
169
- runtime,
170
- guardContext,
171
- "GET",
172
- config.permissionResource,
173
- "read",
174
- config.feature,
175
- async () => {
176
- const item = await readAccessible(
177
- config,
178
- requiredId(params),
179
- guardContext.actor,
180
- );
181
- if (item instanceof Response) return item;
182
- return item
183
- ? projectEntity(config, item)
184
- : notFound(config.path, requiredId(params));
185
- },
186
- );
187
- });
188
-
189
- if (config.readonly) {
190
- return;
191
- }
192
-
193
- app.post(`/system/${config.path}/import`, (rawContext) => {
194
- const { guardContext, body } = routeContext(rawContext);
195
- return withPermission(
196
- runtime,
197
- guardContext,
198
- "POST",
199
- config.permissionResource,
200
- "manage",
201
- config.feature,
202
- async () => {
203
- if (config.importable === false) {
204
- return importNotSupported();
205
- }
206
- const payload = parseImportPayload<T>(body);
207
- const result = await accessCall(() =>
208
- config.dataAccess.mode === "request"
209
- ? config.dataAccess.access.importMany(
210
- payload.items,
211
- guardContext.actor,
212
- payload.mode,
213
- )
214
- : config.repository.importMany(
215
- payload.items,
216
- guardContext.actor,
217
- payload.mode,
218
- ),
219
- );
220
- if (result instanceof Response) return result;
221
- await recordMutationAudit(
222
- runtime,
223
- config.path,
224
- resourceEvent(guardContext, "data.import", config.path, undefined, {
225
- created: result.created,
226
- updated: result.updated,
227
- skipped: result.skipped,
228
- total: payload.items.length,
229
- }),
230
- );
231
- return {
232
- ...projectImportResult(config, result),
233
- total: payload.items.length,
234
- };
235
- },
236
- );
237
- });
238
-
239
- app.post(`/system/${config.path}`, (rawContext) => {
240
- const { guardContext, body } = routeContext(rawContext);
241
- return withPermission(
242
- runtime,
243
- guardContext,
244
- "POST",
245
- config.permissionResource,
246
- "manage",
247
- config.feature,
248
- async () => {
249
- const input = asPartial<T>(body);
250
- const validationError = await config.validateInput?.(input);
251
- if (validationError) {
252
- return Response.json(
253
- { error: "INVALID_RESOURCE", message: validationError },
254
- { status: 400 },
255
- );
256
- }
257
- const item = await accessCall(() =>
258
- config.dataAccess.mode === "request"
259
- ? config.dataAccess.access.create(input, guardContext.actor)
260
- : config.repository.create(input, guardContext.actor),
261
- );
262
- if (item instanceof Response) return item;
263
- await recordMutationAudit(
264
- runtime,
265
- config.path,
266
- resourceEvent(guardContext, "crud.create", config.path, item.id),
267
- );
268
- return projectEntity(config, item);
269
- },
270
- );
271
- });
272
-
273
- app.put(`/system/${config.path}/:id`, (rawContext) => {
274
- const { guardContext, params, body } = routeContext(rawContext);
275
- return withPermission(
276
- runtime,
277
- guardContext,
278
- "PUT",
279
- config.permissionResource,
280
- "manage",
281
- config.feature,
282
- async () =>
283
- runResourceMutation(config, async () => {
284
- const id = requiredId(params);
285
- const input = asPartial<T>(body);
286
- const current = await readAccessible(config, id, guardContext.actor);
287
- if (current instanceof Response) return current;
288
- if (!current) return notFound(config.path, id);
289
- const validationError = await config.validateInput?.(input, current);
290
- if (validationError) {
291
- return Response.json(
292
- { error: "INVALID_RESOURCE", message: validationError },
293
- { status: 400 },
294
- );
295
- }
296
- const policyError = await config.beforeUpdate?.(current, input);
297
- if (policyError) return resourceConflict(policyError);
298
- const item = await accessCall(() =>
299
- config.dataAccess.mode === "request"
300
- ? config.dataAccess.access.update(id, input, guardContext.actor)
301
- : config.repository.update(id, input, guardContext.actor),
302
- );
303
- if (item instanceof Response) return item;
304
- if (!item) {
305
- return notFound(config.path, id);
306
- }
307
- await config.afterUpdate?.(current, item, guardContext.actor);
308
- await recordMutationAudit(
309
- runtime,
310
- config.path,
311
- resourceEvent(guardContext, "crud.update", config.path, id),
312
- );
313
- return projectEntity(config, item);
314
- }),
315
- );
316
- });
317
-
318
- app.delete(`/system/${config.path}/:id`, (rawContext) => {
319
- const { guardContext, params } = routeContext(rawContext);
320
- return withPermission(
321
- runtime,
322
- guardContext,
323
- "DELETE",
324
- config.permissionResource,
325
- "manage",
326
- config.feature,
327
- async () =>
328
- runResourceMutation(config, async () => {
329
- const id = requiredId(params);
330
- const current = await readAccessible(config, id, guardContext.actor);
331
- if (current instanceof Response) return current;
332
- if (!current) return notFound(config.path, id);
333
- const policyError = await config.beforeDelete?.(current);
334
- if (policyError) return resourceConflict(policyError);
335
- const item = await accessCall(() =>
336
- config.dataAccess.mode === "request"
337
- ? config.dataAccess.access.softDelete(id, guardContext.actor)
338
- : config.repository.softDelete(id, guardContext.actor),
339
- );
340
- if (item instanceof Response) return item;
341
- if (!item) {
342
- return notFound(config.path, id);
343
- }
344
- await config.afterDelete?.(item, guardContext.actor);
345
- await recordMutationAudit(
346
- runtime,
347
- config.path,
348
- resourceEvent(guardContext, "crud.delete", config.path, id),
349
- );
350
- return projectEntity(config, item);
351
- }),
352
- );
353
- });
354
- }
@@ -26,6 +26,10 @@ import type {
26
26
  ManagedUser,
27
27
  } from "./entities.js";
28
28
  import { userDataScopeBinding } from "./user-data-scope.js";
29
+ import {
30
+ SerialMutationRunner,
31
+ type MutationRunner,
32
+ } from "./mutation-runner.js";
29
33
 
30
34
  export interface SystemRepositories {
31
35
  users: ScopedEntityRepository<ManagedUser>;
@@ -38,6 +42,7 @@ export interface SystemRepositories {
38
42
  licenses: EntityRepository<ManagedLicense>;
39
43
  // @windy-module system.license end
40
44
  menuRegistry: ManifestMenuRegistry;
45
+ organizationMutationRunner: MutationRunner;
41
46
  }
42
47
 
43
48
  export interface CreateSystemRepositoryOptions {
@@ -53,6 +58,7 @@ export function createSystemRepositories(
53
58
  const repositories = createDrizzleSystemRepositories(options.db, seeds);
54
59
  return {
55
60
  ...repositories,
61
+ organizationMutationRunner: repositories.organizationMutationRunner,
56
62
  menuRegistry: new ManifestMenuRegistry(
57
63
  repositories.menus,
58
64
  snapshot.menus,
@@ -81,6 +87,7 @@ export function createSystemRepositories(
81
87
  };
82
88
  return {
83
89
  ...repositories,
90
+ organizationMutationRunner: new SerialMutationRunner(),
84
91
  menuRegistry: new ManifestMenuRegistry(repositories.menus, snapshot.menus),
85
92
  };
86
93
  }
@@ -47,12 +47,19 @@ describePostgres("PostgreSQL Work Order data-scope enforcement", () => {
47
47
  features: [],
48
48
  licenses: [],
49
49
  });
50
- await repositories.departments.create(department("dept_a"), setupActor);
50
+ await repositories.departments.create(department("org_root"), setupActor);
51
+ await repositories.departments.create(
52
+ department("dept_a", "org_root"),
53
+ setupActor,
54
+ );
51
55
  await repositories.departments.create(
52
56
  department("dept_a_child", "dept_a"),
53
57
  setupActor,
54
58
  );
55
- await repositories.departments.create(department("dept_b"), setupActor);
59
+ await repositories.departments.create(
60
+ department("dept_b", "org_root"),
61
+ setupActor,
62
+ );
56
63
  const workOrders = createDrizzleWorkOrderRepository(database);
57
64
  await workOrders.create(
58
65
  ticket("ticket_self", "user_a", "dept_a"),
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { WorkOrder } from "@southwind-ai/example-work-order";
3
+ import { createWorkOrderOpenSearchIndex } from "./opensearch-index.js";
4
+
5
+ describe("工单 OpenSearch 可重建索引", () => {
6
+ test("同步业务事实后把 dataScope 编译进 OpenSearch 查询", async () => {
7
+ const requests: Request[] = [];
8
+ const fetcher = async (
9
+ input: string | URL | Request,
10
+ init?: RequestInit,
11
+ ) => {
12
+ const request = new Request(input, init);
13
+ requests.push(request);
14
+ if (request.method === "HEAD") return new Response(null, { status: 404 });
15
+ if (request.url.endsWith("/_bulk?refresh=wait_for")) {
16
+ return Response.json({ errors: false });
17
+ }
18
+ if (request.url.endsWith("/_search")) {
19
+ return Response.json({
20
+ hits: {
21
+ hits: [
22
+ {
23
+ _id: "ticket_1",
24
+ _score: 3,
25
+ sort: [3, "2026-07-26T00:00:00.000Z", "ticket_1"],
26
+ },
27
+ ],
28
+ },
29
+ });
30
+ }
31
+ return Response.json({});
32
+ };
33
+ const index = createWorkOrderOpenSearchIndex(
34
+ { OPENSEARCH_URL: "http://opensearch:9200" },
35
+ fetcher,
36
+ )!;
37
+
38
+ await index.synchronize(async () => [workOrder()]);
39
+ const result = await index.search("网络", 1, undefined, {
40
+ actorId: "user_1",
41
+ branches: [{ kind: "owner", ownerId: "user_1" }],
42
+ });
43
+
44
+ expect(result.hits).toEqual([{ id: "ticket_1", score: 3 }]);
45
+ expect(result.nextCursor).toBeString();
46
+ const searchRequest = requests.find(({ url }) => url.endsWith("/_search"));
47
+ const body = (await searchRequest?.json()) as {
48
+ query: { bool: { filter: unknown[] } };
49
+ };
50
+ expect(JSON.stringify(body.query.bool.filter)).toContain(
51
+ '"ownerId":"user_1"',
52
+ );
53
+ expect(
54
+ requests.find(({ url }) => url.endsWith("/_bulk?refresh=wait_for")),
55
+ ).toBeDefined();
56
+ });
57
+
58
+ test("无范围授权时使用 match_none", async () => {
59
+ let searchBody = "";
60
+ const index = createWorkOrderOpenSearchIndex(
61
+ { OPENSEARCH_URL: "http://opensearch:9200" },
62
+ async (input, init) => {
63
+ const request = new Request(input, init);
64
+ if (request.method === "HEAD")
65
+ return new Response(null, { status: 200 });
66
+ if (request.url.endsWith("/_search")) {
67
+ searchBody = await request.text();
68
+ return Response.json({ hits: { hits: [] } });
69
+ }
70
+ return Response.json({});
71
+ },
72
+ )!;
73
+ await index.synchronize(async () => []);
74
+ await index.search("工单", 5, undefined, {
75
+ actorId: "user_none",
76
+ branches: [],
77
+ });
78
+ expect(searchBody).toContain('"match_none"');
79
+ });
80
+ });
81
+
82
+ function workOrder(): WorkOrder {
83
+ return {
84
+ id: "ticket_1",
85
+ title: "办公网络中断",
86
+ description: "网络连接不稳定",
87
+ priority: "high",
88
+ workflowStatus: "open",
89
+ ownerId: "user_1",
90
+ departmentId: "dept_1",
91
+ status: "active",
92
+ createdAt: "2026-07-26T00:00:00.000Z",
93
+ createdBy: "user_1",
94
+ updatedAt: "2026-07-26T00:00:00.000Z",
95
+ updatedBy: "user_1",
96
+ };
97
+ }