create-windy 0.6.21 → 0.7.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 +11 -4
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/audit/policy.ts +6 -1
- package/template/apps/server/src/auth/bootstrap-admin.ts +10 -1
- package/template/apps/server/src/auth/composition.ts +5 -0
- package/template/apps/server/src/auth/credential-authorization.test.ts +257 -0
- package/template/apps/server/src/auth/credential-routes.test.ts +1 -0
- package/template/apps/server/src/auth/credential-service.ts +47 -12
- package/template/apps/server/src/auth/drizzle-repository.ts +13 -5
- package/template/apps/server/src/auth/drizzle-session-operations.ts +1 -0
- package/template/apps/server/src/auth/repository.ts +9 -6
- package/template/apps/server/src/auth/routes.test-support.ts +1 -0
- package/template/apps/server/src/auth/runtime.ts +4 -0
- package/template/apps/server/src/auth/service-options.ts +2 -0
- package/template/apps/server/src/auth/service.ts +2 -0
- package/template/apps/server/src/auth/user-credential-access.ts +37 -0
- package/template/apps/server/src/foundation-activation.test.ts +24 -1
- package/template/apps/server/src/foundation.ts +0 -2
- package/template/apps/server/src/index.ts +1 -0
- package/template/apps/server/src/runtime-development.ts +2 -0
- package/template/apps/server/src/runtime.test.ts +13 -4
- package/template/apps/server/src/system/department-organization.ts +58 -59
- package/template/apps/server/src/system/department-read-routes.ts +54 -0
- package/template/apps/server/src/system/department-routes.test.ts +27 -19
- package/template/apps/server/src/system/department-routes.ts +95 -33
- package/template/apps/server/src/system/department-transition-safety.test.ts +148 -0
- package/template/apps/server/src/system/department-validation.ts +79 -0
- package/template/apps/server/src/system/drizzle-repository.ts +14 -0
- package/template/apps/server/src/system/drizzle-system-configs.ts +2 -0
- package/template/apps/server/src/system/identity-read-model.ts +105 -0
- package/template/apps/server/src/system/identity-role-assignment.test.ts +199 -0
- package/template/apps/server/src/system/identity-role-assignment.ts +148 -0
- package/template/apps/server/src/system/identity-route-config.ts +29 -7
- package/template/apps/server/src/system/identity-write-security.test.ts +127 -0
- package/template/apps/server/src/system/identity-write-validation.ts +170 -0
- package/template/apps/server/src/system/organization-read-security.test.ts +221 -0
- package/template/apps/server/src/system/redis-health.test.ts +1 -1
- package/template/apps/server/src/system/repository.ts +29 -2
- package/template/apps/server/src/system/resource-routes.ts +116 -72
- package/template/apps/server/src/system/route-audit.test.ts +117 -0
- package/template/apps/server/src/system/route-helpers.ts +14 -0
- package/template/apps/server/src/system/route-types.ts +11 -2
- package/template/apps/server/src/system/routes.test.ts +0 -68
- package/template/apps/server/src/system/routes.ts +29 -2
- package/template/apps/server/src/system/user-account-policy.test.ts +3 -1
- package/template/apps/server/src/system/user-account-policy.ts +26 -9
- package/template/apps/web/src/pages/system/SystemResourceRoutePage.vue +27 -13
- package/template/apps/web/src/pages/system/SystemResourceRoutePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.vue +131 -0
- package/template/apps/web/src/pages/system/components/PagedSelectionList.webtest.ts +32 -0
- package/template/apps/web/src/pages/system/components/SystemRecordDetailDialog.vue +67 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +114 -38
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.webtest.ts +180 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +8 -0
- package/template/apps/web/src/pages/system/components/SystemResourceForm.vue +8 -2
- package/template/apps/web/src/pages/system/components/SystemResourcePage.department.webtest.ts +89 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +42 -46
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +12 -55
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +14 -2
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +121 -85
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.webtest.ts +57 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.vue +227 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationTable.webtest.ts +62 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +72 -52
- package/template/apps/web/src/pages/system/components/role-policy/RolePermissionTree.vue +150 -79
- package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.vue +98 -8
- package/template/apps/web/src/pages/system/components/role-policy/RolePolicyForm.webtest.ts +108 -0
- package/template/apps/web/src/pages/system/components/selection-options.ts +71 -0
- package/template/apps/web/src/pages/system/components/selection-options.webtest.ts +49 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +19 -27
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +2 -6
- package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.vue +128 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateRoleDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +61 -3
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +144 -90
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +95 -0
- package/template/apps/web/src/pages/system/components/user-account/UserListFilters.vue +168 -0
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +33 -57
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +4 -4
- package/template/apps/web/src/pages/system/components/user-account/save-user-account.ts +4 -1
- package/template/apps/web/src/pages/system/components/user-account/user-account-draft.ts +81 -0
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +6 -10
- package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +72 -0
- package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +110 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +27 -1
- package/template/apps/web/src/pages/system/resource-config.ts +2 -0
- package/template/apps/web/src/services/system-api.ts +25 -0
- package/template/docs/architecture/foundation-infrastructure.md +12 -25
- package/template/docs/development/crud-generator.md +1 -1
- package/template/docs/operations/legacy-data-migration.md +73 -0
- package/template/docs/platform/bulk-data-jobs.md +1 -1
- package/template/docs/platform/module-configuration.md +2 -2
- package/template/docs/platform/organization-membership.md +37 -6
- package/template/docs/platform/scheduled-task-executor.md +1 -1
- package/template/package.json +2 -1
- package/template/packages/config/index.test.ts +1 -5
- package/template/packages/config/package.json +2 -2
- package/template/packages/config/src/platform-boundary.test.ts +39 -0
- package/template/packages/config/src/platform.ts +0 -28
- package/template/packages/database/drizzle/0063_license_project_lifecycle.sql +28 -0
- package/template/packages/database/drizzle/meta/0063_snapshot.json +15053 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +5 -1
- package/template/packages/database/src/schema/index.ts +1 -0
- package/template/packages/database/src/schema/license-issuance-requests.ts +59 -0
- package/template/packages/database/src/schema/license-v1.ts +5 -0
- package/template/packages/jobs/package.json +2 -2
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-api-permissions.ts +14 -0
- package/template/packages/modules/src/system-features.ts +0 -11
- package/template/packages/modules/src/system-module-catalog.test.ts +4 -0
- package/template/packages/modules/src/system-module-catalog.ts +0 -1
- package/template/packages/server-sdk/index.ts +1 -49
- package/template/packages/server-sdk/package.json +3 -2
- package/template/packages/server-sdk/src/ai-runtime-readiness-exports.ts +49 -0
- package/template/packages/shared/index.ts +2 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/license-console-capabilities.ts +163 -0
- package/template/packages/shared/src/license-console.ts +94 -0
- package/template/packages/shared/src/license.ts +5 -0
- package/template/docs/adapters/ruoyi-vue-compatibility.md +0 -93
- package/template/packages/shared/src/ruoyi.ts +0 -98
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { IdentityRoleAssignment } from "../system/identity-role-assignment.js";
|
|
2
|
+
import type { RequestActor } from "../guards.js";
|
|
3
|
+
import { departmentDescendantResolver } from "../data-access/department-tree.js";
|
|
4
|
+
import {
|
|
5
|
+
DataScopedResourceService,
|
|
6
|
+
DataAccessDeniedError,
|
|
7
|
+
} from "../data-access/scoped-resource.js";
|
|
8
|
+
import type { SystemRepositories } from "../system/seed.js";
|
|
9
|
+
|
|
10
|
+
export type UserCredentialAccess = (
|
|
11
|
+
id: string,
|
|
12
|
+
actor: RequestActor,
|
|
13
|
+
) => Promise<boolean>;
|
|
14
|
+
|
|
15
|
+
export function createUserCredentialAccess(
|
|
16
|
+
repositories: SystemRepositories,
|
|
17
|
+
): UserCredentialAccess {
|
|
18
|
+
const access = new DataScopedResourceService(
|
|
19
|
+
repositories.users,
|
|
20
|
+
departmentDescendantResolver(repositories.departments),
|
|
21
|
+
);
|
|
22
|
+
const assignment = new IdentityRoleAssignment(
|
|
23
|
+
repositories.departments,
|
|
24
|
+
repositories.roles,
|
|
25
|
+
);
|
|
26
|
+
return async (id, actor) => {
|
|
27
|
+
try {
|
|
28
|
+
const target = await access.get(id, actor);
|
|
29
|
+
return Boolean(
|
|
30
|
+
target && !(await assignment.validateUser({}, target, actor)),
|
|
31
|
+
);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof DataAccessDeniedError) return false;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { loadPlatformConfig } from "@southwind-ai/config";
|
|
2
3
|
import {
|
|
3
4
|
// @windy-module system.settings begin
|
|
4
5
|
composeModuleMenus,
|
|
@@ -6,9 +7,31 @@ import {
|
|
|
6
7
|
// @windy-module system.settings end
|
|
7
8
|
systemModuleManifests,
|
|
8
9
|
} from "@southwind-ai/modules";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
applyPlatformModuleActivation,
|
|
12
|
+
createFoundationSnapshot,
|
|
13
|
+
} from "./foundation.js";
|
|
10
14
|
|
|
11
15
|
describe("平台 Module 运行时启停", () => {
|
|
16
|
+
test("平台快照只公开当前运行能力,不携带旧库兼容状态", () => {
|
|
17
|
+
const snapshot = createFoundationSnapshot(
|
|
18
|
+
loadPlatformConfig({}),
|
|
19
|
+
systemModuleManifests(),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(Object.keys(snapshot.platform).sort()).toEqual([
|
|
23
|
+
"auditEnabled",
|
|
24
|
+
"databaseDialect",
|
|
25
|
+
"enableAI",
|
|
26
|
+
"licenseEnabled",
|
|
27
|
+
"name",
|
|
28
|
+
]);
|
|
29
|
+
expect(snapshot.platform.databaseDialect).toBe("postgresql");
|
|
30
|
+
expect(snapshot.features.map(({ key }) => key)).not.toContain(
|
|
31
|
+
"system.ruoyi-compat",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
12
35
|
// @windy-module system.operations begin
|
|
13
36
|
test("停用只关闭能力与入口,保留静态实现声明用于一致性校验", () => {
|
|
14
37
|
const source = systemModuleManifests().find(
|
|
@@ -26,7 +26,6 @@ export interface FoundationSnapshot {
|
|
|
26
26
|
databaseDialect: string;
|
|
27
27
|
licenseEnabled: boolean;
|
|
28
28
|
auditEnabled: boolean;
|
|
29
|
-
ruoyiCompatibilityEnabled: boolean;
|
|
30
29
|
};
|
|
31
30
|
modules: Array<
|
|
32
31
|
Pick<ModuleManifest, "name" | "version" | "label" | "description">
|
|
@@ -147,7 +146,6 @@ export function createFoundationSnapshot(
|
|
|
147
146
|
metadata?.runtimeActivation !== "disabled",
|
|
148
147
|
),
|
|
149
148
|
auditEnabled: config.audit.enabled,
|
|
150
|
-
ruoyiCompatibilityEnabled: config.compatibility.ruoyiVue.enabled,
|
|
151
149
|
},
|
|
152
150
|
modules: modules.map(({ name, version, label, description }) => ({
|
|
153
151
|
name,
|
|
@@ -23,6 +23,8 @@ export function developmentAdminActor(): RequestActor {
|
|
|
23
23
|
permissionKeys: [
|
|
24
24
|
"system.admin.access",
|
|
25
25
|
"system.menu.read",
|
|
26
|
+
"system.profile.read",
|
|
27
|
+
"system.profile.manage",
|
|
26
28
|
"system.user.read",
|
|
27
29
|
"system.user.manage",
|
|
28
30
|
"system.user-directory.read",
|
|
@@ -22,6 +22,15 @@ describe("server runtime", () => {
|
|
|
22
22
|
|
|
23
23
|
expect(context.actor.id).toBe("user_dev_admin");
|
|
24
24
|
expect(context.actor.permissionKeys).toContain("system.menu.read");
|
|
25
|
+
const profilePermissions = createFoundationSnapshot()
|
|
26
|
+
.permissions.filter(({ key }) => key.startsWith("system.profile."))
|
|
27
|
+
.map(({ key }) => key);
|
|
28
|
+
expect(profilePermissions).toEqual(
|
|
29
|
+
expect.arrayContaining(["system.profile.read", "system.profile.manage"]),
|
|
30
|
+
);
|
|
31
|
+
expect(context.actor.permissionKeys).toEqual(
|
|
32
|
+
expect.arrayContaining(profilePermissions),
|
|
33
|
+
);
|
|
25
34
|
// @windy-module work-order begin
|
|
26
35
|
expect(context.actor.permissionKeys).toContain("work-order.ticket.read");
|
|
27
36
|
// @windy-module work-order end
|
|
@@ -58,7 +67,7 @@ describe("server runtime", () => {
|
|
|
58
67
|
|
|
59
68
|
test("未知 token 保持匿名,License 可由环境变量提供 Entitlement", async () => {
|
|
60
69
|
const runtime = createServerRuntime({
|
|
61
|
-
WINDY_LICENSE_ENTITLEMENTS: "capability.
|
|
70
|
+
WINDY_LICENSE_ENTITLEMENTS: "capability.example, capability.agent",
|
|
62
71
|
});
|
|
63
72
|
const request = new Request("http://localhost/api/foundation", {
|
|
64
73
|
headers: { authorization: "Bearer missing" },
|
|
@@ -68,7 +77,7 @@ describe("server runtime", () => {
|
|
|
68
77
|
|
|
69
78
|
expect(context.actor.type).toBe("anonymous");
|
|
70
79
|
expect(context.license?.entitlements).toEqual([
|
|
71
|
-
entitlementKey("capability.
|
|
80
|
+
entitlementKey("capability.example"),
|
|
72
81
|
entitlementKey("capability.agent"),
|
|
73
82
|
]);
|
|
74
83
|
});
|
|
@@ -186,7 +195,7 @@ describe("server runtime", () => {
|
|
|
186
195
|
licenseResolver: {
|
|
187
196
|
async resolve() {
|
|
188
197
|
return {
|
|
189
|
-
entitlements: [entitlementKey("capability.
|
|
198
|
+
entitlements: [entitlementKey("capability.example")],
|
|
190
199
|
};
|
|
191
200
|
},
|
|
192
201
|
},
|
|
@@ -197,7 +206,7 @@ describe("server runtime", () => {
|
|
|
197
206
|
const context = await runtime.createGuardContext(request);
|
|
198
207
|
|
|
199
208
|
expect(context.license?.entitlements).toEqual([
|
|
200
|
-
entitlementKey("capability.
|
|
209
|
+
entitlementKey("capability.example"),
|
|
201
210
|
]);
|
|
202
211
|
});
|
|
203
212
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { validateDepartmentInput } from "./department-validation.js";
|
|
2
|
+
import { resolveSystemActorForUser } from "./actor.js";
|
|
2
3
|
import type { RequestActor } from "../guards.js";
|
|
3
4
|
import type {
|
|
4
5
|
ManagedDepartment,
|
|
@@ -27,58 +28,22 @@ interface OrganizationRepositories {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export class DepartmentOrganizationPolicy {
|
|
30
|
-
constructor(
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly repositories: OrganizationRepositories,
|
|
33
|
+
private readonly changed?: (
|
|
34
|
+
actors: RequestActor[],
|
|
35
|
+
occurredAt: string,
|
|
36
|
+
) => Promise<void>,
|
|
37
|
+
) {}
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
input: Partial<ManagedDepartment>,
|
|
34
|
-
current?: ManagedDepartment,
|
|
35
|
-
): Promise<ResourcePolicyResult> {
|
|
36
|
-
if (
|
|
37
|
-
input.code !== undefined &&
|
|
38
|
-
(typeof input.code !== "string" ||
|
|
39
|
-
!/^[a-zA-Z][a-zA-Z0-9_-]{1,79}$/.test(input.code.trim()))
|
|
40
|
-
) {
|
|
41
|
-
return invalid(
|
|
42
|
-
"INVALID_DEPARTMENT_CODE",
|
|
43
|
-
"部门编码需以字母开头且至少 2 位",
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
if (
|
|
47
|
-
input.name !== undefined &&
|
|
48
|
-
(typeof input.name !== "string" || !input.name.trim())
|
|
49
|
-
) {
|
|
50
|
-
return invalid("INVALID_DEPARTMENT_NAME", "部门名称不能为空");
|
|
51
|
-
}
|
|
52
|
-
if (
|
|
53
|
-
input.order !== undefined &&
|
|
54
|
-
(!Number.isSafeInteger(input.order) || input.order < 0)
|
|
55
|
-
) {
|
|
56
|
-
return invalid("INVALID_DEPARTMENT_ORDER", "部门排序必须是非负整数");
|
|
57
|
-
}
|
|
58
|
-
if (input.status !== undefined && !isLifecycleStatus(input.status)) {
|
|
59
|
-
return invalid("INVALID_DEPARTMENT_STATUS", "部门状态不合法");
|
|
60
|
-
}
|
|
61
|
-
if (
|
|
62
|
-
!current &&
|
|
63
|
-
(typeof input.code !== "string" ||
|
|
64
|
-
typeof input.name !== "string" ||
|
|
65
|
-
typeof input.parentId !== "string" ||
|
|
66
|
-
!input.parentId)
|
|
67
|
-
) {
|
|
68
|
-
return invalid(
|
|
69
|
-
"DEPARTMENT_PARENT_REQUIRED",
|
|
70
|
-
"平台只允许一个根组织,新部门必须选择上级部门",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
39
|
+
validateInput = validateDepartmentInput;
|
|
75
40
|
|
|
76
41
|
async beforeCreate(
|
|
77
42
|
input: Partial<ManagedDepartment>,
|
|
78
43
|
): Promise<ResourcePolicyResult> {
|
|
79
|
-
const duplicate = (
|
|
80
|
-
(
|
|
81
|
-
);
|
|
44
|
+
const duplicate = (
|
|
45
|
+
await this.repositories.departments.all({ includeDeleted: true })
|
|
46
|
+
).find((department) => department.code === input.code?.trim());
|
|
82
47
|
if (duplicate) {
|
|
83
48
|
return conflict("DUPLICATE_DEPARTMENT_CODE", "部门编码已存在");
|
|
84
49
|
}
|
|
@@ -90,7 +55,9 @@ export class DepartmentOrganizationPolicy {
|
|
|
90
55
|
input: Partial<ManagedDepartment>,
|
|
91
56
|
): Promise<ResourcePolicyResult> {
|
|
92
57
|
if (input.code && input.code !== current.code) {
|
|
93
|
-
const duplicate = (
|
|
58
|
+
const duplicate = (
|
|
59
|
+
await this.repositories.departments.all({ includeDeleted: true })
|
|
60
|
+
).find(
|
|
94
61
|
(department) =>
|
|
95
62
|
department.id !== current.id &&
|
|
96
63
|
department.code === input.code?.trim(),
|
|
@@ -111,7 +78,7 @@ export class DepartmentOrganizationPolicy {
|
|
|
111
78
|
return this.validateCanLeaveActive(current);
|
|
112
79
|
}
|
|
113
80
|
if (input.status === "active") {
|
|
114
|
-
return this.validateParent(current, current.parentId);
|
|
81
|
+
return this.validateParent(current, input.parentId ?? current.parentId);
|
|
115
82
|
}
|
|
116
83
|
return undefined;
|
|
117
84
|
}
|
|
@@ -125,6 +92,7 @@ export class DepartmentOrganizationPolicy {
|
|
|
125
92
|
action: DepartmentTransitionAction,
|
|
126
93
|
targetDepartmentId: string | undefined,
|
|
127
94
|
actor: RequestActor,
|
|
95
|
+
updates: Partial<ManagedDepartment> = {},
|
|
128
96
|
): Promise<DepartmentTransitionResult | ResourcePolicyError> {
|
|
129
97
|
const source = await this.repositories.departments.get(id);
|
|
130
98
|
if (!source) {
|
|
@@ -133,6 +101,13 @@ export class DepartmentOrganizationPolicy {
|
|
|
133
101
|
if (!source.parentId) {
|
|
134
102
|
return conflict("ROOT_DEPARTMENT_IMMUTABLE", "根组织不能停用或删除");
|
|
135
103
|
}
|
|
104
|
+
const updateError =
|
|
105
|
+
(await this.validateInput(updates, source)) ||
|
|
106
|
+
(await this.beforeUpdate(source, updates));
|
|
107
|
+
if (updateError)
|
|
108
|
+
return typeof updateError === "string"
|
|
109
|
+
? conflict("INVALID_DEPARTMENT", updateError)
|
|
110
|
+
: updateError;
|
|
136
111
|
const references = await this.references(source.id);
|
|
137
112
|
const referenceCount =
|
|
138
113
|
references.children.length +
|
|
@@ -156,6 +131,15 @@ export class DepartmentOrganizationPolicy {
|
|
|
156
131
|
if (targetError) return targetError;
|
|
157
132
|
}
|
|
158
133
|
|
|
134
|
+
const previousActors = this.changed
|
|
135
|
+
? (
|
|
136
|
+
await Promise.all(
|
|
137
|
+
(
|
|
138
|
+
await this.repositories.users.all({ status: "active" })
|
|
139
|
+
).map((user) => resolveSystemActorForUser(this.repositories, user)),
|
|
140
|
+
)
|
|
141
|
+
).filter((value): value is RequestActor => !!value)
|
|
142
|
+
: [];
|
|
159
143
|
if (target) {
|
|
160
144
|
await this.repositories.departments.updateMany(
|
|
161
145
|
references.children.map((child) => ({
|
|
@@ -190,12 +174,13 @@ export class DepartmentOrganizationPolicy {
|
|
|
190
174
|
? await this.repositories.departments.softDelete(source.id, actor)
|
|
191
175
|
: await this.repositories.departments.update(
|
|
192
176
|
source.id,
|
|
193
|
-
{ status: "disabled" },
|
|
177
|
+
{ ...updates, status: "disabled" },
|
|
194
178
|
actor,
|
|
195
179
|
);
|
|
196
180
|
if (!department) {
|
|
197
181
|
return conflict("DEPARTMENT_NOT_FOUND", "部门不存在或已删除");
|
|
198
182
|
}
|
|
183
|
+
await this.changed?.(previousActors, department.updatedAt);
|
|
199
184
|
return {
|
|
200
185
|
department,
|
|
201
186
|
movedChildren: references.children.length,
|
|
@@ -277,6 +262,28 @@ export class DepartmentOrganizationPolicy {
|
|
|
277
262
|
return false;
|
|
278
263
|
}
|
|
279
264
|
|
|
265
|
+
async afterUpdate(current: ManagedDepartment, updated: ManagedDepartment) {
|
|
266
|
+
if (
|
|
267
|
+
!this.changed ||
|
|
268
|
+
(current.parentId === updated.parentId &&
|
|
269
|
+
current.status === updated.status)
|
|
270
|
+
)
|
|
271
|
+
return;
|
|
272
|
+
const users = await this.repositories.users.all({ status: "active" });
|
|
273
|
+
const actors = (
|
|
274
|
+
await Promise.all(
|
|
275
|
+
users.map((user) => resolveSystemActorForUser(this.repositories, user)),
|
|
276
|
+
)
|
|
277
|
+
).filter((value): value is RequestActor => !!value);
|
|
278
|
+
await this.changed(actors, updated.updatedAt);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async preview(departmentId: string) {
|
|
282
|
+
if (!(await this.repositories.departments.get(departmentId)))
|
|
283
|
+
return undefined;
|
|
284
|
+
return referenceDetails(await this.references(departmentId));
|
|
285
|
+
}
|
|
286
|
+
|
|
280
287
|
private async references(departmentId: string) {
|
|
281
288
|
const departments = await this.repositories.departments.all();
|
|
282
289
|
const users = await this.repositories.users.all();
|
|
@@ -299,10 +306,6 @@ function referenceDetails(
|
|
|
299
306
|
};
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
function invalid(error: string, message: string): ResourcePolicyError {
|
|
303
|
-
return { error, message };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
309
|
function conflict(
|
|
307
310
|
error: string,
|
|
308
311
|
message: string,
|
|
@@ -314,7 +317,3 @@ function conflict(
|
|
|
314
317
|
function unique(values: string[]): string[] {
|
|
315
318
|
return [...new Set(values)].sort();
|
|
316
319
|
}
|
|
317
|
-
|
|
318
|
-
function isLifecycleStatus(value: unknown): value is LifecycleStatus {
|
|
319
|
-
return ["draft", "active", "disabled", "archived"].includes(String(value));
|
|
320
|
-
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MutationRunner } from "./mutation-runner.js";
|
|
2
|
+
import { accessCall } from "./resource-access-response.js";
|
|
3
|
+
import type { FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
4
|
+
import type { createServerRuntime } from "../runtime.js";
|
|
5
|
+
import { evaluateRouteGuards, findSystemFeature } from "../route-guards.js";
|
|
6
|
+
import { forbidden, routeContext } from "./route-helpers.js";
|
|
7
|
+
import type { DepartmentOrganizationPolicy } from "./department-organization.js";
|
|
8
|
+
import type { identityReadModel } from "./identity-read-model.js";
|
|
9
|
+
import type { RouteApp } from "./route-types.js";
|
|
10
|
+
|
|
11
|
+
export function registerDepartmentReadRoutes(
|
|
12
|
+
app: RouteApp,
|
|
13
|
+
policy: DepartmentOrganizationPolicy,
|
|
14
|
+
readModel: ReturnType<typeof identityReadModel>,
|
|
15
|
+
runtime: ReturnType<typeof createServerRuntime>,
|
|
16
|
+
features: FeatureFlagDefinition[],
|
|
17
|
+
mutationRunner: MutationRunner,
|
|
18
|
+
) {
|
|
19
|
+
app.get("/system/departments/organization", async (raw) => {
|
|
20
|
+
const { guardContext } = routeContext(raw);
|
|
21
|
+
const decision = await evaluateRouteGuards(runtime, guardContext, {
|
|
22
|
+
permission: {
|
|
23
|
+
method: "GET",
|
|
24
|
+
path: "/api/system/departments/organization",
|
|
25
|
+
permissionKey: "system.department.read",
|
|
26
|
+
},
|
|
27
|
+
feature: findSystemFeature(features, "department"),
|
|
28
|
+
});
|
|
29
|
+
if (!decision.allowed) return forbidden(decision.reason);
|
|
30
|
+
return accessCall(() => readModel.organization(guardContext));
|
|
31
|
+
});
|
|
32
|
+
app.get("/system/departments/:id/transition-preview", async (raw) => {
|
|
33
|
+
const { guardContext, params } = routeContext(raw);
|
|
34
|
+
const decision = await evaluateRouteGuards(runtime, guardContext, {
|
|
35
|
+
permission: {
|
|
36
|
+
method: "GET",
|
|
37
|
+
path: "/api/system/departments/:id/transition-preview",
|
|
38
|
+
permissionKey: "system.department.manage",
|
|
39
|
+
},
|
|
40
|
+
feature: findSystemFeature(features, "department"),
|
|
41
|
+
});
|
|
42
|
+
if (!decision.allowed) return forbidden(decision.reason);
|
|
43
|
+
const result = await mutationRunner.run(() =>
|
|
44
|
+
policy.preview(String(params.id ?? "")),
|
|
45
|
+
);
|
|
46
|
+
return (
|
|
47
|
+
result ??
|
|
48
|
+
Response.json(
|
|
49
|
+
{ error: "NOT_FOUND", message: "部门不存在或已删除" },
|
|
50
|
+
{ status: 404 },
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -95,9 +95,15 @@ describe("部门组织路由", () => {
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
test("移动部门会拒绝自身、下级和根组织变更", async () => {
|
|
98
|
-
const { app, context } = await fixture();
|
|
99
|
-
await createDepartment(
|
|
100
|
-
|
|
98
|
+
const { app, context, repositories } = await fixture();
|
|
99
|
+
await createDepartment(
|
|
100
|
+
repositories,
|
|
101
|
+
context,
|
|
102
|
+
"dept_a",
|
|
103
|
+
"部门 A",
|
|
104
|
+
"dept_platform",
|
|
105
|
+
);
|
|
106
|
+
await createDepartment(repositories, context, "dept_b", "部门 B", "dept_a");
|
|
101
107
|
|
|
102
108
|
const cycle = (await app.invoke("PUT", "/system/departments/:id", {
|
|
103
109
|
...context,
|
|
@@ -123,22 +129,21 @@ describe("部门组织路由", () => {
|
|
|
123
129
|
test("有关联数据时要求显式迁移,并原子更新子级、用户和角色范围", async () => {
|
|
124
130
|
const { app, context, repositories, runtime } = await fixture();
|
|
125
131
|
await createDepartment(
|
|
126
|
-
|
|
132
|
+
repositories,
|
|
127
133
|
context,
|
|
128
134
|
"dept_source",
|
|
129
135
|
"待处置部门",
|
|
130
136
|
"dept_platform",
|
|
131
137
|
);
|
|
132
138
|
await createDepartment(
|
|
133
|
-
|
|
139
|
+
repositories,
|
|
134
140
|
context,
|
|
135
141
|
"dept_child",
|
|
136
142
|
"下级部门",
|
|
137
143
|
"dept_source",
|
|
138
144
|
);
|
|
139
|
-
await
|
|
140
|
-
|
|
141
|
-
body: {
|
|
145
|
+
await repositories.roles.create(
|
|
146
|
+
{
|
|
142
147
|
id: "role_custom",
|
|
143
148
|
code: "custom-scope",
|
|
144
149
|
name: "自定义范围",
|
|
@@ -147,17 +152,18 @@ describe("部门组织路由", () => {
|
|
|
147
152
|
permissionKeys: [],
|
|
148
153
|
menuKeys: [],
|
|
149
154
|
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
context.guardContext.actor,
|
|
156
|
+
);
|
|
157
|
+
await repositories.users.create(
|
|
158
|
+
{
|
|
154
159
|
id: "user_source",
|
|
155
160
|
username: "source-user",
|
|
156
161
|
displayName: "来源用户",
|
|
157
162
|
roleIds: ["role_custom"],
|
|
158
163
|
departmentId: "dept_source",
|
|
159
164
|
},
|
|
160
|
-
|
|
165
|
+
context.guardContext.actor,
|
|
166
|
+
);
|
|
161
167
|
|
|
162
168
|
const blocked = (await app.invoke("DELETE", "/system/departments/:id", {
|
|
163
169
|
...context,
|
|
@@ -218,6 +224,8 @@ describe("部门组织路由", () => {
|
|
|
218
224
|
name: "无效范围",
|
|
219
225
|
dataScope: "custom",
|
|
220
226
|
departmentIds: ["dept_missing"],
|
|
227
|
+
permissionKeys: [],
|
|
228
|
+
menuKeys: [],
|
|
221
229
|
},
|
|
222
230
|
})) as Response;
|
|
223
231
|
expect(response.status).toBe(400);
|
|
@@ -228,14 +236,14 @@ describe("部门组织路由", () => {
|
|
|
228
236
|
});
|
|
229
237
|
|
|
230
238
|
function createDepartment(
|
|
231
|
-
|
|
232
|
-
context:
|
|
239
|
+
repositories: Awaited<ReturnType<typeof fixture>>["repositories"],
|
|
240
|
+
context: Awaited<ReturnType<typeof fixture>>["context"],
|
|
233
241
|
id: string,
|
|
234
242
|
name: string,
|
|
235
243
|
parentId: string,
|
|
236
244
|
) {
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
245
|
+
return repositories.departments.create(
|
|
246
|
+
{ id, code: id, name, parentId, order: 10 },
|
|
247
|
+
context.guardContext.actor,
|
|
248
|
+
);
|
|
241
249
|
}
|