create-windy 0.2.19 → 0.2.20
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/README.md +4 -0
- package/dist/cli.js +312 -154
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/application-services.ts +2 -2
- package/template/apps/server/src/audit/search-summary.ts +15 -8
- package/template/apps/server/src/example-modules.ts +58 -70
- package/template/apps/server/src/foundation.ts +8 -4
- package/template/apps/server/src/guards.test.ts +13 -0
- package/template/apps/server/src/guards.ts +12 -2
- package/template/apps/server/src/index.ts +48 -91
- package/template/apps/server/src/installed-business-modules.ts +23 -4
- package/template/apps/server/src/module-bootstrap.ts +83 -0
- package/template/apps/server/src/module-composition.test.ts +45 -1
- package/template/apps/server/src/module-composition.ts +45 -0
- package/template/apps/server/src/module-host/initialize.test.ts +123 -0
- package/template/apps/server/src/module-host/initialize.ts +331 -0
- package/template/apps/server/src/module-host/request-context.test.ts +66 -0
- package/template/apps/server/src/module-host/request-context.ts +156 -0
- package/template/apps/server/src/module-host/role-presets.test.ts +94 -0
- package/template/apps/server/src/module-host/role-presets.ts +76 -0
- package/template/apps/server/src/module-host/route-installer.test.ts +317 -0
- package/template/apps/server/src/module-host/route-installer.ts +97 -0
- package/template/apps/server/src/module-host/route-registration.test.ts +160 -0
- package/template/apps/server/src/module-host/search-providers.ts +31 -0
- package/template/apps/server/src/module-host/storage-port.test.ts +165 -0
- package/template/apps/server/src/module-host/storage-port.ts +59 -0
- package/template/apps/server/src/module-host/task-definitions.ts +41 -0
- package/template/apps/server/src/module-host/test-fixtures.ts +26 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +25 -0
- package/template/apps/server/src/module-host.test.ts +202 -58
- package/template/apps/server/src/module-host.ts +34 -113
- package/template/apps/server/src/route-guards.test.ts +112 -1
- package/template/apps/server/src/runtime-feature.ts +66 -31
- package/template/apps/server/src/runtime.test.ts +1 -0
- package/template/apps/server/src/runtime.ts +3 -1
- package/template/apps/server/src/system/built-in-roles.ts +1 -1
- package/template/apps/server/src/work-order/foundation-modules.test.ts +30 -10
- package/template/apps/server/src/work-order/routes.test.ts +30 -18
- package/template/apps/server/src/work-order/routes.ts +104 -135
- package/template/apps/server/src/work-order/search-api.test.ts +14 -3
- package/template/apps/server/src/work-order/search-provider.test.ts +23 -15
- package/template/apps/server/src/work-order/search-provider.ts +17 -12
- package/template/apps/server/src/work-order/task.test.ts +16 -11
- package/template/apps/web/Dockerfile +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +4 -1
- package/template/apps/web/src/layout/GlobalWatermark.vue +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +4 -1
- package/template/docs/architecture/object-storage.md +12 -0
- package/template/packages/config/index.test.ts +57 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +28 -1
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/runner.ts +7 -9
- package/template/packages/example-work-order/index.ts +1 -0
- package/template/packages/example-work-order/package.json +2 -0
- package/template/packages/example-work-order/src/server-module.ts +61 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/types.ts +7 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/catalog-validation.test.ts +212 -0
- package/template/packages/modules/src/catalog-validation.ts +19 -2
- package/template/packages/modules/src/migration-bundle-validation.test.ts +191 -0
- package/template/packages/server-sdk/index.ts +42 -0
- package/template/packages/server-sdk/package.json +16 -3
- package/template/packages/server-sdk/src/actor.ts +15 -0
- package/template/packages/server-sdk/src/audit-port.ts +22 -0
- package/template/packages/server-sdk/src/jobs-port.ts +24 -0
- package/template/packages/server-sdk/src/module-host.ts +36 -1
- package/template/packages/server-sdk/src/namespace.test.ts +26 -0
- package/template/packages/server-sdk/src/namespace.ts +13 -0
- package/template/packages/server-sdk/src/request-context.ts +21 -0
- package/template/packages/server-sdk/src/role-preset.ts +18 -0
- package/template/packages/server-sdk/src/route-definition.ts +26 -0
- package/template/packages/server-sdk/src/search-provider-port.ts +48 -0
- package/template/packages/server-sdk/src/storage-port.ts +25 -0
- package/template/packages/server-sdk/src/task-registration.ts +18 -0
- package/template/packages/server-sdk/src/tool-registration.ts +29 -0
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/license-catalog.test.ts +73 -1
- package/template/packages/shared/src/license-catalog.ts +95 -0
- package/template/packages/{database/src/bundle-validation.ts → shared/src/migration-bundle-validation.ts} +2 -1
- package/template/apps/server/src/work-order/task.ts +0 -30
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
isLicenseVersionAllowed,
|
|
3
|
+
resolveFeatureState,
|
|
4
|
+
type FeatureFlagDefinition,
|
|
5
|
+
type FeatureFlagState,
|
|
6
|
+
} from "@southwind-ai/shared";
|
|
7
|
+
import type { RequestGuardContext } from "./guards.js";
|
|
2
8
|
|
|
3
9
|
export interface FeatureFlagRuntimeResolver {
|
|
4
10
|
resolve(
|
|
@@ -16,11 +22,16 @@ export interface FeatureFlagRuntimeState {
|
|
|
16
22
|
allowedLicenseVersions?: FeatureFlagDefinition["allowedLicenseVersions"];
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
/** 运行时解析后的 Feature;disabledReason 复用 shared resolveFeatureState 语义。 */
|
|
26
|
+
export interface ResolvedFeatureFlag extends FeatureFlagDefinition {
|
|
27
|
+
disabledReason?: FeatureFlagState["reason"];
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
export class RuntimeFeatureCatalog {
|
|
20
31
|
private readonly definitions = new Map<string, FeatureFlagDefinition>();
|
|
21
32
|
private readonly requestCache = new WeakMap<
|
|
22
33
|
object,
|
|
23
|
-
Map<string,
|
|
34
|
+
Map<string, ResolvedFeatureFlag>
|
|
24
35
|
>();
|
|
25
36
|
|
|
26
37
|
constructor(
|
|
@@ -34,18 +45,18 @@ export class RuntimeFeatureCatalog {
|
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
resolve(
|
|
37
|
-
context:
|
|
48
|
+
context: RequestGuardContext,
|
|
38
49
|
manifestFeature: FeatureFlagDefinition,
|
|
39
|
-
): Promise<
|
|
50
|
+
): Promise<ResolvedFeatureFlag> {
|
|
40
51
|
this.definitions.set(manifestFeature.key, manifestFeature);
|
|
41
52
|
return this.resolveInternal(context, manifestFeature, new Set());
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
private async resolveInternal(
|
|
45
|
-
context:
|
|
56
|
+
context: RequestGuardContext,
|
|
46
57
|
manifestFeature: FeatureFlagDefinition,
|
|
47
58
|
visiting: ReadonlySet<string>,
|
|
48
|
-
): Promise<
|
|
59
|
+
): Promise<ResolvedFeatureFlag> {
|
|
49
60
|
const cache = this.cacheFor(context);
|
|
50
61
|
const cached = cache.get(manifestFeature.key);
|
|
51
62
|
if (cached) return cached;
|
|
@@ -53,33 +64,42 @@ export class RuntimeFeatureCatalog {
|
|
|
53
64
|
return disabledFeature(manifestFeature);
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
const own = await this.resolveOwnState(manifestFeature);
|
|
68
|
+
// dependencies 来自 manifest;启用集合取运行时解析后的依赖状态,
|
|
69
|
+
// 最终判定统一交给 shared resolveFeatureState,避免第二套漂移逻辑。
|
|
70
|
+
const enabledDependencies = new Set<string>();
|
|
71
|
+
if (
|
|
72
|
+
own.enabled &&
|
|
73
|
+
own.visible === "visible" &&
|
|
74
|
+
manifestFeature.dependencies?.length
|
|
75
|
+
) {
|
|
76
|
+
const nextVisiting = new Set(visiting);
|
|
77
|
+
nextVisiting.add(manifestFeature.key);
|
|
78
|
+
for (const dependencyKey of manifestFeature.dependencies) {
|
|
79
|
+
const dependency = this.definitions.get(dependencyKey);
|
|
80
|
+
if (!dependency) continue;
|
|
81
|
+
const dependencyState = await this.resolveInternal(
|
|
82
|
+
context,
|
|
83
|
+
dependency,
|
|
84
|
+
nextVisiting,
|
|
85
|
+
);
|
|
86
|
+
if (
|
|
87
|
+
dependencyState.enabled &&
|
|
88
|
+
dependencyState.visible === "visible" &&
|
|
89
|
+
isFeatureLicensedForContext(dependencyState, context)
|
|
90
|
+
) {
|
|
91
|
+
enabledDependencies.add(dependencyKey);
|
|
92
|
+
}
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
95
|
|
|
96
|
+
const state = resolveFeatureState(own, enabledDependencies, true);
|
|
97
|
+
const resolved: ResolvedFeatureFlag = {
|
|
98
|
+
...own,
|
|
99
|
+
enabled: state.enabled,
|
|
100
|
+
visible: state.visible,
|
|
101
|
+
...(state.reason ? { disabledReason: state.reason } : {}),
|
|
102
|
+
};
|
|
83
103
|
cache.set(manifestFeature.key, resolved);
|
|
84
104
|
return resolved;
|
|
85
105
|
}
|
|
@@ -108,7 +128,9 @@ export class RuntimeFeatureCatalog {
|
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
|
|
111
|
-
private cacheFor(
|
|
131
|
+
private cacheFor(
|
|
132
|
+
context: RequestGuardContext,
|
|
133
|
+
): Map<string, ResolvedFeatureFlag> {
|
|
112
134
|
let cache = this.requestCache.get(context);
|
|
113
135
|
if (!cache) {
|
|
114
136
|
cache = new Map();
|
|
@@ -118,6 +140,19 @@ export class RuntimeFeatureCatalog {
|
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
function isFeatureLicensedForContext(
|
|
144
|
+
feature: FeatureFlagDefinition,
|
|
145
|
+
context: RequestGuardContext,
|
|
146
|
+
): boolean {
|
|
147
|
+
return (
|
|
148
|
+
isLicenseVersionAllowed(feature, context.license?.versionKey) &&
|
|
149
|
+
(!feature.requiredEntitlement ||
|
|
150
|
+
Boolean(
|
|
151
|
+
context.license?.entitlements?.includes(feature.requiredEntitlement),
|
|
152
|
+
))
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
121
156
|
function disabledFeature(
|
|
122
157
|
feature: FeatureFlagDefinition,
|
|
123
158
|
): FeatureFlagDefinition {
|
|
@@ -25,11 +25,13 @@ import { developmentAdminActor } from "./runtime-development.js";
|
|
|
25
25
|
import {
|
|
26
26
|
RuntimeFeatureCatalog,
|
|
27
27
|
type FeatureFlagRuntimeResolver,
|
|
28
|
+
type ResolvedFeatureFlag,
|
|
28
29
|
} from "./runtime-feature.js";
|
|
29
30
|
|
|
30
31
|
export type {
|
|
31
32
|
FeatureFlagRuntimeResolver,
|
|
32
33
|
FeatureFlagRuntimeState,
|
|
34
|
+
ResolvedFeatureFlag,
|
|
33
35
|
} from "./runtime-feature.js";
|
|
34
36
|
|
|
35
37
|
export interface SessionRecord {
|
|
@@ -230,7 +232,7 @@ export function createServerRuntime(
|
|
|
230
232
|
async resolveFeature(
|
|
231
233
|
context: ReturnType<typeof createRequestGuardContext>,
|
|
232
234
|
manifestFeature: FeatureFlagDefinition,
|
|
233
|
-
): Promise<
|
|
235
|
+
): Promise<ResolvedFeatureFlag> {
|
|
234
236
|
return featureCatalog.resolve(context, manifestFeature);
|
|
235
237
|
},
|
|
236
238
|
recordDecisions(decisions: Array<GuardDecision | undefined>) {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { featureKey } from "@southwind-ai/shared";
|
|
3
|
+
import { validateCapabilityCatalogs } from "@southwind-ai/modules";
|
|
3
4
|
import {
|
|
4
5
|
createFoundationSnapshot,
|
|
5
6
|
loadFoundationModules,
|
|
6
7
|
} from "../foundation.js";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
|
|
8
|
+
import { createWorkOrderModule } from "../example-modules.js";
|
|
9
|
+
import {
|
|
10
|
+
adaptModuleSearchProviders,
|
|
11
|
+
initializeModuleHosts,
|
|
12
|
+
} from "../module-host.js";
|
|
10
13
|
|
|
11
14
|
describe("平台模块 composition root", () => {
|
|
12
15
|
test("默认安装平台能力模块与可删除的工单模块", () => {
|
|
@@ -47,6 +50,21 @@ describe("平台模块 composition root", () => {
|
|
|
47
50
|
);
|
|
48
51
|
});
|
|
49
52
|
|
|
53
|
+
test("示例工单的合法 Migration Bundle 通过启动校验", () => {
|
|
54
|
+
const modules = loadFoundationModules();
|
|
55
|
+
const workOrder = modules.find(({ name }) => name === "work-order");
|
|
56
|
+
|
|
57
|
+
expect(() => validateCapabilityCatalogs(modules)).not.toThrow();
|
|
58
|
+
expect(workOrder?.migrationBundle?.migrations.map(({ id }) => id)).toEqual([
|
|
59
|
+
"work-order.0001.create-work-orders",
|
|
60
|
+
]);
|
|
61
|
+
expect(
|
|
62
|
+
workOrder?.migrationBundle?.migrations.every(({ checksum }) =>
|
|
63
|
+
checksum.startsWith("sha256:"),
|
|
64
|
+
),
|
|
65
|
+
).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
50
68
|
test("业务 API 的 package 依赖图不经过 Windy Web 或 Vue", async () => {
|
|
51
69
|
const serverPackage = await packageJson(
|
|
52
70
|
new URL("../../package.json", import.meta.url),
|
|
@@ -66,21 +84,23 @@ describe("平台模块 composition root", () => {
|
|
|
66
84
|
});
|
|
67
85
|
expect(serverPackage.dependencies).not.toHaveProperty("@southwind-ai/web");
|
|
68
86
|
expect(serverPackage.dependencies).not.toHaveProperty("vue");
|
|
69
|
-
expect(businessPackage.dependencies).not.toHaveProperty(
|
|
87
|
+
expect(businessPackage.dependencies).not.toHaveProperty(
|
|
88
|
+
"@southwind-ai/web",
|
|
89
|
+
);
|
|
70
90
|
expect(businessPackage.dependencies).not.toHaveProperty("vue");
|
|
71
91
|
expect(webPackage.dependencies).not.toHaveProperty(
|
|
72
92
|
"@southwind-ai/example-work-order",
|
|
73
93
|
);
|
|
74
94
|
});
|
|
75
95
|
|
|
76
|
-
test("
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
test("示例模块经 Host 声明式装配提供搜索 Provider", async () => {
|
|
97
|
+
const module = createWorkOrderModule();
|
|
98
|
+
const hosts = await initializeModuleHosts({
|
|
99
|
+
modules: [module.manifest],
|
|
100
|
+
installations: [module],
|
|
81
101
|
});
|
|
82
102
|
|
|
83
|
-
expect(
|
|
103
|
+
expect(adaptModuleSearchProviders(hosts).map(({ key }) => key)).toEqual([
|
|
84
104
|
"work-order.ticket.search",
|
|
85
105
|
]);
|
|
86
106
|
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
workOrderModule,
|
|
4
|
+
type WorkOrder,
|
|
5
|
+
} from "@southwind-ai/example-work-order";
|
|
3
6
|
import { licenseVersionKey } from "@southwind-ai/shared";
|
|
7
|
+
import { initializeModuleHosts, installModuleRoutes } from "../module-host.js";
|
|
8
|
+
import { createWorkOrderModule } from "../example-modules.js";
|
|
4
9
|
import { InMemoryEntityRepository } from "../system/repository.js";
|
|
5
10
|
import { InMemoryScopedEntityRepository } from "../data-access/scoped-repository.js";
|
|
6
11
|
import { buildScopeContext } from "../data-access/context.js";
|
|
@@ -8,8 +13,6 @@ import type { ManagedDepartment } from "../system/entities.js";
|
|
|
8
13
|
import { FakeRouteApp } from "../system/route-test-app.js";
|
|
9
14
|
import { createServerRuntime } from "../runtime.js";
|
|
10
15
|
import type { RequestActor } from "../guards.js";
|
|
11
|
-
import { registerWorkOrderRoutes } from "./routes.js";
|
|
12
|
-
import { WorkOrderService } from "./service.js";
|
|
13
16
|
import { workOrderDataScopeBinding } from "./repository.js";
|
|
14
17
|
|
|
15
18
|
describe("示例工单 routes", () => {
|
|
@@ -39,6 +42,9 @@ describe("示例工单 routes", () => {
|
|
|
39
42
|
expect(await reason(rbacDenied.invoke("GET", "/work-orders"))).toBe(
|
|
40
43
|
"missing-permission",
|
|
41
44
|
);
|
|
45
|
+
expect(
|
|
46
|
+
rbacDenied.runtime.auditSink.list().map(({ action }) => action),
|
|
47
|
+
).toContain("security.denied");
|
|
42
48
|
|
|
43
49
|
const licenseDenied = await createFixture({ licenseVersion: "basic" });
|
|
44
50
|
expect(await reason(licenseDenied.invoke("GET", "/work-orders"))).toBe(
|
|
@@ -100,23 +106,29 @@ async function createFixture(options: FixtureOptions = {}) {
|
|
|
100
106
|
},
|
|
101
107
|
},
|
|
102
108
|
);
|
|
109
|
+
const module = createWorkOrderModule();
|
|
110
|
+
const hosts = await initializeModuleHosts({
|
|
111
|
+
modules: [module.manifest],
|
|
112
|
+
installations: [module],
|
|
113
|
+
});
|
|
114
|
+
module.attach({
|
|
115
|
+
departments: new InMemoryEntityRepository<ManagedDepartment>([
|
|
116
|
+
department("dept_a"),
|
|
117
|
+
department("dept_b"),
|
|
118
|
+
]),
|
|
119
|
+
repository: new InMemoryScopedEntityRepository<WorkOrder>(
|
|
120
|
+
[
|
|
121
|
+
ticket("ticket_self", "user_self", "dept_a"),
|
|
122
|
+
ticket("ticket_other", "user_other", "dept_b"),
|
|
123
|
+
],
|
|
124
|
+
workOrderDataScopeBinding,
|
|
125
|
+
),
|
|
126
|
+
});
|
|
103
127
|
const app = new FakeRouteApp();
|
|
104
|
-
|
|
105
|
-
[
|
|
106
|
-
ticket("ticket_self", "user_self", "dept_a"),
|
|
107
|
-
ticket("ticket_other", "user_other", "dept_b"),
|
|
108
|
-
],
|
|
109
|
-
workOrderDataScopeBinding,
|
|
110
|
-
);
|
|
111
|
-
const departments = new InMemoryEntityRepository<ManagedDepartment>([
|
|
112
|
-
department("dept_a"),
|
|
113
|
-
department("dept_b"),
|
|
114
|
-
]);
|
|
115
|
-
registerWorkOrderRoutes(
|
|
116
|
-
app,
|
|
117
|
-
new WorkOrderService(workOrders, departments),
|
|
128
|
+
installModuleRoutes(hosts, app, {
|
|
118
129
|
runtime,
|
|
119
|
-
|
|
130
|
+
features: workOrderModule().features,
|
|
131
|
+
});
|
|
120
132
|
const guardContext = await runtime.createGuardContext(
|
|
121
133
|
new Request("http://localhost/api/work-orders"),
|
|
122
134
|
);
|
|
@@ -1,141 +1,123 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createWorkOrderSchema,
|
|
3
3
|
updateWorkOrderSchema,
|
|
4
|
-
workOrderModule,
|
|
5
4
|
} from "@southwind-ai/example-work-order";
|
|
6
|
-
import type {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { evaluateRouteGuards } from "../route-guards.js";
|
|
10
|
-
import type { createServerRuntime } from "../runtime.js";
|
|
11
|
-
import {
|
|
12
|
-
forbidden,
|
|
13
|
-
notFound,
|
|
14
|
-
resourceEvent,
|
|
15
|
-
routeContext,
|
|
16
|
-
} from "../system/route-helpers.js";
|
|
17
|
-
import type { RouteApp } from "../system/route-types.js";
|
|
5
|
+
import type { ModuleRouteDefinition } from "@southwind-ai/server-sdk";
|
|
6
|
+
import { searchAuditMetadata } from "../audit/search-summary.js";
|
|
7
|
+
import { requestActorFromModule } from "../module-host/request-context.js";
|
|
18
8
|
import { parsePagination } from "../pagination.js";
|
|
9
|
+
import { accessCall } from "../system/resource-access-response.js";
|
|
10
|
+
import { notFound } from "../system/route-helpers.js";
|
|
19
11
|
import type { WorkOrderService } from "./service.js";
|
|
20
|
-
import { searchAuditEvent } from "../audit/search-summary.js";
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
searchAuditEvent(
|
|
45
|
-
context.guardContext,
|
|
46
|
-
"work-order",
|
|
47
|
-
context.query.q,
|
|
48
|
-
result.total,
|
|
13
|
+
/**
|
|
14
|
+
* 示例工单的 Guarded Route 定义。
|
|
15
|
+
*
|
|
16
|
+
* 只声明 method/path/permissionKey 与 Handler;Guard、审计上下文与受限 Port
|
|
17
|
+
* 由宿主在 installModuleRoutes 时统一绑定,不再手工查询 Manifest 绑定。
|
|
18
|
+
* `requireService` 延迟解析宿主装配完成后才存在的模块服务。
|
|
19
|
+
*/
|
|
20
|
+
export function createWorkOrderRouteDefinitions(
|
|
21
|
+
requireService: () => WorkOrderService,
|
|
22
|
+
): ModuleRouteDefinition[] {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: "/api/work-orders",
|
|
27
|
+
permissionKey: "work-order.ticket.read",
|
|
28
|
+
handler: async (context) => {
|
|
29
|
+
const pagination = parsePagination(context.query);
|
|
30
|
+
if (pagination instanceof Response) return pagination;
|
|
31
|
+
const result = await accessCall(() =>
|
|
32
|
+
requireService().list(
|
|
33
|
+
{ ...pagination, q: context.query.q },
|
|
34
|
+
requestActorFromModule(context.actor),
|
|
49
35
|
),
|
|
50
36
|
);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
: item || notFound("work-order", id);
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
app.post("/work-orders", (raw) =>
|
|
70
|
-
guarded(raw, binding("POST", "/api/work-orders"), runtime, async () => {
|
|
71
|
-
const context = routeContext(raw);
|
|
72
|
-
const parsed = createWorkOrderSchema.safeParse(context.body);
|
|
73
|
-
if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
|
|
74
|
-
const item = await accessCall(() =>
|
|
75
|
-
service.create(parsed.data, context.guardContext.actor),
|
|
76
|
-
);
|
|
77
|
-
if (item instanceof Response) return item;
|
|
78
|
-
await audit(runtime, context.guardContext, "crud.create", item.id);
|
|
79
|
-
return item;
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
app.put("/work-orders/:id", (raw) =>
|
|
84
|
-
guarded(raw, binding("PUT", "/api/work-orders/:id"), runtime, async () => {
|
|
85
|
-
const context = routeContext(raw);
|
|
86
|
-
const id = context.params.id || "";
|
|
87
|
-
const parsed = updateWorkOrderSchema.safeParse(context.body);
|
|
88
|
-
if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
|
|
89
|
-
const item = await accessCall(() =>
|
|
90
|
-
service.update(id, parsed.data, context.guardContext.actor),
|
|
91
|
-
);
|
|
92
|
-
if (item instanceof Response) return item;
|
|
93
|
-
if (!item) return notFound("work-order", id);
|
|
94
|
-
await audit(runtime, context.guardContext, "crud.update", id);
|
|
95
|
-
return item;
|
|
96
|
-
}),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
app.delete("/work-orders/:id", (raw) =>
|
|
100
|
-
guarded(
|
|
101
|
-
raw,
|
|
102
|
-
binding("DELETE", "/api/work-orders/:id"),
|
|
103
|
-
runtime,
|
|
104
|
-
async () => {
|
|
105
|
-
const context = routeContext(raw);
|
|
37
|
+
if (!(result instanceof Response) && context.query.q?.trim()) {
|
|
38
|
+
await context.audit.emit("data.search", {
|
|
39
|
+
target: { type: "work-order" },
|
|
40
|
+
metadata: searchAuditMetadata(context.query.q, result.total),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
method: "GET",
|
|
48
|
+
path: "/api/work-orders/:id",
|
|
49
|
+
permissionKey: "work-order.ticket.read",
|
|
50
|
+
handler: async (context) => {
|
|
106
51
|
const id = context.params.id || "";
|
|
107
52
|
const item = await accessCall(() =>
|
|
108
|
-
|
|
53
|
+
requireService().get(id, requestActorFromModule(context.actor)),
|
|
54
|
+
);
|
|
55
|
+
return item instanceof Response
|
|
56
|
+
? item
|
|
57
|
+
: item || notFound("work-order", id);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
method: "POST",
|
|
62
|
+
path: "/api/work-orders",
|
|
63
|
+
permissionKey: "work-order.ticket.create",
|
|
64
|
+
handler: async (context) => {
|
|
65
|
+
const parsed = createWorkOrderSchema.safeParse(context.body);
|
|
66
|
+
if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
|
|
67
|
+
const item = await accessCall(() =>
|
|
68
|
+
requireService().create(
|
|
69
|
+
parsed.data,
|
|
70
|
+
requestActorFromModule(context.actor),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
if (item instanceof Response) return item;
|
|
74
|
+
await context.audit.emit("crud.create", {
|
|
75
|
+
target: { type: "work-order", id: item.id },
|
|
76
|
+
});
|
|
77
|
+
return item;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
method: "PUT",
|
|
82
|
+
path: "/api/work-orders/:id",
|
|
83
|
+
permissionKey: "work-order.ticket.manage",
|
|
84
|
+
handler: async (context) => {
|
|
85
|
+
const id = context.params.id || "";
|
|
86
|
+
const parsed = updateWorkOrderSchema.safeParse(context.body);
|
|
87
|
+
if (!parsed.success) return invalid(parsed.error.issues[0]?.message);
|
|
88
|
+
const item = await accessCall(() =>
|
|
89
|
+
requireService().update(
|
|
90
|
+
id,
|
|
91
|
+
parsed.data,
|
|
92
|
+
requestActorFromModule(context.actor),
|
|
93
|
+
),
|
|
109
94
|
);
|
|
110
95
|
if (item instanceof Response) return item;
|
|
111
96
|
if (!item) return notFound("work-order", id);
|
|
112
|
-
await audit(
|
|
97
|
+
await context.audit.emit("crud.update", {
|
|
98
|
+
target: { type: "work-order", id },
|
|
99
|
+
});
|
|
113
100
|
return item;
|
|
114
101
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const result = manifest.apiPermissions.find(
|
|
135
|
-
(item) => item.method === method && item.path === path,
|
|
136
|
-
);
|
|
137
|
-
if (!result) throw new Error(`示例工单缺少 API 绑定:${method} ${path}`);
|
|
138
|
-
return result;
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
method: "DELETE",
|
|
105
|
+
path: "/api/work-orders/:id",
|
|
106
|
+
permissionKey: "work-order.ticket.manage",
|
|
107
|
+
handler: async (context) => {
|
|
108
|
+
const id = context.params.id || "";
|
|
109
|
+
const item = await accessCall(() =>
|
|
110
|
+
requireService().archive(id, requestActorFromModule(context.actor)),
|
|
111
|
+
);
|
|
112
|
+
if (item instanceof Response) return item;
|
|
113
|
+
if (!item) return notFound("work-order", id);
|
|
114
|
+
await context.audit.emit("crud.delete", {
|
|
115
|
+
target: { type: "work-order", id },
|
|
116
|
+
});
|
|
117
|
+
return item;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
];
|
|
139
121
|
}
|
|
140
122
|
|
|
141
123
|
function invalid(message = "工单内容无效") {
|
|
@@ -144,16 +126,3 @@ function invalid(message = "工单内容无效") {
|
|
|
144
126
|
{ status: 400 },
|
|
145
127
|
);
|
|
146
128
|
}
|
|
147
|
-
|
|
148
|
-
function audit(
|
|
149
|
-
runtime: ServerRuntime,
|
|
150
|
-
context: ReturnType<typeof routeContext>["guardContext"],
|
|
151
|
-
action: "crud.create" | "crud.update" | "crud.delete",
|
|
152
|
-
id: string,
|
|
153
|
-
) {
|
|
154
|
-
return recordMutationAudit(
|
|
155
|
-
runtime,
|
|
156
|
-
"work-orders",
|
|
157
|
-
resourceEvent(context, action, "work-order", id),
|
|
158
|
-
);
|
|
159
|
-
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
workOrderModule,
|
|
4
|
+
type WorkOrder,
|
|
5
|
+
} from "@southwind-ai/example-work-order";
|
|
6
|
+
import {
|
|
7
|
+
systemFoundationModule,
|
|
8
|
+
type ModuleManifest,
|
|
9
|
+
} from "@southwind-ai/modules";
|
|
4
10
|
import { licenseVersionKey } from "@southwind-ai/shared";
|
|
5
11
|
import { buildScopeContext } from "../data-access/context.js";
|
|
6
12
|
import { InMemoryScopedEntityRepository } from "../data-access/scoped-repository.js";
|
|
@@ -11,6 +17,7 @@ import { searchTestContext } from "../search/test-support.js";
|
|
|
11
17
|
import type { ManagedDepartment } from "../system/entities.js";
|
|
12
18
|
import { InMemoryEntityRepository } from "../system/repository.js";
|
|
13
19
|
import type { RouteApp } from "../system/route-types.js";
|
|
20
|
+
import { adaptModuleSearchProvider } from "../module-host.js";
|
|
14
21
|
import { workOrderDataScopeBinding } from "./repository.js";
|
|
15
22
|
import { WorkOrderSearchProvider } from "./search-provider.js";
|
|
16
23
|
import { WorkOrderService } from "./service.js";
|
|
@@ -45,7 +52,11 @@ describe("搜索 API 数据范围", () => {
|
|
|
45
52
|
);
|
|
46
53
|
const service = new SearchService(
|
|
47
54
|
modules,
|
|
48
|
-
[
|
|
55
|
+
[
|
|
56
|
+
adaptModuleSearchProvider(
|
|
57
|
+
new WorkOrderSearchProvider(() => workOrders),
|
|
58
|
+
),
|
|
59
|
+
],
|
|
49
60
|
runtime,
|
|
50
61
|
);
|
|
51
62
|
const context = searchTestContext([
|