@southwind-ai/database 0.1.1 → 0.1.3
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/index.ts +1 -0
- package/package.json +5 -2
- package/src/postgres-module-migrations.ts +69 -0
- package/src/runner.ts +24 -11
- package/src/schema/migration-history.ts +3 -0
- package/src/schema/platform-settings.ts +16 -1
- package/src/sql-store.ts +13 -3
- package/src/store.ts +2 -0
- package/src/bundle-validation.ts +0 -218
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@southwind-ai/database",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit --project tsconfig.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@southwind-ai/shared": "0.1.
|
|
18
|
+
"@southwind-ai/shared": "0.1.2",
|
|
19
19
|
"drizzle-orm": "^0.45.2"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
@@ -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,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
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BundleMigrationDefinition,
|
|
3
|
-
DatabaseCompatibilityAdapter,
|
|
4
|
-
DatabaseDialect,
|
|
5
|
-
MigrationBundle,
|
|
6
|
-
MigrationManifestSource,
|
|
7
|
-
MigrationStatement,
|
|
8
|
-
} from "@southwind-ai/shared";
|
|
9
1
|
import {
|
|
10
2
|
normalizeManifestBundles,
|
|
11
3
|
orderBundleMigrations,
|
|
12
4
|
validateAndOrderBundles,
|
|
13
|
-
|
|
5
|
+
type BundleMigrationDefinition,
|
|
6
|
+
type DatabaseCompatibilityAdapter,
|
|
7
|
+
type DatabaseDialect,
|
|
8
|
+
type MigrationBundle,
|
|
9
|
+
type MigrationManifestSource,
|
|
10
|
+
type MigrationStatement,
|
|
11
|
+
} from "@southwind-ai/shared";
|
|
14
12
|
import { createDialectAdapter } from "./dialects.js";
|
|
15
13
|
import type { MigrationHistoryEntry, MigrationHistoryStore } from "./store.js";
|
|
16
14
|
|
|
@@ -19,11 +17,14 @@ export interface MigrationRunnerOptions {
|
|
|
19
17
|
store: MigrationHistoryStore;
|
|
20
18
|
execute?: (statement: MigrationStatement) => Promise<void>;
|
|
21
19
|
now?: () => Date;
|
|
20
|
+
recordFailures?: boolean;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export interface MigrationPlanItem {
|
|
25
24
|
id: string;
|
|
26
25
|
module: string;
|
|
26
|
+
moduleVersion: string;
|
|
27
|
+
migrationOrder: number;
|
|
27
28
|
description: string;
|
|
28
29
|
direction: "up" | "down";
|
|
29
30
|
checksum: string;
|
|
@@ -64,9 +65,15 @@ export class PlatformMigrationRunner {
|
|
|
64
65
|
): Promise<MigrationPlanItem[]> {
|
|
65
66
|
const ordered = validateAndOrderBundles(bundles);
|
|
66
67
|
const applied = appliedById(await this.options.store.listApplied());
|
|
68
|
+
let migrationOrder = 0;
|
|
67
69
|
return ordered.flatMap((bundle) =>
|
|
68
70
|
orderBundleMigrations(bundle).map((migration) =>
|
|
69
|
-
planItem(
|
|
71
|
+
planItem(
|
|
72
|
+
migration,
|
|
73
|
+
bundle.version,
|
|
74
|
+
migrationOrder++,
|
|
75
|
+
applied.get(migration.id),
|
|
76
|
+
),
|
|
70
77
|
),
|
|
71
78
|
);
|
|
72
79
|
}
|
|
@@ -121,7 +128,7 @@ export class PlatformMigrationRunner {
|
|
|
121
128
|
} catch (error) {
|
|
122
129
|
const message = error instanceof Error ? error.message : String(error);
|
|
123
130
|
failed.push({ id: item.id, error: message });
|
|
124
|
-
if (!dryRun) {
|
|
131
|
+
if (!dryRun && this.options.recordFailures !== false) {
|
|
125
132
|
await this.recordResult(item, stepStartedAt, stepStatements, message);
|
|
126
133
|
}
|
|
127
134
|
break;
|
|
@@ -149,6 +156,8 @@ export class PlatformMigrationRunner {
|
|
|
149
156
|
await this.options.store.record({
|
|
150
157
|
id: item.id,
|
|
151
158
|
module: item.module,
|
|
159
|
+
moduleVersion: item.moduleVersion,
|
|
160
|
+
migrationOrder: item.migrationOrder,
|
|
152
161
|
direction: item.direction,
|
|
153
162
|
status: error ? "failed" : "applied",
|
|
154
163
|
checksum: item.checksum,
|
|
@@ -166,6 +175,8 @@ function appliedById(entries: readonly MigrationHistoryEntry[]) {
|
|
|
166
175
|
|
|
167
176
|
function planItem(
|
|
168
177
|
migration: BundleMigrationDefinition,
|
|
178
|
+
moduleVersion: string,
|
|
179
|
+
migrationOrder: number,
|
|
169
180
|
applied: MigrationHistoryEntry | undefined,
|
|
170
181
|
): MigrationPlanItem {
|
|
171
182
|
if (applied?.module !== undefined && applied.module !== migration.module) {
|
|
@@ -184,6 +195,8 @@ function planItem(
|
|
|
184
195
|
return {
|
|
185
196
|
id: migration.id,
|
|
186
197
|
module: migration.module,
|
|
198
|
+
moduleVersion,
|
|
199
|
+
migrationOrder,
|
|
187
200
|
description: migration.description,
|
|
188
201
|
direction: migration.direction,
|
|
189
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 }),
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
export {};
|
|
3
3
|
|
|
4
4
|
// @windy-module system.configuration begin
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ModuleConfigDiffEntry,
|
|
7
|
+
ModuleConfigValues,
|
|
8
|
+
} from "@southwind-ai/shared";
|
|
6
9
|
import {
|
|
7
10
|
index,
|
|
8
11
|
integer,
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
// @windy-module system.configuration end
|
|
16
19
|
// @windy-module system.settings begin
|
|
17
20
|
import {
|
|
21
|
+
boolean,
|
|
18
22
|
pgTable as settingsTable,
|
|
19
23
|
text,
|
|
20
24
|
varchar as settingsVarchar,
|
|
@@ -33,6 +37,17 @@ export const platformBrandingSettings = settingsTable(
|
|
|
33
37
|
...auditColumns,
|
|
34
38
|
},
|
|
35
39
|
);
|
|
40
|
+
|
|
41
|
+
export const platformWatermarkPolicies = settingsTable(
|
|
42
|
+
"platform_watermark_policies",
|
|
43
|
+
{
|
|
44
|
+
id: settingsVarchar("id", { length: 64 }).primaryKey(),
|
|
45
|
+
enabled: boolean("enabled").notNull(),
|
|
46
|
+
businessPagesEnabled: boolean("business_pages_enabled").notNull(),
|
|
47
|
+
customContent: settingsVarchar("custom_content", { length: 80 }).notNull(),
|
|
48
|
+
...auditColumns,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
36
51
|
// @windy-module system.settings end
|
|
37
52
|
|
|
38
53
|
// @windy-module system.configuration begin
|
package/src/sql-store.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
46
|
-
|
|
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),
|
package/src/store.ts
CHANGED
package/src/bundle-validation.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MIGRATION_BUNDLE_FORMAT_VERSION,
|
|
3
|
-
type BundleMigrationDefinition,
|
|
4
|
-
type MigrationBundle,
|
|
5
|
-
type MigrationDefinition,
|
|
6
|
-
type MigrationManifestSource,
|
|
7
|
-
} from "@southwind-ai/shared";
|
|
8
|
-
|
|
9
|
-
export function normalizeManifestBundles(
|
|
10
|
-
modules: readonly MigrationManifestSource[],
|
|
11
|
-
): MigrationBundle[] {
|
|
12
|
-
return modules.map((manifest) => {
|
|
13
|
-
const bundle =
|
|
14
|
-
manifest.migrationBundle ||
|
|
15
|
-
legacyManifestBundle(manifest, manifest.migrations);
|
|
16
|
-
if (bundle.module !== manifest.name) {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`Migration Bundle ${bundle.module} 与 Manifest ${manifest.name} 不一致`,
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
if (bundle.version !== manifest.version) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
`Migration Bundle ${bundle.module} 版本 ${bundle.version} 与 Manifest ${manifest.version} 不一致`,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
assertSameDependencies(manifest, bundle);
|
|
27
|
-
assertSameMigrations(manifest, bundle);
|
|
28
|
-
assertSchemaMigrationReferences(manifest, bundle);
|
|
29
|
-
return bundle;
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function assertSameDependencies(
|
|
34
|
-
manifest: MigrationManifestSource,
|
|
35
|
-
bundle: MigrationBundle,
|
|
36
|
-
): void {
|
|
37
|
-
const declared = manifest.dependencies || [];
|
|
38
|
-
const bundled = bundle.dependencies || [];
|
|
39
|
-
if (
|
|
40
|
-
declared.length !== bundled.length ||
|
|
41
|
-
declared.some((dependency) => !bundled.includes(dependency))
|
|
42
|
-
) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`Manifest ${manifest.name} 的 dependencies 必须与 Migration Bundle 一致`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function legacyManifestBundle(
|
|
50
|
-
manifest: MigrationManifestSource,
|
|
51
|
-
migrations: readonly MigrationDefinition[],
|
|
52
|
-
): MigrationBundle {
|
|
53
|
-
return {
|
|
54
|
-
formatVersion: MIGRATION_BUNDLE_FORMAT_VERSION,
|
|
55
|
-
module: manifest.name,
|
|
56
|
-
version: manifest.version,
|
|
57
|
-
dependencies: manifest.dependencies,
|
|
58
|
-
migrations: migrations.map((migration) => ({
|
|
59
|
-
...migration,
|
|
60
|
-
checksum:
|
|
61
|
-
migration.checksum ||
|
|
62
|
-
`legacy:${manifest.name}:${manifest.version}:${migration.id}`,
|
|
63
|
-
})),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function assertSameMigrations(
|
|
68
|
-
manifest: MigrationManifestSource,
|
|
69
|
-
bundle: MigrationBundle,
|
|
70
|
-
): void {
|
|
71
|
-
const declared = manifest.migrations.map(({ id }) => id);
|
|
72
|
-
const bundled = bundle.migrations.map(({ id }) => id);
|
|
73
|
-
if (
|
|
74
|
-
declared.length !== bundled.length ||
|
|
75
|
-
declared.some((id, index) => id !== bundled[index])
|
|
76
|
-
) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`Manifest ${manifest.name} 的 migrations 必须与 Migration Bundle 完全一致`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function assertSchemaMigrationReferences(
|
|
84
|
-
manifest: MigrationManifestSource,
|
|
85
|
-
bundle: MigrationBundle,
|
|
86
|
-
): void {
|
|
87
|
-
const ids = new Set(bundle.migrations.map(({ id }) => id));
|
|
88
|
-
for (const schema of manifest.schemaExports || []) {
|
|
89
|
-
for (const migrationId of schema.migrationIds) {
|
|
90
|
-
if (!ids.has(migrationId)) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Schema Export ${schema.key} 引用了 Bundle 中不存在的 Migration:${migrationId}`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function validateAndOrderBundles(
|
|
100
|
-
bundles: readonly MigrationBundle[],
|
|
101
|
-
): MigrationBundle[] {
|
|
102
|
-
const byModule = new Map<string, MigrationBundle>();
|
|
103
|
-
const migrationOwner = new Map<string, string>();
|
|
104
|
-
for (const bundle of bundles) {
|
|
105
|
-
validateBundle(bundle);
|
|
106
|
-
if (byModule.has(bundle.module)) {
|
|
107
|
-
throw new Error(`Migration Bundle 模块重复:${bundle.module}`);
|
|
108
|
-
}
|
|
109
|
-
byModule.set(bundle.module, bundle);
|
|
110
|
-
for (const migration of bundle.migrations) {
|
|
111
|
-
const owner = migrationOwner.get(migration.id);
|
|
112
|
-
if (owner) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`Migration ID 重复:${migration.id}(${owner} / ${bundle.module})`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
migrationOwner.set(migration.id, bundle.module);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
for (const bundle of bundles) {
|
|
121
|
-
for (const dependency of bundle.dependencies || []) {
|
|
122
|
-
if (!byModule.has(dependency)) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
`Migration Bundle ${bundle.module} 依赖未安装模块:${dependency}`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
validateMigrationDependencies(bundle, migrationOwner);
|
|
129
|
-
}
|
|
130
|
-
return topologicalBundles(byModule);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function validateBundle(bundle: MigrationBundle): void {
|
|
134
|
-
if (bundle.formatVersion !== MIGRATION_BUNDLE_FORMAT_VERSION) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Migration Bundle ${bundle.module} 格式版本不受支持:${bundle.formatVersion}`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
if (!bundle.module.trim()) throw new Error("Migration Bundle 缺少 module");
|
|
140
|
-
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(bundle.version)) {
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Migration Bundle ${bundle.module} 版本无效:${bundle.version}`,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
for (const migration of bundle.migrations) {
|
|
146
|
-
if (migration.module !== bundle.module) {
|
|
147
|
-
throw new Error(
|
|
148
|
-
`Migration ${migration.id} 的模块归属应为 ${bundle.module}`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
if (!migration.id.trim()) throw new Error("Migration ID 不能为空");
|
|
152
|
-
if (!migration.checksum.trim()) {
|
|
153
|
-
throw new Error(`Migration ${migration.id} 缺少 checksum`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function validateMigrationDependencies(
|
|
159
|
-
bundle: MigrationBundle,
|
|
160
|
-
owners: ReadonlyMap<string, string>,
|
|
161
|
-
): void {
|
|
162
|
-
for (const migration of bundle.migrations) {
|
|
163
|
-
for (const dependency of migration.dependsOn || []) {
|
|
164
|
-
const owner = owners.get(dependency);
|
|
165
|
-
if (!owner) {
|
|
166
|
-
throw new Error(`Migration ${migration.id} 依赖不存在:${dependency}`);
|
|
167
|
-
}
|
|
168
|
-
if (
|
|
169
|
-
owner !== bundle.module &&
|
|
170
|
-
!(bundle.dependencies || []).includes(owner)
|
|
171
|
-
) {
|
|
172
|
-
throw new Error(
|
|
173
|
-
`Migration ${migration.id} 跨模块依赖 ${owner},但 Bundle 未声明该模块依赖`,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function topologicalBundles(
|
|
181
|
-
pendingSource: ReadonlyMap<string, MigrationBundle>,
|
|
182
|
-
): MigrationBundle[] {
|
|
183
|
-
const pending = new Map(pendingSource);
|
|
184
|
-
const sorted: MigrationBundle[] = [];
|
|
185
|
-
while (pending.size > 0) {
|
|
186
|
-
const ready = Array.from(pending.values()).find((bundle) =>
|
|
187
|
-
(bundle.dependencies || []).every(
|
|
188
|
-
(dependency) => !pending.has(dependency),
|
|
189
|
-
),
|
|
190
|
-
);
|
|
191
|
-
if (!ready) throw new Error("Migration Bundle 模块依赖存在循环");
|
|
192
|
-
sorted.push(ready);
|
|
193
|
-
pending.delete(ready.module);
|
|
194
|
-
}
|
|
195
|
-
return sorted;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function orderBundleMigrations(
|
|
199
|
-
bundle: MigrationBundle,
|
|
200
|
-
): BundleMigrationDefinition[] {
|
|
201
|
-
const pending = new Map(
|
|
202
|
-
bundle.migrations.map((migration) => [migration.id, migration]),
|
|
203
|
-
);
|
|
204
|
-
const sorted: BundleMigrationDefinition[] = [];
|
|
205
|
-
while (pending.size > 0) {
|
|
206
|
-
const ready = Array.from(pending.values()).find((migration) =>
|
|
207
|
-
(migration.dependsOn || []).every(
|
|
208
|
-
(dependency) => !pending.has(dependency),
|
|
209
|
-
),
|
|
210
|
-
);
|
|
211
|
-
if (!ready) {
|
|
212
|
-
throw new Error(`Migration Bundle ${bundle.module} 的迁移依赖存在循环`);
|
|
213
|
-
}
|
|
214
|
-
sorted.push(ready);
|
|
215
|
-
pending.delete(ready.id);
|
|
216
|
-
}
|
|
217
|
-
return sorted;
|
|
218
|
-
}
|