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
|
@@ -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
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
/**
|
|
@@ -409,12 +409,15 @@ export class LegacyAdapter extends BaseAdapter {
|
|
|
409
409
|
break;
|
|
410
410
|
case 'enum':
|
|
411
411
|
const enumAttr = existingAttr;
|
|
412
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
const
|
|
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
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
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
|
|
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 ||
|
|
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>;
|