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.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { AppwriteConfig } from "appwrite-utils";
9
9
  import { type ApiMode, type VersionDetectionResult } from "../utils/versionDetection.js";
10
- import type { DatabaseAdapter } from './DatabaseAdapter.js';
10
+ import { type DatabaseAdapter } from './DatabaseAdapter.js';
11
11
  export interface AdapterFactoryConfig {
12
12
  appwriteEndpoint: string;
13
13
  appwriteProject: string;
@@ -6,6 +6,7 @@
6
6
  * and provides a single entry point for all database operations.
7
7
  */
8
8
  import { detectAppwriteVersionCached, isVersionAtLeast } from "../utils/versionDetection.js";
9
+ import { AdapterError } from './DatabaseAdapter.js';
9
10
  import { TablesDBAdapter } from './TablesDBAdapter.js';
10
11
  import { LegacyAdapter } from './LegacyAdapter.js';
11
12
  import { logger } from '../shared/logging.js';
@@ -161,36 +162,29 @@ export class AdapterFactory {
161
162
  endpoint: config.appwriteEndpoint,
162
163
  operation: 'createTablesDBAdapter'
163
164
  });
164
- // Use pre-configured client or create session-aware client with TablesDB Client
165
- let client;
166
- if (config.preConfiguredClient) {
167
- client = config.preConfiguredClient;
165
+ let client = new Client()
166
+ .setEndpoint(config.appwriteEndpoint)
167
+ .setProject(config.appwriteProject);
168
+ // Set authentication method with mode headers
169
+ // Prefer session with admin mode, fallback to API key with default mode
170
+ if (config.sessionCookie && isValidSessionCookie(config.sessionCookie)) {
171
+ client.setSession(config.sessionCookie);
172
+ client.headers['X-Appwrite-Mode'] = 'admin';
173
+ logger.debug('Using session authentication for TablesDB adapter', {
174
+ project: config.appwriteProject,
175
+ operation: 'createTablesDBAdapter'
176
+ });
177
+ }
178
+ else if (config.appwriteKey) {
179
+ client.setKey(config.appwriteKey);
180
+ client.headers['X-Appwrite-Mode'] = 'default';
181
+ logger.debug('Using API key authentication for TablesDB adapter', {
182
+ project: config.appwriteProject,
183
+ operation: 'createTablesDBAdapter'
184
+ });
168
185
  }
169
186
  else {
170
- client = new Client()
171
- .setEndpoint(config.appwriteEndpoint)
172
- .setProject(config.appwriteProject);
173
- // Set authentication method with mode headers
174
- // Prefer session with admin mode, fallback to API key with default mode
175
- if (config.sessionCookie && isValidSessionCookie(config.sessionCookie)) {
176
- client.setSession(config.sessionCookie);
177
- client.headers['X-Appwrite-Mode'] = 'admin';
178
- logger.debug('Using session authentication for TablesDB adapter', {
179
- project: config.appwriteProject,
180
- operation: 'createTablesDBAdapter'
181
- });
182
- }
183
- else if (config.appwriteKey) {
184
- client.setKey(config.appwriteKey);
185
- client.headers['X-Appwrite-Mode'] = 'default';
186
- logger.debug('Using API key authentication for TablesDB adapter', {
187
- project: config.appwriteProject,
188
- operation: 'createTablesDBAdapter'
189
- });
190
- }
191
- else {
192
- throw new Error("No authentication available for adapter");
193
- }
187
+ throw new Error("No authentication available for adapter");
194
188
  }
195
189
  const adapter = new TablesDBAdapter(client);
196
190
  const totalDuration = Date.now() - startTime;
@@ -204,15 +198,36 @@ export class AdapterFactory {
204
198
  catch (error) {
205
199
  const errorDuration = Date.now() - startTime;
206
200
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
207
- MessageFormatter.warning('Failed to create TablesDB adapter - falling back to legacy', { prefix: "Adapter" });
208
- logger.warn('TablesDB adapter creation failed, falling back to legacy', {
209
- error: errorMessage,
210
- errorDuration,
211
- endpoint: config.appwriteEndpoint,
212
- operation: 'createTablesDBAdapter'
213
- });
214
- // Fallback to legacy adapter if TablesDB creation fails
215
- return this.createLegacyAdapter(config);
201
+ // Analyze the error to determine if fallback is appropriate
202
+ const isAuthError = errorMessage.toLowerCase().includes('unauthorized') ||
203
+ errorMessage.toLowerCase().includes('forbidden') ||
204
+ errorMessage.toLowerCase().includes('invalid') ||
205
+ errorMessage.toLowerCase().includes('authentication');
206
+ const isVersionError = errorMessage.toLowerCase().includes('not found') ||
207
+ errorMessage.toLowerCase().includes('unsupported') ||
208
+ errorMessage.toLowerCase().includes('tablesdb');
209
+ // Only fallback to legacy if this is genuinely a TablesDB support issue
210
+ if (isVersionError) {
211
+ MessageFormatter.warning('TablesDB not supported on this server - using legacy adapter', { prefix: "Adapter" });
212
+ logger.warn('TablesDB not supported, falling back to legacy', {
213
+ error: errorMessage,
214
+ errorDuration,
215
+ endpoint: config.appwriteEndpoint,
216
+ operation: 'createTablesDBAdapter'
217
+ });
218
+ return this.createLegacyAdapter(config);
219
+ }
220
+ else {
221
+ // For auth or other errors, re-throw to surface the real problem
222
+ logger.error('TablesDB adapter creation failed with non-version error', {
223
+ error: errorMessage,
224
+ errorDuration,
225
+ endpoint: config.appwriteEndpoint,
226
+ operation: 'createTablesDBAdapter',
227
+ isAuthError
228
+ });
229
+ throw new AdapterError(`TablesDB adapter creation failed: ${errorMessage}`, 'TABLESDB_ADAPTER_CREATION_FAILED', error instanceof Error ? error : undefined);
230
+ }
216
231
  }
217
232
  }
218
233
  /**
@@ -84,6 +84,7 @@ export interface CreateIndexParams {
84
84
  type: string;
85
85
  attributes: string[];
86
86
  orders?: string[];
87
+ lengths?: number[];
87
88
  }
88
89
  export interface ListIndexesParams {
89
90
  databaseId: string;
@@ -409,12 +409,15 @@ export class LegacyAdapter extends BaseAdapter {
409
409
  break;
410
410
  case 'enum':
411
411
  const enumAttr = existingAttr;
412
- console.log('Updating enum attribute with params:', params);
412
+ // Choose elements to send only when provided, otherwise preserve existing
413
+ const provided = params.elements;
414
+ const existing = enumAttr?.elements;
415
+ const nextElements = (Array.isArray(provided) && provided.length > 0) ? provided : existing;
413
416
  result = await this.databases.updateEnumAttribute({
414
417
  databaseId: params.databaseId,
415
418
  collectionId: params.tableId,
416
419
  key: params.key,
417
- elements: enumAttr.elements || [],
420
+ elements: nextElements,
418
421
  required: params.required ?? enumAttr.required,
419
422
  xdefault: params.default !== undefined ? params.default : enumAttr.default
420
423
  });
@@ -8,6 +8,7 @@
8
8
  import { IndexType, Query, RelationMutate, RelationshipType } from "node-appwrite";
9
9
  import { chunk } from "es-toolkit";
10
10
  import { BaseAdapter, AdapterError } from './DatabaseAdapter.js';
11
+ import {} from 'appwrite-utils';
11
12
  import { TablesDB, Client } from "node-appwrite";
12
13
  /**
13
14
  * TablesDBAdapter implementation for native TablesDB API
@@ -156,8 +157,14 @@ export class TablesDBAdapter extends BaseAdapter {
156
157
  async listIndexes(params) {
157
158
  try {
158
159
  const result = await this.tablesDB.listIndexes(params);
160
+ // Normalize TablesDB response to expose a consistent 'attributes' array
161
+ const normalized = (result.indexes || []).map((idx) => ({
162
+ ...idx,
163
+ attributes: Array.isArray(idx?.attributes) ? idx.attributes : (Array.isArray(idx?.columns) ? idx.columns : []),
164
+ orders: Array.isArray(idx?.orders) ? idx.orders : (Array.isArray(idx?.directions) ? idx.directions : []),
165
+ }));
159
166
  return {
160
- data: result.indexes,
167
+ data: normalized,
161
168
  total: result.total
162
169
  };
163
170
  }
@@ -167,7 +174,15 @@ export class TablesDBAdapter extends BaseAdapter {
167
174
  }
168
175
  async createIndex(params) {
169
176
  try {
170
- const result = await this.tablesDB.createIndex(params.databaseId, params.tableId, params.key, params.type, params.attributes, params.orders || []);
177
+ const result = await this.tablesDB.createIndex({
178
+ databaseId: params.databaseId,
179
+ tableId: params.tableId,
180
+ key: params.key,
181
+ type: params.type,
182
+ columns: params.attributes,
183
+ orders: params.orders || [],
184
+ lengths: params.lengths || [],
185
+ });
171
186
  return { data: result };
172
187
  }
173
188
  catch (error) {
@@ -338,7 +353,7 @@ export class TablesDBAdapter extends BaseAdapter {
338
353
  let result;
339
354
  // Use the appropriate updateXColumn method based on the column type
340
355
  // Cast column to proper Models type to access its specific properties
341
- const columnType = column.type;
356
+ const columnType = (column.type === 'string' && !('format' in column)) || column.type !== 'string' ? column.type : 'enum';
342
357
  switch (columnType) {
343
358
  case 'string':
344
359
  const stringColumn = column;
@@ -474,12 +474,6 @@ const createLegacyAttribute = async (db, dbId, collectionId, attribute) => {
474
474
  * Legacy attribute update using type-specific methods
475
475
  */
476
476
  const updateLegacyAttribute = async (db, dbId, collectionId, attribute) => {
477
- console.log(`DEBUG updateLegacyAttribute before normalizeMinMaxValues:`, {
478
- key: attribute.key,
479
- type: attribute.type,
480
- min: attribute.min,
481
- max: attribute.max
482
- });
483
477
  const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
484
478
  switch (attribute.type) {
485
479
  case "string":
@@ -981,35 +975,10 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
981
975
  // MessageFormatter.info(
982
976
  // `Updating attribute with same key ${attribute.key} but different values`
983
977
  // );
984
- // DEBUG: Log before object merge to detect corruption
985
- if ((attribute.key === 'conversationType' || attribute.key === 'messageStreakCount')) {
986
- console.log(`[DEBUG] MERGE - key="${attribute.key}"`, {
987
- found: {
988
- elements: foundAttribute?.elements,
989
- min: foundAttribute?.min,
990
- max: foundAttribute?.max
991
- },
992
- desired: {
993
- elements: attribute?.elements,
994
- min: attribute?.min,
995
- max: attribute?.max
996
- }
997
- });
998
- }
999
978
  finalAttribute = {
1000
979
  ...foundAttribute,
1001
980
  ...attribute,
1002
981
  };
1003
- // DEBUG: Log after object merge to detect corruption
1004
- if ((finalAttribute.key === 'conversationType' || finalAttribute.key === 'messageStreakCount')) {
1005
- console.log(`[DEBUG] AFTER_MERGE - key="${finalAttribute.key}"`, {
1006
- merged: {
1007
- elements: finalAttribute?.elements,
1008
- min: finalAttribute?.min,
1009
- max: finalAttribute?.max
1010
- }
1011
- });
1012
- }
1013
982
  action = "update";
1014
983
  }
1015
984
  else if (!updateEnabled &&
@@ -1,6 +1,6 @@
1
1
  import { Databases, ID, Permission, Query, } from "node-appwrite";
2
2
  import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
3
- import { nameToIdMapping, processQueue, queuedOperations, clearProcessingState, isCollectionProcessed, markCollectionProcessed } from "../shared/operationQueue.js";
3
+ import { nameToIdMapping, processQueue, queuedOperations, clearProcessingState, isCollectionProcessed, markCollectionProcessed, enqueueOperation } from "../shared/operationQueue.js";
4
4
  import { logger } from "../shared/logging.js";
5
5
  // Legacy attribute/index helpers removed in favor of unified adapter path
6
6
  import { SchemaGenerator } from "../shared/schemaGenerator.js";
@@ -10,6 +10,7 @@ import { MessageFormatter } from "../shared/messageFormatter.js";
10
10
  import { isLegacyDatabases } from "../utils/typeGuards.js";
11
11
  import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
12
12
  import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
13
+ import { createOrUpdateIndexesViaAdapter, deleteObsoleteIndexesViaAdapter } from "../tables/indexManager.js";
13
14
  // Re-export wipe operations
14
15
  export { wipeDatabase, wipeCollection, wipeAllTables, wipeTableRows, } from "./wipeOperations.js";
15
16
  // Re-export transfer operations
@@ -287,63 +288,106 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
287
288
  }
288
289
  MessageFormatter.info(`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`, { prefix: 'Attributes' });
289
290
  }
290
- // Relationship attributes — resolve relatedCollection to ID, then diff and create/update
291
- const rels = (attributes || []).filter((a) => a.type === 'relationship');
292
- if (rels.length > 0) {
293
- for (const attr of rels) {
291
+ // Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
292
+ const relsAll = (attributes || []).filter((a) => a.type === 'relationship');
293
+ if (relsAll.length > 0) {
294
+ const relsResolved = [];
295
+ const relsDeferred = [];
296
+ // Resolve related collections (names -> IDs) using cache or lookup.
297
+ // If not resolvable yet (target table created later in the same push), queue for later.
298
+ for (const attr of relsAll) {
294
299
  const relNameOrId = attr.relatedCollection;
295
300
  if (!relNameOrId)
296
301
  continue;
297
302
  let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
298
- if (!nameToIdMapping.has(relNameOrId)) {
303
+ let resolved = false;
304
+ if (nameToIdMapping.has(relNameOrId)) {
305
+ resolved = true;
306
+ }
307
+ else {
308
+ // Try resolve by name
299
309
  try {
300
310
  const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
301
311
  const relItems = relList.tables || [];
302
312
  if (relItems[0]?.$id) {
303
313
  relId = relItems[0].$id;
304
314
  nameToIdMapping.set(relNameOrId, relId);
315
+ resolved = true;
305
316
  }
306
317
  }
307
318
  catch { }
319
+ // If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
320
+ if (!resolved && relNameOrId && relNameOrId.length >= 10) {
321
+ try {
322
+ const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
323
+ if (probe.data?.$id) {
324
+ nameToIdMapping.set(relNameOrId, relNameOrId);
325
+ relId = relNameOrId;
326
+ resolved = true;
327
+ }
328
+ }
329
+ catch { }
330
+ }
308
331
  }
309
- if (relId && typeof relId === 'string')
332
+ if (resolved && relId && typeof relId === 'string') {
310
333
  attr.relatedCollection = relId;
334
+ relsResolved.push(attr);
335
+ }
336
+ else {
337
+ // Defer until related table exists; queue a surgical operation
338
+ enqueueOperation({
339
+ type: 'attribute',
340
+ collectionId: tableId,
341
+ attribute: attr,
342
+ dependencies: [relNameOrId]
343
+ });
344
+ relsDeferred.push(attr);
345
+ }
311
346
  }
347
+ // Compute a detailed plan for immediately resolvable relationships
312
348
  const tableInfo2 = await adapter.getTable({ databaseId, tableId });
313
349
  const existingCols2 = tableInfo2.data?.columns || tableInfo2.data?.attributes || [];
314
- const { toCreate: relCreate, toUpdate: relUpdate, unchanged: relUnchanged } = diffTableColumns(existingCols2, rels);
315
- // Relationship plan with icons
350
+ const relPlan = diffColumnsDetailed(relsResolved, existingCols2);
351
+ // Relationship plan with icons (includes recreates)
316
352
  {
317
353
  const parts = [];
318
- if (relCreate.length)
319
- parts.push(`➕ ${relCreate.length} (${relCreate.map((a) => a.key).join(', ')})`);
320
- if (relUpdate.length)
321
- parts.push(`🔧 ${relUpdate.length} (${relUpdate.map((a) => a.key).join(', ')})`);
322
- if (relUnchanged.length)
323
- parts.push(`⏭️ ${relUnchanged.length}`);
354
+ if (relPlan.toCreate.length)
355
+ parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a) => a.key).join(', ')})`);
356
+ if (relPlan.toUpdate.length)
357
+ parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u) => u.attribute?.key ?? u.key).join(', ')})`);
358
+ if (relPlan.toRecreate.length)
359
+ parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r) => r.newAttribute?.key ?? r?.key).join(', ')})`);
360
+ if (relPlan.unchanged.length)
361
+ parts.push(`⏭️ ${relPlan.unchanged.length}`);
324
362
  MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
325
363
  }
326
- for (const attr of relUpdate) {
327
- try {
328
- await updateAttr(tableId, attr);
364
+ // Execute plan using the same operation executor to properly handle deletes/recreates
365
+ const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
366
+ if (relResults.success.length > 0) {
367
+ const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
368
+ const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
369
+ if (relResults.success.length !== activeRelationships) {
370
+ // Show both counts when they differ (usually due to recreations)
371
+ MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
329
372
  }
330
- catch (e) {
331
- MessageFormatter.error(`Failed to update relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
373
+ else {
374
+ MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
332
375
  }
333
376
  }
334
- for (const attr of relCreate) {
335
- try {
336
- await createAttr(tableId, attr);
337
- }
338
- catch (e) {
339
- MessageFormatter.error(`Failed to create relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
377
+ if (relResults.errors.length > 0) {
378
+ MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
379
+ for (const err of relResults.errors) {
380
+ MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
340
381
  }
341
382
  }
383
+ if (relsDeferred.length > 0) {
384
+ MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
385
+ }
342
386
  }
343
387
  // Wait for all attributes to become available before creating indexes
344
388
  const allAttrKeys = [
345
389
  ...nonRel.map((a) => a.key),
346
- ...rels.filter((a) => a.relatedCollection).map((a) => a.key)
390
+ ...relsAll.filter((a) => a.relatedCollection).map((a) => a.key)
347
391
  ];
348
392
  if (allAttrKeys.length > 0) {
349
393
  for (const attrKey of allAttrKeys) {
@@ -378,164 +422,14 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
378
422
  }
379
423
  }
380
424
  }
381
- // Prefer local config indexes, but fall back to collection's own indexes if no local config exists (TablesDB path)
425
+ // Index management: create/update indexes using clean adapter-based system
382
426
  const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
383
427
  const idxs = (localTableConfig?.indexes ?? indexes ?? []);
384
- // Compare with existing indexes and create/update accordingly with status checks
385
- try {
386
- const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
387
- const existingIdx = existingIdxRes.data || existingIdxRes.indexes || [];
388
- MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i) => i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
389
- // Show a concise plan with icons before executing
390
- const idxPlanPlus = [];
391
- const idxPlanPlusMinus = [];
392
- const idxPlanSkip = [];
393
- for (const idx of idxs) {
394
- const found = existingIdx.find((i) => i.key === idx.key);
395
- if (found) {
396
- if (isIndexEqualToIndex(found, idx))
397
- idxPlanSkip.push(idx.key);
398
- else
399
- idxPlanPlusMinus.push(idx.key);
400
- }
401
- else
402
- idxPlanPlus.push(idx.key);
403
- }
404
- const planParts = [];
405
- if (idxPlanPlus.length)
406
- planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
407
- if (idxPlanPlusMinus.length)
408
- planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
409
- if (idxPlanSkip.length)
410
- planParts.push(`⏭️ ${idxPlanSkip.length}`);
411
- MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
412
- const created = [];
413
- const updated = [];
414
- const skipped = [];
415
- for (const idx of idxs) {
416
- const found = existingIdx.find((i) => i.key === idx.key);
417
- if (found) {
418
- if (isIndexEqualToIndex(found, idx)) {
419
- MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
420
- skipped.push(idx.key);
421
- }
422
- else {
423
- try {
424
- await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
425
- await delay(100);
426
- }
427
- catch { }
428
- try {
429
- await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
430
- updated.push(idx.key);
431
- }
432
- catch (e) {
433
- const msg = (e?.message || '').toString().toLowerCase();
434
- if (msg.includes('already exists')) {
435
- MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
436
- skipped.push(idx.key);
437
- }
438
- else {
439
- throw e;
440
- }
441
- }
442
- }
443
- }
444
- else {
445
- try {
446
- await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
447
- created.push(idx.key);
448
- }
449
- catch (e) {
450
- const msg = (e?.message || '').toString().toLowerCase();
451
- if (msg.includes('already exists')) {
452
- MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
453
- skipped.push(idx.key);
454
- }
455
- else {
456
- throw e;
457
- }
458
- }
459
- }
460
- // Wait for index availability
461
- const maxWait = 60000;
462
- const start = Date.now();
463
- let lastStatus = '';
464
- while (Date.now() - start < maxWait) {
465
- try {
466
- const li = await adapter.listIndexes({ databaseId, tableId });
467
- const list = li.data || li.indexes || [];
468
- const cur = list.find((i) => i.key === idx.key);
469
- if (cur) {
470
- if (cur.status === 'available')
471
- break;
472
- if (cur.status === 'failed' || cur.status === 'stuck') {
473
- throw new Error(cur.error || `Index ${idx.key} failed`);
474
- }
475
- lastStatus = cur.status;
476
- }
477
- await delay(2000);
478
- }
479
- catch {
480
- await delay(2000);
481
- }
482
- }
483
- await delay(150);
484
- }
485
- MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}`, { prefix: 'Indexes' });
486
- }
487
- catch (e) {
488
- MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
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
- }
428
+ // Create/update indexes with proper planning and execution
429
+ await createOrUpdateIndexesViaAdapter(adapter, databaseId, tableId, idxs, indexes);
430
+ // Handle obsolete index deletions
431
+ const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
432
+ await deleteObsoleteIndexesViaAdapter(adapter, databaseId, tableId, desiredIndexKeys);
539
433
  // Deletions: remove columns/attributes that are present remotely but not in desired config
540
434
  try {
541
435
  const desiredKeys = new Set((attributes || []).map((a) => a.key));
@@ -555,7 +449,9 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
555
449
  const idxRes = await adapter.listIndexes({ databaseId, tableId });
556
450
  const ilist = idxRes.data || idxRes.indexes || [];
557
451
  for (const idx of ilist) {
558
- const attrs = Array.isArray(idx.attributes) ? idx.attributes : [];
452
+ const attrs = Array.isArray(idx.attributes)
453
+ ? idx.attributes
454
+ : (Array.isArray(idx.columns) ? idx.columns : []);
559
455
  if (attrs.includes(key)) {
560
456
  MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
561
457
  await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
@@ -647,6 +543,15 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
647
543
  }
648
544
  }
649
545
  }
546
+ // Process any remaining queued operations to complete relationship sync
547
+ try {
548
+ MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
549
+ await processQueue(adapter, databaseId);
550
+ MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
551
+ }
552
+ catch (error) {
553
+ MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
554
+ }
650
555
  };
651
556
  export const generateMockData = async (database, databaseId, configCollections) => {
652
557
  for (const { collection, mockFunction } of configCollections) {