@njdamstra/appwrite-utils-cli 1.11.4 → 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.
@@ -320,7 +320,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
320
320
  MessageFormatter.info(` Creating backup attribute ${backupKey}...`, {
321
321
  prefix: "Migrate",
322
322
  });
323
- await tryAwaitWithRetry(() => adapter.createAttribute({
323
+ await createAttributeIfNotExists(adapter, {
324
324
  databaseId,
325
325
  tableId: collectionId,
326
326
  key: backupKey,
@@ -328,7 +328,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
328
328
  size: entry.currentSize,
329
329
  required: false, // always optional for backup
330
330
  array: entry.isArray,
331
- }));
331
+ });
332
332
  const available = await waitForAttribute(adapter, databaseId, collectionId, backupKey);
333
333
  if (!available)
334
334
  throw new Error(`Backup attribute ${backupKey} stuck`);
@@ -385,7 +385,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
385
385
  if (entry.hasDefault && entry.defaultValue !== undefined) {
386
386
  createParams.default = entry.defaultValue;
387
387
  }
388
- await tryAwaitWithRetry(() => adapter.createAttribute(createParams));
388
+ await createAttributeIfNotExists(adapter, createParams);
389
389
  const available = await waitForAttribute(adapter, databaseId, collectionId, attributeKey);
390
390
  if (!available)
391
391
  throw new Error(`New attribute ${attributeKey} stuck after creation`);
@@ -507,7 +507,7 @@ async function verifyDataCopy(adapter, databaseId, collectionId, sourceKey, targ
507
507
  for (const doc of docs) {
508
508
  if (!(sourceKey in doc))
509
509
  continue;
510
- if (doc[sourceKey] !== doc[targetKey]) {
510
+ if (JSON.stringify(doc[sourceKey]) !== JSON.stringify(doc[targetKey])) {
511
511
  throw new Error(`Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`);
512
512
  }
513
513
  }
@@ -515,7 +515,7 @@ async function verifyDataCopy(adapter, databaseId, collectionId, sourceKey, targ
515
515
  // ────────────────────────────────────────────────────────
516
516
  // Helper: wait for attribute to become available
517
517
  // ────────────────────────────────────────────────────────
518
- async function waitForAttribute(adapter, databaseId, collectionId, key, maxWaitMs = 60_000) {
518
+ async function waitForAttribute(adapter, databaseId, collectionId, key, maxWaitMs = 120_000) {
519
519
  const start = Date.now();
520
520
  const checkInterval = 2000;
521
521
  while (Date.now() - start < maxWaitMs) {
@@ -726,6 +726,22 @@ function printDryRunSummary(plan) {
726
726
  // ────────────────────────────────────────────────────────
727
727
  // Utility
728
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
+ }
729
745
  function delay(ms) {
730
746
  return new Promise((resolve) => setTimeout(resolve, ms));
731
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",
4
+ "version": "1.11.5",
5
5
  "main": "dist/main.js",
6
6
  "type": "module",
7
7
  "repository": {
@@ -458,17 +458,15 @@ async function migrateOneAttribute(
458
458
  MessageFormatter.info(` Creating backup attribute ${backupKey}...`, {
459
459
  prefix: "Migrate",
460
460
  });
461
- await tryAwaitWithRetry(() =>
462
- adapter.createAttribute({
463
- databaseId,
464
- tableId: collectionId,
465
- key: backupKey,
466
- type: "string", // backup keeps original type
467
- size: entry.currentSize,
468
- required: false, // always optional for backup
469
- array: entry.isArray,
470
- })
471
- );
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
+ });
472
470
  const available = await waitForAttribute(
473
471
  adapter,
474
472
  databaseId,
@@ -560,7 +558,7 @@ async function migrateOneAttribute(
560
558
  createParams.default = entry.defaultValue;
561
559
  }
562
560
 
563
- await tryAwaitWithRetry(() => adapter.createAttribute(createParams as any));
561
+ await createAttributeIfNotExists(adapter, createParams as any);
564
562
  const available = await waitForAttribute(
565
563
  adapter,
566
564
  databaseId,
@@ -756,7 +754,7 @@ async function verifyDataCopy(
756
754
  const docs = res?.documents || res?.rows || [];
757
755
  for (const doc of docs) {
758
756
  if (!(sourceKey in doc)) continue;
759
- if (doc[sourceKey] !== doc[targetKey]) {
757
+ if (JSON.stringify(doc[sourceKey]) !== JSON.stringify(doc[targetKey])) {
760
758
  throw new Error(
761
759
  `Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`
762
760
  );
@@ -773,7 +771,7 @@ async function waitForAttribute(
773
771
  databaseId: string,
774
772
  collectionId: string,
775
773
  key: string,
776
- maxWaitMs: number = 60_000
774
+ maxWaitMs: number = 120_000
777
775
  ): Promise<boolean> {
778
776
  const start = Date.now();
779
777
  const checkInterval = 2000;
@@ -1081,6 +1079,25 @@ function printDryRunSummary(plan: MigrationPlan): void {
1081
1079
  // Utility
1082
1080
  // ────────────────────────────────────────────────────────
1083
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
+
1084
1101
  function delay(ms: number): Promise<void> {
1085
1102
  return new Promise((resolve) => setTimeout(resolve, ms));
1086
1103
  }