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
|
@@ -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 &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Databases, ID, Permission, Query, } from "node-appwrite";
|
|
2
2
|
import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
|
|
3
|
-
import { nameToIdMapping, processQueue, queuedOperations, clearProcessingState, isCollectionProcessed, markCollectionProcessed } from "../shared/operationQueue.js";
|
|
3
|
+
import { nameToIdMapping, processQueue, queuedOperations, clearProcessingState, isCollectionProcessed, markCollectionProcessed, enqueueOperation } from "../shared/operationQueue.js";
|
|
4
4
|
import { logger } from "../shared/logging.js";
|
|
5
5
|
// Legacy attribute/index helpers removed in favor of unified adapter path
|
|
6
6
|
import { SchemaGenerator } from "../shared/schemaGenerator.js";
|
|
@@ -10,6 +10,7 @@ import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
|
10
10
|
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
11
11
|
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
|
|
12
12
|
import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
|
|
13
|
+
import { createOrUpdateIndexesViaAdapter, deleteObsoleteIndexesViaAdapter } from "../tables/indexManager.js";
|
|
13
14
|
// Re-export wipe operations
|
|
14
15
|
export { wipeDatabase, wipeCollection, wipeAllTables, wipeTableRows, } from "./wipeOperations.js";
|
|
15
16
|
// Re-export transfer operations
|
|
@@ -287,63 +288,106 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
287
288
|
}
|
|
288
289
|
MessageFormatter.info(`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`, { prefix: 'Attributes' });
|
|
289
290
|
}
|
|
290
|
-
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
291
|
+
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
|
|
292
|
+
const relsAll = (attributes || []).filter((a) => a.type === 'relationship');
|
|
293
|
+
if (relsAll.length > 0) {
|
|
294
|
+
const relsResolved = [];
|
|
295
|
+
const relsDeferred = [];
|
|
296
|
+
// Resolve related collections (names -> IDs) using cache or lookup.
|
|
297
|
+
// If not resolvable yet (target table created later in the same push), queue for later.
|
|
298
|
+
for (const attr of relsAll) {
|
|
294
299
|
const relNameOrId = attr.relatedCollection;
|
|
295
300
|
if (!relNameOrId)
|
|
296
301
|
continue;
|
|
297
302
|
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
|
298
|
-
|
|
303
|
+
let resolved = false;
|
|
304
|
+
if (nameToIdMapping.has(relNameOrId)) {
|
|
305
|
+
resolved = true;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Try resolve by name
|
|
299
309
|
try {
|
|
300
310
|
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
|
301
311
|
const relItems = relList.tables || [];
|
|
302
312
|
if (relItems[0]?.$id) {
|
|
303
313
|
relId = relItems[0].$id;
|
|
304
314
|
nameToIdMapping.set(relNameOrId, relId);
|
|
315
|
+
resolved = true;
|
|
305
316
|
}
|
|
306
317
|
}
|
|
307
318
|
catch { }
|
|
319
|
+
// If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
|
|
320
|
+
if (!resolved && relNameOrId && relNameOrId.length >= 10) {
|
|
321
|
+
try {
|
|
322
|
+
const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
|
|
323
|
+
if (probe.data?.$id) {
|
|
324
|
+
nameToIdMapping.set(relNameOrId, relNameOrId);
|
|
325
|
+
relId = relNameOrId;
|
|
326
|
+
resolved = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
}
|
|
308
331
|
}
|
|
309
|
-
if (relId && typeof relId === 'string')
|
|
332
|
+
if (resolved && relId && typeof relId === 'string') {
|
|
310
333
|
attr.relatedCollection = relId;
|
|
334
|
+
relsResolved.push(attr);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Defer until related table exists; queue a surgical operation
|
|
338
|
+
enqueueOperation({
|
|
339
|
+
type: 'attribute',
|
|
340
|
+
collectionId: tableId,
|
|
341
|
+
attribute: attr,
|
|
342
|
+
dependencies: [relNameOrId]
|
|
343
|
+
});
|
|
344
|
+
relsDeferred.push(attr);
|
|
345
|
+
}
|
|
311
346
|
}
|
|
347
|
+
// Compute a detailed plan for immediately resolvable relationships
|
|
312
348
|
const tableInfo2 = await adapter.getTable({ databaseId, tableId });
|
|
313
349
|
const existingCols2 = tableInfo2.data?.columns || tableInfo2.data?.attributes || [];
|
|
314
|
-
const
|
|
315
|
-
// Relationship plan with icons
|
|
350
|
+
const relPlan = diffColumnsDetailed(relsResolved, existingCols2);
|
|
351
|
+
// Relationship plan with icons (includes recreates)
|
|
316
352
|
{
|
|
317
353
|
const parts = [];
|
|
318
|
-
if (
|
|
319
|
-
parts.push(`➕ ${
|
|
320
|
-
if (
|
|
321
|
-
parts.push(`🔧 ${
|
|
322
|
-
if (
|
|
323
|
-
parts.push(
|
|
354
|
+
if (relPlan.toCreate.length)
|
|
355
|
+
parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a) => a.key).join(', ')})`);
|
|
356
|
+
if (relPlan.toUpdate.length)
|
|
357
|
+
parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u) => u.attribute?.key ?? u.key).join(', ')})`);
|
|
358
|
+
if (relPlan.toRecreate.length)
|
|
359
|
+
parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r) => r.newAttribute?.key ?? r?.key).join(', ')})`);
|
|
360
|
+
if (relPlan.unchanged.length)
|
|
361
|
+
parts.push(`⏭️ ${relPlan.unchanged.length}`);
|
|
324
362
|
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
|
|
325
363
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
364
|
+
// Execute plan using the same operation executor to properly handle deletes/recreates
|
|
365
|
+
const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
|
|
366
|
+
if (relResults.success.length > 0) {
|
|
367
|
+
const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
|
|
368
|
+
const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
|
|
369
|
+
if (relResults.success.length !== activeRelationships) {
|
|
370
|
+
// Show both counts when they differ (usually due to recreations)
|
|
371
|
+
MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
329
372
|
}
|
|
330
|
-
|
|
331
|
-
MessageFormatter.
|
|
373
|
+
else {
|
|
374
|
+
MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
332
375
|
}
|
|
333
376
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
catch (e) {
|
|
339
|
-
MessageFormatter.error(`Failed to create relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
|
|
377
|
+
if (relResults.errors.length > 0) {
|
|
378
|
+
MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
|
|
379
|
+
for (const err of relResults.errors) {
|
|
380
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
|
|
340
381
|
}
|
|
341
382
|
}
|
|
383
|
+
if (relsDeferred.length > 0) {
|
|
384
|
+
MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
|
|
385
|
+
}
|
|
342
386
|
}
|
|
343
387
|
// Wait for all attributes to become available before creating indexes
|
|
344
388
|
const allAttrKeys = [
|
|
345
389
|
...nonRel.map((a) => a.key),
|
|
346
|
-
...
|
|
390
|
+
...relsAll.filter((a) => a.relatedCollection).map((a) => a.key)
|
|
347
391
|
];
|
|
348
392
|
if (allAttrKeys.length > 0) {
|
|
349
393
|
for (const attrKey of allAttrKeys) {
|
|
@@ -378,164 +422,14 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
378
422
|
}
|
|
379
423
|
}
|
|
380
424
|
}
|
|
381
|
-
//
|
|
425
|
+
// Index management: create/update indexes using clean adapter-based system
|
|
382
426
|
const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
|
|
383
427
|
const idxs = (localTableConfig?.indexes ?? indexes ?? []);
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Show a concise plan with icons before executing
|
|
390
|
-
const idxPlanPlus = [];
|
|
391
|
-
const idxPlanPlusMinus = [];
|
|
392
|
-
const idxPlanSkip = [];
|
|
393
|
-
for (const idx of idxs) {
|
|
394
|
-
const found = existingIdx.find((i) => i.key === idx.key);
|
|
395
|
-
if (found) {
|
|
396
|
-
if (isIndexEqualToIndex(found, idx))
|
|
397
|
-
idxPlanSkip.push(idx.key);
|
|
398
|
-
else
|
|
399
|
-
idxPlanPlusMinus.push(idx.key);
|
|
400
|
-
}
|
|
401
|
-
else
|
|
402
|
-
idxPlanPlus.push(idx.key);
|
|
403
|
-
}
|
|
404
|
-
const planParts = [];
|
|
405
|
-
if (idxPlanPlus.length)
|
|
406
|
-
planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
|
|
407
|
-
if (idxPlanPlusMinus.length)
|
|
408
|
-
planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
|
|
409
|
-
if (idxPlanSkip.length)
|
|
410
|
-
planParts.push(`⏭️ ${idxPlanSkip.length}`);
|
|
411
|
-
MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
|
|
412
|
-
const created = [];
|
|
413
|
-
const updated = [];
|
|
414
|
-
const skipped = [];
|
|
415
|
-
for (const idx of idxs) {
|
|
416
|
-
const found = existingIdx.find((i) => i.key === idx.key);
|
|
417
|
-
if (found) {
|
|
418
|
-
if (isIndexEqualToIndex(found, idx)) {
|
|
419
|
-
MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
|
|
420
|
-
skipped.push(idx.key);
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
try {
|
|
424
|
-
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
425
|
-
await delay(100);
|
|
426
|
-
}
|
|
427
|
-
catch { }
|
|
428
|
-
try {
|
|
429
|
-
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
430
|
-
updated.push(idx.key);
|
|
431
|
-
}
|
|
432
|
-
catch (e) {
|
|
433
|
-
const msg = (e?.message || '').toString().toLowerCase();
|
|
434
|
-
if (msg.includes('already exists')) {
|
|
435
|
-
MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
|
|
436
|
-
skipped.push(idx.key);
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
throw e;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
try {
|
|
446
|
-
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
447
|
-
created.push(idx.key);
|
|
448
|
-
}
|
|
449
|
-
catch (e) {
|
|
450
|
-
const msg = (e?.message || '').toString().toLowerCase();
|
|
451
|
-
if (msg.includes('already exists')) {
|
|
452
|
-
MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
|
|
453
|
-
skipped.push(idx.key);
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
throw e;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// Wait for index availability
|
|
461
|
-
const maxWait = 60000;
|
|
462
|
-
const start = Date.now();
|
|
463
|
-
let lastStatus = '';
|
|
464
|
-
while (Date.now() - start < maxWait) {
|
|
465
|
-
try {
|
|
466
|
-
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
467
|
-
const list = li.data || li.indexes || [];
|
|
468
|
-
const cur = list.find((i) => i.key === idx.key);
|
|
469
|
-
if (cur) {
|
|
470
|
-
if (cur.status === 'available')
|
|
471
|
-
break;
|
|
472
|
-
if (cur.status === 'failed' || cur.status === 'stuck') {
|
|
473
|
-
throw new Error(cur.error || `Index ${idx.key} failed`);
|
|
474
|
-
}
|
|
475
|
-
lastStatus = cur.status;
|
|
476
|
-
}
|
|
477
|
-
await delay(2000);
|
|
478
|
-
}
|
|
479
|
-
catch {
|
|
480
|
-
await delay(2000);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
await delay(150);
|
|
484
|
-
}
|
|
485
|
-
MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}`, { prefix: 'Indexes' });
|
|
486
|
-
}
|
|
487
|
-
catch (e) {
|
|
488
|
-
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
489
|
-
}
|
|
490
|
-
// Deletions for indexes: remove remote indexes not declared in YAML/config
|
|
491
|
-
try {
|
|
492
|
-
const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
|
|
493
|
-
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
494
|
-
const existingIdx = idxRes.data || idxRes.indexes || [];
|
|
495
|
-
const extraIdx = existingIdx
|
|
496
|
-
.filter((i) => i?.key && !desiredIndexKeys.has(i.key))
|
|
497
|
-
.map((i) => i.key);
|
|
498
|
-
if (extraIdx.length > 0) {
|
|
499
|
-
MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
|
|
500
|
-
const deleted = [];
|
|
501
|
-
const errors = [];
|
|
502
|
-
for (const key of extraIdx) {
|
|
503
|
-
try {
|
|
504
|
-
await adapter.deleteIndex({ databaseId, tableId, key });
|
|
505
|
-
// Optionally wait for index to disappear
|
|
506
|
-
const start = Date.now();
|
|
507
|
-
const maxWait = 30000;
|
|
508
|
-
while (Date.now() - start < maxWait) {
|
|
509
|
-
try {
|
|
510
|
-
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
511
|
-
const list = li.data || li.indexes || [];
|
|
512
|
-
if (!list.find((ix) => ix.key === key))
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
515
|
-
catch { }
|
|
516
|
-
await delay(1000);
|
|
517
|
-
}
|
|
518
|
-
deleted.push(key);
|
|
519
|
-
}
|
|
520
|
-
catch (e) {
|
|
521
|
-
errors.push({ key, error: e?.message || String(e) });
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (deleted.length) {
|
|
525
|
-
MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
|
|
526
|
-
}
|
|
527
|
-
if (errors.length) {
|
|
528
|
-
MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
|
|
529
|
-
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
catch (e) {
|
|
537
|
-
MessageFormatter.warning(`Could not evaluate index deletions: ${e?.message || e}`, { prefix: 'Indexes' });
|
|
538
|
-
}
|
|
428
|
+
// Create/update indexes with proper planning and execution
|
|
429
|
+
await createOrUpdateIndexesViaAdapter(adapter, databaseId, tableId, idxs, indexes);
|
|
430
|
+
// Handle obsolete index deletions
|
|
431
|
+
const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
|
|
432
|
+
await deleteObsoleteIndexesViaAdapter(adapter, databaseId, tableId, desiredIndexKeys);
|
|
539
433
|
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
540
434
|
try {
|
|
541
435
|
const desiredKeys = new Set((attributes || []).map((a) => a.key));
|
|
@@ -555,7 +449,9 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
555
449
|
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
556
450
|
const ilist = idxRes.data || idxRes.indexes || [];
|
|
557
451
|
for (const idx of ilist) {
|
|
558
|
-
const attrs = Array.isArray(idx.attributes)
|
|
452
|
+
const attrs = Array.isArray(idx.attributes)
|
|
453
|
+
? idx.attributes
|
|
454
|
+
: (Array.isArray(idx.columns) ? idx.columns : []);
|
|
559
455
|
if (attrs.includes(key)) {
|
|
560
456
|
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
561
457
|
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
@@ -647,6 +543,15 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
647
543
|
}
|
|
648
544
|
}
|
|
649
545
|
}
|
|
546
|
+
// Process any remaining queued operations to complete relationship sync
|
|
547
|
+
try {
|
|
548
|
+
MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
|
|
549
|
+
await processQueue(adapter, databaseId);
|
|
550
|
+
MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
|
|
554
|
+
}
|
|
650
555
|
};
|
|
651
556
|
export const generateMockData = async (database, databaseId, configCollections) => {
|
|
652
557
|
for (const { collection, mockFunction } of configCollections) {
|