appwrite-utils-cli 1.8.5 → 1.8.7
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/collections/methods.js +148 -4
- package/dist/utilsController.js +11 -1
- package/package.json +1 -1
- package/src/collections/methods.ts +181 -43
- package/src/utilsController.ts +30 -20
|
@@ -182,11 +182,34 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
// Find existing table by name
|
|
186
|
-
|
|
187
|
-
const items = list.tables || [];
|
|
188
|
-
let table = items[0];
|
|
185
|
+
// Find existing table — prefer lookup by ID (if provided), then by name
|
|
186
|
+
let table;
|
|
189
187
|
let tableId;
|
|
188
|
+
// 1) Try by explicit $id first (handles rename scenarios)
|
|
189
|
+
if (collectionData.$id) {
|
|
190
|
+
try {
|
|
191
|
+
const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
|
|
192
|
+
table = byId.data || byId.tables?.[0];
|
|
193
|
+
if (table?.$id) {
|
|
194
|
+
MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Not found by ID; fall back to name lookup
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 2) If not found by ID, try by name
|
|
202
|
+
if (!table) {
|
|
203
|
+
const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
|
|
204
|
+
const items = list.tables || [];
|
|
205
|
+
table = items[0];
|
|
206
|
+
if (table?.$id) {
|
|
207
|
+
// If local has $id that differs from remote, prefer remote (IDs are immutable)
|
|
208
|
+
if (collectionData.$id && collectionData.$id !== table.$id) {
|
|
209
|
+
MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
190
213
|
if (!table) {
|
|
191
214
|
// Determine ID (prefer provided $id or re-use deleted one)
|
|
192
215
|
let foundColl = deletedCollections?.find((coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", ""));
|
|
@@ -235,6 +258,11 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
235
258
|
const plusminus = plan.toUpdate.map((u) => u.attribute.key);
|
|
236
259
|
const minus = plan.toRecreate.map((r) => r.newAttribute.key);
|
|
237
260
|
const skip = plan.unchanged;
|
|
261
|
+
// Compute deletions (remote extras not present locally)
|
|
262
|
+
const desiredKeysForDelete = new Set((attributes || []).map((a) => a.key));
|
|
263
|
+
const extraRemoteKeys = (existingCols || [])
|
|
264
|
+
.map((c) => c?.key)
|
|
265
|
+
.filter((k) => !!k && !desiredKeysForDelete.has(k));
|
|
238
266
|
const parts = [];
|
|
239
267
|
if (plus.length)
|
|
240
268
|
parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
|
|
@@ -244,6 +272,7 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
244
272
|
parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
|
|
245
273
|
if (skip.length)
|
|
246
274
|
parts.push(`⏭️ ${skip.length}`);
|
|
275
|
+
parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
|
|
247
276
|
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
|
|
248
277
|
// Execute
|
|
249
278
|
const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
|
|
@@ -458,6 +487,121 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
458
487
|
catch (e) {
|
|
459
488
|
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
460
489
|
}
|
|
490
|
+
// Deletions for indexes: remove remote indexes not declared in YAML/config
|
|
491
|
+
try {
|
|
492
|
+
const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
|
|
493
|
+
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
494
|
+
const existingIdx = idxRes.data || idxRes.indexes || [];
|
|
495
|
+
const extraIdx = existingIdx
|
|
496
|
+
.filter((i) => i?.key && !desiredIndexKeys.has(i.key))
|
|
497
|
+
.map((i) => i.key);
|
|
498
|
+
if (extraIdx.length > 0) {
|
|
499
|
+
MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
|
|
500
|
+
const deleted = [];
|
|
501
|
+
const errors = [];
|
|
502
|
+
for (const key of extraIdx) {
|
|
503
|
+
try {
|
|
504
|
+
await adapter.deleteIndex({ databaseId, tableId, key });
|
|
505
|
+
// Optionally wait for index to disappear
|
|
506
|
+
const start = Date.now();
|
|
507
|
+
const maxWait = 30000;
|
|
508
|
+
while (Date.now() - start < maxWait) {
|
|
509
|
+
try {
|
|
510
|
+
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
511
|
+
const list = li.data || li.indexes || [];
|
|
512
|
+
if (!list.find((ix) => ix.key === key))
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
catch { }
|
|
516
|
+
await delay(1000);
|
|
517
|
+
}
|
|
518
|
+
deleted.push(key);
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
errors.push({ key, error: e?.message || String(e) });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (deleted.length) {
|
|
525
|
+
MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
|
|
526
|
+
}
|
|
527
|
+
if (errors.length) {
|
|
528
|
+
MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
|
|
529
|
+
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (e) {
|
|
537
|
+
MessageFormatter.warning(`Could not evaluate index deletions: ${e?.message || e}`, { prefix: 'Indexes' });
|
|
538
|
+
}
|
|
539
|
+
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
540
|
+
try {
|
|
541
|
+
const desiredKeys = new Set((attributes || []).map((a) => a.key));
|
|
542
|
+
const tableInfo3 = await adapter.getTable({ databaseId, tableId });
|
|
543
|
+
const existingCols3 = tableInfo3.data?.columns || tableInfo3.data?.attributes || [];
|
|
544
|
+
const toDelete = existingCols3
|
|
545
|
+
.filter((col) => col?.key && !desiredKeys.has(col.key))
|
|
546
|
+
.map((col) => col.key);
|
|
547
|
+
if (toDelete.length > 0) {
|
|
548
|
+
MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
|
|
549
|
+
const deleted = [];
|
|
550
|
+
const errors = [];
|
|
551
|
+
for (const key of toDelete) {
|
|
552
|
+
try {
|
|
553
|
+
// Drop any indexes that reference this attribute to avoid server errors
|
|
554
|
+
try {
|
|
555
|
+
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
556
|
+
const ilist = idxRes.data || idxRes.indexes || [];
|
|
557
|
+
for (const idx of ilist) {
|
|
558
|
+
const attrs = Array.isArray(idx.attributes) ? idx.attributes : [];
|
|
559
|
+
if (attrs.includes(key)) {
|
|
560
|
+
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
561
|
+
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
562
|
+
await delay(500);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch { }
|
|
567
|
+
await adapter.deleteAttribute({ databaseId, tableId, key });
|
|
568
|
+
// Wait briefly for deletion to settle
|
|
569
|
+
const start = Date.now();
|
|
570
|
+
const maxWaitMs = 60000;
|
|
571
|
+
while (Date.now() - start < maxWaitMs) {
|
|
572
|
+
try {
|
|
573
|
+
const tinfo = await adapter.getTable({ databaseId, tableId });
|
|
574
|
+
const cols = tinfo.data?.columns || tinfo.data?.attributes || [];
|
|
575
|
+
const found = cols.find((c) => c.key === key);
|
|
576
|
+
if (!found)
|
|
577
|
+
break;
|
|
578
|
+
if (found.status && found.status !== 'deleting')
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
catch { }
|
|
582
|
+
await delay(1000);
|
|
583
|
+
}
|
|
584
|
+
deleted.push(key);
|
|
585
|
+
}
|
|
586
|
+
catch (e) {
|
|
587
|
+
errors.push({ key, error: e?.message || String(e) });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (deleted.length) {
|
|
591
|
+
MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
|
|
592
|
+
}
|
|
593
|
+
if (errors.length) {
|
|
594
|
+
MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
|
|
595
|
+
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
MessageFormatter.warning(`Could not evaluate deletions: ${e?.message || e}`, { prefix: 'Attributes' });
|
|
604
|
+
}
|
|
461
605
|
// Mark this table as fully processed to prevent re-processing
|
|
462
606
|
markCollectionProcessed(tableId, collectionData.name);
|
|
463
607
|
}
|
package/dist/utilsController.js
CHANGED
|
@@ -619,6 +619,15 @@ export class UtilsController {
|
|
|
619
619
|
MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
|
|
620
620
|
return;
|
|
621
621
|
}
|
|
622
|
+
// Always reload config from disk so pushes use current local YAML/Ts definitions
|
|
623
|
+
try {
|
|
624
|
+
await this.reloadConfig();
|
|
625
|
+
MessageFormatter.info("Reloaded config from disk for push", { prefix: "Controller" });
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
// Non-fatal; continue with existing config
|
|
629
|
+
MessageFormatter.warning("Could not reload config; continuing with current in-memory config", { prefix: "Controller" });
|
|
630
|
+
}
|
|
622
631
|
MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
|
|
623
632
|
// Convert database selections to Models.Database format
|
|
624
633
|
const selectedDatabases = [];
|
|
@@ -665,7 +674,8 @@ export class UtilsController {
|
|
|
665
674
|
// Check if this collection was selected for THIS database
|
|
666
675
|
if (dbSelection.tableIds.includes(collectionId)) {
|
|
667
676
|
collectionsForDatabase.push(collection);
|
|
668
|
-
|
|
677
|
+
const source = collection._isFromTablesDir ? 'tables/' : 'collections/';
|
|
678
|
+
MessageFormatter.info(` - Selected collection: ${collection.name || collectionId} for database ${dbSelection.databaseId} [source: ${source}]`, { prefix: "Controller" });
|
|
669
679
|
}
|
|
670
680
|
}
|
|
671
681
|
databaseCollectionsMap.set(dbSelection.databaseId, collectionsForDatabase);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "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.8.
|
|
4
|
+
"version": "1.8.7",
|
|
5
5
|
"main": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -262,45 +262,69 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
// Find existing table by name
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
265
|
+
// Find existing table — prefer lookup by ID (if provided), then by name
|
|
266
|
+
let table: any | undefined;
|
|
267
|
+
let tableId: string;
|
|
268
|
+
|
|
269
|
+
// 1) Try by explicit $id first (handles rename scenarios)
|
|
270
|
+
if (collectionData.$id) {
|
|
271
|
+
try {
|
|
272
|
+
const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
|
|
273
|
+
table = (byId as any).data || (byId as any).tables?.[0];
|
|
274
|
+
if (table?.$id) {
|
|
275
|
+
MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// Not found by ID; fall back to name lookup
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 2) If not found by ID, try by name
|
|
283
|
+
if (!table) {
|
|
284
|
+
const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
|
|
285
|
+
const items: any[] = (list as any).tables || [];
|
|
286
|
+
table = items[0];
|
|
287
|
+
if (table?.$id) {
|
|
288
|
+
// If local has $id that differs from remote, prefer remote (IDs are immutable)
|
|
289
|
+
if (collectionData.$id && collectionData.$id !== table.$id) {
|
|
290
|
+
MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!table) {
|
|
296
|
+
// Determine ID (prefer provided $id or re-use deleted one)
|
|
297
|
+
let foundColl = deletedCollections?.find(
|
|
298
|
+
(coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", "")
|
|
299
|
+
);
|
|
300
|
+
if (collectionData.$id) tableId = collectionData.$id;
|
|
301
|
+
else if (foundColl && !usedIds.has(foundColl.collectionId)) tableId = foundColl.collectionId;
|
|
302
|
+
else tableId = ID.unique();
|
|
303
|
+
usedIds.add(tableId);
|
|
304
|
+
|
|
305
|
+
const res = await adapter.createTable({
|
|
306
|
+
databaseId,
|
|
307
|
+
id: tableId,
|
|
308
|
+
name: collectionData.name,
|
|
309
|
+
permissions,
|
|
310
|
+
documentSecurity: !!collectionData.documentSecurity,
|
|
311
|
+
enabled: collectionData.enabled !== false
|
|
312
|
+
});
|
|
313
|
+
table = (res as any).data || res;
|
|
314
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
|
315
|
+
} else {
|
|
316
|
+
tableId = table.$id;
|
|
317
|
+
await adapter.updateTable({
|
|
318
|
+
databaseId,
|
|
319
|
+
id: tableId,
|
|
320
|
+
name: collectionData.name,
|
|
321
|
+
permissions,
|
|
322
|
+
documentSecurity: !!collectionData.documentSecurity,
|
|
323
|
+
enabled: collectionData.enabled !== false
|
|
324
|
+
});
|
|
325
|
+
// Cache the existing table ID
|
|
326
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
|
327
|
+
}
|
|
304
328
|
|
|
305
329
|
// Add small delay after table create/update
|
|
306
330
|
await delay(250);
|
|
@@ -319,11 +343,18 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
319
343
|
const minus = plan.toRecreate.map((r: any) => (r.newAttribute as any).key);
|
|
320
344
|
const skip = plan.unchanged;
|
|
321
345
|
|
|
346
|
+
// Compute deletions (remote extras not present locally)
|
|
347
|
+
const desiredKeysForDelete = new Set((attributes || []).map((a: any) => a.key));
|
|
348
|
+
const extraRemoteKeys = (existingCols || [])
|
|
349
|
+
.map((c: any) => c?.key)
|
|
350
|
+
.filter((k: any): k is string => !!k && !desiredKeysForDelete.has(k));
|
|
351
|
+
|
|
322
352
|
const parts: string[] = [];
|
|
323
353
|
if (plus.length) parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
|
|
324
354
|
if (plusminus.length) parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
|
|
325
355
|
if (minus.length) parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
|
|
326
356
|
if (skip.length) parts.push(`⏭️ ${skip.length}`);
|
|
357
|
+
parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
|
|
327
358
|
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
|
|
328
359
|
|
|
329
360
|
// Execute
|
|
@@ -510,10 +541,117 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
510
541
|
} catch (e) {
|
|
511
542
|
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
512
543
|
}
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
544
|
+
|
|
545
|
+
// Deletions for indexes: remove remote indexes not declared in YAML/config
|
|
546
|
+
try {
|
|
547
|
+
const desiredIndexKeys = new Set((indexes || []).map((i: any) => i.key));
|
|
548
|
+
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
549
|
+
const existingIdx: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
550
|
+
const extraIdx = existingIdx
|
|
551
|
+
.filter((i: any) => i?.key && !desiredIndexKeys.has(i.key))
|
|
552
|
+
.map((i: any) => i.key as string);
|
|
553
|
+
if (extraIdx.length > 0) {
|
|
554
|
+
MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
|
|
555
|
+
const deleted: string[] = [];
|
|
556
|
+
const errors: Array<{ key: string; error: string }> = [];
|
|
557
|
+
for (const key of extraIdx) {
|
|
558
|
+
try {
|
|
559
|
+
await adapter.deleteIndex({ databaseId, tableId, key });
|
|
560
|
+
// Optionally wait for index to disappear
|
|
561
|
+
const start = Date.now();
|
|
562
|
+
const maxWait = 30000;
|
|
563
|
+
while (Date.now() - start < maxWait) {
|
|
564
|
+
try {
|
|
565
|
+
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
566
|
+
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
567
|
+
if (!list.find((ix: any) => ix.key === key)) break;
|
|
568
|
+
} catch {}
|
|
569
|
+
await delay(1000);
|
|
570
|
+
}
|
|
571
|
+
deleted.push(key);
|
|
572
|
+
} catch (e: any) {
|
|
573
|
+
errors.push({ key, error: e?.message || String(e) });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (deleted.length) {
|
|
577
|
+
MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
|
|
578
|
+
}
|
|
579
|
+
if (errors.length) {
|
|
580
|
+
MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
|
|
581
|
+
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
|
|
585
|
+
}
|
|
586
|
+
} catch (e) {
|
|
587
|
+
MessageFormatter.warning(`Could not evaluate index deletions: ${(e as Error)?.message || e}`, { prefix: 'Indexes' });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
591
|
+
try {
|
|
592
|
+
const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
|
|
593
|
+
const tableInfo3 = await adapter.getTable({ databaseId, tableId });
|
|
594
|
+
const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
|
|
595
|
+
const toDelete = existingCols3
|
|
596
|
+
.filter((col: any) => col?.key && !desiredKeys.has(col.key))
|
|
597
|
+
.map((col: any) => col.key as string);
|
|
598
|
+
|
|
599
|
+
if (toDelete.length > 0) {
|
|
600
|
+
MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
|
|
601
|
+
const deleted: string[] = [];
|
|
602
|
+
const errors: Array<{ key: string; error: string }> = [];
|
|
603
|
+
for (const key of toDelete) {
|
|
604
|
+
try {
|
|
605
|
+
// Drop any indexes that reference this attribute to avoid server errors
|
|
606
|
+
try {
|
|
607
|
+
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
608
|
+
const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
609
|
+
for (const idx of ilist) {
|
|
610
|
+
const attrs: string[] = Array.isArray(idx.attributes) ? idx.attributes : [];
|
|
611
|
+
if (attrs.includes(key)) {
|
|
612
|
+
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
613
|
+
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
614
|
+
await delay(500);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch {}
|
|
618
|
+
|
|
619
|
+
await adapter.deleteAttribute({ databaseId, tableId, key });
|
|
620
|
+
// Wait briefly for deletion to settle
|
|
621
|
+
const start = Date.now();
|
|
622
|
+
const maxWaitMs = 60000;
|
|
623
|
+
while (Date.now() - start < maxWaitMs) {
|
|
624
|
+
try {
|
|
625
|
+
const tinfo = await adapter.getTable({ databaseId, tableId });
|
|
626
|
+
const cols = (tinfo as any).data?.columns || (tinfo as any).data?.attributes || [];
|
|
627
|
+
const found = cols.find((c: any) => c.key === key);
|
|
628
|
+
if (!found) break;
|
|
629
|
+
if (found.status && found.status !== 'deleting') break;
|
|
630
|
+
} catch {}
|
|
631
|
+
await delay(1000);
|
|
632
|
+
}
|
|
633
|
+
deleted.push(key);
|
|
634
|
+
} catch (e: any) {
|
|
635
|
+
errors.push({ key, error: e?.message || String(e) });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (deleted.length) {
|
|
639
|
+
MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
|
|
640
|
+
}
|
|
641
|
+
if (errors.length) {
|
|
642
|
+
MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
|
|
643
|
+
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
|
|
647
|
+
}
|
|
648
|
+
} catch (e) {
|
|
649
|
+
MessageFormatter.warning(`Could not evaluate deletions: ${(e as Error)?.message || e}`, { prefix: 'Attributes' });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Mark this table as fully processed to prevent re-processing
|
|
653
|
+
markCollectionProcessed(tableId, collectionData.name);
|
|
654
|
+
}
|
|
517
655
|
|
|
518
656
|
// Process queued relationships once mapping likely populated
|
|
519
657
|
if (relQueue.length > 0) {
|
package/src/utilsController.ts
CHANGED
|
@@ -848,15 +848,24 @@ export class UtilsController {
|
|
|
848
848
|
MessageFormatter.success("Selective pull completed successfully! Remote config pulled to local.", { prefix: "Controller" });
|
|
849
849
|
}
|
|
850
850
|
|
|
851
|
-
async selectivePush(
|
|
852
|
-
databaseSelections: DatabaseSelection[],
|
|
853
|
-
bucketSelections: BucketSelection[]
|
|
854
|
-
): Promise<void> {
|
|
855
|
-
await this.init();
|
|
856
|
-
if (!this.database) {
|
|
857
|
-
MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
851
|
+
async selectivePush(
|
|
852
|
+
databaseSelections: DatabaseSelection[],
|
|
853
|
+
bucketSelections: BucketSelection[]
|
|
854
|
+
): Promise<void> {
|
|
855
|
+
await this.init();
|
|
856
|
+
if (!this.database) {
|
|
857
|
+
MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Always reload config from disk so pushes use current local YAML/Ts definitions
|
|
862
|
+
try {
|
|
863
|
+
await this.reloadConfig();
|
|
864
|
+
MessageFormatter.info("Reloaded config from disk for push", { prefix: "Controller" });
|
|
865
|
+
} catch (e) {
|
|
866
|
+
// Non-fatal; continue with existing config
|
|
867
|
+
MessageFormatter.warning("Could not reload config; continuing with current in-memory config", { prefix: "Controller" });
|
|
868
|
+
}
|
|
860
869
|
|
|
861
870
|
MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
|
|
862
871
|
|
|
@@ -900,7 +909,7 @@ export class UtilsController {
|
|
|
900
909
|
const databaseCollectionsMap = new Map<string, any[]>();
|
|
901
910
|
|
|
902
911
|
// Get all collections/tables from config (they're at the root level, not nested in databases)
|
|
903
|
-
const allCollections = this.config?.collections || this.config?.tables || [];
|
|
912
|
+
const allCollections = this.config?.collections || this.config?.tables || [];
|
|
904
913
|
|
|
905
914
|
// Create database-specific collection mapping to preserve relationships
|
|
906
915
|
for (const dbSelection of databaseSelections) {
|
|
@@ -908,16 +917,17 @@ export class UtilsController {
|
|
|
908
917
|
|
|
909
918
|
MessageFormatter.info(`Processing collections for database: ${dbSelection.databaseId}`, { prefix: "Controller" });
|
|
910
919
|
|
|
911
|
-
// Filter collections that were selected for THIS specific database
|
|
912
|
-
for (const collection of allCollections) {
|
|
913
|
-
const collectionId = collection.$id || (collection as any).id;
|
|
914
|
-
|
|
915
|
-
// Check if this collection was selected for THIS database
|
|
916
|
-
if (dbSelection.tableIds.includes(collectionId)) {
|
|
917
|
-
collectionsForDatabase.push(collection);
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
920
|
+
// Filter collections that were selected for THIS specific database
|
|
921
|
+
for (const collection of allCollections) {
|
|
922
|
+
const collectionId = collection.$id || (collection as any).id;
|
|
923
|
+
|
|
924
|
+
// Check if this collection was selected for THIS database
|
|
925
|
+
if (dbSelection.tableIds.includes(collectionId)) {
|
|
926
|
+
collectionsForDatabase.push(collection);
|
|
927
|
+
const source = (collection as any)._isFromTablesDir ? 'tables/' : 'collections/';
|
|
928
|
+
MessageFormatter.info(` - Selected collection: ${collection.name || collectionId} for database ${dbSelection.databaseId} [source: ${source}]`, { prefix: "Controller" });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
921
931
|
|
|
922
932
|
databaseCollectionsMap.set(dbSelection.databaseId, collectionsForDatabase);
|
|
923
933
|
MessageFormatter.info(`Database ${dbSelection.databaseId}: ${collectionsForDatabase.length} collections selected`, { prefix: "Controller" });
|