create-windy 0.2.34 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +167 -17
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +5 -0
- package/template/README.md +3 -1
- package/template/apps/agent-server/Dockerfile +1 -0
- package/template/apps/agent-server/README.md +18 -5
- package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
- package/template/apps/agent-server/pyproject.toml +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
- package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
- package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
- package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
- package/template/apps/server/package.json +4 -1
- package/template/apps/server/src/agent/run-facade.ts +2 -7
- package/template/apps/server/src/agent/tool-registry.ts +2 -6
- package/template/apps/server/src/application-services.ts +50 -0
- package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
- package/template/apps/server/src/audit/search-summary.ts +2 -6
- package/template/apps/server/src/auth/credential-restriction.ts +1 -0
- package/template/apps/server/src/auth/password.ts +30 -21
- package/template/apps/server/src/auth/routes.test.ts +44 -3
- package/template/apps/server/src/auth/routes.ts +6 -1
- package/template/apps/server/src/auth/runtime.ts +4 -1
- package/template/apps/server/src/auth/service.ts +25 -7
- package/template/apps/server/src/configuration/bootstrap.ts +20 -1
- package/template/apps/server/src/configuration/definition.test.ts +71 -0
- package/template/apps/server/src/configuration/definition.ts +242 -12
- package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
- package/template/apps/server/src/configuration/repository.ts +27 -2
- package/template/apps/server/src/configuration/routes.ts +4 -1
- package/template/apps/server/src/configuration/service.test.ts +100 -0
- package/template/apps/server/src/configuration/service.ts +50 -10
- package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
- package/template/apps/server/src/data-governance/export/service.ts +2 -6
- package/template/apps/server/src/example-modules.ts +4 -1
- package/template/apps/server/src/guards.ts +18 -5
- package/template/apps/server/src/http/request-source.test.ts +85 -0
- package/template/apps/server/src/http/request-source.ts +90 -0
- package/template/apps/server/src/index.ts +23 -6
- package/template/apps/server/src/license/routes.ts +2 -0
- package/template/apps/server/src/license/runtime-service.ts +10 -11
- package/template/apps/server/src/module-bootstrap.ts +2 -2
- package/template/apps/server/src/module-host/request-context.ts +6 -6
- package/template/apps/server/src/module-host/storage-scope.ts +2 -9
- package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
- package/template/apps/server/src/notification/channel-adapter.ts +46 -0
- package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
- package/template/apps/server/src/notification/delivery-policy.ts +37 -0
- package/template/apps/server/src/notification/delivery-repository.ts +198 -0
- package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
- package/template/apps/server/src/notification/delivery-worker.ts +88 -0
- package/template/apps/server/src/notification/drizzle-management.ts +180 -0
- package/template/apps/server/src/notification/drizzle-query.ts +160 -0
- package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
- package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
- package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
- package/template/apps/server/src/notification/extension-routes.ts +111 -0
- package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
- package/template/apps/server/src/notification/memory-support.ts +72 -0
- package/template/apps/server/src/notification/preference-repository.ts +95 -0
- package/template/apps/server/src/notification/recipient-directory.ts +104 -0
- package/template/apps/server/src/notification/repository.test.ts +171 -0
- package/template/apps/server/src/notification/repository.ts +179 -32
- package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
- package/template/apps/server/src/notification/route-support.ts +225 -0
- package/template/apps/server/src/notification/routes.test.ts +182 -0
- package/template/apps/server/src/notification/routes.ts +225 -132
- package/template/apps/server/src/notification/service.test.ts +104 -0
- package/template/apps/server/src/notification/service.ts +140 -0
- package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
- package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
- package/template/apps/server/src/persistence.integration.test.ts +41 -2
- package/template/apps/server/src/route-guards.ts +1 -0
- package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/server/src/runtime.test.ts +23 -9
- package/template/apps/server/src/runtime.ts +39 -2
- package/template/apps/server/src/scheduler/audit.ts +2 -11
- package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
- package/template/apps/server/src/scheduler/definitions.ts +7 -0
- package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
- package/template/apps/server/src/scheduler/routes.ts +2 -7
- package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
- package/template/apps/server/src/search/opensearch-client.ts +145 -0
- package/template/apps/server/src/search/service.ts +2 -6
- package/template/apps/server/src/settings/routes.ts +15 -3
- package/template/apps/server/src/settings/runtime.ts +10 -1
- package/template/apps/server/src/storage/runtime.ts +13 -0
- package/template/apps/server/src/system/audit-query.ts +33 -4
- package/template/apps/server/src/system/audit-routes.test.ts +58 -4
- package/template/apps/server/src/system/audit-routes.ts +6 -5
- package/template/apps/server/src/system/department-organization.ts +320 -0
- package/template/apps/server/src/system/department-routes.test.ts +207 -48
- package/template/apps/server/src/system/department-routes.ts +114 -33
- package/template/apps/server/src/system/drizzle-repository.ts +15 -7
- package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
- package/template/apps/server/src/system/drizzle-system.ts +37 -9
- package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
- package/template/apps/server/src/system/identity-route-config.ts +7 -2
- package/template/apps/server/src/system/mutation-runner.ts +23 -0
- package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
- package/template/apps/server/src/system/resource-access-response.ts +12 -6
- package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
- package/template/apps/server/src/system/route-helpers.ts +2 -5
- package/template/apps/server/src/system/route-types.ts +12 -3
- package/template/apps/server/src/system/routes-validation.test.ts +26 -0
- package/template/apps/server/src/system/routes.test.ts +10 -2
- package/template/apps/server/src/system/routes.ts +70 -36
- package/template/apps/server/src/system/seed.ts +7 -0
- package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
- package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
- package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
- package/template/apps/server/src/work-order/search-provider.ts +60 -1
- package/template/apps/server/src/work-order/service.ts +42 -10
- package/template/apps/web/src/app/error-pages.ts +9 -0
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
- package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
- package/template/apps/web/src/components/common/FormDialog.vue +16 -2
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
- package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
- package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
- package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
- package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
- package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
- package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
- package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
- package/template/apps/web/src/lib/date-time.ts +22 -10
- package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
- package/template/apps/web/src/main.ts +14 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
- package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
- package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
- package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
- package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
- package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
- package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
- package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
- package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
- package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
- package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
- package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
- package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
- package/template/apps/web/src/pages/system/resource-config.ts +12 -4
- package/template/apps/web/src/router/error-navigation.ts +50 -0
- package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
- package/template/apps/web/src/router/index.ts +71 -0
- package/template/apps/web/src/router/index.webtest.ts +29 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/auth-api.ts +5 -0
- package/template/apps/web/src/services/http.ts +43 -6
- package/template/apps/web/src/services/http.webtest.ts +70 -0
- package/template/apps/web/src/services/notification-api.ts +85 -2
- package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
- package/template/apps/web/src/services/search-api.ts +39 -0
- package/template/apps/web/src/services/system-api.ts +18 -0
- package/template/apps/web/vitest.config.ts +1 -0
- package/template/docker-compose.yml +66 -0
- package/template/docs/architecture/ai-runtime.md +14 -5
- package/template/docs/architecture/search-provider-protocol.md +38 -2
- package/template/docs/development/custom-error-pages.md +78 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +32 -6
- package/template/docs/platform/audit-reliability.md +46 -2
- package/template/docs/platform/module-configuration.md +13 -4
- package/template/docs/platform/notifications.md +67 -11
- package/template/docs/platform/organization-membership.md +17 -3
- package/template/docs/platform/system-crud-api.md +16 -0
- package/template/docs/platform/web-system-crud.md +6 -2
- package/template/package.json +4 -0
- package/template/packages/config/index.test.ts +45 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +169 -48
- package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
- package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
- package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
- package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
- package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
- package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
- package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
- package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/identity.ts +7 -0
- package/template/packages/database/src/schema/notifications.ts +135 -0
- package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
- package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-api-permissions.ts +54 -0
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +1 -1
- package/template/packages/modules/src/system-module-catalog.ts +2 -1
- package/template/packages/modules/src/system-modules.test.ts +13 -1
- package/template/packages/modules/src/system-modules.ts +12 -0
- package/template/packages/modules/src/system-permissions.ts +9 -5
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.ts +25 -0
- package/template/packages/shared/src/module-configuration.ts +5 -0
- package/template/packages/shared/src/notification.ts +92 -1
- package/template/packages/shared/src/password.ts +7 -2
- package/template/packages/shared/src/platform-settings.ts +2 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/upload-service.test.ts +16 -0
- package/template/packages/storage/src/upload-service.ts +5 -1
- 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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
162
|
+
const rows = (await this.database()
|
|
161
163
|
.update(this.table())
|
|
162
164
|
.set(this.config.toRow(entity))
|
|
163
165
|
.where(this.idWhere(id, scope))
|
|
@@ -44,6 +44,11 @@ import {
|
|
|
44
44
|
import type { EntityRepository } from "./repository.js";
|
|
45
45
|
import type { ScopedEntityRepository } from "../data-access/scoped-repository.js";
|
|
46
46
|
import { userDataScopeBinding } from "./user-data-scope.js";
|
|
47
|
+
import { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
|
|
48
|
+
import {
|
|
49
|
+
PostgresAdvisoryMutationRunner,
|
|
50
|
+
type MutationRunner,
|
|
51
|
+
} from "./mutation-runner.js";
|
|
47
52
|
|
|
48
53
|
export interface DrizzleSystemRepositorySeeds {
|
|
49
54
|
users: ManagedUser[];
|
|
@@ -67,24 +72,43 @@ export interface DrizzleSystemRepositories {
|
|
|
67
72
|
// @windy-module system.license begin
|
|
68
73
|
licenses: EntityRepository<ManagedLicense>;
|
|
69
74
|
// @windy-module system.license end
|
|
75
|
+
organizationMutationRunner: MutationRunner;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
export function createDrizzleSystemRepositories(
|
|
73
79
|
db: NodePgDatabase,
|
|
74
80
|
seeds: DrizzleSystemRepositorySeeds,
|
|
75
81
|
): DrizzleSystemRepositories {
|
|
82
|
+
const transactionContext = new DrizzleTransactionContext();
|
|
76
83
|
return {
|
|
77
|
-
users: new DrizzleScopedEntityRepository(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
users: new DrizzleScopedEntityRepository(
|
|
85
|
+
db,
|
|
86
|
+
userConfig(seeds.users),
|
|
87
|
+
transactionContext,
|
|
88
|
+
),
|
|
89
|
+
roles: repo(db, roleConfig(seeds.roles), transactionContext),
|
|
90
|
+
departments: repo(
|
|
91
|
+
db,
|
|
92
|
+
departmentConfig(seeds.departments),
|
|
93
|
+
transactionContext,
|
|
94
|
+
),
|
|
95
|
+
menus: repo(db, menuConfig(seeds.menus), transactionContext),
|
|
96
|
+
dictionaries: repo(
|
|
97
|
+
db,
|
|
98
|
+
dictionaryConfig(seeds.dictionaries),
|
|
99
|
+
transactionContext,
|
|
100
|
+
),
|
|
82
101
|
features: new ManifestFeatureRepository(
|
|
83
|
-
repo(db, featureConfig(seeds.features)),
|
|
102
|
+
repo(db, featureConfig(seeds.features), transactionContext),
|
|
84
103
|
),
|
|
85
104
|
// @windy-module system.license begin
|
|
86
|
-
licenses: repo(db, licenseConfig(seeds.licenses)),
|
|
105
|
+
licenses: repo(db, licenseConfig(seeds.licenses), transactionContext),
|
|
87
106
|
// @windy-module system.license end
|
|
107
|
+
organizationMutationRunner: new PostgresAdvisoryMutationRunner(
|
|
108
|
+
db,
|
|
109
|
+
"windy:system:organization",
|
|
110
|
+
transactionContext,
|
|
111
|
+
),
|
|
88
112
|
};
|
|
89
113
|
}
|
|
90
114
|
|
|
@@ -99,8 +123,12 @@ function repo<
|
|
|
99
123
|
// @windy-module system.license begin
|
|
100
124
|
| ManagedLicense,
|
|
101
125
|
// @windy-module system.license end
|
|
102
|
-
>(
|
|
103
|
-
|
|
126
|
+
>(
|
|
127
|
+
db: NodePgDatabase,
|
|
128
|
+
config: DrizzleEntityRepositoryConfig<T>,
|
|
129
|
+
transactionContext: DrizzleTransactionContext,
|
|
130
|
+
) {
|
|
131
|
+
return new DrizzleEntityRepository<T>(db, config, transactionContext);
|
|
104
132
|
}
|
|
105
133
|
|
|
106
134
|
function userConfig(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
|
|
4
|
+
export class DrizzleTransactionContext {
|
|
5
|
+
private readonly storage = new AsyncLocalStorage<NodePgDatabase>();
|
|
6
|
+
|
|
7
|
+
current(fallback: NodePgDatabase): NodePgDatabase {
|
|
8
|
+
return this.storage.getStore() || fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
run<T>(database: NodePgDatabase, mutation: () => Promise<T>): Promise<T> {
|
|
12
|
+
return this.storage.run(database, mutation);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -8,8 +8,8 @@ import type { ResourceRouteConfig } from "./route-types.js";
|
|
|
8
8
|
import type { SystemRepositories } from "./seed.js";
|
|
9
9
|
import { UserAccountPolicy } from "./user-account-policy.js";
|
|
10
10
|
import { userDepartmentValidator } from "./user-department-validation.js";
|
|
11
|
-
import { SerialMutationRunner } from "./mutation-runner.js";
|
|
12
11
|
import { projectMaskedUser } from "./user-projection.js";
|
|
12
|
+
import { RoleDepartmentScopePolicy } from "./role-department-scope-policy.js";
|
|
13
13
|
|
|
14
14
|
export function identityRouteConfigs(
|
|
15
15
|
repositories: SystemRepositories,
|
|
@@ -24,7 +24,10 @@ export function identityRouteConfigs(
|
|
|
24
24
|
repositories.roles,
|
|
25
25
|
authRepository,
|
|
26
26
|
);
|
|
27
|
-
const
|
|
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 {
|
|
5
|
+
import type {
|
|
6
|
+
ResourcePolicyResult,
|
|
7
|
+
ResourceRouteConfig,
|
|
8
|
+
} from "./route-types.js";
|
|
6
9
|
|
|
7
|
-
export function resourceConflict(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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() {
|