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.
- package/dist/adapters/AdapterFactory.d.ts +1 -1
- package/dist/adapters/AdapterFactory.js +52 -37
- package/dist/adapters/DatabaseAdapter.d.ts +1 -0
- package/dist/adapters/LegacyAdapter.js +5 -2
- package/dist/adapters/TablesDBAdapter.js +18 -3
- package/dist/collections/attributes.js +0 -31
- package/dist/collections/methods.js +10 -157
- package/dist/collections/tableOperations.js +25 -12
- package/dist/config/ConfigManager.js +25 -0
- package/dist/shared/attributeMapper.js +1 -1
- package/dist/tables/indexManager.d.ts +65 -0
- package/dist/tables/indexManager.js +294 -0
- package/package.json +1 -1
- package/src/adapters/AdapterFactory.ts +146 -127
- package/src/adapters/DatabaseAdapter.ts +1 -0
- package/src/adapters/LegacyAdapter.ts +5 -2
- package/src/adapters/TablesDBAdapter.ts +18 -10
- package/src/collections/attributes.ts +0 -34
- package/src/collections/methods.ts +11 -126
- package/src/collections/tableOperations.ts +28 -13
- package/src/config/ConfigManager.ts +32 -0
- package/src/shared/attributeMapper.ts +1 -1
- package/src/tables/indexManager.ts +409 -0
- package/dist/shared/indexManager.d.ts +0 -24
- package/dist/shared/indexManager.js +0 -151
- package/src/shared/indexManager.ts +0 -254
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type AdapterMetadata,
|
|
34
34
|
AdapterError
|
|
35
35
|
} from './DatabaseAdapter.js';
|
|
36
|
+
import { type Column } from 'appwrite-utils';
|
|
36
37
|
import { TablesDB, Client } from "node-appwrite";
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -256,8 +257,14 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
|
256
257
|
async listIndexes(params: ListIndexesParams): Promise<ApiResponse> {
|
|
257
258
|
try {
|
|
258
259
|
const result = await this.tablesDB.listIndexes(params);
|
|
260
|
+
// Normalize TablesDB response to expose a consistent 'attributes' array
|
|
261
|
+
const normalized = (result.indexes || []).map((idx: any) => ({
|
|
262
|
+
...idx,
|
|
263
|
+
attributes: Array.isArray(idx?.attributes) ? idx.attributes : (Array.isArray(idx?.columns) ? idx.columns : []),
|
|
264
|
+
orders: Array.isArray(idx?.orders) ? idx.orders : (Array.isArray(idx?.directions) ? idx.directions : []),
|
|
265
|
+
}));
|
|
259
266
|
return {
|
|
260
|
-
data:
|
|
267
|
+
data: normalized,
|
|
261
268
|
total: result.total
|
|
262
269
|
};
|
|
263
270
|
} catch (error) {
|
|
@@ -271,14 +278,15 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
|
271
278
|
|
|
272
279
|
async createIndex(params: CreateIndexParams): Promise<ApiResponse> {
|
|
273
280
|
try {
|
|
274
|
-
const result = await this.tablesDB.createIndex(
|
|
275
|
-
params.databaseId,
|
|
276
|
-
params.tableId,
|
|
277
|
-
params.key,
|
|
278
|
-
params.type as IndexType,
|
|
279
|
-
params.attributes,
|
|
280
|
-
params.orders || []
|
|
281
|
-
|
|
281
|
+
const result = await this.tablesDB.createIndex({
|
|
282
|
+
databaseId: params.databaseId,
|
|
283
|
+
tableId: params.tableId,
|
|
284
|
+
key: params.key,
|
|
285
|
+
type: params.type as IndexType,
|
|
286
|
+
columns: params.attributes,
|
|
287
|
+
orders: params.orders || [],
|
|
288
|
+
lengths: params.lengths || [],
|
|
289
|
+
});
|
|
282
290
|
return { data: result };
|
|
283
291
|
} catch (error) {
|
|
284
292
|
throw new AdapterError(
|
|
@@ -494,7 +502,7 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
|
494
502
|
|
|
495
503
|
// Use the appropriate updateXColumn method based on the column type
|
|
496
504
|
// Cast column to proper Models type to access its specific properties
|
|
497
|
-
const columnType = (column
|
|
505
|
+
const columnType = (column.type === 'string' && !('format' in column)) || column.type !== 'string' ? column.type : 'enum';
|
|
498
506
|
|
|
499
507
|
switch (columnType) {
|
|
500
508
|
case 'string':
|
|
@@ -651,13 +651,6 @@ const updateLegacyAttribute = async (
|
|
|
651
651
|
collectionId: string,
|
|
652
652
|
attribute: Attribute
|
|
653
653
|
): Promise<void> => {
|
|
654
|
-
console.log(`DEBUG updateLegacyAttribute before normalizeMinMaxValues:`, {
|
|
655
|
-
key: attribute.key,
|
|
656
|
-
type: attribute.type,
|
|
657
|
-
min: (attribute as any).min,
|
|
658
|
-
max: (attribute as any).max
|
|
659
|
-
});
|
|
660
|
-
|
|
661
654
|
const { min: normalizedMin, max: normalizedMax } =
|
|
662
655
|
normalizeMinMaxValues(attribute);
|
|
663
656
|
|
|
@@ -1515,37 +1508,10 @@ export const createOrUpdateAttribute = async (
|
|
|
1515
1508
|
// `Updating attribute with same key ${attribute.key} but different values`
|
|
1516
1509
|
// );
|
|
1517
1510
|
|
|
1518
|
-
// DEBUG: Log before object merge to detect corruption
|
|
1519
|
-
if ((attribute.key === 'conversationType' || attribute.key === 'messageStreakCount')) {
|
|
1520
|
-
console.log(`[DEBUG] MERGE - key="${attribute.key}"`, {
|
|
1521
|
-
found: {
|
|
1522
|
-
elements: (foundAttribute as any)?.elements,
|
|
1523
|
-
min: (foundAttribute as any)?.min,
|
|
1524
|
-
max: (foundAttribute as any)?.max
|
|
1525
|
-
},
|
|
1526
|
-
desired: {
|
|
1527
|
-
elements: (attribute as any)?.elements,
|
|
1528
|
-
min: (attribute as any)?.min,
|
|
1529
|
-
max: (attribute as any)?.max
|
|
1530
|
-
}
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
1511
|
finalAttribute = {
|
|
1535
1512
|
...foundAttribute,
|
|
1536
1513
|
...attribute,
|
|
1537
1514
|
};
|
|
1538
|
-
|
|
1539
|
-
// DEBUG: Log after object merge to detect corruption
|
|
1540
|
-
if ((finalAttribute.key === 'conversationType' || finalAttribute.key === 'messageStreakCount')) {
|
|
1541
|
-
console.log(`[DEBUG] AFTER_MERGE - key="${finalAttribute.key}"`, {
|
|
1542
|
-
merged: {
|
|
1543
|
-
elements: finalAttribute?.elements,
|
|
1544
|
-
min: (finalAttribute as any)?.min,
|
|
1545
|
-
max: (finalAttribute as any)?.max
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
1515
|
action = "update";
|
|
1550
1516
|
} else if (
|
|
1551
1517
|
!updateEnabled &&
|
|
@@ -32,6 +32,7 @@ import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
|
32
32
|
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
33
33
|
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
|
|
34
34
|
import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
|
|
35
|
+
import { createOrUpdateIndexesViaAdapter, deleteObsoleteIndexesViaAdapter } from "../tables/indexManager.js";
|
|
35
36
|
|
|
36
37
|
// Re-export wipe operations
|
|
37
38
|
export {
|
|
@@ -517,136 +518,18 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
517
518
|
}
|
|
518
519
|
}
|
|
519
520
|
|
|
520
|
-
//
|
|
521
|
+
// Index management: create/update indexes using clean adapter-based system
|
|
521
522
|
const localTableConfig = config.collections?.find(
|
|
522
523
|
c => c.name === collectionData.name || c.$id === collectionData.$id
|
|
523
524
|
);
|
|
524
525
|
const idxs = (localTableConfig?.indexes ?? indexes ?? []) as any[];
|
|
525
|
-
// Compare with existing indexes and create/update accordingly with status checks
|
|
526
|
-
try {
|
|
527
|
-
const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
528
|
-
const existingIdx: any[] = (existingIdxRes as any).data || (existingIdxRes as any).indexes || [];
|
|
529
|
-
MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i:any)=>i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
|
|
530
|
-
// Show a concise plan with icons before executing
|
|
531
|
-
const idxPlanPlus: string[] = [];
|
|
532
|
-
const idxPlanPlusMinus: string[] = [];
|
|
533
|
-
const idxPlanSkip: string[] = [];
|
|
534
|
-
for (const idx of idxs) {
|
|
535
|
-
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
536
|
-
if (found) {
|
|
537
|
-
if (isIndexEqualToIndex(found, idx)) idxPlanSkip.push(idx.key);
|
|
538
|
-
else idxPlanPlusMinus.push(idx.key);
|
|
539
|
-
} else idxPlanPlus.push(idx.key);
|
|
540
|
-
}
|
|
541
|
-
const planParts: string[] = [];
|
|
542
|
-
if (idxPlanPlus.length) planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
|
|
543
|
-
if (idxPlanPlusMinus.length) planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
|
|
544
|
-
if (idxPlanSkip.length) planParts.push(`⏭️ ${idxPlanSkip.length}`);
|
|
545
|
-
MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
|
|
546
|
-
const created: string[] = [];
|
|
547
|
-
const updated: string[] = [];
|
|
548
|
-
const skipped: string[] = [];
|
|
549
|
-
for (const idx of idxs) {
|
|
550
|
-
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
551
|
-
if (found) {
|
|
552
|
-
if (isIndexEqualToIndex(found, idx)) {
|
|
553
|
-
MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
|
|
554
|
-
skipped.push(idx.key);
|
|
555
|
-
} else {
|
|
556
|
-
try { await adapter.deleteIndex({ databaseId, tableId, key: idx.key }); await delay(100); } catch {}
|
|
557
|
-
try {
|
|
558
|
-
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
559
|
-
updated.push(idx.key);
|
|
560
|
-
} catch (e: any) {
|
|
561
|
-
const msg = (e?.message || '').toString().toLowerCase();
|
|
562
|
-
if (msg.includes('already exists')) {
|
|
563
|
-
MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
|
|
564
|
-
skipped.push(idx.key);
|
|
565
|
-
} else {
|
|
566
|
-
throw e;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
} else {
|
|
571
|
-
try {
|
|
572
|
-
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
573
|
-
created.push(idx.key);
|
|
574
|
-
} catch (e: any) {
|
|
575
|
-
const msg = (e?.message || '').toString().toLowerCase();
|
|
576
|
-
if (msg.includes('already exists')) {
|
|
577
|
-
MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
|
|
578
|
-
skipped.push(idx.key);
|
|
579
|
-
} else {
|
|
580
|
-
throw e;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
// Wait for index availability
|
|
585
|
-
const maxWait = 60000; const start = Date.now(); let lastStatus = '';
|
|
586
|
-
while (Date.now() - start < maxWait) {
|
|
587
|
-
try {
|
|
588
|
-
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
589
|
-
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
590
|
-
const cur = list.find((i: any) => i.key === idx.key);
|
|
591
|
-
if (cur) {
|
|
592
|
-
if (cur.status === 'available') break;
|
|
593
|
-
if (cur.status === 'failed' || cur.status === 'stuck') { throw new Error(cur.error || `Index ${idx.key} failed`); }
|
|
594
|
-
lastStatus = cur.status;
|
|
595
|
-
}
|
|
596
|
-
await delay(2000);
|
|
597
|
-
} catch { await delay(2000); }
|
|
598
|
-
}
|
|
599
|
-
await delay(150);
|
|
600
|
-
}
|
|
601
|
-
MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}` , { prefix: 'Indexes' });
|
|
602
|
-
} catch (e) {
|
|
603
|
-
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
604
|
-
}
|
|
605
526
|
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
.filter((i: any) => i?.key && !desiredIndexKeys.has(i.key))
|
|
613
|
-
.map((i: any) => i.key as string);
|
|
614
|
-
if (extraIdx.length > 0) {
|
|
615
|
-
MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
|
|
616
|
-
const deleted: string[] = [];
|
|
617
|
-
const errors: Array<{ key: string; error: string }> = [];
|
|
618
|
-
for (const key of extraIdx) {
|
|
619
|
-
try {
|
|
620
|
-
await adapter.deleteIndex({ databaseId, tableId, key });
|
|
621
|
-
// Optionally wait for index to disappear
|
|
622
|
-
const start = Date.now();
|
|
623
|
-
const maxWait = 30000;
|
|
624
|
-
while (Date.now() - start < maxWait) {
|
|
625
|
-
try {
|
|
626
|
-
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
627
|
-
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
628
|
-
if (!list.find((ix: any) => ix.key === key)) break;
|
|
629
|
-
} catch {}
|
|
630
|
-
await delay(1000);
|
|
631
|
-
}
|
|
632
|
-
deleted.push(key);
|
|
633
|
-
} catch (e: any) {
|
|
634
|
-
errors.push({ key, error: e?.message || String(e) });
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
if (deleted.length) {
|
|
638
|
-
MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
|
|
639
|
-
}
|
|
640
|
-
if (errors.length) {
|
|
641
|
-
MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
|
|
642
|
-
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
|
|
643
|
-
}
|
|
644
|
-
} else {
|
|
645
|
-
MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
|
|
646
|
-
}
|
|
647
|
-
} catch (e) {
|
|
648
|
-
MessageFormatter.warning(`Could not evaluate index deletions: ${(e as Error)?.message || e}`, { prefix: 'Indexes' });
|
|
649
|
-
}
|
|
527
|
+
// Create/update indexes with proper planning and execution
|
|
528
|
+
await createOrUpdateIndexesViaAdapter(adapter, databaseId, tableId, idxs, indexes);
|
|
529
|
+
|
|
530
|
+
// Handle obsolete index deletions
|
|
531
|
+
const desiredIndexKeys: Set<string> = new Set((indexes || []).map((i: any) => i.key as string));
|
|
532
|
+
await deleteObsoleteIndexesViaAdapter(adapter, databaseId, tableId, desiredIndexKeys);
|
|
650
533
|
|
|
651
534
|
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
652
535
|
try {
|
|
@@ -668,7 +551,9 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
668
551
|
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
669
552
|
const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
670
553
|
for (const idx of ilist) {
|
|
671
|
-
const attrs: string[] = Array.isArray(idx.attributes)
|
|
554
|
+
const attrs: string[] = Array.isArray(idx.attributes)
|
|
555
|
+
? idx.attributes
|
|
556
|
+
: (Array.isArray((idx as any).columns) ? (idx as any).columns : []);
|
|
672
557
|
if (attrs.includes(key)) {
|
|
673
558
|
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
674
559
|
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
@@ -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
|
-
|
|
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
|
-
|
|
231
|
-
const
|
|
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
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
|
|
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)
|
|
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
|
|
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
|
|
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,
|