create-windy 0.3.0 → 0.3.2
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 +30 -3
- package/dist/cli.js +7640 -3255
- package/package.json +3 -3
- package/template/.windy-template.json +2 -2
- package/template/README.md +25 -3
- package/template/_gitignore +4 -0
- package/template/apps/agent-server/tests/__init__.py +1 -0
- package/template/apps/agent-server/tests/openai_provider_test_support.py +131 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +7 -123
- package/template/apps/server/src/auth/routes.test-support.ts +94 -0
- package/template/apps/server/src/auth/routes.test.ts +20 -88
- package/template/apps/server/src/index.ts +17 -34
- package/template/apps/server/src/mcp/adapter.test.ts +3 -2
- package/template/apps/server/src/mcp/adapter.ts +9 -9
- package/template/apps/server/src/mcp/routes.ts +4 -3
- package/template/apps/server/src/platform-http-runtime.ts +87 -0
- package/template/apps/server/src/system/drizzle-system-configs.ts +319 -0
- package/template/apps/server/src/system/drizzle-system.ts +35 -315
- package/template/apps/server/src/system/resource-routes.ts +270 -0
- package/template/apps/server/src/system/routes.ts +3 -269
- package/template/apps/web/src/pages/system/components/SystemResourceLoadError.vue +21 -0
- package/template/apps/web/src/pages/system/components/SystemResourceLoadError.webtest.ts +17 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +5 -10
- package/template/docs/architecture/mcp-business-capabilities.md +107 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +5 -5
- package/template/docs/platform/agent-tools.md +4 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/composition-fixtures.test-support.ts +164 -0
- package/template/packages/modules/src/composition.test.ts +5 -158
- package/template/packages/modules/src/system-api-operational.ts +234 -0
- package/template/packages/modules/src/system-api-permissions.ts +8 -229
- package/template/packages/modules/src/system-module-tasks.ts +86 -0
- package/template/packages/modules/src/system-modules-access.test.ts +60 -0
- package/template/packages/modules/src/system-modules.test.ts +0 -53
- package/template/packages/modules/src/system-modules.ts +2 -84
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/mcp.test.ts +12 -0
- package/template/packages/shared/src/mcp.ts +7 -0
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
departments,
|
|
3
|
-
dictionaries,
|
|
4
|
-
featureFlags,
|
|
5
|
-
// @windy-module system.license begin
|
|
6
|
-
legacyLicenses,
|
|
7
|
-
// @windy-module system.license end
|
|
8
|
-
menus,
|
|
9
|
-
roles,
|
|
10
|
-
users,
|
|
11
|
-
} from "@southwind-ai/database";
|
|
12
1
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
13
2
|
import { ManifestFeatureRepository } from "../feature/manifest-feature-repository.js";
|
|
14
3
|
import type {
|
|
@@ -22,33 +11,26 @@ import type {
|
|
|
22
11
|
ManagedRole,
|
|
23
12
|
ManagedUser,
|
|
24
13
|
} from "./entities.js";
|
|
25
|
-
import {
|
|
26
|
-
array,
|
|
27
|
-
auditEntity,
|
|
28
|
-
auditRow,
|
|
29
|
-
// @windy-module system.license begin
|
|
30
|
-
date,
|
|
31
|
-
iso,
|
|
32
|
-
optionalIso,
|
|
33
|
-
// @windy-module system.license end
|
|
34
|
-
pick,
|
|
35
|
-
} from "./drizzle-mapping.js";
|
|
36
|
-
import {
|
|
37
|
-
DrizzleEntityRepository,
|
|
38
|
-
type DrizzleEntityRepositoryConfig,
|
|
39
|
-
} from "./drizzle-repository.js";
|
|
40
|
-
import {
|
|
41
|
-
DrizzleScopedEntityRepository,
|
|
42
|
-
type DrizzleScopedEntityRepositoryConfig,
|
|
43
|
-
} from "./drizzle-scoped-repository.js";
|
|
14
|
+
import { DrizzleScopedEntityRepository } from "./drizzle-scoped-repository.js";
|
|
44
15
|
import type { EntityRepository } from "./repository.js";
|
|
45
16
|
import type { ScopedEntityRepository } from "../data-access/scoped-repository.js";
|
|
46
|
-
import { userDataScopeBinding } from "./user-data-scope.js";
|
|
47
17
|
import { DrizzleTransactionContext } from "./drizzle-transaction-context.js";
|
|
48
18
|
import {
|
|
49
19
|
PostgresAdvisoryMutationRunner,
|
|
50
20
|
type MutationRunner,
|
|
51
21
|
} from "./mutation-runner.js";
|
|
22
|
+
import {
|
|
23
|
+
createSystemEntityRepository,
|
|
24
|
+
departmentConfig,
|
|
25
|
+
dictionaryConfig,
|
|
26
|
+
featureConfig,
|
|
27
|
+
// @windy-module system.license begin
|
|
28
|
+
licenseConfig,
|
|
29
|
+
// @windy-module system.license end
|
|
30
|
+
menuConfig,
|
|
31
|
+
roleConfig,
|
|
32
|
+
userConfig,
|
|
33
|
+
} from "./drizzle-system-configs.js";
|
|
52
34
|
|
|
53
35
|
export interface DrizzleSystemRepositorySeeds {
|
|
54
36
|
users: ManagedUser[];
|
|
@@ -86,23 +68,39 @@ export function createDrizzleSystemRepositories(
|
|
|
86
68
|
userConfig(seeds.users),
|
|
87
69
|
transactionContext,
|
|
88
70
|
),
|
|
89
|
-
roles:
|
|
90
|
-
|
|
71
|
+
roles: createSystemEntityRepository(
|
|
72
|
+
db,
|
|
73
|
+
roleConfig(seeds.roles),
|
|
74
|
+
transactionContext,
|
|
75
|
+
),
|
|
76
|
+
departments: createSystemEntityRepository(
|
|
91
77
|
db,
|
|
92
78
|
departmentConfig(seeds.departments),
|
|
93
79
|
transactionContext,
|
|
94
80
|
),
|
|
95
|
-
menus:
|
|
96
|
-
|
|
81
|
+
menus: createSystemEntityRepository(
|
|
82
|
+
db,
|
|
83
|
+
menuConfig(seeds.menus),
|
|
84
|
+
transactionContext,
|
|
85
|
+
),
|
|
86
|
+
dictionaries: createSystemEntityRepository(
|
|
97
87
|
db,
|
|
98
88
|
dictionaryConfig(seeds.dictionaries),
|
|
99
89
|
transactionContext,
|
|
100
90
|
),
|
|
101
91
|
features: new ManifestFeatureRepository(
|
|
102
|
-
|
|
92
|
+
createSystemEntityRepository(
|
|
93
|
+
db,
|
|
94
|
+
featureConfig(seeds.features),
|
|
95
|
+
transactionContext,
|
|
96
|
+
),
|
|
103
97
|
),
|
|
104
98
|
// @windy-module system.license begin
|
|
105
|
-
licenses:
|
|
99
|
+
licenses: createSystemEntityRepository(
|
|
100
|
+
db,
|
|
101
|
+
licenseConfig(seeds.licenses),
|
|
102
|
+
transactionContext,
|
|
103
|
+
),
|
|
106
104
|
// @windy-module system.license end
|
|
107
105
|
organizationMutationRunner: new PostgresAdvisoryMutationRunner(
|
|
108
106
|
db,
|
|
@@ -111,281 +109,3 @@ export function createDrizzleSystemRepositories(
|
|
|
111
109
|
),
|
|
112
110
|
};
|
|
113
111
|
}
|
|
114
|
-
|
|
115
|
-
function repo<
|
|
116
|
-
T extends
|
|
117
|
-
| ManagedUser
|
|
118
|
-
| ManagedRole
|
|
119
|
-
| ManagedDepartment
|
|
120
|
-
| ManagedMenu
|
|
121
|
-
| ManagedDictionary
|
|
122
|
-
| ManagedFeature
|
|
123
|
-
// @windy-module system.license begin
|
|
124
|
-
| ManagedLicense,
|
|
125
|
-
// @windy-module system.license end
|
|
126
|
-
>(
|
|
127
|
-
db: NodePgDatabase,
|
|
128
|
-
config: DrizzleEntityRepositoryConfig<T>,
|
|
129
|
-
transactionContext: DrizzleTransactionContext,
|
|
130
|
-
) {
|
|
131
|
-
return new DrizzleEntityRepository<T>(db, config, transactionContext);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function userConfig(
|
|
135
|
-
seedItems: ManagedUser[],
|
|
136
|
-
): DrizzleScopedEntityRepositoryConfig<ManagedUser> {
|
|
137
|
-
return {
|
|
138
|
-
table: users,
|
|
139
|
-
idColumn: "id",
|
|
140
|
-
seedItems,
|
|
141
|
-
dataScope: {
|
|
142
|
-
drizzle: {
|
|
143
|
-
ownerId: users.id,
|
|
144
|
-
tenant: { mode: "fixed", tenantId: "single-tenant" },
|
|
145
|
-
dimensions: { departmentId: users.departmentId },
|
|
146
|
-
},
|
|
147
|
-
entity: userDataScopeBinding,
|
|
148
|
-
},
|
|
149
|
-
toRow: (item) => ({
|
|
150
|
-
...auditRow(item),
|
|
151
|
-
...pick(item, [
|
|
152
|
-
"id",
|
|
153
|
-
"username",
|
|
154
|
-
"displayName",
|
|
155
|
-
"email",
|
|
156
|
-
"phone",
|
|
157
|
-
"departmentId",
|
|
158
|
-
"roleIds",
|
|
159
|
-
"postIds",
|
|
160
|
-
"status",
|
|
161
|
-
]),
|
|
162
|
-
}),
|
|
163
|
-
toEntity: (row) =>
|
|
164
|
-
({
|
|
165
|
-
...auditEntity(row),
|
|
166
|
-
...pick(row, [
|
|
167
|
-
"id",
|
|
168
|
-
"username",
|
|
169
|
-
"displayName",
|
|
170
|
-
"email",
|
|
171
|
-
"phone",
|
|
172
|
-
"departmentId",
|
|
173
|
-
"postIds",
|
|
174
|
-
"status",
|
|
175
|
-
]),
|
|
176
|
-
roleIds: array(row.roleIds),
|
|
177
|
-
}) as ManagedUser,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function roleConfig(
|
|
182
|
-
seedItems: ManagedRole[],
|
|
183
|
-
): DrizzleEntityRepositoryConfig<ManagedRole> {
|
|
184
|
-
return {
|
|
185
|
-
table: roles,
|
|
186
|
-
idColumn: "id",
|
|
187
|
-
seedItems,
|
|
188
|
-
toRow: (item) => ({
|
|
189
|
-
...auditRow(item),
|
|
190
|
-
...pick(item, [
|
|
191
|
-
"id",
|
|
192
|
-
"code",
|
|
193
|
-
"name",
|
|
194
|
-
"dataScope",
|
|
195
|
-
"permissionKeys",
|
|
196
|
-
"menuKeys",
|
|
197
|
-
"departmentIds",
|
|
198
|
-
"status",
|
|
199
|
-
]),
|
|
200
|
-
}),
|
|
201
|
-
toEntity: (row) =>
|
|
202
|
-
({
|
|
203
|
-
...auditEntity(row),
|
|
204
|
-
...pick(row, ["id", "code", "name", "dataScope", "status"]),
|
|
205
|
-
permissionKeys: array(row.permissionKeys),
|
|
206
|
-
menuKeys: array(row.menuKeys),
|
|
207
|
-
departmentIds: array(row.departmentIds),
|
|
208
|
-
}) as ManagedRole,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function departmentConfig(
|
|
213
|
-
seedItems: ManagedDepartment[],
|
|
214
|
-
): DrizzleEntityRepositoryConfig<ManagedDepartment> {
|
|
215
|
-
return {
|
|
216
|
-
table: departments,
|
|
217
|
-
idColumn: "id",
|
|
218
|
-
seedItems,
|
|
219
|
-
toRow: (item) => ({
|
|
220
|
-
...auditRow(item),
|
|
221
|
-
...pick(item, ["id", "parentId", "name", "code", "order", "status"]),
|
|
222
|
-
}),
|
|
223
|
-
toEntity: (row) =>
|
|
224
|
-
({
|
|
225
|
-
...auditEntity(row),
|
|
226
|
-
...pick(row, ["id", "parentId", "name", "code", "order", "status"]),
|
|
227
|
-
}) as ManagedDepartment,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function menuConfig(
|
|
232
|
-
seedItems: ManagedMenu[],
|
|
233
|
-
): DrizzleEntityRepositoryConfig<ManagedMenu> {
|
|
234
|
-
return {
|
|
235
|
-
table: menus,
|
|
236
|
-
idColumn: "id",
|
|
237
|
-
seedItems,
|
|
238
|
-
toRow: (item) => ({
|
|
239
|
-
...auditRow(item),
|
|
240
|
-
...pick(item, [
|
|
241
|
-
"id",
|
|
242
|
-
"key",
|
|
243
|
-
"title",
|
|
244
|
-
"type",
|
|
245
|
-
"order",
|
|
246
|
-
"path",
|
|
247
|
-
"component",
|
|
248
|
-
"icon",
|
|
249
|
-
"permissionKey",
|
|
250
|
-
"featureKey",
|
|
251
|
-
"parentKey",
|
|
252
|
-
"status",
|
|
253
|
-
]),
|
|
254
|
-
visible: item.visible ? "visible" : "hidden",
|
|
255
|
-
}),
|
|
256
|
-
toEntity: (row) =>
|
|
257
|
-
({
|
|
258
|
-
...auditEntity(row),
|
|
259
|
-
...pick(row, [
|
|
260
|
-
"id",
|
|
261
|
-
"key",
|
|
262
|
-
"title",
|
|
263
|
-
"type",
|
|
264
|
-
"order",
|
|
265
|
-
"path",
|
|
266
|
-
"component",
|
|
267
|
-
"icon",
|
|
268
|
-
"permissionKey",
|
|
269
|
-
"featureKey",
|
|
270
|
-
"parentKey",
|
|
271
|
-
"status",
|
|
272
|
-
]),
|
|
273
|
-
visible: row.visible !== "hidden",
|
|
274
|
-
}) as ManagedMenu,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function dictionaryConfig(
|
|
279
|
-
seedItems: ManagedDictionary[],
|
|
280
|
-
): DrizzleEntityRepositoryConfig<ManagedDictionary> {
|
|
281
|
-
return {
|
|
282
|
-
table: dictionaries,
|
|
283
|
-
idColumn: "id",
|
|
284
|
-
seedItems,
|
|
285
|
-
toRow: (item) => ({
|
|
286
|
-
...auditRow(item),
|
|
287
|
-
...pick(item, ["id", "code", "name", "description", "status"]),
|
|
288
|
-
items: item.items || [],
|
|
289
|
-
}),
|
|
290
|
-
toEntity: (row) =>
|
|
291
|
-
({
|
|
292
|
-
...auditEntity(row),
|
|
293
|
-
...pick(row, ["id", "code", "name", "description", "status"]),
|
|
294
|
-
items: array(row.items),
|
|
295
|
-
}) as ManagedDictionary,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function featureConfig(
|
|
300
|
-
seedItems: ManagedFeature[],
|
|
301
|
-
): DrizzleEntityRepositoryConfig<ManagedFeature> {
|
|
302
|
-
return {
|
|
303
|
-
table: featureFlags,
|
|
304
|
-
idColumn: "key",
|
|
305
|
-
seedItems,
|
|
306
|
-
toRow: (item) => ({
|
|
307
|
-
...auditRow(item),
|
|
308
|
-
key: item.key || item.id,
|
|
309
|
-
module: item.module,
|
|
310
|
-
label: item.label,
|
|
311
|
-
description: item.description,
|
|
312
|
-
enabled: item.enabled,
|
|
313
|
-
visible: item.visible,
|
|
314
|
-
stage: item.stage,
|
|
315
|
-
licenseRequired: false,
|
|
316
|
-
licenseKey: item.requiredEntitlement,
|
|
317
|
-
allowedLicenseVersions: item.licenseVersionOverride,
|
|
318
|
-
dependencies: item.dependencies || [],
|
|
319
|
-
status: item.status,
|
|
320
|
-
}),
|
|
321
|
-
toEntity: (row) =>
|
|
322
|
-
({
|
|
323
|
-
...auditEntity(row),
|
|
324
|
-
id: String(row.key),
|
|
325
|
-
key: String(row.key),
|
|
326
|
-
module: String(row.module),
|
|
327
|
-
label: String(row.label),
|
|
328
|
-
description: row.description as string | undefined,
|
|
329
|
-
enabled: Boolean(row.enabled),
|
|
330
|
-
visible: row.visible === "hidden" ? "hidden" : "visible",
|
|
331
|
-
stage: String(row.stage || "stable") as ManagedFeature["stage"],
|
|
332
|
-
requiredEntitlement: row.licenseKey
|
|
333
|
-
? (row.licenseKey as NonNullable<
|
|
334
|
-
ManagedFeature["requiredEntitlement"]
|
|
335
|
-
>)
|
|
336
|
-
: undefined,
|
|
337
|
-
licenseVersionOverride: Array.isArray(row.allowedLicenseVersions)
|
|
338
|
-
? (row.allowedLicenseVersions as NonNullable<
|
|
339
|
-
ManagedFeature["licenseVersionOverride"]
|
|
340
|
-
>)
|
|
341
|
-
: undefined,
|
|
342
|
-
dependencies: array(row.dependencies),
|
|
343
|
-
status: String(row.status || "active") as ManagedFeature["status"],
|
|
344
|
-
}) as ManagedFeature,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// @windy-module system.license begin
|
|
349
|
-
function licenseConfig(
|
|
350
|
-
seedItems: ManagedLicense[],
|
|
351
|
-
): DrizzleEntityRepositoryConfig<ManagedLicense> {
|
|
352
|
-
return {
|
|
353
|
-
table: legacyLicenses,
|
|
354
|
-
idColumn: "id",
|
|
355
|
-
seedItems,
|
|
356
|
-
toRow: (item) => ({
|
|
357
|
-
...auditRow(item),
|
|
358
|
-
...pick(item, [
|
|
359
|
-
"id",
|
|
360
|
-
"licenseKey",
|
|
361
|
-
"subject",
|
|
362
|
-
"machineCode",
|
|
363
|
-
"publicKeyId",
|
|
364
|
-
"edition",
|
|
365
|
-
"modules",
|
|
366
|
-
"status",
|
|
367
|
-
]),
|
|
368
|
-
issuedAt: date(item.issuedAt),
|
|
369
|
-
expiresAt: item.expiresAt ? date(item.expiresAt) : undefined,
|
|
370
|
-
payload: { modules: item.modules },
|
|
371
|
-
signature: "",
|
|
372
|
-
}),
|
|
373
|
-
toEntity: (row) =>
|
|
374
|
-
({
|
|
375
|
-
...auditEntity(row),
|
|
376
|
-
...pick(row, [
|
|
377
|
-
"id",
|
|
378
|
-
"licenseKey",
|
|
379
|
-
"subject",
|
|
380
|
-
"machineCode",
|
|
381
|
-
"publicKeyId",
|
|
382
|
-
"edition",
|
|
383
|
-
"status",
|
|
384
|
-
]),
|
|
385
|
-
modules: array(row.modules),
|
|
386
|
-
issuedAt: iso(row.issuedAt),
|
|
387
|
-
expiresAt: optionalIso(row.expiresAt),
|
|
388
|
-
}) as ManagedLicense,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
// @windy-module system.license end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { recordMutationAudit } from "../audit/policy.js";
|
|
2
|
+
import { searchAuditEvent } from "../audit/search-summary.js";
|
|
3
|
+
import type { createServerRuntime } from "../runtime.js";
|
|
4
|
+
import type { ManagedEntity } from "./entities.js";
|
|
5
|
+
import {
|
|
6
|
+
asPartial,
|
|
7
|
+
notFound,
|
|
8
|
+
parseImportPayload,
|
|
9
|
+
parseListQuery,
|
|
10
|
+
requiredId,
|
|
11
|
+
resourceEvent,
|
|
12
|
+
routeContext,
|
|
13
|
+
} from "./route-helpers.js";
|
|
14
|
+
import type { ResourceRouteConfig, RouteApp } from "./route-types.js";
|
|
15
|
+
import { withPermission } from "./resource-route-support.js";
|
|
16
|
+
import {
|
|
17
|
+
accessCall,
|
|
18
|
+
importNotSupported,
|
|
19
|
+
readAccessible,
|
|
20
|
+
resourceConflict,
|
|
21
|
+
} from "./resource-access-response.js";
|
|
22
|
+
import { runResourceMutation } from "./mutation-runner.js";
|
|
23
|
+
import { registerResourceExportRoute } from "./resource-export-route.js";
|
|
24
|
+
import {
|
|
25
|
+
projectEntity,
|
|
26
|
+
projectEntities,
|
|
27
|
+
projectImportResult,
|
|
28
|
+
} from "./response-projection.js";
|
|
29
|
+
|
|
30
|
+
export function registerResourceRoutes<T extends ManagedEntity>(
|
|
31
|
+
app: RouteApp,
|
|
32
|
+
config: ResourceRouteConfig<T>,
|
|
33
|
+
runtime: ReturnType<typeof createServerRuntime>,
|
|
34
|
+
) {
|
|
35
|
+
app.get(`/system/${config.path}`, (rawContext) => {
|
|
36
|
+
const { guardContext, query } = routeContext(rawContext);
|
|
37
|
+
return withPermission(
|
|
38
|
+
runtime,
|
|
39
|
+
guardContext,
|
|
40
|
+
"GET",
|
|
41
|
+
config.permissionResource,
|
|
42
|
+
"read",
|
|
43
|
+
config.feature,
|
|
44
|
+
async () => {
|
|
45
|
+
const listQuery = parseListQuery(query);
|
|
46
|
+
if (listQuery instanceof Response) return listQuery;
|
|
47
|
+
const result = await accessCall(() =>
|
|
48
|
+
config.dataAccess.mode === "request"
|
|
49
|
+
? config.dataAccess.access.list(
|
|
50
|
+
listQuery,
|
|
51
|
+
config.searchFields,
|
|
52
|
+
guardContext.actor,
|
|
53
|
+
)
|
|
54
|
+
: config.repository.list(listQuery, config.searchFields),
|
|
55
|
+
);
|
|
56
|
+
if (result instanceof Response) return result;
|
|
57
|
+
const search = listQuery.q?.trim();
|
|
58
|
+
if (search) {
|
|
59
|
+
runtime.auditSink.recordEvent(
|
|
60
|
+
searchAuditEvent(guardContext, config.path, search, result.total),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return { ...result, items: projectEntities(config, result.items) };
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (config.backupExport !== false) {
|
|
69
|
+
registerResourceExportRoute(app, config, runtime);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
app.get(`/system/${config.path}/:id`, (rawContext) => {
|
|
73
|
+
const { guardContext, params } = routeContext(rawContext);
|
|
74
|
+
return withPermission(
|
|
75
|
+
runtime,
|
|
76
|
+
guardContext,
|
|
77
|
+
"GET",
|
|
78
|
+
config.permissionResource,
|
|
79
|
+
"read",
|
|
80
|
+
config.feature,
|
|
81
|
+
async () => {
|
|
82
|
+
const item = await readAccessible(
|
|
83
|
+
config,
|
|
84
|
+
requiredId(params),
|
|
85
|
+
guardContext.actor,
|
|
86
|
+
);
|
|
87
|
+
if (item instanceof Response) return item;
|
|
88
|
+
return item
|
|
89
|
+
? projectEntity(config, item)
|
|
90
|
+
: notFound(config.path, requiredId(params));
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (config.readonly) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
app.post(`/system/${config.path}/import`, (rawContext) => {
|
|
100
|
+
const { guardContext, body } = routeContext(rawContext);
|
|
101
|
+
return withPermission(
|
|
102
|
+
runtime,
|
|
103
|
+
guardContext,
|
|
104
|
+
"POST",
|
|
105
|
+
config.permissionResource,
|
|
106
|
+
"manage",
|
|
107
|
+
config.feature,
|
|
108
|
+
async () => {
|
|
109
|
+
if (config.importable === false) {
|
|
110
|
+
return importNotSupported();
|
|
111
|
+
}
|
|
112
|
+
const payload = parseImportPayload<T>(body);
|
|
113
|
+
const result = await accessCall(() =>
|
|
114
|
+
config.dataAccess.mode === "request"
|
|
115
|
+
? config.dataAccess.access.importMany(
|
|
116
|
+
payload.items,
|
|
117
|
+
guardContext.actor,
|
|
118
|
+
payload.mode,
|
|
119
|
+
)
|
|
120
|
+
: config.repository.importMany(
|
|
121
|
+
payload.items,
|
|
122
|
+
guardContext.actor,
|
|
123
|
+
payload.mode,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
if (result instanceof Response) return result;
|
|
127
|
+
await recordMutationAudit(
|
|
128
|
+
runtime,
|
|
129
|
+
config.path,
|
|
130
|
+
resourceEvent(guardContext, "data.import", config.path, undefined, {
|
|
131
|
+
created: result.created,
|
|
132
|
+
updated: result.updated,
|
|
133
|
+
skipped: result.skipped,
|
|
134
|
+
total: payload.items.length,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
return {
|
|
138
|
+
...projectImportResult(config, result),
|
|
139
|
+
total: payload.items.length,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
app.post(`/system/${config.path}`, (rawContext) => {
|
|
146
|
+
const { guardContext, body } = routeContext(rawContext);
|
|
147
|
+
return withPermission(
|
|
148
|
+
runtime,
|
|
149
|
+
guardContext,
|
|
150
|
+
"POST",
|
|
151
|
+
config.permissionResource,
|
|
152
|
+
"manage",
|
|
153
|
+
config.feature,
|
|
154
|
+
async () =>
|
|
155
|
+
runResourceMutation(config, async () => {
|
|
156
|
+
const input = asPartial<T>(body);
|
|
157
|
+
const validationError = await config.validateInput?.(input);
|
|
158
|
+
if (validationError) return invalidResource(validationError);
|
|
159
|
+
const policyError = await config.beforeCreate?.(input);
|
|
160
|
+
if (policyError) return resourceConflict(policyError);
|
|
161
|
+
const item = await accessCall(() =>
|
|
162
|
+
config.dataAccess.mode === "request"
|
|
163
|
+
? config.dataAccess.access.create(input, guardContext.actor)
|
|
164
|
+
: config.repository.create(input, guardContext.actor),
|
|
165
|
+
);
|
|
166
|
+
if (item instanceof Response) return item;
|
|
167
|
+
await recordMutationAudit(
|
|
168
|
+
runtime,
|
|
169
|
+
config.path,
|
|
170
|
+
resourceEvent(guardContext, "crud.create", config.path, item.id),
|
|
171
|
+
);
|
|
172
|
+
return projectEntity(config, item);
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
app.put(`/system/${config.path}/:id`, (rawContext) => {
|
|
178
|
+
const { guardContext, params, body } = routeContext(rawContext);
|
|
179
|
+
return withPermission(
|
|
180
|
+
runtime,
|
|
181
|
+
guardContext,
|
|
182
|
+
"PUT",
|
|
183
|
+
config.permissionResource,
|
|
184
|
+
"manage",
|
|
185
|
+
config.feature,
|
|
186
|
+
async () =>
|
|
187
|
+
runResourceMutation(config, async () => {
|
|
188
|
+
const id = requiredId(params);
|
|
189
|
+
const input = asPartial<T>(body);
|
|
190
|
+
const current = await readAccessible(config, id, guardContext.actor);
|
|
191
|
+
if (current instanceof Response) return current;
|
|
192
|
+
if (!current) return notFound(config.path, id);
|
|
193
|
+
const validationError = await config.validateInput?.(input, current);
|
|
194
|
+
if (validationError) return invalidResource(validationError);
|
|
195
|
+
const policyError = await config.beforeUpdate?.(current, input);
|
|
196
|
+
if (policyError) return resourceConflict(policyError);
|
|
197
|
+
const item = await accessCall(() =>
|
|
198
|
+
config.dataAccess.mode === "request"
|
|
199
|
+
? config.dataAccess.access.update(id, input, guardContext.actor)
|
|
200
|
+
: config.repository.update(id, input, guardContext.actor),
|
|
201
|
+
);
|
|
202
|
+
if (item instanceof Response) return item;
|
|
203
|
+
if (!item) {
|
|
204
|
+
return notFound(config.path, id);
|
|
205
|
+
}
|
|
206
|
+
await config.afterUpdate?.(current, item, guardContext.actor);
|
|
207
|
+
await recordMutationAudit(
|
|
208
|
+
runtime,
|
|
209
|
+
config.path,
|
|
210
|
+
resourceEvent(guardContext, "crud.update", config.path, id),
|
|
211
|
+
);
|
|
212
|
+
return projectEntity(config, item);
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.delete(`/system/${config.path}/:id`, (rawContext) => {
|
|
218
|
+
const { guardContext, params } = routeContext(rawContext);
|
|
219
|
+
return withPermission(
|
|
220
|
+
runtime,
|
|
221
|
+
guardContext,
|
|
222
|
+
"DELETE",
|
|
223
|
+
config.permissionResource,
|
|
224
|
+
"manage",
|
|
225
|
+
config.feature,
|
|
226
|
+
async () =>
|
|
227
|
+
runResourceMutation(config, async () => {
|
|
228
|
+
const id = requiredId(params);
|
|
229
|
+
const current = await readAccessible(config, id, guardContext.actor);
|
|
230
|
+
if (current instanceof Response) return current;
|
|
231
|
+
if (!current) return notFound(config.path, id);
|
|
232
|
+
const policyError = await config.beforeDelete?.(current);
|
|
233
|
+
if (policyError) return resourceConflict(policyError);
|
|
234
|
+
const item = await accessCall(() =>
|
|
235
|
+
config.dataAccess.mode === "request"
|
|
236
|
+
? config.dataAccess.access.softDelete(id, guardContext.actor)
|
|
237
|
+
: config.repository.softDelete(id, guardContext.actor),
|
|
238
|
+
);
|
|
239
|
+
if (item instanceof Response) return item;
|
|
240
|
+
if (!item) {
|
|
241
|
+
return notFound(config.path, id);
|
|
242
|
+
}
|
|
243
|
+
await config.afterDelete?.(item, guardContext.actor);
|
|
244
|
+
await recordMutationAudit(
|
|
245
|
+
runtime,
|
|
246
|
+
config.path,
|
|
247
|
+
resourceEvent(guardContext, "crud.delete", config.path, id),
|
|
248
|
+
);
|
|
249
|
+
return projectEntity(config, item);
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function invalidResource(
|
|
256
|
+
error: NonNullable<
|
|
257
|
+
Awaited<
|
|
258
|
+
ReturnType<
|
|
259
|
+
NonNullable<ResourceRouteConfig<ManagedEntity>["validateInput"]>
|
|
260
|
+
>
|
|
261
|
+
>
|
|
262
|
+
>,
|
|
263
|
+
): Response {
|
|
264
|
+
return Response.json(
|
|
265
|
+
typeof error === "string"
|
|
266
|
+
? { error: "INVALID_RESOURCE", message: error }
|
|
267
|
+
: error,
|
|
268
|
+
{ status: 400 },
|
|
269
|
+
);
|
|
270
|
+
}
|