@njdamstra/appwrite-utils-cli 1.11.1 → 1.11.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/dist/cli/commands/migrateCommands.js +48 -1
- package/dist/main.js +15 -0
- package/dist/migrations/migrateStrings.js +11 -1
- package/dist/migrations/migrateStringsTypes.d.ts +2 -0
- package/dist/migrations/migrateStringsTypes.js +5 -4
- package/package.json +1 -1
- package/src/cli/commands/migrateCommands.ts +48 -1
- package/src/main.ts +17 -0
- package/src/migrations/migrateStrings.ts +11 -1
- package/src/migrations/migrateStringsTypes.ts +7 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import { MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
|
|
4
5
|
import { analyzeStringAttributes, executeMigrationPlan, } from "../../migrations/migrateStrings.js";
|
|
5
6
|
export const migrateCommands = {
|
|
@@ -37,6 +38,30 @@ export const migrateCommands = {
|
|
|
37
38
|
MessageFormatter.error("No database adapter available. Ensure a server connection is established.", undefined, { prefix: "Analyze" });
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
// Prompt for database selection
|
|
42
|
+
const allDatabases = controller.config.databases || [];
|
|
43
|
+
let databaseIds;
|
|
44
|
+
if (allDatabases.length > 1) {
|
|
45
|
+
const { selectedDbs } = await inquirer.prompt([
|
|
46
|
+
{
|
|
47
|
+
type: "checkbox",
|
|
48
|
+
name: "selectedDbs",
|
|
49
|
+
message: "Select databases to include in the analysis:",
|
|
50
|
+
choices: allDatabases.map((db) => ({
|
|
51
|
+
name: `${db.name} (${db.$id})`,
|
|
52
|
+
value: db.$id,
|
|
53
|
+
checked: true,
|
|
54
|
+
})),
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
if (selectedDbs.length === 0) {
|
|
58
|
+
MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (selectedDbs.length < allDatabases.length) {
|
|
62
|
+
databaseIds = selectedDbs;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
40
65
|
// Prompt for output path
|
|
41
66
|
const { outputPath } = await inquirer.prompt([
|
|
42
67
|
{
|
|
@@ -46,7 +71,7 @@ export const migrateCommands = {
|
|
|
46
71
|
default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
|
|
47
72
|
},
|
|
48
73
|
]);
|
|
49
|
-
const options = { outputPath };
|
|
74
|
+
const options = { outputPath, databaseIds };
|
|
50
75
|
try {
|
|
51
76
|
await analyzeStringAttributes(controller.adapter, controller.config, options);
|
|
52
77
|
MessageFormatter.success("Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.", { prefix: "Analyze" });
|
|
@@ -70,6 +95,27 @@ export const migrateCommands = {
|
|
|
70
95
|
default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
|
|
71
96
|
},
|
|
72
97
|
]);
|
|
98
|
+
// Check for existing checkpoint
|
|
99
|
+
let freshRun = false;
|
|
100
|
+
const checkpointPath = planPath.replace(/\.ya?ml$/, ".checkpoint.json");
|
|
101
|
+
if (fs.existsSync(checkpointPath)) {
|
|
102
|
+
const { cpAction } = await inquirer.prompt([
|
|
103
|
+
{
|
|
104
|
+
type: "list",
|
|
105
|
+
name: "cpAction",
|
|
106
|
+
message: "Found an existing checkpoint from a previous run. What would you like to do?",
|
|
107
|
+
choices: [
|
|
108
|
+
{ name: "Resume — continue from where it left off", value: "resume" },
|
|
109
|
+
{ name: "Start fresh — delete checkpoint and start over", value: "fresh" },
|
|
110
|
+
{ name: "Cancel", value: "cancel" },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
if (cpAction === "cancel")
|
|
115
|
+
return;
|
|
116
|
+
if (cpAction === "fresh")
|
|
117
|
+
freshRun = true;
|
|
118
|
+
}
|
|
73
119
|
const { keepBackups } = await inquirer.prompt([
|
|
74
120
|
{
|
|
75
121
|
type: "confirm",
|
|
@@ -90,6 +136,7 @@ export const migrateCommands = {
|
|
|
90
136
|
planPath,
|
|
91
137
|
keepBackups,
|
|
92
138
|
dryRun,
|
|
139
|
+
freshRun,
|
|
93
140
|
};
|
|
94
141
|
try {
|
|
95
142
|
const results = await executeMigrationPlan(controller.adapter, options);
|
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"],
|
|
@@ -480,6 +485,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
480
485
|
alias: ["migrate-strings-dry-run"],
|
|
481
486
|
type: "boolean",
|
|
482
487
|
description: "Dry run — show what would happen without making changes",
|
|
488
|
+
})
|
|
489
|
+
.option("migrateStringsFresh", {
|
|
490
|
+
alias: ["migrate-strings-fresh"],
|
|
491
|
+
type: "boolean",
|
|
492
|
+
description: "Ignore existing checkpoint and start migration fresh",
|
|
483
493
|
})
|
|
484
494
|
.parse();
|
|
485
495
|
async function main() {
|
|
@@ -652,8 +662,12 @@ async function main() {
|
|
|
652
662
|
MessageFormatter.error("No database adapter available. Ensure config has valid credentials.", undefined, { prefix: "Migration" });
|
|
653
663
|
return;
|
|
654
664
|
}
|
|
665
|
+
const databaseIds = argv.migrateStringsDbIds
|
|
666
|
+
? argv.migrateStringsDbIds.split(",").map((s) => s.trim()).filter(Boolean)
|
|
667
|
+
: undefined;
|
|
655
668
|
await analyzeStringAttributes(controller.adapter, controller.config, {
|
|
656
669
|
outputPath: argv.migrateStringsOutput,
|
|
670
|
+
databaseIds,
|
|
657
671
|
});
|
|
658
672
|
}
|
|
659
673
|
else if (argv.migrateStringsExecute) {
|
|
@@ -665,6 +679,7 @@ async function main() {
|
|
|
665
679
|
planPath: argv.migrateStringsExecute,
|
|
666
680
|
keepBackups: argv.migrateStringsKeepBackups ?? true,
|
|
667
681
|
dryRun: argv.migrateStringsDryRun ?? false,
|
|
682
|
+
freshRun: argv.migrateStringsFresh ?? false,
|
|
668
683
|
});
|
|
669
684
|
if (results.failed > 0) {
|
|
670
685
|
process.exit(1);
|
|
@@ -11,7 +11,11 @@ import { MigrationPlanSchema, MigrationCheckpointSchema, suggestTargetType, gene
|
|
|
11
11
|
// Phase 1: Analyze — queries Appwrite server for real state
|
|
12
12
|
// ────────────────────────────────────────────────────────
|
|
13
13
|
export async function analyzeStringAttributes(adapter, config, options = {}) {
|
|
14
|
-
|
|
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;
|
|
15
19
|
if (databases.length === 0) {
|
|
16
20
|
MessageFormatter.warning("No databases configured. Nothing to analyze.", {
|
|
17
21
|
prefix: "Analyze",
|
|
@@ -189,6 +193,12 @@ export async function executeMigrationPlan(adapter, options) {
|
|
|
189
193
|
// Load or create checkpoint
|
|
190
194
|
const checkpointPath = options.checkpointPath ||
|
|
191
195
|
options.planPath.replace(/\.ya?ml$/, ".checkpoint.json");
|
|
196
|
+
if (options.freshRun && fs.existsSync(checkpointPath)) {
|
|
197
|
+
fs.unlinkSync(checkpointPath);
|
|
198
|
+
MessageFormatter.info("Deleted old checkpoint — starting fresh.", {
|
|
199
|
+
prefix: "Checkpoint",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
192
202
|
const checkpoint = loadOrCreateCheckpoint(checkpointPath, options.planPath);
|
|
193
203
|
const batchSize = options.batchSize || 100;
|
|
194
204
|
const batchDelayMs = options.batchDelayMs || 50;
|
|
@@ -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;
|
|
@@ -190,6 +191,7 @@ export interface ExecuteOptions {
|
|
|
190
191
|
batchSize?: number;
|
|
191
192
|
batchDelayMs?: number;
|
|
192
193
|
checkpointPath?: string;
|
|
194
|
+
freshRun?: boolean;
|
|
193
195
|
}
|
|
194
196
|
export declare function suggestTargetType(size: number, hasIndex: boolean): MigrationTargetType;
|
|
195
197
|
export declare function generateBackupKey(originalKey: string): 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.3",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import { MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
|
|
4
5
|
import type { InteractiveCLI } from "../../interactiveCLI.js";
|
|
5
6
|
import {
|
|
@@ -48,6 +49,31 @@ export const migrateCommands = {
|
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Prompt for database selection
|
|
53
|
+
const allDatabases = controller.config.databases || [];
|
|
54
|
+
let databaseIds: string[] | undefined;
|
|
55
|
+
if (allDatabases.length > 1) {
|
|
56
|
+
const { selectedDbs } = await inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: "checkbox",
|
|
59
|
+
name: "selectedDbs",
|
|
60
|
+
message: "Select databases to include in the analysis:",
|
|
61
|
+
choices: allDatabases.map((db: any) => ({
|
|
62
|
+
name: `${db.name} (${db.$id})`,
|
|
63
|
+
value: db.$id,
|
|
64
|
+
checked: true,
|
|
65
|
+
})),
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
if (selectedDbs.length === 0) {
|
|
69
|
+
MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (selectedDbs.length < allDatabases.length) {
|
|
73
|
+
databaseIds = selectedDbs;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
// Prompt for output path
|
|
52
78
|
const { outputPath } = await inquirer.prompt([
|
|
53
79
|
{
|
|
@@ -58,7 +84,7 @@ export const migrateCommands = {
|
|
|
58
84
|
},
|
|
59
85
|
]);
|
|
60
86
|
|
|
61
|
-
const options: AnalyzeOptions = { outputPath };
|
|
87
|
+
const options: AnalyzeOptions = { outputPath, databaseIds };
|
|
62
88
|
|
|
63
89
|
try {
|
|
64
90
|
await analyzeStringAttributes(controller.adapter, controller.config, options);
|
|
@@ -96,6 +122,26 @@ export const migrateCommands = {
|
|
|
96
122
|
},
|
|
97
123
|
]);
|
|
98
124
|
|
|
125
|
+
// Check for existing checkpoint
|
|
126
|
+
let freshRun = false;
|
|
127
|
+
const checkpointPath = planPath.replace(/\.ya?ml$/, ".checkpoint.json");
|
|
128
|
+
if (fs.existsSync(checkpointPath)) {
|
|
129
|
+
const { cpAction } = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: "list",
|
|
132
|
+
name: "cpAction",
|
|
133
|
+
message: "Found an existing checkpoint from a previous run. What would you like to do?",
|
|
134
|
+
choices: [
|
|
135
|
+
{ name: "Resume — continue from where it left off", value: "resume" },
|
|
136
|
+
{ name: "Start fresh — delete checkpoint and start over", value: "fresh" },
|
|
137
|
+
{ name: "Cancel", value: "cancel" },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
if (cpAction === "cancel") return;
|
|
142
|
+
if (cpAction === "fresh") freshRun = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
const { keepBackups } = await inquirer.prompt([
|
|
100
146
|
{
|
|
101
147
|
type: "confirm",
|
|
@@ -118,6 +164,7 @@ export const migrateCommands = {
|
|
|
118
164
|
planPath,
|
|
119
165
|
keepBackups,
|
|
120
166
|
dryRun,
|
|
167
|
+
freshRun,
|
|
121
168
|
};
|
|
122
169
|
|
|
123
170
|
try {
|
package/src/main.ts
CHANGED
|
@@ -86,8 +86,10 @@ 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;
|
|
92
|
+
migrateStringsFresh?: boolean;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
type ParsedArgv = ArgumentsCamelCase<CliOptions>;
|
|
@@ -642,6 +644,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
642
644
|
type: "string",
|
|
643
645
|
description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
|
|
644
646
|
})
|
|
647
|
+
.option("migrateStringsDbIds", {
|
|
648
|
+
alias: ["migrate-strings-db-ids"],
|
|
649
|
+
type: "string",
|
|
650
|
+
description: "Comma-separated database IDs to include in analysis (default: all)",
|
|
651
|
+
})
|
|
645
652
|
.option("migrateStringsKeepBackups", {
|
|
646
653
|
alias: ["migrate-strings-keep-backups"],
|
|
647
654
|
type: "boolean",
|
|
@@ -653,6 +660,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
653
660
|
type: "boolean",
|
|
654
661
|
description: "Dry run — show what would happen without making changes",
|
|
655
662
|
})
|
|
663
|
+
.option("migrateStringsFresh", {
|
|
664
|
+
alias: ["migrate-strings-fresh"],
|
|
665
|
+
type: "boolean",
|
|
666
|
+
description: "Ignore existing checkpoint and start migration fresh",
|
|
667
|
+
})
|
|
656
668
|
.parse() as ParsedArgv;
|
|
657
669
|
|
|
658
670
|
async function main() {
|
|
@@ -889,8 +901,12 @@ async function main() {
|
|
|
889
901
|
);
|
|
890
902
|
return;
|
|
891
903
|
}
|
|
904
|
+
const databaseIds = argv.migrateStringsDbIds
|
|
905
|
+
? argv.migrateStringsDbIds.split(",").map((s: string) => s.trim()).filter(Boolean)
|
|
906
|
+
: undefined;
|
|
892
907
|
await analyzeStringAttributes(controller.adapter, controller.config, {
|
|
893
908
|
outputPath: argv.migrateStringsOutput,
|
|
909
|
+
databaseIds,
|
|
894
910
|
});
|
|
895
911
|
} else if (argv.migrateStringsExecute) {
|
|
896
912
|
if (!controller.adapter) {
|
|
@@ -905,6 +921,7 @@ async function main() {
|
|
|
905
921
|
planPath: argv.migrateStringsExecute,
|
|
906
922
|
keepBackups: argv.migrateStringsKeepBackups ?? true,
|
|
907
923
|
dryRun: argv.migrateStringsDryRun ?? false,
|
|
924
|
+
freshRun: argv.migrateStringsFresh ?? false,
|
|
908
925
|
});
|
|
909
926
|
if (results.failed > 0) {
|
|
910
927
|
process.exit(1);
|
|
@@ -34,7 +34,11 @@ export async function analyzeStringAttributes(
|
|
|
34
34
|
config: AppwriteConfig,
|
|
35
35
|
options: AnalyzeOptions = {}
|
|
36
36
|
): Promise<MigrationPlan> {
|
|
37
|
-
|
|
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;
|
|
38
42
|
if (databases.length === 0) {
|
|
39
43
|
MessageFormatter.warning("No databases configured. Nothing to analyze.", {
|
|
40
44
|
prefix: "Analyze",
|
|
@@ -260,6 +264,12 @@ export async function executeMigrationPlan(
|
|
|
260
264
|
const checkpointPath =
|
|
261
265
|
options.checkpointPath ||
|
|
262
266
|
options.planPath.replace(/\.ya?ml$/, ".checkpoint.json");
|
|
267
|
+
if (options.freshRun && fs.existsSync(checkpointPath)) {
|
|
268
|
+
fs.unlinkSync(checkpointPath);
|
|
269
|
+
MessageFormatter.info("Deleted old checkpoint — starting fresh.", {
|
|
270
|
+
prefix: "Checkpoint",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
263
273
|
const checkpoint = loadOrCreateCheckpoint(checkpointPath, options.planPath);
|
|
264
274
|
|
|
265
275
|
const batchSize = options.batchSize || 100;
|
|
@@ -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 {
|
|
@@ -117,6 +118,7 @@ export interface ExecuteOptions {
|
|
|
117
118
|
batchSize?: number;
|
|
118
119
|
batchDelayMs?: number;
|
|
119
120
|
checkpointPath?: string;
|
|
121
|
+
freshRun?: boolean;
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
// ── Helper: suggest target type from size + index presence ──
|
|
@@ -136,17 +138,18 @@ export function suggestTargetType(
|
|
|
136
138
|
// ── Helper: generate backup key with length limits ──
|
|
137
139
|
|
|
138
140
|
const MAX_KEY_LENGTH = 36;
|
|
139
|
-
const BACKUP_PREFIX = "
|
|
141
|
+
const BACKUP_PREFIX = "mig_";
|
|
140
142
|
|
|
141
143
|
export function generateBackupKey(originalKey: string): string {
|
|
142
144
|
const candidate = `${BACKUP_PREFIX}${originalKey}`;
|
|
143
145
|
if (candidate.length <= MAX_KEY_LENGTH) {
|
|
144
146
|
return candidate;
|
|
145
147
|
}
|
|
146
|
-
// Truncate + 4-char hash for uniqueness
|
|
148
|
+
// Truncate + 4-char hash for uniqueness: m_ + orig + _ + hash(4)
|
|
147
149
|
const hash = simpleHash(originalKey);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
+
const TRUNC_PREFIX = "m_";
|
|
151
|
+
const maxOrigLen = MAX_KEY_LENGTH - TRUNC_PREFIX.length - 1 - 4;
|
|
152
|
+
return `${TRUNC_PREFIX}${originalKey.slice(0, maxOrigLen)}_${hash}`;
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
function simpleHash(str: string): string {
|