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