create-windy 0.2.25 → 0.2.27

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 (29) hide show
  1. package/dist/cli.js +52 -1
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/server/src/agent/data-scope-routes.test.ts +5 -8
  5. package/template/apps/server/src/agent/routes.test.ts +4 -7
  6. package/template/apps/server/src/agent/tool-registry.test.ts +5 -8
  7. package/template/apps/server/src/foundation.ts +7 -1
  8. package/template/apps/server/src/http-app.test.ts +5 -2
  9. package/template/apps/server/src/license/runtime-service.test.ts +2 -5
  10. package/template/apps/server/src/mcp/adapter.test.ts +2 -5
  11. package/template/apps/server/src/module-bootstrap.ts +11 -2
  12. package/template/apps/server/src/module-migrations.test.ts +43 -0
  13. package/template/apps/server/src/module-migrations.ts +19 -0
  14. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +14 -0
  15. package/template/docs/architecture/module-manifest-composition.md +11 -0
  16. package/template/docs/platform/agent-runtime.md +3 -3
  17. package/template/packages/database/drizzle/0032_marvelous_pepper_potts.sql +2 -0
  18. package/template/packages/database/drizzle/meta/0032_snapshot.json +7399 -0
  19. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  20. package/template/packages/database/index.ts +1 -0
  21. package/template/packages/database/package.json +4 -1
  22. package/template/packages/database/src/postgres-module-migrations.integration.test.ts +170 -0
  23. package/template/packages/database/src/postgres-module-migrations.ts +69 -0
  24. package/template/packages/database/src/runner.test.ts +5 -1
  25. package/template/packages/database/src/runner.ts +17 -2
  26. package/template/packages/database/src/schema/migration-history.ts +3 -0
  27. package/template/packages/database/src/sql-store.test.ts +7 -1
  28. package/template/packages/database/src/sql-store.ts +13 -3
  29. package/template/packages/database/src/store.ts +2 -0
@@ -225,6 +225,13 @@
225
225
  "when": 1784837513572,
226
226
  "tag": "0031_panoramic_typhoid_mary",
227
227
  "breakpoints": true
228
+ },
229
+ {
230
+ "idx": 32,
231
+ "version": "7",
232
+ "when": 1784922914250,
233
+ "tag": "0032_marvelous_pepper_potts",
234
+ "breakpoints": true
228
235
  }
229
236
  ]
230
237
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./src/dialects.js";
2
+ export * from "./src/postgres-module-migrations.js";
2
3
  export * from "./src/runner.js";
3
4
  export * from "./src/schema/index.js";
4
5
  export * from "./src/sql-store.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/database",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,5 +23,8 @@
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
26
+ },
27
+ "devDependencies": {
28
+ "pg": "^8.22.0"
26
29
  }
27
30
  }
@@ -0,0 +1,170 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import {
3
+ MIGRATION_BUNDLE_FORMAT_VERSION,
4
+ type MigrationBundle,
5
+ } from "@southwind-ai/shared";
6
+ import { Pool } from "pg";
7
+ import { runPostgresMigrationBundles } from "./postgres-module-migrations.js";
8
+
9
+ const databaseUrl = process.env.MODULE_MIGRATION_TEST_DATABASE_URL;
10
+ const describePostgres = databaseUrl ? describe : describe.skip;
11
+
12
+ describePostgres("PostgreSQL Module Migration 启动执行", () => {
13
+ let pool: Pool;
14
+
15
+ beforeAll(() => {
16
+ pool = new Pool({ connectionString: databaseUrl });
17
+ });
18
+
19
+ afterAll(async () => {
20
+ await pool?.end();
21
+ });
22
+
23
+ test("并发启动按拓扑串行、记录完整历史、重复幂等并拒绝 checksum 漂移", async () => {
24
+ const base = bundle("customer-domain", "1.2.0", [
25
+ migration(
26
+ "customer-domain",
27
+ "customer-domain.0040",
28
+ "create table module_migration_base (id integer primary key)",
29
+ ),
30
+ ]);
31
+ const feature = bundle(
32
+ "case-domain",
33
+ "2.0.0",
34
+ [
35
+ migration(
36
+ "case-domain",
37
+ "case-domain.0041",
38
+ "create table module_migration_feature (id integer primary key)",
39
+ ["customer-domain.0040"],
40
+ ),
41
+ ],
42
+ ["customer-domain"],
43
+ );
44
+
45
+ const concurrent = await Promise.all([
46
+ runPostgresMigrationBundles({ pool, bundles: [feature, base] }),
47
+ runPostgresMigrationBundles({ pool, bundles: [feature, base] }),
48
+ ]);
49
+ expect(concurrent.flatMap(({ applied }) => applied).sort()).toEqual([
50
+ "case-domain.0041",
51
+ "customer-domain.0040",
52
+ ]);
53
+
54
+ const history = await pool.query<{
55
+ id: string;
56
+ module: string;
57
+ module_version: string;
58
+ migration_order: number;
59
+ checksum: string;
60
+ }>(
61
+ `select id, module, module_version, migration_order, checksum
62
+ from windy_migration_history
63
+ where id = any($1::text[])
64
+ order by migration_order`,
65
+ [["customer-domain.0040", "case-domain.0041"]],
66
+ );
67
+ expect(history.rows).toEqual([
68
+ {
69
+ id: "customer-domain.0040",
70
+ module: "customer-domain",
71
+ module_version: "1.2.0",
72
+ migration_order: 0,
73
+ checksum: "sha256:customer-domain.0040",
74
+ },
75
+ {
76
+ id: "case-domain.0041",
77
+ module: "case-domain",
78
+ module_version: "2.0.0",
79
+ migration_order: 1,
80
+ checksum: "sha256:case-domain.0041",
81
+ },
82
+ ]);
83
+
84
+ const repeated = await runPostgresMigrationBundles({
85
+ pool,
86
+ bundles: [feature, base],
87
+ });
88
+ expect(repeated.applied).toEqual([]);
89
+
90
+ const driftedBase = bundle("customer-domain", "1.2.0", [
91
+ {
92
+ ...base.migrations[0]!,
93
+ checksum: "sha256:changed",
94
+ },
95
+ ]);
96
+ await expect(
97
+ runPostgresMigrationBundles({
98
+ pool,
99
+ bundles: [driftedBase, feature],
100
+ }),
101
+ ).rejects.toThrow("checksum 与当前 Bundle 不一致");
102
+ });
103
+
104
+ test("任一 statement 失败会回滚本次全部 schema 与 history", async () => {
105
+ const broken = bundle("broken-domain", "1.0.0", [
106
+ migration(
107
+ "broken-domain",
108
+ "broken-domain.0001",
109
+ "create table module_migration_rollback (id integer primary key)",
110
+ ),
111
+ migration(
112
+ "broken-domain",
113
+ "broken-domain.0002",
114
+ "select * from module_migration_missing_relation",
115
+ ["broken-domain.0001"],
116
+ ),
117
+ ]);
118
+
119
+ await expect(
120
+ runPostgresMigrationBundles({ pool, bundles: [broken] }),
121
+ ).rejects.toThrow("broken-domain.0002");
122
+ const state = await pool.query<{
123
+ relation: string | null;
124
+ history_count: string;
125
+ }>(
126
+ `select
127
+ to_regclass('public.module_migration_rollback')::text as relation,
128
+ (select count(*)::text from windy_migration_history
129
+ where module = 'broken-domain') as history_count`,
130
+ );
131
+ expect(state.rows[0]).toEqual({
132
+ relation: null,
133
+ history_count: "0",
134
+ });
135
+ });
136
+ });
137
+
138
+ function bundle(
139
+ module: string,
140
+ version: string,
141
+ migrations: MigrationBundle["migrations"],
142
+ dependencies: readonly string[] = [],
143
+ ): MigrationBundle {
144
+ return {
145
+ formatVersion: MIGRATION_BUNDLE_FORMAT_VERSION,
146
+ module,
147
+ version,
148
+ dependencies,
149
+ migrations,
150
+ };
151
+ }
152
+
153
+ function migration(
154
+ module: string,
155
+ id: string,
156
+ sql: string,
157
+ dependsOn: readonly string[] = [],
158
+ ): MigrationBundle["migrations"][number] {
159
+ return {
160
+ id,
161
+ module,
162
+ description: id,
163
+ direction: "up",
164
+ checksum: `sha256:${id}`,
165
+ dependsOn,
166
+ async run(context) {
167
+ await context.execute({ sql });
168
+ },
169
+ };
170
+ }
@@ -0,0 +1,69 @@
1
+ import type { MigrationBundle } from "@southwind-ai/shared";
2
+ import { PlatformMigrationRunner, type MigrationRunReport } from "./runner.js";
3
+ import {
4
+ createPostgresStatementExecutor,
5
+ PostgresMigrationHistoryStore,
6
+ type QueryableClient,
7
+ } from "./sql-store.js";
8
+
9
+ const MODULE_MIGRATION_LOCK = "windy.module-migrations.v1";
10
+
11
+ export interface PostgresTransactionClient extends QueryableClient {
12
+ release(): void;
13
+ }
14
+
15
+ export interface PostgresTransactionPool {
16
+ connect(): Promise<PostgresTransactionClient>;
17
+ }
18
+
19
+ export interface PostgresMigrationBundleOptions {
20
+ pool: PostgresTransactionPool;
21
+ bundles: readonly MigrationBundle[];
22
+ now?: () => Date;
23
+ }
24
+
25
+ export async function runPostgresMigrationBundles(
26
+ options: PostgresMigrationBundleOptions,
27
+ ): Promise<MigrationRunReport> {
28
+ const client = await options.pool.connect();
29
+ let transactionOpen = false;
30
+ try {
31
+ await client.query("begin");
32
+ transactionOpen = true;
33
+ await client.query("select pg_advisory_xact_lock(hashtext($1))", [
34
+ MODULE_MIGRATION_LOCK,
35
+ ]);
36
+ const runner = new PlatformMigrationRunner({
37
+ dialect: "postgresql",
38
+ store: new PostgresMigrationHistoryStore(client),
39
+ execute: createPostgresStatementExecutor(client),
40
+ now: options.now,
41
+ recordFailures: false,
42
+ });
43
+ const report = await runner.runBundles(options.bundles);
44
+ const failure = report.failed[0];
45
+ if (failure) {
46
+ throw new Error(
47
+ `Module Migration ${failure.id} 执行失败:${failure.error}`,
48
+ );
49
+ }
50
+ await client.query("commit");
51
+ transactionOpen = false;
52
+ return report;
53
+ } catch (error) {
54
+ if (transactionOpen) await rollbackWithoutMasking(client);
55
+ throw error;
56
+ } finally {
57
+ client.release();
58
+ }
59
+ }
60
+
61
+ async function rollbackWithoutMasking(
62
+ client: PostgresTransactionClient,
63
+ ): Promise<void> {
64
+ try {
65
+ await client.query("rollback");
66
+ } catch {
67
+ // 原始 migration 错误决定启动失败原因,连接释放后由 Pool 负责处置。
68
+ }
69
+ }
@@ -141,7 +141,11 @@ describe("PlatformMigrationRunner", () => {
141
141
  "select 'feature.001'",
142
142
  "select 'feature.002'",
143
143
  ]);
144
- expect((await store.listApplied())[0]?.checksum).toBe("sha256:system-1");
144
+ expect((await store.listApplied())[0]).toMatchObject({
145
+ checksum: "sha256:system-1",
146
+ moduleVersion: "1.0.0",
147
+ migrationOrder: 0,
148
+ });
145
149
  });
146
150
 
147
151
  test("repeat bundle 跳过 checksum 相同的已应用迁移", async () => {
@@ -17,11 +17,14 @@ export interface MigrationRunnerOptions {
17
17
  store: MigrationHistoryStore;
18
18
  execute?: (statement: MigrationStatement) => Promise<void>;
19
19
  now?: () => Date;
20
+ recordFailures?: boolean;
20
21
  }
21
22
 
22
23
  export interface MigrationPlanItem {
23
24
  id: string;
24
25
  module: string;
26
+ moduleVersion: string;
27
+ migrationOrder: number;
25
28
  description: string;
26
29
  direction: "up" | "down";
27
30
  checksum: string;
@@ -62,9 +65,15 @@ export class PlatformMigrationRunner {
62
65
  ): Promise<MigrationPlanItem[]> {
63
66
  const ordered = validateAndOrderBundles(bundles);
64
67
  const applied = appliedById(await this.options.store.listApplied());
68
+ let migrationOrder = 0;
65
69
  return ordered.flatMap((bundle) =>
66
70
  orderBundleMigrations(bundle).map((migration) =>
67
- planItem(migration, applied.get(migration.id)),
71
+ planItem(
72
+ migration,
73
+ bundle.version,
74
+ migrationOrder++,
75
+ applied.get(migration.id),
76
+ ),
68
77
  ),
69
78
  );
70
79
  }
@@ -119,7 +128,7 @@ export class PlatformMigrationRunner {
119
128
  } catch (error) {
120
129
  const message = error instanceof Error ? error.message : String(error);
121
130
  failed.push({ id: item.id, error: message });
122
- if (!dryRun) {
131
+ if (!dryRun && this.options.recordFailures !== false) {
123
132
  await this.recordResult(item, stepStartedAt, stepStatements, message);
124
133
  }
125
134
  break;
@@ -147,6 +156,8 @@ export class PlatformMigrationRunner {
147
156
  await this.options.store.record({
148
157
  id: item.id,
149
158
  module: item.module,
159
+ moduleVersion: item.moduleVersion,
160
+ migrationOrder: item.migrationOrder,
150
161
  direction: item.direction,
151
162
  status: error ? "failed" : "applied",
152
163
  checksum: item.checksum,
@@ -164,6 +175,8 @@ function appliedById(entries: readonly MigrationHistoryEntry[]) {
164
175
 
165
176
  function planItem(
166
177
  migration: BundleMigrationDefinition,
178
+ moduleVersion: string,
179
+ migrationOrder: number,
167
180
  applied: MigrationHistoryEntry | undefined,
168
181
  ): MigrationPlanItem {
169
182
  if (applied?.module !== undefined && applied.module !== migration.module) {
@@ -182,6 +195,8 @@ function planItem(
182
195
  return {
183
196
  id: migration.id,
184
197
  module: migration.module,
198
+ moduleVersion,
199
+ migrationOrder,
185
200
  description: migration.description,
186
201
  direction: migration.direction,
187
202
  checksum: migration.checksum,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  index,
3
+ integer,
3
4
  jsonb,
4
5
  pgTable,
5
6
  text,
@@ -12,6 +13,8 @@ export const windyMigrationHistory = pgTable(
12
13
  {
13
14
  id: varchar("id", { length: 160 }).primaryKey(),
14
15
  module: varchar("module", { length: 96 }).notNull(),
16
+ moduleVersion: varchar("module_version", { length: 64 }),
17
+ migrationOrder: integer("migration_order"),
15
18
  direction: varchar("direction", { length: 16 }).notNull(),
16
19
  status: varchar("status", { length: 16 }).notNull(),
17
20
  checksum: varchar("checksum", { length: 128 }),
@@ -24,6 +24,8 @@ describe("PostgresMigrationHistoryStore", () => {
24
24
  {
25
25
  id: "system.001",
26
26
  module: "system",
27
+ module_version: "1.0.0",
28
+ migration_order: 0,
27
29
  direction: "up",
28
30
  status: "applied",
29
31
  checksum: null,
@@ -39,6 +41,8 @@ describe("PostgresMigrationHistoryStore", () => {
39
41
  {
40
42
  id: "system.001",
41
43
  module: "system",
44
+ moduleVersion: "1.0.0",
45
+ migrationOrder: 0,
42
46
  direction: "up",
43
47
  status: "applied",
44
48
  startedAt: "2026-07-09T00:00:00.000Z",
@@ -55,6 +59,8 @@ describe("PostgresMigrationHistoryStore", () => {
55
59
  await store.record({
56
60
  id: "system.001",
57
61
  module: "system",
62
+ moduleVersion: "1.0.0",
63
+ migrationOrder: 0,
58
64
  direction: "up",
59
65
  status: "applied",
60
66
  startedAt: "2026-07-09T00:00:00.000Z",
@@ -63,7 +69,7 @@ describe("PostgresMigrationHistoryStore", () => {
63
69
  });
64
70
 
65
71
  expect(client.calls[0]?.sql).toContain("on conflict (id) do update");
66
- expect(client.calls[0]?.params?.[5]).toBe(
72
+ expect(client.calls[0]?.params?.[7]).toBe(
67
73
  JSON.stringify(["create table users (id varchar(64))"]),
68
74
  );
69
75
  });
@@ -15,6 +15,8 @@ export interface QueryableClient {
15
15
  interface MigrationHistoryRow {
16
16
  id: string;
17
17
  module: string;
18
+ module_version: string | null;
19
+ migration_order: number | null;
18
20
  direction: "up" | "down";
19
21
  status: "applied" | "failed";
20
22
  checksum: string | null;
@@ -29,7 +31,8 @@ export class PostgresMigrationHistoryStore implements MigrationHistoryStore {
29
31
 
30
32
  async listApplied(): Promise<MigrationHistoryEntry[]> {
31
33
  const result = await this.client.query<MigrationHistoryRow>(
32
- `select id, module, direction, status, checksum, statements, error, started_at, finished_at
34
+ `select id, module, module_version, migration_order, direction, status,
35
+ checksum, statements, error, started_at, finished_at
33
36
  from windy_migration_history
34
37
  where status = $1
35
38
  order by finished_at asc`,
@@ -42,10 +45,13 @@ export class PostgresMigrationHistoryStore implements MigrationHistoryStore {
42
45
  async record(entry: MigrationHistoryEntry): Promise<void> {
43
46
  await this.client.query(
44
47
  `insert into windy_migration_history
45
- (id, module, direction, status, checksum, statements, error, started_at, finished_at)
46
- values ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9)
48
+ (id, module, module_version, migration_order, direction, status,
49
+ checksum, statements, error, started_at, finished_at)
50
+ values ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)
47
51
  on conflict (id) do update set
48
52
  module = excluded.module,
53
+ module_version = excluded.module_version,
54
+ migration_order = excluded.migration_order,
49
55
  direction = excluded.direction,
50
56
  status = excluded.status,
51
57
  checksum = excluded.checksum,
@@ -56,6 +62,8 @@ export class PostgresMigrationHistoryStore implements MigrationHistoryStore {
56
62
  [
57
63
  entry.id,
58
64
  entry.module,
65
+ entry.moduleVersion || null,
66
+ entry.migrationOrder ?? null,
59
67
  entry.direction,
60
68
  entry.status,
61
69
  entry.checksum || null,
@@ -86,6 +94,8 @@ function mapHistoryRow(row: MigrationHistoryRow): MigrationHistoryEntry {
86
94
  return {
87
95
  id: row.id,
88
96
  module: row.module,
97
+ moduleVersion: row.module_version || undefined,
98
+ migrationOrder: row.migration_order ?? undefined,
89
99
  direction: row.direction,
90
100
  status: row.status,
91
101
  startedAt: toIsoString(row.started_at),
@@ -5,6 +5,8 @@ export type MigrationStatus = "applied" | "failed";
5
5
  export interface MigrationHistoryEntry {
6
6
  id: string;
7
7
  module: string;
8
+ moduleVersion?: string;
9
+ migrationOrder?: number;
8
10
  direction: "up" | "down";
9
11
  status: MigrationStatus;
10
12
  startedAt: ISODateTime;