appwrite-utils-cli 1.9.6 → 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 &&
@@ -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
@@ -421,164 +422,14 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
421
422
  }
422
423
  }
423
424
  }
424
- // 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
425
426
  const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
426
427
  const idxs = (localTableConfig?.indexes ?? indexes ?? []);
427
- // Compare with existing indexes and create/update accordingly with status checks
428
- try {
429
- const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
430
- const existingIdx = existingIdxRes.data || existingIdxRes.indexes || [];
431
- MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i) => i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
432
- // Show a concise plan with icons before executing
433
- const idxPlanPlus = [];
434
- const idxPlanPlusMinus = [];
435
- const idxPlanSkip = [];
436
- for (const idx of idxs) {
437
- const found = existingIdx.find((i) => i.key === idx.key);
438
- if (found) {
439
- if (isIndexEqualToIndex(found, idx))
440
- idxPlanSkip.push(idx.key);
441
- else
442
- idxPlanPlusMinus.push(idx.key);
443
- }
444
- else
445
- idxPlanPlus.push(idx.key);
446
- }
447
- const planParts = [];
448
- if (idxPlanPlus.length)
449
- planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
450
- if (idxPlanPlusMinus.length)
451
- planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
452
- if (idxPlanSkip.length)
453
- planParts.push(`⏭️ ${idxPlanSkip.length}`);
454
- MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
455
- const created = [];
456
- const updated = [];
457
- const skipped = [];
458
- for (const idx of idxs) {
459
- const found = existingIdx.find((i) => i.key === idx.key);
460
- if (found) {
461
- if (isIndexEqualToIndex(found, idx)) {
462
- MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
463
- skipped.push(idx.key);
464
- }
465
- else {
466
- try {
467
- await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
468
- await delay(100);
469
- }
470
- catch { }
471
- try {
472
- await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
473
- updated.push(idx.key);
474
- }
475
- catch (e) {
476
- const msg = (e?.message || '').toString().toLowerCase();
477
- if (msg.includes('already exists')) {
478
- MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
479
- skipped.push(idx.key);
480
- }
481
- else {
482
- throw e;
483
- }
484
- }
485
- }
486
- }
487
- else {
488
- try {
489
- await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
490
- created.push(idx.key);
491
- }
492
- catch (e) {
493
- const msg = (e?.message || '').toString().toLowerCase();
494
- if (msg.includes('already exists')) {
495
- MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
496
- skipped.push(idx.key);
497
- }
498
- else {
499
- throw e;
500
- }
501
- }
502
- }
503
- // Wait for index availability
504
- const maxWait = 60000;
505
- const start = Date.now();
506
- let lastStatus = '';
507
- while (Date.now() - start < maxWait) {
508
- try {
509
- const li = await adapter.listIndexes({ databaseId, tableId });
510
- const list = li.data || li.indexes || [];
511
- const cur = list.find((i) => i.key === idx.key);
512
- if (cur) {
513
- if (cur.status === 'available')
514
- break;
515
- if (cur.status === 'failed' || cur.status === 'stuck') {
516
- throw new Error(cur.error || `Index ${idx.key} failed`);
517
- }
518
- lastStatus = cur.status;
519
- }
520
- await delay(2000);
521
- }
522
- catch {
523
- await delay(2000);
524
- }
525
- }
526
- await delay(150);
527
- }
528
- MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}`, { prefix: 'Indexes' });
529
- }
530
- catch (e) {
531
- MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
532
- }
533
- // Deletions for indexes: remove remote indexes not declared in YAML/config
534
- try {
535
- const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
536
- const idxRes = await adapter.listIndexes({ databaseId, tableId });
537
- const existingIdx = idxRes.data || idxRes.indexes || [];
538
- const extraIdx = existingIdx
539
- .filter((i) => i?.key && !desiredIndexKeys.has(i.key))
540
- .map((i) => i.key);
541
- if (extraIdx.length > 0) {
542
- MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
543
- const deleted = [];
544
- const errors = [];
545
- for (const key of extraIdx) {
546
- try {
547
- await adapter.deleteIndex({ databaseId, tableId, key });
548
- // Optionally wait for index to disappear
549
- const start = Date.now();
550
- const maxWait = 30000;
551
- while (Date.now() - start < maxWait) {
552
- try {
553
- const li = await adapter.listIndexes({ databaseId, tableId });
554
- const list = li.data || li.indexes || [];
555
- if (!list.find((ix) => ix.key === key))
556
- break;
557
- }
558
- catch { }
559
- await delay(1000);
560
- }
561
- deleted.push(key);
562
- }
563
- catch (e) {
564
- errors.push({ key, error: e?.message || String(e) });
565
- }
566
- }
567
- if (deleted.length) {
568
- MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
569
- }
570
- if (errors.length) {
571
- MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
572
- errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
573
- }
574
- }
575
- else {
576
- MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
577
- }
578
- }
579
- catch (e) {
580
- MessageFormatter.warning(`Could not evaluate index deletions: ${e?.message || e}`, { prefix: 'Indexes' });
581
- }
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);
582
433
  // Deletions: remove columns/attributes that are present remotely but not in desired config
583
434
  try {
584
435
  const desiredKeys = new Set((attributes || []).map((a) => a.key));
@@ -598,7 +449,9 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
598
449
  const idxRes = await adapter.listIndexes({ databaseId, tableId });
599
450
  const ilist = idxRes.data || idxRes.indexes || [];
600
451
  for (const idx of ilist) {
601
- 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 : []);
602
455
  if (attrs.includes(key)) {
603
456
  MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
604
457
  await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
@@ -90,11 +90,13 @@ export function normalizeAttributeToComparable(attr) {
90
90
  return base;
91
91
  }
92
92
  export function normalizeColumnToComparable(col) {
93
- // Detect enum surfaced as string+elements from server and normalize to enum for comparison
93
+ // Detect enum surfaced as string+elements or string+format:enum from server and normalize to enum for comparison
94
94
  let t = String((col?.type ?? col?.columnType ?? '')).toLowerCase();
95
95
  const hasElements = Array.isArray(col?.elements) && col.elements.length > 0;
96
- if (t === 'string' && hasElements)
96
+ const hasEnumFormat = (col?.format === 'enum');
97
+ if (t === 'string' && (hasElements || hasEnumFormat)) {
97
98
  t = 'enum';
99
+ }
98
100
  const base = {
99
101
  key: col?.key,
100
102
  type: t,
@@ -185,18 +187,27 @@ export function isIndexEqualToIndex(a, b) {
185
187
  if (String(a.type).toLowerCase() !== String(b.type).toLowerCase())
186
188
  return false;
187
189
  // Compare attributes as sets (order-insensitive)
188
- const attrsA = Array.isArray(a.attributes) ? [...a.attributes].sort() : [];
189
- const attrsB = Array.isArray(b.attributes) ? [...b.attributes].sort() : [];
190
+ // Support TablesDB which returns 'columns' instead of 'attributes'
191
+ const attrsAraw = Array.isArray(a.attributes)
192
+ ? a.attributes
193
+ : (Array.isArray(a.columns) ? a.columns : []);
194
+ const attrsA = [...attrsAraw].sort();
195
+ const attrsB = Array.isArray(b.attributes)
196
+ ? [...b.attributes].sort()
197
+ : (Array.isArray(b.columns) ? [...b.columns].sort() : []);
190
198
  if (attrsA.length !== attrsB.length)
191
199
  return false;
192
200
  for (let i = 0; i < attrsA.length; i++)
193
201
  if (attrsA[i] !== attrsB[i])
194
202
  return false;
195
- // Orders are only considered if BOTH have orders defined
196
- const hasOrdersA = Array.isArray(a.orders) && a.orders.length > 0;
197
- const hasOrdersB = Array.isArray(b.orders) && b.orders.length > 0;
198
- if (hasOrdersA && hasOrdersB) {
199
- const ordersA = [...a.orders].sort();
203
+ // Orders are only considered if CONFIG (b) has orders defined
204
+ // This prevents false positives when Appwrite returns orders but user didn't specify them
205
+ const hasConfigOrders = Array.isArray(b.orders) && b.orders.length > 0;
206
+ if (hasConfigOrders) {
207
+ // Some APIs may expose 'directions' instead of 'orders'
208
+ const ordersA = Array.isArray(a.orders)
209
+ ? [...a.orders].sort()
210
+ : (Array.isArray(a.directions) ? [...a.directions].sort() : []);
200
211
  const ordersB = [...b.orders].sort();
201
212
  if (ordersA.length !== ordersB.length)
202
213
  return false;
@@ -204,7 +215,6 @@ export function isIndexEqualToIndex(a, b) {
204
215
  if (ordersA[i] !== ordersB[i])
205
216
  return false;
206
217
  }
207
- // If only one side has orders, treat as equal (orders unspecified by user)
208
218
  return true;
209
219
  }
210
220
  /**
@@ -213,6 +223,7 @@ export function isIndexEqualToIndex(a, b) {
213
223
  function compareColumnProperties(oldColumn, newAttribute, columnType) {
214
224
  const changes = [];
215
225
  const t = String(columnType || newAttribute.type || '').toLowerCase();
226
+ const key = newAttribute?.key || 'unknown';
216
227
  const mutableProps = MUTABLE_PROPERTIES[t] || [];
217
228
  const immutableProps = IMMUTABLE_PROPERTIES[t] || [];
218
229
  const getNewVal = (prop) => {
@@ -233,8 +244,9 @@ function compareColumnProperties(oldColumn, newAttribute, columnType) {
233
244
  let newValue = getNewVal(prop);
234
245
  // Special-case: enum elements empty/missing should not trigger updates
235
246
  if (t === 'enum' && prop === 'elements') {
236
- if (!Array.isArray(newValue) || newValue.length === 0)
247
+ if (!Array.isArray(newValue) || newValue.length === 0) {
237
248
  newValue = oldValue;
249
+ }
238
250
  }
239
251
  if (Array.isArray(oldValue) && Array.isArray(newValue)) {
240
252
  if (oldValue.length !== newValue.length || oldValue.some((v, i) => v !== newValue[i])) {
@@ -260,7 +272,8 @@ function compareColumnProperties(oldColumn, newAttribute, columnType) {
260
272
  // Type change requires recreate (normalize string+elements to enum on old side)
261
273
  const oldTypeRaw = String(oldColumn?.type || oldColumn?.columnType || '').toLowerCase();
262
274
  const oldHasElements = Array.isArray(oldColumn?.elements) && oldColumn.elements.length > 0;
263
- const oldType = oldTypeRaw === 'string' && oldHasElements ? 'enum' : oldTypeRaw;
275
+ const oldHasEnumFormat = (oldColumn?.format === 'enum');
276
+ const oldType = oldTypeRaw === 'string' && (oldHasElements || oldHasEnumFormat) ? 'enum' : oldTypeRaw;
264
277
  if (oldType && t && oldType !== t && TYPE_CHANGE_REQUIRES_RECREATE.includes(oldType)) {
265
278
  changes.push({ property: 'type', oldValue: oldType, newValue: t, requiresRecreate: true });
266
279
  }
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import { ConfigDiscoveryService, ConfigLoaderService, ConfigMergeService, ConfigValidationService, SessionAuthService, } from "./services/index.js";
4
4
  import { MessageFormatter } from "../shared/messageFormatter.js";
5
5
  import { logger } from "../shared/logging.js";
6
+ import { detectAppwriteVersionCached } from "../utils/versionDetection.js";
6
7
  /**
7
8
  * Centralized configuration manager with intelligent caching and session management.
8
9
  *
@@ -178,6 +179,30 @@ export class ConfigManager {
178
179
  `Run with reportValidation: true to see details.`);
179
180
  }
180
181
  }
182
+ // 8. Run version detection and set apiMode if not explicitly configured
183
+ if (!config.apiMode || config.apiMode === 'auto') {
184
+ try {
185
+ logger.debug('Running version detection for API mode detection', {
186
+ prefix: "ConfigManager",
187
+ endpoint: config.appwriteEndpoint
188
+ });
189
+ const versionResult = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
190
+ config.apiMode = versionResult.apiMode;
191
+ logger.info(`API mode detected: ${config.apiMode}`, {
192
+ prefix: "ConfigManager",
193
+ method: versionResult.detectionMethod,
194
+ confidence: versionResult.confidence,
195
+ serverVersion: versionResult.serverVersion,
196
+ });
197
+ }
198
+ catch (error) {
199
+ logger.warn('Version detection failed, defaulting to legacy mode', {
200
+ prefix: "ConfigManager",
201
+ error: error instanceof Error ? error.message : 'Unknown error',
202
+ });
203
+ config.apiMode = 'legacy';
204
+ }
205
+ }
181
206
  // 8. Cache the config
182
207
  this.cachedConfig = config;
183
208
  this.cachedConfigPath = configPath;
@@ -140,7 +140,7 @@ export function mapToCreateAttributeParams(attr, base) {
140
140
  * so we never send an empty elements array (preserve existing on server).
141
141
  */
142
142
  export function mapToUpdateAttributeParams(attr, base) {
143
- const type = String(attr.type || "").toLowerCase();
143
+ const type = String((attr.type == 'string' && attr.format !== 'enum') || attr.type !== 'string' ? attr.type : 'enum').toLowerCase();
144
144
  const params = {
145
145
  databaseId: base.databaseId,
146
146
  tableId: base.tableId,
@@ -0,0 +1,65 @@
1
+ import type { Index } from "appwrite-utils";
2
+ import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
3
+ import type { Models } from "node-appwrite";
4
+ export interface IndexOperation {
5
+ type: 'create' | 'update' | 'skip' | 'delete';
6
+ index: Index;
7
+ existingIndex?: Models.Index;
8
+ reason?: string;
9
+ }
10
+ export interface IndexOperationPlan {
11
+ toCreate: IndexOperation[];
12
+ toUpdate: IndexOperation[];
13
+ toSkip: IndexOperation[];
14
+ toDelete: IndexOperation[];
15
+ }
16
+ export interface IndexExecutionResult {
17
+ created: string[];
18
+ updated: string[];
19
+ skipped: string[];
20
+ deleted: string[];
21
+ errors: Array<{
22
+ key: string;
23
+ error: string;
24
+ }>;
25
+ summary: {
26
+ total: number;
27
+ created: number;
28
+ updated: number;
29
+ skipped: number;
30
+ deleted: number;
31
+ errors: number;
32
+ };
33
+ }
34
+ /**
35
+ * Plan index operations by comparing desired indexes with existing ones
36
+ * Uses the existing isIndexEqualToIndex function for consistent comparison
37
+ */
38
+ export declare function planIndexOperations(desiredIndexes: Index[], existingIndexes: Models.Index[]): IndexOperationPlan;
39
+ /**
40
+ * Plan index deletions for indexes that exist but aren't in the desired configuration
41
+ */
42
+ export declare function planIndexDeletions(desiredIndexKeys: Set<string>, existingIndexes: Models.Index[]): IndexOperation[];
43
+ /**
44
+ * Execute index operations with proper error handling and status monitoring
45
+ */
46
+ export declare function executeIndexOperations(adapter: DatabaseAdapter, databaseId: string, tableId: string, plan: IndexOperationPlan): Promise<IndexExecutionResult>;
47
+ /**
48
+ * Execute index deletions with proper error handling
49
+ */
50
+ export declare function executeIndexDeletions(adapter: DatabaseAdapter, databaseId: string, tableId: string, deletions: IndexOperation[]): Promise<{
51
+ deleted: string[];
52
+ errors: Array<{
53
+ key: string;
54
+ error: string;
55
+ }>;
56
+ }>;
57
+ /**
58
+ * Main function to create/update indexes via adapter
59
+ * This replaces the messy inline code in methods.ts
60
+ */
61
+ export declare function createOrUpdateIndexesViaAdapter(adapter: DatabaseAdapter, databaseId: string, tableId: string, desiredIndexes: Index[], configIndexes?: Index[]): Promise<void>;
62
+ /**
63
+ * Handle index deletions for obsolete indexes
64
+ */
65
+ export declare function deleteObsoleteIndexesViaAdapter(adapter: DatabaseAdapter, databaseId: string, tableId: string, desiredIndexKeys: Set<string>): Promise<void>;