@njdamstra/appwrite-utils-cli 1.11.0 → 1.11.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.
- package/dist/cli/commands/migrateCommands.js +4 -13
- package/dist/main.js +3 -5
- package/dist/migrations/migrateStrings.d.ts +1 -1
- package/dist/migrations/migrateStrings.js +47 -36
- package/package.json +1 -1
- package/src/cli/commands/migrateCommands.ts +4 -18
- package/src/main.ts +7 -5
- package/src/migrations/migrateStrings.ts +62 -42
|
@@ -11,7 +11,7 @@ export const migrateCommands = {
|
|
|
11
11
|
message: "String attribute migration:",
|
|
12
12
|
choices: [
|
|
13
13
|
{
|
|
14
|
-
name: "Analyze — scan
|
|
14
|
+
name: "Analyze — scan Appwrite server, generate migration plan (YAML)",
|
|
15
15
|
value: "analyze",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
@@ -33,17 +33,8 @@ export const migrateCommands = {
|
|
|
33
33
|
},
|
|
34
34
|
async analyzePhase(cli) {
|
|
35
35
|
const controller = cli.controller;
|
|
36
|
-
if (!controller?.
|
|
37
|
-
MessageFormatter.error("No
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const config = controller.config;
|
|
41
|
-
const collections = [
|
|
42
|
-
...(config.collections || []),
|
|
43
|
-
...(config.tables || []),
|
|
44
|
-
];
|
|
45
|
-
if (collections.length === 0) {
|
|
46
|
-
MessageFormatter.warning("No collections/tables found in local config. Nothing to analyze.", { prefix: "Analyze" });
|
|
36
|
+
if (!controller?.adapter) {
|
|
37
|
+
MessageFormatter.error("No database adapter available. Ensure a server connection is established.", undefined, { prefix: "Analyze" });
|
|
47
38
|
return;
|
|
48
39
|
}
|
|
49
40
|
// Prompt for output path
|
|
@@ -57,7 +48,7 @@ export const migrateCommands = {
|
|
|
57
48
|
]);
|
|
58
49
|
const options = { outputPath };
|
|
59
50
|
try {
|
|
60
|
-
analyzeStringAttributes(config, options);
|
|
51
|
+
await analyzeStringAttributes(controller.adapter, controller.config, options);
|
|
61
52
|
MessageFormatter.success("Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.", { prefix: "Analyze" });
|
|
62
53
|
}
|
|
63
54
|
catch (err) {
|
package/dist/main.js
CHANGED
|
@@ -648,13 +648,11 @@ async function main() {
|
|
|
648
648
|
if (argv.migrateStringsAnalyze || argv.migrateStringsExecute) {
|
|
649
649
|
const { analyzeStringAttributes, executeMigrationPlan } = await import("./migrations/migrateStrings.js");
|
|
650
650
|
if (argv.migrateStringsAnalyze) {
|
|
651
|
-
if (!controller.
|
|
652
|
-
MessageFormatter.error("No
|
|
653
|
-
prefix: "Migration",
|
|
654
|
-
});
|
|
651
|
+
if (!controller.adapter) {
|
|
652
|
+
MessageFormatter.error("No database adapter available. Ensure config has valid credentials.", undefined, { prefix: "Migration" });
|
|
655
653
|
return;
|
|
656
654
|
}
|
|
657
|
-
analyzeStringAttributes(controller.config, {
|
|
655
|
+
await analyzeStringAttributes(controller.adapter, controller.config, {
|
|
658
656
|
outputPath: argv.migrateStringsOutput,
|
|
659
657
|
});
|
|
660
658
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
|
|
2
2
|
import type { AppwriteConfig } from "@njdamstra/appwrite-utils";
|
|
3
3
|
import { type MigrationPlan, type AnalyzeOptions, type ExecuteOptions } from "./migrateStringsTypes.js";
|
|
4
|
-
export declare function analyzeStringAttributes(config: AppwriteConfig, options?: AnalyzeOptions): MigrationPlan
|
|
4
|
+
export declare function analyzeStringAttributes(adapter: DatabaseAdapter, config: AppwriteConfig, options?: AnalyzeOptions): Promise<MigrationPlan>;
|
|
5
5
|
export declare function executeMigrationPlan(adapter: DatabaseAdapter, options: ExecuteOptions): Promise<{
|
|
6
6
|
succeeded: number;
|
|
7
7
|
failed: number;
|
|
@@ -8,55 +8,52 @@ import { MessageFormatter, tryAwaitWithRetry, } from "@njdamstra/appwrite-utils-
|
|
|
8
8
|
import { ProgressManager } from "../shared/progressManager.js";
|
|
9
9
|
import { MigrationPlanSchema, MigrationCheckpointSchema, suggestTargetType, generateBackupKey, } from "./migrateStringsTypes.js";
|
|
10
10
|
// ────────────────────────────────────────────────────────
|
|
11
|
-
// Phase 1: Analyze —
|
|
11
|
+
// Phase 1: Analyze — queries Appwrite server for real state
|
|
12
12
|
// ────────────────────────────────────────────────────────
|
|
13
|
-
export function analyzeStringAttributes(config, options = {}) {
|
|
14
|
-
const collections = [
|
|
15
|
-
...(config.collections || []),
|
|
16
|
-
...(config.tables || []),
|
|
17
|
-
];
|
|
13
|
+
export async function analyzeStringAttributes(adapter, config, options = {}) {
|
|
18
14
|
const databases = config.databases || [];
|
|
15
|
+
if (databases.length === 0) {
|
|
16
|
+
MessageFormatter.warning("No databases configured. Nothing to analyze.", {
|
|
17
|
+
prefix: "Analyze",
|
|
18
|
+
});
|
|
19
|
+
return emptyPlan(config);
|
|
20
|
+
}
|
|
19
21
|
const entries = [];
|
|
20
|
-
const collectionsSeen = new Set();
|
|
21
22
|
for (const db of databases) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const coll = c;
|
|
25
|
-
if (coll.databaseId)
|
|
26
|
-
return coll.databaseId === db.$id;
|
|
27
|
-
if (coll.databaseIds?.length)
|
|
28
|
-
return coll.databaseIds.includes(db.$id);
|
|
29
|
-
return true; // unassigned → applied to all databases
|
|
23
|
+
MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
|
|
24
|
+
prefix: "Analyze",
|
|
30
25
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
26
|
+
// Fetch all collections/tables from server
|
|
27
|
+
const tablesRes = await tryAwaitWithRetry(() => adapter.listTables({ databaseId: db.$id }));
|
|
28
|
+
const tables = tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
|
|
29
|
+
for (const table of tables) {
|
|
30
|
+
const tableId = table.$id || table.key || table.name;
|
|
31
|
+
const tableName = table.name || tableId;
|
|
32
|
+
// Fetch full schema from server
|
|
33
|
+
const schemaRes = await tryAwaitWithRetry(() => adapter.getTable({ databaseId: db.$id, tableId }));
|
|
34
|
+
const attributes = schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
|
|
35
|
+
// Fetch indexes from server
|
|
36
|
+
const indexRes = await tryAwaitWithRetry(() => adapter.listIndexes({ databaseId: db.$id, tableId }));
|
|
37
|
+
const indexes = indexRes?.data || [];
|
|
39
38
|
for (const attr of attributes) {
|
|
40
39
|
if (attr.type !== "string")
|
|
41
40
|
continue;
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const isEncrypted = !!stringAttr.encrypt;
|
|
41
|
+
const size = attr.size || 50;
|
|
42
|
+
const isEncrypted = !!attr.encrypt;
|
|
45
43
|
const isRequired = !!attr.required;
|
|
46
44
|
const isArray = !!attr.array;
|
|
47
|
-
const hasDefault =
|
|
45
|
+
const hasDefault = attr.xdefault !== undefined && attr.xdefault !== null;
|
|
48
46
|
// Find indexes that reference this attribute
|
|
49
47
|
const affectedIndexes = indexes
|
|
50
|
-
.filter((idx) => idx.attributes
|
|
48
|
+
.filter((idx) => idx.attributes?.includes(attr.key))
|
|
51
49
|
.map((idx) => idx.key);
|
|
52
50
|
const hasIndex = affectedIndexes.length > 0;
|
|
53
51
|
const suggested = suggestTargetType(size, hasIndex);
|
|
54
|
-
// Build entry
|
|
55
52
|
const entry = {
|
|
56
53
|
databaseId: db.$id,
|
|
57
54
|
databaseName: db.name,
|
|
58
|
-
collectionId:
|
|
59
|
-
collectionName:
|
|
55
|
+
collectionId: tableId,
|
|
56
|
+
collectionName: tableName,
|
|
60
57
|
attributeKey: attr.key,
|
|
61
58
|
currentType: "string",
|
|
62
59
|
currentSize: size,
|
|
@@ -64,7 +61,7 @@ export function analyzeStringAttributes(config, options = {}) {
|
|
|
64
61
|
isArray,
|
|
65
62
|
isEncrypted,
|
|
66
63
|
hasDefault,
|
|
67
|
-
defaultValue: hasDefault ?
|
|
64
|
+
defaultValue: hasDefault ? attr.xdefault : undefined,
|
|
68
65
|
suggestedType: suggested,
|
|
69
66
|
targetType: suggested,
|
|
70
67
|
targetSize: suggested === "varchar" ? size : undefined,
|
|
@@ -72,10 +69,8 @@ export function analyzeStringAttributes(config, options = {}) {
|
|
|
72
69
|
skipReason: isEncrypted ? "encrypted" : undefined,
|
|
73
70
|
indexesAffected: affectedIndexes,
|
|
74
71
|
};
|
|
75
|
-
//
|
|
76
|
-
if (hasIndex &&
|
|
77
|
-
suggested !== "varchar" &&
|
|
78
|
-
!isEncrypted) {
|
|
72
|
+
// Indexed attrs must stay varchar
|
|
73
|
+
if (hasIndex && suggested !== "varchar" && !isEncrypted) {
|
|
79
74
|
entry.targetType = "varchar";
|
|
80
75
|
entry.targetSize = size;
|
|
81
76
|
}
|
|
@@ -116,6 +111,22 @@ export function analyzeStringAttributes(config, options = {}) {
|
|
|
116
111
|
printPlanSummary(plan);
|
|
117
112
|
return plan;
|
|
118
113
|
}
|
|
114
|
+
function emptyPlan(config) {
|
|
115
|
+
return {
|
|
116
|
+
version: 1,
|
|
117
|
+
generatedAt: new Date().toISOString(),
|
|
118
|
+
appwriteEndpoint: config.appwriteEndpoint,
|
|
119
|
+
appwriteProject: config.appwriteProject,
|
|
120
|
+
summary: {
|
|
121
|
+
totalStringAttributes: 0,
|
|
122
|
+
toMigrate: 0,
|
|
123
|
+
toSkip: 0,
|
|
124
|
+
databaseCount: 0,
|
|
125
|
+
collectionCount: 0,
|
|
126
|
+
},
|
|
127
|
+
entries: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
119
130
|
function printPlanSummary(plan) {
|
|
120
131
|
const { summary } = plan;
|
|
121
132
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@njdamstra/appwrite-utils-cli",
|
|
3
3
|
"description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
|
|
4
|
-
"version": "1.11.
|
|
4
|
+
"version": "1.11.1",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -17,7 +17,7 @@ export const migrateCommands = {
|
|
|
17
17
|
message: "String attribute migration:",
|
|
18
18
|
choices: [
|
|
19
19
|
{
|
|
20
|
-
name: "Analyze — scan
|
|
20
|
+
name: "Analyze — scan Appwrite server, generate migration plan (YAML)",
|
|
21
21
|
value: "analyze",
|
|
22
22
|
},
|
|
23
23
|
{
|
|
@@ -39,29 +39,15 @@ export const migrateCommands = {
|
|
|
39
39
|
|
|
40
40
|
async analyzePhase(cli: InteractiveCLI): Promise<void> {
|
|
41
41
|
const controller = (cli as any).controller;
|
|
42
|
-
if (!controller?.
|
|
42
|
+
if (!controller?.adapter) {
|
|
43
43
|
MessageFormatter.error(
|
|
44
|
-
"No
|
|
44
|
+
"No database adapter available. Ensure a server connection is established.",
|
|
45
45
|
undefined,
|
|
46
46
|
{ prefix: "Analyze" }
|
|
47
47
|
);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const config = controller.config;
|
|
52
|
-
const collections = [
|
|
53
|
-
...(config.collections || []),
|
|
54
|
-
...(config.tables || []),
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
if (collections.length === 0) {
|
|
58
|
-
MessageFormatter.warning(
|
|
59
|
-
"No collections/tables found in local config. Nothing to analyze.",
|
|
60
|
-
{ prefix: "Analyze" }
|
|
61
|
-
);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
51
|
// Prompt for output path
|
|
66
52
|
const { outputPath } = await inquirer.prompt([
|
|
67
53
|
{
|
|
@@ -75,7 +61,7 @@ export const migrateCommands = {
|
|
|
75
61
|
const options: AnalyzeOptions = { outputPath };
|
|
76
62
|
|
|
77
63
|
try {
|
|
78
|
-
analyzeStringAttributes(config, options);
|
|
64
|
+
await analyzeStringAttributes(controller.adapter, controller.config, options);
|
|
79
65
|
MessageFormatter.success(
|
|
80
66
|
"Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.",
|
|
81
67
|
{ prefix: "Analyze" }
|
package/src/main.ts
CHANGED
|
@@ -881,13 +881,15 @@ async function main() {
|
|
|
881
881
|
);
|
|
882
882
|
|
|
883
883
|
if (argv.migrateStringsAnalyze) {
|
|
884
|
-
if (!controller.
|
|
885
|
-
MessageFormatter.error(
|
|
886
|
-
|
|
887
|
-
|
|
884
|
+
if (!controller.adapter) {
|
|
885
|
+
MessageFormatter.error(
|
|
886
|
+
"No database adapter available. Ensure config has valid credentials.",
|
|
887
|
+
undefined,
|
|
888
|
+
{ prefix: "Migration" }
|
|
889
|
+
);
|
|
888
890
|
return;
|
|
889
891
|
}
|
|
890
|
-
analyzeStringAttributes(controller.config, {
|
|
892
|
+
await analyzeStringAttributes(controller.adapter, controller.config, {
|
|
891
893
|
outputPath: argv.migrateStringsOutput,
|
|
892
894
|
});
|
|
893
895
|
} else if (argv.migrateStringsExecute) {
|
|
@@ -9,11 +9,7 @@ import {
|
|
|
9
9
|
MessageFormatter,
|
|
10
10
|
tryAwaitWithRetry,
|
|
11
11
|
} from "@njdamstra/appwrite-utils-helpers";
|
|
12
|
-
import type {
|
|
13
|
-
AppwriteConfig,
|
|
14
|
-
Attribute,
|
|
15
|
-
Index,
|
|
16
|
-
} from "@njdamstra/appwrite-utils";
|
|
12
|
+
import type { AppwriteConfig } from "@njdamstra/appwrite-utils";
|
|
17
13
|
import { ProgressManager } from "../shared/progressManager.js";
|
|
18
14
|
import {
|
|
19
15
|
type MigrationPlan,
|
|
@@ -30,65 +26,76 @@ import {
|
|
|
30
26
|
} from "./migrateStringsTypes.js";
|
|
31
27
|
|
|
32
28
|
// ────────────────────────────────────────────────────────
|
|
33
|
-
// Phase 1: Analyze —
|
|
29
|
+
// Phase 1: Analyze — queries Appwrite server for real state
|
|
34
30
|
// ────────────────────────────────────────────────────────
|
|
35
31
|
|
|
36
|
-
export function analyzeStringAttributes(
|
|
32
|
+
export async function analyzeStringAttributes(
|
|
33
|
+
adapter: DatabaseAdapter,
|
|
37
34
|
config: AppwriteConfig,
|
|
38
35
|
options: AnalyzeOptions = {}
|
|
39
|
-
): MigrationPlan {
|
|
40
|
-
const collections = [
|
|
41
|
-
...(config.collections || []),
|
|
42
|
-
...(config.tables || []),
|
|
43
|
-
];
|
|
36
|
+
): Promise<MigrationPlan> {
|
|
44
37
|
const databases = config.databases || [];
|
|
38
|
+
if (databases.length === 0) {
|
|
39
|
+
MessageFormatter.warning("No databases configured. Nothing to analyze.", {
|
|
40
|
+
prefix: "Analyze",
|
|
41
|
+
});
|
|
42
|
+
return emptyPlan(config);
|
|
43
|
+
}
|
|
45
44
|
|
|
46
45
|
const entries: MigrationPlanEntry[] = [];
|
|
47
|
-
const collectionsSeen = new Set<string>();
|
|
48
46
|
|
|
49
47
|
for (const db of databases) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const coll = c as any;
|
|
53
|
-
if (coll.databaseId) return coll.databaseId === db.$id;
|
|
54
|
-
if (coll.databaseIds?.length) return coll.databaseIds.includes(db.$id);
|
|
55
|
-
return true; // unassigned → applied to all databases
|
|
48
|
+
MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
|
|
49
|
+
prefix: "Analyze",
|
|
56
50
|
});
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
// Fetch all collections/tables from server
|
|
53
|
+
const tablesRes = await tryAwaitWithRetry(() =>
|
|
54
|
+
adapter.listTables({ databaseId: db.$id })
|
|
55
|
+
);
|
|
56
|
+
const tables: any[] =
|
|
57
|
+
tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
|
|
58
|
+
|
|
59
|
+
for (const table of tables) {
|
|
60
|
+
const tableId: string = table.$id || table.key || table.name;
|
|
61
|
+
const tableName: string = table.name || tableId;
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
const
|
|
63
|
+
// Fetch full schema from server
|
|
64
|
+
const schemaRes = await tryAwaitWithRetry(() =>
|
|
65
|
+
adapter.getTable({ databaseId: db.$id, tableId })
|
|
66
|
+
);
|
|
67
|
+
const attributes: any[] =
|
|
68
|
+
schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
|
|
69
|
+
|
|
70
|
+
// Fetch indexes from server
|
|
71
|
+
const indexRes = await tryAwaitWithRetry(() =>
|
|
72
|
+
adapter.listIndexes({ databaseId: db.$id, tableId })
|
|
73
|
+
);
|
|
74
|
+
const indexes: any[] = indexRes?.data || [];
|
|
66
75
|
|
|
67
76
|
for (const attr of attributes) {
|
|
68
77
|
if (attr.type !== "string") continue;
|
|
69
78
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const isEncrypted = !!stringAttr.encrypt;
|
|
79
|
+
const size: number = attr.size || 50;
|
|
80
|
+
const isEncrypted = !!(attr as any).encrypt;
|
|
73
81
|
const isRequired = !!attr.required;
|
|
74
82
|
const isArray = !!attr.array;
|
|
75
83
|
const hasDefault =
|
|
76
|
-
|
|
84
|
+
attr.xdefault !== undefined && attr.xdefault !== null;
|
|
77
85
|
|
|
78
86
|
// Find indexes that reference this attribute
|
|
79
87
|
const affectedIndexes = indexes
|
|
80
|
-
.filter((idx) => idx.attributes
|
|
81
|
-
.map((idx) => idx.key);
|
|
88
|
+
.filter((idx: any) => idx.attributes?.includes(attr.key))
|
|
89
|
+
.map((idx: any) => idx.key);
|
|
82
90
|
const hasIndex = affectedIndexes.length > 0;
|
|
83
91
|
|
|
84
92
|
const suggested = suggestTargetType(size, hasIndex);
|
|
85
93
|
|
|
86
|
-
// Build entry
|
|
87
94
|
const entry: MigrationPlanEntry = {
|
|
88
95
|
databaseId: db.$id,
|
|
89
96
|
databaseName: db.name,
|
|
90
|
-
collectionId:
|
|
91
|
-
collectionName:
|
|
97
|
+
collectionId: tableId,
|
|
98
|
+
collectionName: tableName,
|
|
92
99
|
attributeKey: attr.key,
|
|
93
100
|
currentType: "string",
|
|
94
101
|
currentSize: size,
|
|
@@ -96,7 +103,7 @@ export function analyzeStringAttributes(
|
|
|
96
103
|
isArray,
|
|
97
104
|
isEncrypted,
|
|
98
105
|
hasDefault,
|
|
99
|
-
defaultValue: hasDefault ?
|
|
106
|
+
defaultValue: hasDefault ? attr.xdefault : undefined,
|
|
100
107
|
suggestedType: suggested,
|
|
101
108
|
targetType: suggested,
|
|
102
109
|
targetSize: suggested === "varchar" ? size : undefined,
|
|
@@ -105,12 +112,8 @@ export function analyzeStringAttributes(
|
|
|
105
112
|
indexesAffected: affectedIndexes,
|
|
106
113
|
};
|
|
107
114
|
|
|
108
|
-
//
|
|
109
|
-
if (
|
|
110
|
-
hasIndex &&
|
|
111
|
-
suggested !== "varchar" &&
|
|
112
|
-
!isEncrypted
|
|
113
|
-
) {
|
|
115
|
+
// Indexed attrs must stay varchar
|
|
116
|
+
if (hasIndex && suggested !== "varchar" && !isEncrypted) {
|
|
114
117
|
entry.targetType = "varchar";
|
|
115
118
|
entry.targetSize = size;
|
|
116
119
|
}
|
|
@@ -161,6 +164,23 @@ export function analyzeStringAttributes(
|
|
|
161
164
|
return plan;
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
function emptyPlan(config: AppwriteConfig): MigrationPlan {
|
|
168
|
+
return {
|
|
169
|
+
version: 1,
|
|
170
|
+
generatedAt: new Date().toISOString(),
|
|
171
|
+
appwriteEndpoint: config.appwriteEndpoint,
|
|
172
|
+
appwriteProject: config.appwriteProject,
|
|
173
|
+
summary: {
|
|
174
|
+
totalStringAttributes: 0,
|
|
175
|
+
toMigrate: 0,
|
|
176
|
+
toSkip: 0,
|
|
177
|
+
databaseCount: 0,
|
|
178
|
+
collectionCount: 0,
|
|
179
|
+
},
|
|
180
|
+
entries: [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
164
184
|
function printPlanSummary(plan: MigrationPlan): void {
|
|
165
185
|
const { summary } = plan;
|
|
166
186
|
console.log("");
|