create-windy 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +26 -3
  2. package/dist/cli.js +8077 -3852
  3. package/package.json +3 -3
  4. package/template/.windy-template.json +2 -2
  5. package/template/README.md +25 -3
  6. package/template/_gitignore +4 -0
  7. package/template/apps/agent-server/tests/__init__.py +1 -0
  8. package/template/apps/agent-server/tests/openai_provider_test_support.py +131 -0
  9. package/template/apps/agent-server/tests/test_openai_provider.py +7 -123
  10. package/template/apps/server/src/auth/routes.test-support.ts +94 -0
  11. package/template/apps/server/src/auth/routes.test.ts +20 -88
  12. package/template/apps/server/src/index.ts +17 -34
  13. package/template/apps/server/src/mcp/adapter.test.ts +3 -2
  14. package/template/apps/server/src/mcp/adapter.ts +9 -9
  15. package/template/apps/server/src/mcp/routes.ts +4 -3
  16. package/template/apps/server/src/platform-http-runtime.ts +87 -0
  17. package/template/apps/server/src/system/drizzle-system-configs.ts +319 -0
  18. package/template/apps/server/src/system/drizzle-system.ts +35 -315
  19. package/template/apps/server/src/system/resource-routes.ts +270 -0
  20. package/template/apps/server/src/system/routes.ts +3 -269
  21. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.vue +21 -0
  22. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.webtest.ts +17 -0
  23. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +5 -10
  24. package/template/docs/architecture/mcp-business-capabilities.md +107 -0
  25. package/template/docs/development/custom-login-page.md +3 -3
  26. package/template/docs/platform/agent-runtime.md +5 -5
  27. package/template/docs/platform/agent-tools.md +4 -0
  28. package/template/packages/modules/package.json +1 -1
  29. package/template/packages/modules/src/composition-fixtures.test-support.ts +164 -0
  30. package/template/packages/modules/src/composition.test.ts +5 -158
  31. package/template/packages/modules/src/system-api-operational.ts +234 -0
  32. package/template/packages/modules/src/system-api-permissions.ts +8 -229
  33. package/template/packages/modules/src/system-module-tasks.ts +86 -0
  34. package/template/packages/modules/src/system-modules-access.test.ts +60 -0
  35. package/template/packages/modules/src/system-modules.test.ts +0 -53
  36. package/template/packages/modules/src/system-modules.ts +2 -84
  37. package/template/packages/shared/index.ts +1 -0
  38. package/template/packages/shared/package.json +1 -1
  39. package/template/packages/shared/src/mcp.test.ts +12 -0
  40. 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: repo(db, roleConfig(seeds.roles), transactionContext),
90
- departments: repo(
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: repo(db, menuConfig(seeds.menus), transactionContext),
96
- dictionaries: repo(
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
- repo(db, featureConfig(seeds.features), transactionContext),
92
+ createSystemEntityRepository(
93
+ db,
94
+ featureConfig(seeds.features),
95
+ transactionContext,
96
+ ),
103
97
  ),
104
98
  // @windy-module system.license begin
105
- licenses: repo(db, licenseConfig(seeds.licenses), transactionContext),
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
+ }