@njdamstra/appwrite-utils-cli 1.11.3 → 1.11.5
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.
|
@@ -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";
|
|
@@ -319,7 +320,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
|
|
|
319
320
|
MessageFormatter.info(` Creating backup attribute ${backupKey}...`, {
|
|
320
321
|
prefix: "Migrate",
|
|
321
322
|
});
|
|
322
|
-
await
|
|
323
|
+
await createAttributeIfNotExists(adapter, {
|
|
323
324
|
databaseId,
|
|
324
325
|
tableId: collectionId,
|
|
325
326
|
key: backupKey,
|
|
@@ -327,7 +328,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
|
|
|
327
328
|
size: entry.currentSize,
|
|
328
329
|
required: false, // always optional for backup
|
|
329
330
|
array: entry.isArray,
|
|
330
|
-
})
|
|
331
|
+
});
|
|
331
332
|
const available = await waitForAttribute(adapter, databaseId, collectionId, backupKey);
|
|
332
333
|
if (!available)
|
|
333
334
|
throw new Error(`Backup attribute ${backupKey} stuck`);
|
|
@@ -384,7 +385,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
|
|
|
384
385
|
if (entry.hasDefault && entry.defaultValue !== undefined) {
|
|
385
386
|
createParams.default = entry.defaultValue;
|
|
386
387
|
}
|
|
387
|
-
await
|
|
388
|
+
await createAttributeIfNotExists(adapter, createParams);
|
|
388
389
|
const available = await waitForAttribute(adapter, databaseId, collectionId, attributeKey);
|
|
389
390
|
if (!available)
|
|
390
391
|
throw new Error(`New attribute ${attributeKey} stuck after creation`);
|
|
@@ -461,6 +462,7 @@ async function copyAttributeData(adapter, databaseId, collectionId, sourceKey, t
|
|
|
461
462
|
title: ` Copy ${sourceKey} → ${targetKey}`,
|
|
462
463
|
})
|
|
463
464
|
: undefined;
|
|
465
|
+
const limit = pLimit(5);
|
|
464
466
|
while (true) {
|
|
465
467
|
const queries = [Query.limit(batchSize)];
|
|
466
468
|
if (lastId)
|
|
@@ -469,34 +471,18 @@ async function copyAttributeData(adapter, databaseId, collectionId, sourceKey, t
|
|
|
469
471
|
const docs = res?.documents || res?.rows || [];
|
|
470
472
|
if (docs.length === 0)
|
|
471
473
|
break;
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
rows,
|
|
485
|
-
}));
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
else {
|
|
489
|
-
for (const doc of docs) {
|
|
490
|
-
if (doc[sourceKey] === undefined)
|
|
491
|
-
continue;
|
|
492
|
-
await tryAwaitWithRetry(() => adapter.updateRow({
|
|
493
|
-
databaseId,
|
|
494
|
-
tableId: collectionId,
|
|
495
|
-
id: doc.$id,
|
|
496
|
-
data: { [targetKey]: doc[sourceKey] },
|
|
497
|
-
}));
|
|
498
|
-
}
|
|
499
|
-
}
|
|
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);
|
|
500
486
|
totalCopied += docs.length;
|
|
501
487
|
lastId = docs[docs.length - 1].$id;
|
|
502
488
|
progress?.update(totalCopied);
|
|
@@ -519,9 +505,9 @@ async function verifyDataCopy(adapter, databaseId, collectionId, sourceKey, targ
|
|
|
519
505
|
}));
|
|
520
506
|
const docs = res?.documents || res?.rows || [];
|
|
521
507
|
for (const doc of docs) {
|
|
522
|
-
if (
|
|
508
|
+
if (!(sourceKey in doc))
|
|
523
509
|
continue;
|
|
524
|
-
if (doc[sourceKey] !== doc[targetKey]) {
|
|
510
|
+
if (JSON.stringify(doc[sourceKey]) !== JSON.stringify(doc[targetKey])) {
|
|
525
511
|
throw new Error(`Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`);
|
|
526
512
|
}
|
|
527
513
|
}
|
|
@@ -529,7 +515,7 @@ async function verifyDataCopy(adapter, databaseId, collectionId, sourceKey, targ
|
|
|
529
515
|
// ────────────────────────────────────────────────────────
|
|
530
516
|
// Helper: wait for attribute to become available
|
|
531
517
|
// ────────────────────────────────────────────────────────
|
|
532
|
-
async function waitForAttribute(adapter, databaseId, collectionId, key, maxWaitMs =
|
|
518
|
+
async function waitForAttribute(adapter, databaseId, collectionId, key, maxWaitMs = 120_000) {
|
|
533
519
|
const start = Date.now();
|
|
534
520
|
const checkInterval = 2000;
|
|
535
521
|
while (Date.now() - start < maxWaitMs) {
|
|
@@ -740,6 +726,22 @@ function printDryRunSummary(plan) {
|
|
|
740
726
|
// ────────────────────────────────────────────────────────
|
|
741
727
|
// Utility
|
|
742
728
|
// ────────────────────────────────────────────────────────
|
|
729
|
+
async function createAttributeIfNotExists(adapter, params) {
|
|
730
|
+
try {
|
|
731
|
+
await tryAwaitWithRetry(() => adapter.createAttribute(params), 0, true);
|
|
732
|
+
}
|
|
733
|
+
catch (err) {
|
|
734
|
+
const code = err?.code || err?.originalError?.code;
|
|
735
|
+
const type = err?.originalError?.type || "";
|
|
736
|
+
if (code === 409 || type === "column_already_exists") {
|
|
737
|
+
MessageFormatter.info(` (backup attribute already exists, reusing)`, {
|
|
738
|
+
prefix: "Migrate",
|
|
739
|
+
});
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
743
745
|
function delay(ms) {
|
|
744
746
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
745
747
|
}
|
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.5",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -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,
|
|
@@ -457,17 +458,15 @@ async function migrateOneAttribute(
|
|
|
457
458
|
MessageFormatter.info(` Creating backup attribute ${backupKey}...`, {
|
|
458
459
|
prefix: "Migrate",
|
|
459
460
|
});
|
|
460
|
-
await
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
})
|
|
470
|
-
);
|
|
461
|
+
await createAttributeIfNotExists(adapter, {
|
|
462
|
+
databaseId,
|
|
463
|
+
tableId: collectionId,
|
|
464
|
+
key: backupKey,
|
|
465
|
+
type: "string", // backup keeps original type
|
|
466
|
+
size: entry.currentSize,
|
|
467
|
+
required: false, // always optional for backup
|
|
468
|
+
array: entry.isArray,
|
|
469
|
+
});
|
|
471
470
|
const available = await waitForAttribute(
|
|
472
471
|
adapter,
|
|
473
472
|
databaseId,
|
|
@@ -559,7 +558,7 @@ async function migrateOneAttribute(
|
|
|
559
558
|
createParams.default = entry.defaultValue;
|
|
560
559
|
}
|
|
561
560
|
|
|
562
|
-
await
|
|
561
|
+
await createAttributeIfNotExists(adapter, createParams as any);
|
|
563
562
|
const available = await waitForAttribute(
|
|
564
563
|
adapter,
|
|
565
564
|
databaseId,
|
|
@@ -688,6 +687,8 @@ async function copyAttributeData(
|
|
|
688
687
|
})
|
|
689
688
|
: undefined;
|
|
690
689
|
|
|
690
|
+
const limit = pLimit(5);
|
|
691
|
+
|
|
691
692
|
while (true) {
|
|
692
693
|
const queries: string[] = [Query.limit(batchSize)];
|
|
693
694
|
if (lastId) queries.push(Query.cursorAfter(lastId));
|
|
@@ -699,37 +700,26 @@ async function copyAttributeData(
|
|
|
699
700
|
const docs = res?.documents || res?.rows || [];
|
|
700
701
|
if (docs.length === 0) break;
|
|
701
702
|
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if (doc[sourceKey] === undefined) continue;
|
|
723
|
-
await tryAwaitWithRetry(() =>
|
|
724
|
-
adapter.updateRow({
|
|
725
|
-
databaseId,
|
|
726
|
-
tableId: collectionId,
|
|
727
|
-
id: doc.$id,
|
|
728
|
-
data: { [targetKey]: doc[sourceKey] },
|
|
729
|
-
})
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
703
|
+
// Partial update: copy sourceKey → targetKey using updateRow (not bulkUpsert
|
|
704
|
+
// which requires complete document structure and fails on partial payloads)
|
|
705
|
+
const updatePromises = docs
|
|
706
|
+
.filter((d: any) => d[sourceKey] !== undefined)
|
|
707
|
+
.map((d: any) =>
|
|
708
|
+
limit(() =>
|
|
709
|
+
tryAwaitWithRetry(
|
|
710
|
+
() =>
|
|
711
|
+
adapter.updateRow({
|
|
712
|
+
databaseId,
|
|
713
|
+
tableId: collectionId,
|
|
714
|
+
id: d.$id,
|
|
715
|
+
data: { [targetKey]: d[sourceKey] },
|
|
716
|
+
}),
|
|
717
|
+
0,
|
|
718
|
+
true // throwError — surface 400s immediately
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
);
|
|
722
|
+
await Promise.all(updatePromises);
|
|
733
723
|
|
|
734
724
|
totalCopied += docs.length;
|
|
735
725
|
lastId = docs[docs.length - 1].$id;
|
|
@@ -763,8 +753,8 @@ async function verifyDataCopy(
|
|
|
763
753
|
);
|
|
764
754
|
const docs = res?.documents || res?.rows || [];
|
|
765
755
|
for (const doc of docs) {
|
|
766
|
-
if (
|
|
767
|
-
if (doc[sourceKey] !== doc[targetKey]) {
|
|
756
|
+
if (!(sourceKey in doc)) continue;
|
|
757
|
+
if (JSON.stringify(doc[sourceKey]) !== JSON.stringify(doc[targetKey])) {
|
|
768
758
|
throw new Error(
|
|
769
759
|
`Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`
|
|
770
760
|
);
|
|
@@ -781,7 +771,7 @@ async function waitForAttribute(
|
|
|
781
771
|
databaseId: string,
|
|
782
772
|
collectionId: string,
|
|
783
773
|
key: string,
|
|
784
|
-
maxWaitMs: number =
|
|
774
|
+
maxWaitMs: number = 120_000
|
|
785
775
|
): Promise<boolean> {
|
|
786
776
|
const start = Date.now();
|
|
787
777
|
const checkInterval = 2000;
|
|
@@ -1089,6 +1079,25 @@ function printDryRunSummary(plan: MigrationPlan): void {
|
|
|
1089
1079
|
// Utility
|
|
1090
1080
|
// ────────────────────────────────────────────────────────
|
|
1091
1081
|
|
|
1082
|
+
async function createAttributeIfNotExists(
|
|
1083
|
+
adapter: DatabaseAdapter,
|
|
1084
|
+
params: Parameters<DatabaseAdapter["createAttribute"]>[0]
|
|
1085
|
+
): Promise<void> {
|
|
1086
|
+
try {
|
|
1087
|
+
await tryAwaitWithRetry(() => adapter.createAttribute(params), 0, true);
|
|
1088
|
+
} catch (err: any) {
|
|
1089
|
+
const code = err?.code || err?.originalError?.code;
|
|
1090
|
+
const type = err?.originalError?.type || "";
|
|
1091
|
+
if (code === 409 || type === "column_already_exists") {
|
|
1092
|
+
MessageFormatter.info(` (backup attribute already exists, reusing)`, {
|
|
1093
|
+
prefix: "Migrate",
|
|
1094
|
+
});
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
throw err;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1092
1101
|
function delay(ms: number): Promise<void> {
|
|
1093
1102
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1094
1103
|
}
|