@njdamstra/appwrite-utils-cli 1.11.2 → 1.11.4
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 +23 -0
- package/dist/main.js +6 -0
- package/dist/migrations/migrateStrings.js +21 -29
- package/dist/migrations/migrateStringsTypes.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli/commands/migrateCommands.ts +22 -0
- package/src/main.ts +7 -0
- package/src/migrations/migrateStrings.ts +30 -32
- package/src/migrations/migrateStringsTypes.ts +1 -0
|
@@ -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 = {
|
|
@@ -94,6 +95,27 @@ export const migrateCommands = {
|
|
|
94
95
|
default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
|
|
95
96
|
},
|
|
96
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
|
+
}
|
|
97
119
|
const { keepBackups } = await inquirer.prompt([
|
|
98
120
|
{
|
|
99
121
|
type: "confirm",
|
|
@@ -114,6 +136,7 @@ export const migrateCommands = {
|
|
|
114
136
|
planPath,
|
|
115
137
|
keepBackups,
|
|
116
138
|
dryRun,
|
|
139
|
+
freshRun,
|
|
117
140
|
};
|
|
118
141
|
try {
|
|
119
142
|
const results = await executeMigrationPlan(controller.adapter, options);
|
package/dist/main.js
CHANGED
|
@@ -485,6 +485,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
485
485
|
alias: ["migrate-strings-dry-run"],
|
|
486
486
|
type: "boolean",
|
|
487
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",
|
|
488
493
|
})
|
|
489
494
|
.parse();
|
|
490
495
|
async function main() {
|
|
@@ -674,6 +679,7 @@ async function main() {
|
|
|
674
679
|
planPath: argv.migrateStringsExecute,
|
|
675
680
|
keepBackups: argv.migrateStringsKeepBackups ?? true,
|
|
676
681
|
dryRun: argv.migrateStringsDryRun ?? false,
|
|
682
|
+
freshRun: argv.migrateStringsFresh ?? false,
|
|
677
683
|
});
|
|
678
684
|
if (results.failed > 0) {
|
|
679
685
|
process.exit(1);
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
+
import pLimit from "p-limit";
|
|
7
8
|
import { MessageFormatter, tryAwaitWithRetry, } from "@njdamstra/appwrite-utils-helpers";
|
|
8
9
|
import { ProgressManager } from "../shared/progressManager.js";
|
|
9
10
|
import { MigrationPlanSchema, MigrationCheckpointSchema, suggestTargetType, generateBackupKey, } from "./migrateStringsTypes.js";
|
|
@@ -193,6 +194,12 @@ export async function executeMigrationPlan(adapter, options) {
|
|
|
193
194
|
// Load or create checkpoint
|
|
194
195
|
const checkpointPath = options.checkpointPath ||
|
|
195
196
|
options.planPath.replace(/\.ya?ml$/, ".checkpoint.json");
|
|
197
|
+
if (options.freshRun && fs.existsSync(checkpointPath)) {
|
|
198
|
+
fs.unlinkSync(checkpointPath);
|
|
199
|
+
MessageFormatter.info("Deleted old checkpoint — starting fresh.", {
|
|
200
|
+
prefix: "Checkpoint",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
196
203
|
const checkpoint = loadOrCreateCheckpoint(checkpointPath, options.planPath);
|
|
197
204
|
const batchSize = options.batchSize || 100;
|
|
198
205
|
const batchDelayMs = options.batchDelayMs || 50;
|
|
@@ -455,6 +462,7 @@ async function copyAttributeData(adapter, databaseId, collectionId, sourceKey, t
|
|
|
455
462
|
title: ` Copy ${sourceKey} → ${targetKey}`,
|
|
456
463
|
})
|
|
457
464
|
: undefined;
|
|
465
|
+
const limit = pLimit(5);
|
|
458
466
|
while (true) {
|
|
459
467
|
const queries = [Query.limit(batchSize)];
|
|
460
468
|
if (lastId)
|
|
@@ -463,34 +471,18 @@ async function copyAttributeData(adapter, databaseId, collectionId, sourceKey, t
|
|
|
463
471
|
const docs = res?.documents || res?.rows || [];
|
|
464
472
|
if (docs.length === 0)
|
|
465
473
|
break;
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
rows,
|
|
479
|
-
}));
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
for (const doc of docs) {
|
|
484
|
-
if (doc[sourceKey] === undefined)
|
|
485
|
-
continue;
|
|
486
|
-
await tryAwaitWithRetry(() => adapter.updateRow({
|
|
487
|
-
databaseId,
|
|
488
|
-
tableId: collectionId,
|
|
489
|
-
id: doc.$id,
|
|
490
|
-
data: { [targetKey]: doc[sourceKey] },
|
|
491
|
-
}));
|
|
492
|
-
}
|
|
493
|
-
}
|
|
474
|
+
// Partial update: copy sourceKey → targetKey using updateRow (not bulkUpsert
|
|
475
|
+
// which requires complete document structure and fails on partial payloads)
|
|
476
|
+
const updatePromises = docs
|
|
477
|
+
.filter((d) => d[sourceKey] !== undefined)
|
|
478
|
+
.map((d) => limit(() => tryAwaitWithRetry(() => adapter.updateRow({
|
|
479
|
+
databaseId,
|
|
480
|
+
tableId: collectionId,
|
|
481
|
+
id: d.$id,
|
|
482
|
+
data: { [targetKey]: d[sourceKey] },
|
|
483
|
+
}), 0, true // throwError — surface 400s immediately
|
|
484
|
+
)));
|
|
485
|
+
await Promise.all(updatePromises);
|
|
494
486
|
totalCopied += docs.length;
|
|
495
487
|
lastId = docs[docs.length - 1].$id;
|
|
496
488
|
progress?.update(totalCopied);
|
|
@@ -513,7 +505,7 @@ async function verifyDataCopy(adapter, databaseId, collectionId, sourceKey, targ
|
|
|
513
505
|
}));
|
|
514
506
|
const docs = res?.documents || res?.rows || [];
|
|
515
507
|
for (const doc of docs) {
|
|
516
|
-
if (
|
|
508
|
+
if (!(sourceKey in doc))
|
|
517
509
|
continue;
|
|
518
510
|
if (doc[sourceKey] !== doc[targetKey]) {
|
|
519
511
|
throw new Error(`Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`);
|
|
@@ -191,6 +191,7 @@ export interface ExecuteOptions {
|
|
|
191
191
|
batchSize?: number;
|
|
192
192
|
batchDelayMs?: number;
|
|
193
193
|
checkpointPath?: string;
|
|
194
|
+
freshRun?: boolean;
|
|
194
195
|
}
|
|
195
196
|
export declare function suggestTargetType(size: number, hasIndex: boolean): MigrationTargetType;
|
|
196
197
|
export declare function generateBackupKey(originalKey: string): string;
|
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.4",
|
|
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 {
|
|
@@ -121,6 +122,26 @@ export const migrateCommands = {
|
|
|
121
122
|
},
|
|
122
123
|
]);
|
|
123
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
|
+
|
|
124
145
|
const { keepBackups } = await inquirer.prompt([
|
|
125
146
|
{
|
|
126
147
|
type: "confirm",
|
|
@@ -143,6 +164,7 @@ export const migrateCommands = {
|
|
|
143
164
|
planPath,
|
|
144
165
|
keepBackups,
|
|
145
166
|
dryRun,
|
|
167
|
+
freshRun,
|
|
146
168
|
};
|
|
147
169
|
|
|
148
170
|
try {
|
package/src/main.ts
CHANGED
|
@@ -89,6 +89,7 @@ interface CliOptions {
|
|
|
89
89
|
migrateStringsDbIds?: string;
|
|
90
90
|
migrateStringsKeepBackups?: boolean;
|
|
91
91
|
migrateStringsDryRun?: boolean;
|
|
92
|
+
migrateStringsFresh?: boolean;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
type ParsedArgv = ArgumentsCamelCase<CliOptions>;
|
|
@@ -659,6 +660,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
659
660
|
type: "boolean",
|
|
660
661
|
description: "Dry run — show what would happen without making changes",
|
|
661
662
|
})
|
|
663
|
+
.option("migrateStringsFresh", {
|
|
664
|
+
alias: ["migrate-strings-fresh"],
|
|
665
|
+
type: "boolean",
|
|
666
|
+
description: "Ignore existing checkpoint and start migration fresh",
|
|
667
|
+
})
|
|
662
668
|
.parse() as ParsedArgv;
|
|
663
669
|
|
|
664
670
|
async function main() {
|
|
@@ -915,6 +921,7 @@ async function main() {
|
|
|
915
921
|
planPath: argv.migrateStringsExecute,
|
|
916
922
|
keepBackups: argv.migrateStringsKeepBackups ?? true,
|
|
917
923
|
dryRun: argv.migrateStringsDryRun ?? false,
|
|
924
|
+
freshRun: argv.migrateStringsFresh ?? false,
|
|
918
925
|
});
|
|
919
926
|
if (results.failed > 0) {
|
|
920
927
|
process.exit(1);
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
+
import pLimit from "p-limit";
|
|
7
8
|
import {
|
|
8
9
|
type DatabaseAdapter,
|
|
9
10
|
MessageFormatter,
|
|
@@ -264,6 +265,12 @@ export async function executeMigrationPlan(
|
|
|
264
265
|
const checkpointPath =
|
|
265
266
|
options.checkpointPath ||
|
|
266
267
|
options.planPath.replace(/\.ya?ml$/, ".checkpoint.json");
|
|
268
|
+
if (options.freshRun && fs.existsSync(checkpointPath)) {
|
|
269
|
+
fs.unlinkSync(checkpointPath);
|
|
270
|
+
MessageFormatter.info("Deleted old checkpoint — starting fresh.", {
|
|
271
|
+
prefix: "Checkpoint",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
267
274
|
const checkpoint = loadOrCreateCheckpoint(checkpointPath, options.planPath);
|
|
268
275
|
|
|
269
276
|
const batchSize = options.batchSize || 100;
|
|
@@ -682,6 +689,8 @@ async function copyAttributeData(
|
|
|
682
689
|
})
|
|
683
690
|
: undefined;
|
|
684
691
|
|
|
692
|
+
const limit = pLimit(5);
|
|
693
|
+
|
|
685
694
|
while (true) {
|
|
686
695
|
const queries: string[] = [Query.limit(batchSize)];
|
|
687
696
|
if (lastId) queries.push(Query.cursorAfter(lastId));
|
|
@@ -693,37 +702,26 @@ async function copyAttributeData(
|
|
|
693
702
|
const docs = res?.documents || res?.rows || [];
|
|
694
703
|
if (docs.length === 0) break;
|
|
695
704
|
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (doc[sourceKey] === undefined) continue;
|
|
717
|
-
await tryAwaitWithRetry(() =>
|
|
718
|
-
adapter.updateRow({
|
|
719
|
-
databaseId,
|
|
720
|
-
tableId: collectionId,
|
|
721
|
-
id: doc.$id,
|
|
722
|
-
data: { [targetKey]: doc[sourceKey] },
|
|
723
|
-
})
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
705
|
+
// Partial update: copy sourceKey → targetKey using updateRow (not bulkUpsert
|
|
706
|
+
// which requires complete document structure and fails on partial payloads)
|
|
707
|
+
const updatePromises = docs
|
|
708
|
+
.filter((d: any) => d[sourceKey] !== undefined)
|
|
709
|
+
.map((d: any) =>
|
|
710
|
+
limit(() =>
|
|
711
|
+
tryAwaitWithRetry(
|
|
712
|
+
() =>
|
|
713
|
+
adapter.updateRow({
|
|
714
|
+
databaseId,
|
|
715
|
+
tableId: collectionId,
|
|
716
|
+
id: d.$id,
|
|
717
|
+
data: { [targetKey]: d[sourceKey] },
|
|
718
|
+
}),
|
|
719
|
+
0,
|
|
720
|
+
true // throwError — surface 400s immediately
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
);
|
|
724
|
+
await Promise.all(updatePromises);
|
|
727
725
|
|
|
728
726
|
totalCopied += docs.length;
|
|
729
727
|
lastId = docs[docs.length - 1].$id;
|
|
@@ -757,7 +755,7 @@ async function verifyDataCopy(
|
|
|
757
755
|
);
|
|
758
756
|
const docs = res?.documents || res?.rows || [];
|
|
759
757
|
for (const doc of docs) {
|
|
760
|
-
if (
|
|
758
|
+
if (!(sourceKey in doc)) continue;
|
|
761
759
|
if (doc[sourceKey] !== doc[targetKey]) {
|
|
762
760
|
throw new Error(
|
|
763
761
|
`Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`
|