@njdamstra/appwrite-utils-cli 1.11.0 → 1.11.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/dist/cli/commands/migrateCommands.js +28 -13
- package/dist/main.js +12 -5
- package/dist/migrations/migrateStrings.d.ts +1 -1
- package/dist/migrations/migrateStrings.js +52 -37
- package/dist/migrations/migrateStringsTypes.d.ts +1 -0
- package/dist/migrations/migrateStringsTypes.js +5 -4
- package/package.json +1 -1
- package/src/cli/commands/migrateCommands.ts +28 -17
- package/src/main.ts +17 -5
- package/src/migrations/migrateStrings.ts +67 -43
- package/src/migrations/migrateStringsTypes.ts +6 -4
|
@@ -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,18 +33,33 @@ export const migrateCommands = {
|
|
|
33
33
|
},
|
|
34
34
|
async analyzePhase(cli) {
|
|
35
35
|
const controller = cli.controller;
|
|
36
|
-
if (!controller?.
|
|
37
|
-
MessageFormatter.error("No
|
|
36
|
+
if (!controller?.adapter) {
|
|
37
|
+
MessageFormatter.error("No database adapter available. Ensure a server connection is established.", undefined, { prefix: "Analyze" });
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
// Prompt for database selection
|
|
41
|
+
const allDatabases = controller.config.databases || [];
|
|
42
|
+
let databaseIds;
|
|
43
|
+
if (allDatabases.length > 1) {
|
|
44
|
+
const { selectedDbs } = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: "checkbox",
|
|
47
|
+
name: "selectedDbs",
|
|
48
|
+
message: "Select databases to include in the analysis:",
|
|
49
|
+
choices: allDatabases.map((db) => ({
|
|
50
|
+
name: `${db.name} (${db.$id})`,
|
|
51
|
+
value: db.$id,
|
|
52
|
+
checked: true,
|
|
53
|
+
})),
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
if (selectedDbs.length === 0) {
|
|
57
|
+
MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (selectedDbs.length < allDatabases.length) {
|
|
61
|
+
databaseIds = selectedDbs;
|
|
62
|
+
}
|
|
48
63
|
}
|
|
49
64
|
// Prompt for output path
|
|
50
65
|
const { outputPath } = await inquirer.prompt([
|
|
@@ -55,9 +70,9 @@ export const migrateCommands = {
|
|
|
55
70
|
default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
|
|
56
71
|
},
|
|
57
72
|
]);
|
|
58
|
-
const options = { outputPath };
|
|
73
|
+
const options = { outputPath, databaseIds };
|
|
59
74
|
try {
|
|
60
|
-
analyzeStringAttributes(config, options);
|
|
75
|
+
await analyzeStringAttributes(controller.adapter, controller.config, options);
|
|
61
76
|
MessageFormatter.success("Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.", { prefix: "Analyze" });
|
|
62
77
|
}
|
|
63
78
|
catch (err) {
|
package/dist/main.js
CHANGED
|
@@ -469,6 +469,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
469
469
|
alias: ["migrate-strings-output"],
|
|
470
470
|
type: "string",
|
|
471
471
|
description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
|
|
472
|
+
})
|
|
473
|
+
.option("migrateStringsDbIds", {
|
|
474
|
+
alias: ["migrate-strings-db-ids"],
|
|
475
|
+
type: "string",
|
|
476
|
+
description: "Comma-separated database IDs to include in analysis (default: all)",
|
|
472
477
|
})
|
|
473
478
|
.option("migrateStringsKeepBackups", {
|
|
474
479
|
alias: ["migrate-strings-keep-backups"],
|
|
@@ -648,14 +653,16 @@ async function main() {
|
|
|
648
653
|
if (argv.migrateStringsAnalyze || argv.migrateStringsExecute) {
|
|
649
654
|
const { analyzeStringAttributes, executeMigrationPlan } = await import("./migrations/migrateStrings.js");
|
|
650
655
|
if (argv.migrateStringsAnalyze) {
|
|
651
|
-
if (!controller.
|
|
652
|
-
MessageFormatter.error("No
|
|
653
|
-
prefix: "Migration",
|
|
654
|
-
});
|
|
656
|
+
if (!controller.adapter) {
|
|
657
|
+
MessageFormatter.error("No database adapter available. Ensure config has valid credentials.", undefined, { prefix: "Migration" });
|
|
655
658
|
return;
|
|
656
659
|
}
|
|
657
|
-
|
|
660
|
+
const databaseIds = argv.migrateStringsDbIds
|
|
661
|
+
? argv.migrateStringsDbIds.split(",").map((s) => s.trim()).filter(Boolean)
|
|
662
|
+
: undefined;
|
|
663
|
+
await analyzeStringAttributes(controller.adapter, controller.config, {
|
|
658
664
|
outputPath: argv.migrateStringsOutput,
|
|
665
|
+
databaseIds,
|
|
659
666
|
});
|
|
660
667
|
}
|
|
661
668
|
else if (argv.migrateStringsExecute) {
|
|
@@ -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,56 @@ 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const databases =
|
|
13
|
+
export async function analyzeStringAttributes(adapter, config, options = {}) {
|
|
14
|
+
let databasesToScan = config.databases || [];
|
|
15
|
+
if (options.databaseIds?.length) {
|
|
16
|
+
databasesToScan = databasesToScan.filter(db => options.databaseIds.includes(db.$id));
|
|
17
|
+
}
|
|
18
|
+
const databases = databasesToScan;
|
|
19
|
+
if (databases.length === 0) {
|
|
20
|
+
MessageFormatter.warning("No databases configured. Nothing to analyze.", {
|
|
21
|
+
prefix: "Analyze",
|
|
22
|
+
});
|
|
23
|
+
return emptyPlan(config);
|
|
24
|
+
}
|
|
19
25
|
const entries = [];
|
|
20
|
-
const collectionsSeen = new Set();
|
|
21
26
|
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
|
|
27
|
+
MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
|
|
28
|
+
prefix: "Analyze",
|
|
30
29
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
30
|
+
// Fetch all collections/tables from server
|
|
31
|
+
const tablesRes = await tryAwaitWithRetry(() => adapter.listTables({ databaseId: db.$id }));
|
|
32
|
+
const tables = tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
|
|
33
|
+
for (const table of tables) {
|
|
34
|
+
const tableId = table.$id || table.key || table.name;
|
|
35
|
+
const tableName = table.name || tableId;
|
|
36
|
+
// Fetch full schema from server
|
|
37
|
+
const schemaRes = await tryAwaitWithRetry(() => adapter.getTable({ databaseId: db.$id, tableId }));
|
|
38
|
+
const attributes = schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
|
|
39
|
+
// Fetch indexes from server
|
|
40
|
+
const indexRes = await tryAwaitWithRetry(() => adapter.listIndexes({ databaseId: db.$id, tableId }));
|
|
41
|
+
const indexes = indexRes?.data || [];
|
|
39
42
|
for (const attr of attributes) {
|
|
40
43
|
if (attr.type !== "string")
|
|
41
44
|
continue;
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const isEncrypted = !!stringAttr.encrypt;
|
|
45
|
+
const size = attr.size || 50;
|
|
46
|
+
const isEncrypted = !!attr.encrypt;
|
|
45
47
|
const isRequired = !!attr.required;
|
|
46
48
|
const isArray = !!attr.array;
|
|
47
|
-
const hasDefault =
|
|
49
|
+
const hasDefault = attr.xdefault !== undefined && attr.xdefault !== null;
|
|
48
50
|
// Find indexes that reference this attribute
|
|
49
51
|
const affectedIndexes = indexes
|
|
50
|
-
.filter((idx) => idx.attributes
|
|
52
|
+
.filter((idx) => idx.attributes?.includes(attr.key))
|
|
51
53
|
.map((idx) => idx.key);
|
|
52
54
|
const hasIndex = affectedIndexes.length > 0;
|
|
53
55
|
const suggested = suggestTargetType(size, hasIndex);
|
|
54
|
-
// Build entry
|
|
55
56
|
const entry = {
|
|
56
57
|
databaseId: db.$id,
|
|
57
58
|
databaseName: db.name,
|
|
58
|
-
collectionId:
|
|
59
|
-
collectionName:
|
|
59
|
+
collectionId: tableId,
|
|
60
|
+
collectionName: tableName,
|
|
60
61
|
attributeKey: attr.key,
|
|
61
62
|
currentType: "string",
|
|
62
63
|
currentSize: size,
|
|
@@ -64,7 +65,7 @@ export function analyzeStringAttributes(config, options = {}) {
|
|
|
64
65
|
isArray,
|
|
65
66
|
isEncrypted,
|
|
66
67
|
hasDefault,
|
|
67
|
-
defaultValue: hasDefault ?
|
|
68
|
+
defaultValue: hasDefault ? attr.xdefault : undefined,
|
|
68
69
|
suggestedType: suggested,
|
|
69
70
|
targetType: suggested,
|
|
70
71
|
targetSize: suggested === "varchar" ? size : undefined,
|
|
@@ -72,10 +73,8 @@ export function analyzeStringAttributes(config, options = {}) {
|
|
|
72
73
|
skipReason: isEncrypted ? "encrypted" : undefined,
|
|
73
74
|
indexesAffected: affectedIndexes,
|
|
74
75
|
};
|
|
75
|
-
//
|
|
76
|
-
if (hasIndex &&
|
|
77
|
-
suggested !== "varchar" &&
|
|
78
|
-
!isEncrypted) {
|
|
76
|
+
// Indexed attrs must stay varchar
|
|
77
|
+
if (hasIndex && suggested !== "varchar" && !isEncrypted) {
|
|
79
78
|
entry.targetType = "varchar";
|
|
80
79
|
entry.targetSize = size;
|
|
81
80
|
}
|
|
@@ -116,6 +115,22 @@ export function analyzeStringAttributes(config, options = {}) {
|
|
|
116
115
|
printPlanSummary(plan);
|
|
117
116
|
return plan;
|
|
118
117
|
}
|
|
118
|
+
function emptyPlan(config) {
|
|
119
|
+
return {
|
|
120
|
+
version: 1,
|
|
121
|
+
generatedAt: new Date().toISOString(),
|
|
122
|
+
appwriteEndpoint: config.appwriteEndpoint,
|
|
123
|
+
appwriteProject: config.appwriteProject,
|
|
124
|
+
summary: {
|
|
125
|
+
totalStringAttributes: 0,
|
|
126
|
+
toMigrate: 0,
|
|
127
|
+
toSkip: 0,
|
|
128
|
+
databaseCount: 0,
|
|
129
|
+
collectionCount: 0,
|
|
130
|
+
},
|
|
131
|
+
entries: [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
119
134
|
function printPlanSummary(plan) {
|
|
120
135
|
const { summary } = plan;
|
|
121
136
|
console.log("");
|
|
@@ -182,6 +182,7 @@ export type MigrationCheckpoint = z.infer<typeof MigrationCheckpointSchema>;
|
|
|
182
182
|
export interface AnalyzeOptions {
|
|
183
183
|
outputPath?: string;
|
|
184
184
|
verbose?: boolean;
|
|
185
|
+
databaseIds?: string[];
|
|
185
186
|
}
|
|
186
187
|
export interface ExecuteOptions {
|
|
187
188
|
planPath: string;
|
|
@@ -97,16 +97,17 @@ export function suggestTargetType(size, hasIndex) {
|
|
|
97
97
|
}
|
|
98
98
|
// ── Helper: generate backup key with length limits ──
|
|
99
99
|
const MAX_KEY_LENGTH = 36;
|
|
100
|
-
const BACKUP_PREFIX = "
|
|
100
|
+
const BACKUP_PREFIX = "mig_";
|
|
101
101
|
export function generateBackupKey(originalKey) {
|
|
102
102
|
const candidate = `${BACKUP_PREFIX}${originalKey}`;
|
|
103
103
|
if (candidate.length <= MAX_KEY_LENGTH) {
|
|
104
104
|
return candidate;
|
|
105
105
|
}
|
|
106
|
-
// Truncate + 4-char hash for uniqueness
|
|
106
|
+
// Truncate + 4-char hash for uniqueness: m_ + orig + _ + hash(4)
|
|
107
107
|
const hash = simpleHash(originalKey);
|
|
108
|
-
const
|
|
109
|
-
|
|
108
|
+
const TRUNC_PREFIX = "m_";
|
|
109
|
+
const maxOrigLen = MAX_KEY_LENGTH - TRUNC_PREFIX.length - 1 - 4;
|
|
110
|
+
return `${TRUNC_PREFIX}${originalKey.slice(0, maxOrigLen)}_${hash}`;
|
|
110
111
|
}
|
|
111
112
|
function simpleHash(str) {
|
|
112
113
|
let h = 0;
|
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.2",
|
|
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,27 +39,38 @@ 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
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
// Prompt for database selection
|
|
52
|
+
const allDatabases = controller.config.databases || [];
|
|
53
|
+
let databaseIds: string[] | undefined;
|
|
54
|
+
if (allDatabases.length > 1) {
|
|
55
|
+
const { selectedDbs } = await inquirer.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: "checkbox",
|
|
58
|
+
name: "selectedDbs",
|
|
59
|
+
message: "Select databases to include in the analysis:",
|
|
60
|
+
choices: allDatabases.map((db: any) => ({
|
|
61
|
+
name: `${db.name} (${db.$id})`,
|
|
62
|
+
value: db.$id,
|
|
63
|
+
checked: true,
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
if (selectedDbs.length === 0) {
|
|
68
|
+
MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (selectedDbs.length < allDatabases.length) {
|
|
72
|
+
databaseIds = selectedDbs;
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
// Prompt for output path
|
|
@@ -72,10 +83,10 @@ export const migrateCommands = {
|
|
|
72
83
|
},
|
|
73
84
|
]);
|
|
74
85
|
|
|
75
|
-
const options: AnalyzeOptions = { outputPath };
|
|
86
|
+
const options: AnalyzeOptions = { outputPath, databaseIds };
|
|
76
87
|
|
|
77
88
|
try {
|
|
78
|
-
analyzeStringAttributes(config, options);
|
|
89
|
+
await analyzeStringAttributes(controller.adapter, controller.config, options);
|
|
79
90
|
MessageFormatter.success(
|
|
80
91
|
"Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.",
|
|
81
92
|
{ prefix: "Analyze" }
|
package/src/main.ts
CHANGED
|
@@ -86,6 +86,7 @@ interface CliOptions {
|
|
|
86
86
|
migrateStringsAnalyze?: boolean;
|
|
87
87
|
migrateStringsExecute?: string;
|
|
88
88
|
migrateStringsOutput?: string;
|
|
89
|
+
migrateStringsDbIds?: string;
|
|
89
90
|
migrateStringsKeepBackups?: boolean;
|
|
90
91
|
migrateStringsDryRun?: boolean;
|
|
91
92
|
}
|
|
@@ -642,6 +643,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
642
643
|
type: "string",
|
|
643
644
|
description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
|
|
644
645
|
})
|
|
646
|
+
.option("migrateStringsDbIds", {
|
|
647
|
+
alias: ["migrate-strings-db-ids"],
|
|
648
|
+
type: "string",
|
|
649
|
+
description: "Comma-separated database IDs to include in analysis (default: all)",
|
|
650
|
+
})
|
|
645
651
|
.option("migrateStringsKeepBackups", {
|
|
646
652
|
alias: ["migrate-strings-keep-backups"],
|
|
647
653
|
type: "boolean",
|
|
@@ -881,14 +887,20 @@ async function main() {
|
|
|
881
887
|
);
|
|
882
888
|
|
|
883
889
|
if (argv.migrateStringsAnalyze) {
|
|
884
|
-
if (!controller.
|
|
885
|
-
MessageFormatter.error(
|
|
886
|
-
|
|
887
|
-
|
|
890
|
+
if (!controller.adapter) {
|
|
891
|
+
MessageFormatter.error(
|
|
892
|
+
"No database adapter available. Ensure config has valid credentials.",
|
|
893
|
+
undefined,
|
|
894
|
+
{ prefix: "Migration" }
|
|
895
|
+
);
|
|
888
896
|
return;
|
|
889
897
|
}
|
|
890
|
-
|
|
898
|
+
const databaseIds = argv.migrateStringsDbIds
|
|
899
|
+
? argv.migrateStringsDbIds.split(",").map((s: string) => s.trim()).filter(Boolean)
|
|
900
|
+
: undefined;
|
|
901
|
+
await analyzeStringAttributes(controller.adapter, controller.config, {
|
|
891
902
|
outputPath: argv.migrateStringsOutput,
|
|
903
|
+
databaseIds,
|
|
892
904
|
});
|
|
893
905
|
} else if (argv.migrateStringsExecute) {
|
|
894
906
|
if (!controller.adapter) {
|
|
@@ -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,80 @@ 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const databases =
|
|
36
|
+
): Promise<MigrationPlan> {
|
|
37
|
+
let databasesToScan = config.databases || [];
|
|
38
|
+
if (options.databaseIds?.length) {
|
|
39
|
+
databasesToScan = databasesToScan.filter(db => options.databaseIds!.includes(db.$id));
|
|
40
|
+
}
|
|
41
|
+
const databases = databasesToScan;
|
|
42
|
+
if (databases.length === 0) {
|
|
43
|
+
MessageFormatter.warning("No databases configured. Nothing to analyze.", {
|
|
44
|
+
prefix: "Analyze",
|
|
45
|
+
});
|
|
46
|
+
return emptyPlan(config);
|
|
47
|
+
}
|
|
45
48
|
|
|
46
49
|
const entries: MigrationPlanEntry[] = [];
|
|
47
|
-
const collectionsSeen = new Set<string>();
|
|
48
50
|
|
|
49
51
|
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
|
|
52
|
+
MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
|
|
53
|
+
prefix: "Analyze",
|
|
56
54
|
});
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
// Fetch all collections/tables from server
|
|
57
|
+
const tablesRes = await tryAwaitWithRetry(() =>
|
|
58
|
+
adapter.listTables({ databaseId: db.$id })
|
|
59
|
+
);
|
|
60
|
+
const tables: any[] =
|
|
61
|
+
tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
|
|
62
|
+
|
|
63
|
+
for (const table of tables) {
|
|
64
|
+
const tableId: string = table.$id || table.key || table.name;
|
|
65
|
+
const tableName: string = table.name || tableId;
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
const
|
|
67
|
+
// Fetch full schema from server
|
|
68
|
+
const schemaRes = await tryAwaitWithRetry(() =>
|
|
69
|
+
adapter.getTable({ databaseId: db.$id, tableId })
|
|
70
|
+
);
|
|
71
|
+
const attributes: any[] =
|
|
72
|
+
schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
|
|
73
|
+
|
|
74
|
+
// Fetch indexes from server
|
|
75
|
+
const indexRes = await tryAwaitWithRetry(() =>
|
|
76
|
+
adapter.listIndexes({ databaseId: db.$id, tableId })
|
|
77
|
+
);
|
|
78
|
+
const indexes: any[] = indexRes?.data || [];
|
|
66
79
|
|
|
67
80
|
for (const attr of attributes) {
|
|
68
81
|
if (attr.type !== "string") continue;
|
|
69
82
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const isEncrypted = !!stringAttr.encrypt;
|
|
83
|
+
const size: number = attr.size || 50;
|
|
84
|
+
const isEncrypted = !!(attr as any).encrypt;
|
|
73
85
|
const isRequired = !!attr.required;
|
|
74
86
|
const isArray = !!attr.array;
|
|
75
87
|
const hasDefault =
|
|
76
|
-
|
|
88
|
+
attr.xdefault !== undefined && attr.xdefault !== null;
|
|
77
89
|
|
|
78
90
|
// Find indexes that reference this attribute
|
|
79
91
|
const affectedIndexes = indexes
|
|
80
|
-
.filter((idx) => idx.attributes
|
|
81
|
-
.map((idx) => idx.key);
|
|
92
|
+
.filter((idx: any) => idx.attributes?.includes(attr.key))
|
|
93
|
+
.map((idx: any) => idx.key);
|
|
82
94
|
const hasIndex = affectedIndexes.length > 0;
|
|
83
95
|
|
|
84
96
|
const suggested = suggestTargetType(size, hasIndex);
|
|
85
97
|
|
|
86
|
-
// Build entry
|
|
87
98
|
const entry: MigrationPlanEntry = {
|
|
88
99
|
databaseId: db.$id,
|
|
89
100
|
databaseName: db.name,
|
|
90
|
-
collectionId:
|
|
91
|
-
collectionName:
|
|
101
|
+
collectionId: tableId,
|
|
102
|
+
collectionName: tableName,
|
|
92
103
|
attributeKey: attr.key,
|
|
93
104
|
currentType: "string",
|
|
94
105
|
currentSize: size,
|
|
@@ -96,7 +107,7 @@ export function analyzeStringAttributes(
|
|
|
96
107
|
isArray,
|
|
97
108
|
isEncrypted,
|
|
98
109
|
hasDefault,
|
|
99
|
-
defaultValue: hasDefault ?
|
|
110
|
+
defaultValue: hasDefault ? attr.xdefault : undefined,
|
|
100
111
|
suggestedType: suggested,
|
|
101
112
|
targetType: suggested,
|
|
102
113
|
targetSize: suggested === "varchar" ? size : undefined,
|
|
@@ -105,12 +116,8 @@ export function analyzeStringAttributes(
|
|
|
105
116
|
indexesAffected: affectedIndexes,
|
|
106
117
|
};
|
|
107
118
|
|
|
108
|
-
//
|
|
109
|
-
if (
|
|
110
|
-
hasIndex &&
|
|
111
|
-
suggested !== "varchar" &&
|
|
112
|
-
!isEncrypted
|
|
113
|
-
) {
|
|
119
|
+
// Indexed attrs must stay varchar
|
|
120
|
+
if (hasIndex && suggested !== "varchar" && !isEncrypted) {
|
|
114
121
|
entry.targetType = "varchar";
|
|
115
122
|
entry.targetSize = size;
|
|
116
123
|
}
|
|
@@ -161,6 +168,23 @@ export function analyzeStringAttributes(
|
|
|
161
168
|
return plan;
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
function emptyPlan(config: AppwriteConfig): MigrationPlan {
|
|
172
|
+
return {
|
|
173
|
+
version: 1,
|
|
174
|
+
generatedAt: new Date().toISOString(),
|
|
175
|
+
appwriteEndpoint: config.appwriteEndpoint,
|
|
176
|
+
appwriteProject: config.appwriteProject,
|
|
177
|
+
summary: {
|
|
178
|
+
totalStringAttributes: 0,
|
|
179
|
+
toMigrate: 0,
|
|
180
|
+
toSkip: 0,
|
|
181
|
+
databaseCount: 0,
|
|
182
|
+
collectionCount: 0,
|
|
183
|
+
},
|
|
184
|
+
entries: [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
function printPlanSummary(plan: MigrationPlan): void {
|
|
165
189
|
const { summary } = plan;
|
|
166
190
|
console.log("");
|
|
@@ -108,6 +108,7 @@ export type MigrationCheckpoint = z.infer<typeof MigrationCheckpointSchema>;
|
|
|
108
108
|
export interface AnalyzeOptions {
|
|
109
109
|
outputPath?: string;
|
|
110
110
|
verbose?: boolean;
|
|
111
|
+
databaseIds?: string[];
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
export interface ExecuteOptions {
|
|
@@ -136,17 +137,18 @@ export function suggestTargetType(
|
|
|
136
137
|
// ── Helper: generate backup key with length limits ──
|
|
137
138
|
|
|
138
139
|
const MAX_KEY_LENGTH = 36;
|
|
139
|
-
const BACKUP_PREFIX = "
|
|
140
|
+
const BACKUP_PREFIX = "mig_";
|
|
140
141
|
|
|
141
142
|
export function generateBackupKey(originalKey: string): string {
|
|
142
143
|
const candidate = `${BACKUP_PREFIX}${originalKey}`;
|
|
143
144
|
if (candidate.length <= MAX_KEY_LENGTH) {
|
|
144
145
|
return candidate;
|
|
145
146
|
}
|
|
146
|
-
// Truncate + 4-char hash for uniqueness
|
|
147
|
+
// Truncate + 4-char hash for uniqueness: m_ + orig + _ + hash(4)
|
|
147
148
|
const hash = simpleHash(originalKey);
|
|
148
|
-
const
|
|
149
|
-
|
|
149
|
+
const TRUNC_PREFIX = "m_";
|
|
150
|
+
const maxOrigLen = MAX_KEY_LENGTH - TRUNC_PREFIX.length - 1 - 4;
|
|
151
|
+
return `${TRUNC_PREFIX}${originalKey.slice(0, maxOrigLen)}_${hash}`;
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
function simpleHash(str: string): string {
|