appwrite-utils-cli 1.9.5 → 1.9.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.
@@ -143,10 +143,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
143
143
  }
144
144
 
145
145
  export function normalizeColumnToComparable(col: any): ComparableColumn {
146
- // Detect enum surfaced as string+elements from server and normalize to enum for comparison
146
+ // Detect enum surfaced as string+elements or string+format:enum from server and normalize to enum for comparison
147
147
  let t = String((col?.type ?? col?.columnType ?? '')).toLowerCase();
148
148
  const hasElements = Array.isArray(col?.elements) && (col.elements as any[]).length > 0;
149
- if (t === 'string' && hasElements) t = 'enum';
149
+ const hasEnumFormat = (col?.format === 'enum');
150
+ if (t === 'string' && (hasElements || hasEnumFormat)) {
151
+ t = 'enum';
152
+ }
150
153
  const base: ComparableColumn = {
151
154
  key: col?.key,
152
155
  type: t,
@@ -227,21 +230,29 @@ export function isIndexEqualToIndex(a: any, b: any): boolean {
227
230
  if (String(a.type).toLowerCase() !== String(b.type).toLowerCase()) return false;
228
231
 
229
232
  // Compare attributes as sets (order-insensitive)
230
- const attrsA = Array.isArray(a.attributes) ? [...a.attributes].sort() : [];
231
- const attrsB = Array.isArray(b.attributes) ? [...b.attributes].sort() : [];
233
+ // Support TablesDB which returns 'columns' instead of 'attributes'
234
+ const attrsAraw = Array.isArray(a.attributes)
235
+ ? a.attributes
236
+ : (Array.isArray((a as any).columns) ? (a as any).columns : []);
237
+ const attrsA = [...attrsAraw].sort();
238
+ const attrsB = Array.isArray(b.attributes)
239
+ ? [...b.attributes].sort()
240
+ : (Array.isArray((b as any).columns) ? [...(b as any).columns].sort() : []);
232
241
  if (attrsA.length !== attrsB.length) return false;
233
242
  for (let i = 0; i < attrsA.length; i++) if (attrsA[i] !== attrsB[i]) return false;
234
243
 
235
- // Orders are only considered if BOTH have orders defined
236
- const hasOrdersA = Array.isArray(a.orders) && a.orders.length > 0;
237
- const hasOrdersB = Array.isArray(b.orders) && b.orders.length > 0;
238
- if (hasOrdersA && hasOrdersB) {
239
- const ordersA = [...a.orders].sort();
244
+ // Orders are only considered if CONFIG (b) has orders defined
245
+ // This prevents false positives when Appwrite returns orders but user didn't specify them
246
+ const hasConfigOrders = Array.isArray(b.orders) && b.orders.length > 0;
247
+ if (hasConfigOrders) {
248
+ // Some APIs may expose 'directions' instead of 'orders'
249
+ const ordersA = Array.isArray(a.orders)
250
+ ? [...a.orders].sort()
251
+ : (Array.isArray((a as any).directions) ? [...(a as any).directions].sort() : []);
240
252
  const ordersB = [...b.orders].sort();
241
253
  if (ordersA.length !== ordersB.length) return false;
242
254
  for (let i = 0; i < ordersA.length; i++) if (ordersA[i] !== ordersB[i]) return false;
243
255
  }
244
- // If only one side has orders, treat as equal (orders unspecified by user)
245
256
  return true;
246
257
  }
247
258
 
@@ -255,6 +266,8 @@ function compareColumnProperties(
255
266
  ): ColumnPropertyChange[] {
256
267
  const changes: ColumnPropertyChange[] = [];
257
268
  const t = String(columnType || (newAttribute as any).type || '').toLowerCase();
269
+ const key = newAttribute?.key || 'unknown';
270
+
258
271
  const mutableProps = (MUTABLE_PROPERTIES as any)[t] || [];
259
272
  const immutableProps = (IMMUTABLE_PROPERTIES as any)[t] || [];
260
273
 
@@ -274,7 +287,9 @@ function compareColumnProperties(
274
287
  let newValue = getNewVal(prop);
275
288
  // Special-case: enum elements empty/missing should not trigger updates
276
289
  if (t === 'enum' && prop === 'elements') {
277
- if (!Array.isArray(newValue) || newValue.length === 0) newValue = oldValue;
290
+ if (!Array.isArray(newValue) || newValue.length === 0) {
291
+ newValue = oldValue;
292
+ }
278
293
  }
279
294
  if (Array.isArray(oldValue) && Array.isArray(newValue)) {
280
295
  if (oldValue.length !== newValue.length || oldValue.some((v: any, i: number) => v !== newValue[i])) {
@@ -300,11 +315,11 @@ function compareColumnProperties(
300
315
  // Type change requires recreate (normalize string+elements to enum on old side)
301
316
  const oldTypeRaw = String(oldColumn?.type || oldColumn?.columnType || '').toLowerCase();
302
317
  const oldHasElements = Array.isArray(oldColumn?.elements) && (oldColumn.elements as any[]).length > 0;
303
- const oldType = oldTypeRaw === 'string' && oldHasElements ? 'enum' : oldTypeRaw;
318
+ const oldHasEnumFormat = (oldColumn?.format === 'enum');
319
+ const oldType = oldTypeRaw === 'string' && (oldHasElements || oldHasEnumFormat) ? 'enum' : oldTypeRaw;
304
320
  if (oldType && t && oldType !== t && TYPE_CHANGE_REQUIRES_RECREATE.includes(oldType)) {
305
321
  changes.push({ property: 'type', oldValue: oldType, newValue: t, requiresRecreate: true });
306
322
  }
307
-
308
323
  return changes;
309
324
  }
310
325
 
@@ -14,6 +14,7 @@ import {
14
14
  } from "./services/index.js";
15
15
  import { MessageFormatter } from "../shared/messageFormatter.js";
16
16
  import { logger } from "../shared/logging.js";
17
+ import { detectAppwriteVersionCached, type ApiMode } from "../utils/versionDetection.js";
17
18
 
18
19
  /**
19
20
  * Database type from AppwriteConfig
@@ -297,6 +298,37 @@ export class ConfigManager {
297
298
  }
298
299
  }
299
300
 
301
+ // 8. Run version detection and set apiMode if not explicitly configured
302
+ if (!config.apiMode || config.apiMode === 'auto') {
303
+ try {
304
+ logger.debug('Running version detection for API mode detection', {
305
+ prefix: "ConfigManager",
306
+ endpoint: config.appwriteEndpoint
307
+ });
308
+
309
+ const versionResult = await detectAppwriteVersionCached(
310
+ config.appwriteEndpoint,
311
+ config.appwriteProject,
312
+ config.appwriteKey
313
+ );
314
+
315
+ config.apiMode = versionResult.apiMode;
316
+ logger.info(`API mode detected: ${config.apiMode}`, {
317
+ prefix: "ConfigManager",
318
+ method: versionResult.detectionMethod,
319
+ confidence: versionResult.confidence,
320
+ serverVersion: versionResult.serverVersion,
321
+ });
322
+
323
+ } catch (error) {
324
+ logger.warn('Version detection failed, defaulting to legacy mode', {
325
+ prefix: "ConfigManager",
326
+ error: error instanceof Error ? error.message : 'Unknown error',
327
+ });
328
+ config.apiMode = 'legacy';
329
+ }
330
+ }
331
+
300
332
  // 8. Cache the config
301
333
  this.cachedConfig = config;
302
334
  this.cachedConfigPath = configPath;
@@ -161,7 +161,7 @@ export function mapToUpdateAttributeParams(
161
161
  attr: Attribute,
162
162
  base: { databaseId: string; tableId: string }
163
163
  ): UpdateAttributeParams {
164
- const type = String((attr as any).type || "").toLowerCase();
164
+ const type = String((attr.type == 'string' && attr.format !== 'enum') || attr.type !== 'string' ? (attr as any).type : 'enum').toLowerCase();
165
165
  const params: UpdateAttributeParams = {
166
166
  databaseId: base.databaseId,
167
167
  tableId: base.tableId,
@@ -0,0 +1,409 @@
1
+ import type { Index } from "appwrite-utils";
2
+ import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
3
+ import type { Models } from "node-appwrite";
4
+ import { isIndexEqualToIndex } from "../collections/tableOperations.js";
5
+ import { MessageFormatter } from "../shared/messageFormatter.js";
6
+ import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
7
+
8
+ // Enhanced index operation interfaces
9
+ export interface IndexOperation {
10
+ type: 'create' | 'update' | 'skip' | 'delete';
11
+ index: Index;
12
+ existingIndex?: Models.Index;
13
+ reason?: string;
14
+ }
15
+
16
+ export interface IndexOperationPlan {
17
+ toCreate: IndexOperation[];
18
+ toUpdate: IndexOperation[];
19
+ toSkip: IndexOperation[];
20
+ toDelete: IndexOperation[];
21
+ }
22
+
23
+ export interface IndexExecutionResult {
24
+ created: string[];
25
+ updated: string[];
26
+ skipped: string[];
27
+ deleted: string[];
28
+ errors: Array<{ key: string; error: string }>;
29
+ summary: {
30
+ total: number;
31
+ created: number;
32
+ updated: number;
33
+ skipped: number;
34
+ deleted: number;
35
+ errors: number;
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Plan index operations by comparing desired indexes with existing ones
41
+ * Uses the existing isIndexEqualToIndex function for consistent comparison
42
+ */
43
+ export function planIndexOperations(
44
+ desiredIndexes: Index[],
45
+ existingIndexes: Models.Index[]
46
+ ): IndexOperationPlan {
47
+ const plan: IndexOperationPlan = {
48
+ toCreate: [],
49
+ toUpdate: [],
50
+ toSkip: [],
51
+ toDelete: []
52
+ };
53
+
54
+ for (const desiredIndex of desiredIndexes) {
55
+ const existingIndex = existingIndexes.find(idx => idx.key === desiredIndex.key);
56
+
57
+ if (!existingIndex) {
58
+ // Index doesn't exist - create it
59
+ plan.toCreate.push({
60
+ type: 'create',
61
+ index: desiredIndex,
62
+ reason: 'New index'
63
+ });
64
+ } else if (isIndexEqualToIndex(existingIndex, desiredIndex)) {
65
+ // Index exists and is identical - skip it
66
+ plan.toSkip.push({
67
+ type: 'skip',
68
+ index: desiredIndex,
69
+ existingIndex,
70
+ reason: 'Index unchanged'
71
+ });
72
+ } else {
73
+ // Index exists but is different - update it
74
+ plan.toUpdate.push({
75
+ type: 'update',
76
+ index: desiredIndex,
77
+ existingIndex,
78
+ reason: 'Index configuration changed'
79
+ });
80
+ }
81
+ }
82
+
83
+ return plan;
84
+ }
85
+
86
+ /**
87
+ * Plan index deletions for indexes that exist but aren't in the desired configuration
88
+ */
89
+ export function planIndexDeletions(
90
+ desiredIndexKeys: Set<string>,
91
+ existingIndexes: Models.Index[]
92
+ ): IndexOperation[] {
93
+ const deletions: IndexOperation[] = [];
94
+
95
+ for (const existingIndex of existingIndexes) {
96
+ if (!desiredIndexKeys.has(existingIndex.key)) {
97
+ deletions.push({
98
+ type: 'delete',
99
+ index: existingIndex as Index, // Convert Models.Index to Index for compatibility
100
+ reason: 'Obsolete index'
101
+ });
102
+ }
103
+ }
104
+
105
+ return deletions;
106
+ }
107
+
108
+ /**
109
+ * Execute index operations with proper error handling and status monitoring
110
+ */
111
+ export async function executeIndexOperations(
112
+ adapter: DatabaseAdapter,
113
+ databaseId: string,
114
+ tableId: string,
115
+ plan: IndexOperationPlan
116
+ ): Promise<IndexExecutionResult> {
117
+ const result: IndexExecutionResult = {
118
+ created: [],
119
+ updated: [],
120
+ skipped: [],
121
+ deleted: [],
122
+ errors: [],
123
+ summary: {
124
+ total: 0,
125
+ created: 0,
126
+ updated: 0,
127
+ skipped: 0,
128
+ deleted: 0,
129
+ errors: 0
130
+ }
131
+ };
132
+
133
+ // Execute creates
134
+ for (const operation of plan.toCreate) {
135
+ try {
136
+ await adapter.createIndex({
137
+ databaseId,
138
+ tableId,
139
+ key: operation.index.key,
140
+ type: operation.index.type,
141
+ attributes: operation.index.attributes,
142
+ orders: operation.index.orders || []
143
+ });
144
+
145
+ result.created.push(operation.index.key);
146
+ MessageFormatter.success(`Created index ${operation.index.key}`, { prefix: 'Indexes' });
147
+
148
+ // Wait for index to become available
149
+ await waitForIndexAvailable(adapter, databaseId, tableId, operation.index.key);
150
+
151
+ await delay(150); // Brief delay between operations
152
+ } catch (error) {
153
+ const errorMessage = error instanceof Error ? error.message : String(error);
154
+ result.errors.push({ key: operation.index.key, error: errorMessage });
155
+ MessageFormatter.error(`Failed to create index ${operation.index.key}`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Indexes' });
156
+ }
157
+ }
158
+
159
+ // Execute updates (delete + recreate)
160
+ for (const operation of plan.toUpdate) {
161
+ try {
162
+ // Delete existing index first
163
+ await adapter.deleteIndex({
164
+ databaseId,
165
+ tableId,
166
+ key: operation.index.key
167
+ });
168
+
169
+ await delay(100); // Brief delay for deletion to settle
170
+
171
+ // Create new index
172
+ await adapter.createIndex({
173
+ databaseId,
174
+ tableId,
175
+ key: operation.index.key,
176
+ type: operation.index.type,
177
+ attributes: operation.index.attributes,
178
+ orders: operation.index.orders || operation.existingIndex?.orders || []
179
+ });
180
+
181
+ result.updated.push(operation.index.key);
182
+ MessageFormatter.success(`Updated index ${operation.index.key}`, { prefix: 'Indexes' });
183
+
184
+ // Wait for index to become available
185
+ await waitForIndexAvailable(adapter, databaseId, tableId, operation.index.key);
186
+
187
+ await delay(150); // Brief delay between operations
188
+ } catch (error) {
189
+ const errorMessage = error instanceof Error ? error.message : String(error);
190
+ result.errors.push({ key: operation.index.key, error: errorMessage });
191
+ MessageFormatter.error(`Failed to update index ${operation.index.key}`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Indexes' });
192
+ }
193
+ }
194
+
195
+ // Execute skips
196
+ for (const operation of plan.toSkip) {
197
+ result.skipped.push(operation.index.key);
198
+ MessageFormatter.info(`Index ${operation.index.key} unchanged`, { prefix: 'Indexes' });
199
+ }
200
+
201
+ // Calculate summary
202
+ result.summary.total = result.created.length + result.updated.length + result.skipped.length + result.deleted.length;
203
+ result.summary.created = result.created.length;
204
+ result.summary.updated = result.updated.length;
205
+ result.summary.skipped = result.skipped.length;
206
+ result.summary.deleted = result.deleted.length;
207
+ result.summary.errors = result.errors.length;
208
+
209
+ return result;
210
+ }
211
+
212
+ /**
213
+ * Execute index deletions with proper error handling
214
+ */
215
+ export async function executeIndexDeletions(
216
+ adapter: DatabaseAdapter,
217
+ databaseId: string,
218
+ tableId: string,
219
+ deletions: IndexOperation[]
220
+ ): Promise<{ deleted: string[]; errors: Array<{ key: string; error: string }> }> {
221
+ const result = {
222
+ deleted: [] as string[],
223
+ errors: [] as Array<{ key: string; error: string }>
224
+ };
225
+
226
+ for (const operation of deletions) {
227
+ try {
228
+ await adapter.deleteIndex({
229
+ databaseId,
230
+ tableId,
231
+ key: operation.index.key
232
+ });
233
+
234
+ result.deleted.push(operation.index.key);
235
+ MessageFormatter.info(`Deleted obsolete index ${operation.index.key}`, { prefix: 'Indexes' });
236
+
237
+ // Wait briefly for deletion to settle
238
+ await delay(500);
239
+ } catch (error) {
240
+ const errorMessage = error instanceof Error ? error.message : String(error);
241
+ result.errors.push({ key: operation.index.key, error: errorMessage });
242
+ MessageFormatter.error(`Failed to delete index ${operation.index.key}`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Indexes' });
243
+ }
244
+ }
245
+
246
+ return result;
247
+ }
248
+
249
+ /**
250
+ * Wait for an index to become available with timeout and retry logic
251
+ * This is an adapter-aware version of the logic from collections/indexes.ts
252
+ */
253
+ async function waitForIndexAvailable(
254
+ adapter: DatabaseAdapter,
255
+ databaseId: string,
256
+ tableId: string,
257
+ indexKey: string,
258
+ maxWaitTime: number = 60000, // 1 minute
259
+ checkInterval: number = 2000 // 2 seconds
260
+ ): Promise<boolean> {
261
+ const startTime = Date.now();
262
+
263
+ while (Date.now() - startTime < maxWaitTime) {
264
+ try {
265
+ const indexList = await adapter.listIndexes({ databaseId, tableId });
266
+ const indexes: any[] = (indexList as any).data || (indexList as any).indexes || [];
267
+ const index = indexes.find((idx: any) => idx.key === indexKey);
268
+
269
+ if (!index) {
270
+ MessageFormatter.error(`Index '${indexKey}' not found after creation`, undefined, { prefix: 'Indexes' });
271
+ return false;
272
+ }
273
+
274
+ switch (index.status) {
275
+ case 'available':
276
+ return true;
277
+
278
+ case 'failed':
279
+ MessageFormatter.error(`Index '${indexKey}' failed: ${index.error || 'unknown error'}`, undefined, { prefix: 'Indexes' });
280
+ return false;
281
+
282
+ case 'stuck':
283
+ MessageFormatter.warning(`Index '${indexKey}' is stuck`, { prefix: 'Indexes' });
284
+ return false;
285
+
286
+ case 'processing':
287
+ case 'deleting':
288
+ // Continue waiting
289
+ break;
290
+
291
+ default:
292
+ MessageFormatter.warning(`Unknown status '${index.status}' for index '${indexKey}'`, { prefix: 'Indexes' });
293
+ break;
294
+ }
295
+ } catch (error) {
296
+ MessageFormatter.error(`Error checking index '${indexKey}' status: ${error}`, undefined, { prefix: 'Indexes' });
297
+ }
298
+
299
+ await delay(checkInterval);
300
+ }
301
+
302
+ MessageFormatter.warning(`Timeout waiting for index '${indexKey}' to become available (${maxWaitTime}ms)`, { prefix: 'Indexes' });
303
+ return false;
304
+ }
305
+
306
+ /**
307
+ * Main function to create/update indexes via adapter
308
+ * This replaces the messy inline code in methods.ts
309
+ */
310
+ export async function createOrUpdateIndexesViaAdapter(
311
+ adapter: DatabaseAdapter,
312
+ databaseId: string,
313
+ tableId: string,
314
+ desiredIndexes: Index[],
315
+ configIndexes?: Index[]
316
+ ): Promise<void> {
317
+ if (!desiredIndexes || desiredIndexes.length === 0) {
318
+ MessageFormatter.info('No indexes to process', { prefix: 'Indexes' });
319
+ return;
320
+ }
321
+
322
+ MessageFormatter.info(`Processing ${desiredIndexes.length} indexes for table ${tableId}`, { prefix: 'Indexes' });
323
+
324
+ try {
325
+ // Get existing indexes
326
+ const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
327
+ const existingIndexes: Models.Index[] = (existingIdxRes as any).data || (existingIdxRes as any).indexes || [];
328
+
329
+ // Plan operations
330
+ const plan = planIndexOperations(desiredIndexes, existingIndexes);
331
+
332
+ // Show plan with icons (consistent with attribute handling)
333
+ const planParts: string[] = [];
334
+ if (plan.toCreate.length) planParts.push(`➕ ${plan.toCreate.length} (${plan.toCreate.map(op => op.index.key).join(', ')})`);
335
+ if (plan.toUpdate.length) planParts.push(`🔧 ${plan.toUpdate.length} (${plan.toUpdate.map(op => op.index.key).join(', ')})`);
336
+ if (plan.toSkip.length) planParts.push(`⏭️ ${plan.toSkip.length}`);
337
+
338
+ MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
339
+
340
+ // Execute operations
341
+ const result = await executeIndexOperations(adapter, databaseId, tableId, plan);
342
+
343
+ // Show summary
344
+ MessageFormatter.info(
345
+ `Summary → ➕ ${result.summary.created} | 🔧 ${result.summary.updated} | ⏭️ ${result.summary.skipped}`,
346
+ { prefix: 'Indexes' }
347
+ );
348
+
349
+ // Handle errors if any
350
+ if (result.errors.length > 0) {
351
+ MessageFormatter.error(`${result.errors.length} index operations failed:`, undefined, { prefix: 'Indexes' });
352
+ for (const error of result.errors) {
353
+ MessageFormatter.error(` ${error.key}: ${error.error}`, undefined, { prefix: 'Indexes' });
354
+ }
355
+ }
356
+
357
+ } catch (error) {
358
+ MessageFormatter.error('Failed to process indexes', error instanceof Error ? error : new Error(String(error)), { prefix: 'Indexes' });
359
+ throw error;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Handle index deletions for obsolete indexes
365
+ */
366
+ export async function deleteObsoleteIndexesViaAdapter(
367
+ adapter: DatabaseAdapter,
368
+ databaseId: string,
369
+ tableId: string,
370
+ desiredIndexKeys: Set<string>
371
+ ): Promise<void> {
372
+ try {
373
+ // Get existing indexes
374
+ const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
375
+ const existingIndexes: Models.Index[] = (existingIdxRes as any).data || (existingIdxRes as any).indexes || [];
376
+
377
+ // Plan deletions
378
+ const deletions = planIndexDeletions(desiredIndexKeys, existingIndexes);
379
+
380
+ if (deletions.length === 0) {
381
+ MessageFormatter.info('Plan → 🗑️ 0 indexes', { prefix: 'Indexes' });
382
+ return;
383
+ }
384
+
385
+ // Show deletion plan
386
+ MessageFormatter.info(
387
+ `Plan → 🗑️ ${deletions.length} (${deletions.map(op => op.index.key).join(', ')})`,
388
+ { prefix: 'Indexes' }
389
+ );
390
+
391
+ // Execute deletions
392
+ const result = await executeIndexDeletions(adapter, databaseId, tableId, deletions);
393
+
394
+ // Show results
395
+ if (result.deleted.length > 0) {
396
+ MessageFormatter.success(`Deleted ${result.deleted.length} indexes: ${result.deleted.join(', ')}`, { prefix: 'Indexes' });
397
+ }
398
+
399
+ if (result.errors.length > 0) {
400
+ MessageFormatter.error(`${result.errors.length} index deletions failed:`, undefined, { prefix: 'Indexes' });
401
+ for (const error of result.errors) {
402
+ MessageFormatter.error(` ${error.key}: ${error.error}`, undefined, { prefix: 'Indexes' });
403
+ }
404
+ }
405
+
406
+ } catch (error) {
407
+ MessageFormatter.warning(`Could not evaluate index deletions: ${(error as Error)?.message || error}`, { prefix: 'Indexes' });
408
+ }
409
+ }
@@ -82,6 +82,7 @@ import {
82
82
  import { ConfigManager } from "./config/ConfigManager.js";
83
83
  import { ClientFactory } from "./utils/ClientFactory.js";
84
84
  import type { DatabaseSelection, BucketSelection } from "./shared/selectionDialogs.js";
85
+ import { clearProcessingState, processQueue } from "./shared/operationQueue.js";
85
86
 
86
87
  export interface SetupOptions {
87
88
  databases?: Models.Database[];
@@ -629,7 +630,6 @@ export class UtilsController {
629
630
  // Ensure we don't carry state between databases in a multi-db push
630
631
  // This resets processed sets and name->id mapping per database
631
632
  try {
632
- const { clearProcessingState } = await import('./shared/operationQueue.js');
633
633
  clearProcessingState();
634
634
  } catch {}
635
635
 
@@ -657,6 +657,15 @@ export class UtilsController {
657
657
  collections
658
658
  );
659
659
  }
660
+
661
+ // Safety net: Process any remaining queued operations to complete relationship sync
662
+ try {
663
+ MessageFormatter.info(`🔄 Processing final operation queue for database ${database.$id}`, { prefix: "UtilsController" });
664
+ await processQueue(this.adapter || this.database!, database.$id);
665
+ MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "UtilsController" });
666
+ } catch (error) {
667
+ MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'UtilsController' });
668
+ }
660
669
  }
661
670
 
662
671
  async generateSchemas() {
@@ -1,24 +0,0 @@
1
- import { type Index, type CollectionCreate } from "appwrite-utils";
2
- import { Databases, type Models } from "node-appwrite";
3
- export declare const indexesSame: (databaseIndex: Models.Index, configIndex: Index) => boolean;
4
- export declare const createOrUpdateIndex: (dbId: string, db: Databases, collectionId: string, index: Index, options?: {
5
- verbose?: boolean;
6
- forceRecreate?: boolean;
7
- }) => Promise<Models.Index | null>;
8
- export declare const createOrUpdateIndexes: (dbId: string, db: Databases, collectionId: string, indexes: Index[], options?: {
9
- verbose?: boolean;
10
- forceRecreate?: boolean;
11
- }) => Promise<void>;
12
- export declare const createUpdateCollectionIndexes: (db: Databases, dbId: string, collection: Models.Collection, collectionConfig: CollectionCreate, options?: {
13
- verbose?: boolean;
14
- forceRecreate?: boolean;
15
- }) => Promise<void>;
16
- export declare const deleteObsoleteIndexes: (db: Databases, dbId: string, collection: Models.Collection, collectionConfig: CollectionCreate, options?: {
17
- verbose?: boolean;
18
- }) => Promise<void>;
19
- export declare const validateIndexConfiguration: (indexes: Index[], options?: {
20
- verbose?: boolean;
21
- }) => {
22
- valid: boolean;
23
- errors: string[];
24
- };