@njdamstra/appwrite-utils-cli 1.11.6 → 1.11.8

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.
@@ -250,6 +250,31 @@ export async function executeMigrationPlan(adapter, options) {
250
250
  skipped += entries.length;
251
251
  continue;
252
252
  }
253
+ // Temporarily set ALL required attributes in this collection to optional.
254
+ // Appwrite rejects partial updateRow calls if a required attribute is missing
255
+ // from the payload — even if the field exists in the document. This affects
256
+ // every data-copy step during migration.
257
+ const schemaRes = await tryAwaitWithRetry(() => adapter.getTable({ databaseId: first.databaseId, tableId: first.collectionId }));
258
+ const allAttrs = schemaRes?.data?.attributes || schemaRes?.data?.columns || [];
259
+ const originallyRequired = allAttrs
260
+ .filter((a) => a.required === true && a.status === "available")
261
+ .map((a) => a.key);
262
+ if (originallyRequired.length > 0) {
263
+ MessageFormatter.info(` Temporarily setting ${originallyRequired.length} required attribute(s) to optional...`, { prefix: "Execute" });
264
+ for (const key of originallyRequired) {
265
+ try {
266
+ await tryAwaitWithRetry(() => adapter.updateAttribute({
267
+ databaseId: first.databaseId,
268
+ tableId: first.collectionId,
269
+ key,
270
+ required: false,
271
+ }));
272
+ }
273
+ catch {
274
+ // Non-fatal — attribute might not support updating required
275
+ }
276
+ }
277
+ }
253
278
  // Migrate each attribute in this collection
254
279
  for (const entry of entries) {
255
280
  const cpEntry = getOrCreateCheckpointEntry(checkpoint, entry);
@@ -275,6 +300,25 @@ export async function executeMigrationPlan(adapter, options) {
275
300
  MessageFormatter.error(` ${entry.attributeKey}: FAILED — ${cpEntry.error}`, undefined, { prefix: "Execute" });
276
301
  }
277
302
  }
303
+ // Restore required flags for ALL originally-required attributes.
304
+ // This covers both migrated attributes (recreated as optional) and
305
+ // non-migrated attributes (temporarily set to optional above).
306
+ if (originallyRequired.length > 0) {
307
+ MessageFormatter.info(` Restoring ${originallyRequired.length} required attribute(s)...`, { prefix: "Execute" });
308
+ for (const key of originallyRequired) {
309
+ try {
310
+ await tryAwaitWithRetry(() => adapter.updateAttribute({
311
+ databaseId: first.databaseId,
312
+ tableId: first.collectionId,
313
+ key,
314
+ required: true,
315
+ }));
316
+ }
317
+ catch {
318
+ MessageFormatter.info(` Warning: could not restore required flag for ${key}`, { prefix: "Execute" });
319
+ }
320
+ }
321
+ }
278
322
  // After collection completes, offer to update local YAML
279
323
  const successInGroup = entries.filter((e) => {
280
324
  const cp = findCheckpointEntry(checkpoint, e);
@@ -426,21 +470,8 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
426
470
  advance("backup_deleted");
427
471
  }
428
472
  // Step 9: Mark completed
429
- // If the original attribute was required, update it now (after data is in place)
430
- if (entry.isRequired) {
431
- try {
432
- await tryAwaitWithRetry(() => adapter.updateAttribute({
433
- databaseId,
434
- tableId: collectionId,
435
- key: attributeKey,
436
- required: true,
437
- }));
438
- }
439
- catch {
440
- // Non-fatal — attribute is migrated, just not set back to required
441
- MessageFormatter.info(` Warning: could not set ${attributeKey} back to required`, { prefix: "Migrate" });
442
- }
443
- }
473
+ // NOTE: required flag is restored AFTER all attributes in the collection
474
+ // are migrated, to avoid partial-update validation errors on other attributes.
444
475
  advance("completed");
445
476
  }
446
477
  // ────────────────────────────────────────────────────────
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.6",
4
+ "version": "1.11.8",
5
5
  "main": "dist/main.js",
6
6
  "type": "module",
7
7
  "repository": {
@@ -338,6 +338,40 @@ export async function executeMigrationPlan(
338
338
  continue;
339
339
  }
340
340
 
341
+ // Temporarily set ALL required attributes in this collection to optional.
342
+ // Appwrite rejects partial updateRow calls if a required attribute is missing
343
+ // from the payload — even if the field exists in the document. This affects
344
+ // every data-copy step during migration.
345
+ const schemaRes = await tryAwaitWithRetry(() =>
346
+ adapter.getTable({ databaseId: first.databaseId, tableId: first.collectionId })
347
+ );
348
+ const allAttrs: any[] =
349
+ schemaRes?.data?.attributes || schemaRes?.data?.columns || [];
350
+ const originallyRequired = allAttrs
351
+ .filter((a: any) => a.required === true && a.status === "available")
352
+ .map((a: any) => a.key as string);
353
+
354
+ if (originallyRequired.length > 0) {
355
+ MessageFormatter.info(
356
+ ` Temporarily setting ${originallyRequired.length} required attribute(s) to optional...`,
357
+ { prefix: "Execute" }
358
+ );
359
+ for (const key of originallyRequired) {
360
+ try {
361
+ await tryAwaitWithRetry(() =>
362
+ adapter.updateAttribute({
363
+ databaseId: first.databaseId,
364
+ tableId: first.collectionId,
365
+ key,
366
+ required: false,
367
+ } as any)
368
+ );
369
+ } catch {
370
+ // Non-fatal — attribute might not support updating required
371
+ }
372
+ }
373
+ }
374
+
341
375
  // Migrate each attribute in this collection
342
376
  for (const entry of entries) {
343
377
  const cpEntry = getOrCreateCheckpointEntry(checkpoint, entry);
@@ -382,6 +416,33 @@ export async function executeMigrationPlan(
382
416
  }
383
417
  }
384
418
 
419
+ // Restore required flags for ALL originally-required attributes.
420
+ // This covers both migrated attributes (recreated as optional) and
421
+ // non-migrated attributes (temporarily set to optional above).
422
+ if (originallyRequired.length > 0) {
423
+ MessageFormatter.info(
424
+ ` Restoring ${originallyRequired.length} required attribute(s)...`,
425
+ { prefix: "Execute" }
426
+ );
427
+ for (const key of originallyRequired) {
428
+ try {
429
+ await tryAwaitWithRetry(() =>
430
+ adapter.updateAttribute({
431
+ databaseId: first.databaseId,
432
+ tableId: first.collectionId,
433
+ key,
434
+ required: true,
435
+ } as any)
436
+ );
437
+ } catch {
438
+ MessageFormatter.info(
439
+ ` Warning: could not restore required flag for ${key}`,
440
+ { prefix: "Execute" }
441
+ );
442
+ }
443
+ }
444
+ }
445
+
385
446
  // After collection completes, offer to update local YAML
386
447
  const successInGroup = entries.filter((e) => {
387
448
  const cp = findCheckpointEntry(checkpoint, e);
@@ -630,25 +691,8 @@ async function migrateOneAttribute(
630
691
  }
631
692
 
632
693
  // Step 9: Mark completed
633
- // If the original attribute was required, update it now (after data is in place)
634
- if (entry.isRequired) {
635
- try {
636
- await tryAwaitWithRetry(() =>
637
- adapter.updateAttribute({
638
- databaseId,
639
- tableId: collectionId,
640
- key: attributeKey,
641
- required: true,
642
- } as any)
643
- );
644
- } catch {
645
- // Non-fatal — attribute is migrated, just not set back to required
646
- MessageFormatter.info(
647
- ` Warning: could not set ${attributeKey} back to required`,
648
- { prefix: "Migrate" }
649
- );
650
- }
651
- }
694
+ // NOTE: required flag is restored AFTER all attributes in the collection
695
+ // are migrated, to avoid partial-update validation errors on other attributes.
652
696
  advance("completed");
653
697
  }
654
698