appwrite-utils-cli 1.7.9 → 1.8.2
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/CHANGELOG.md +14 -199
- package/README.md +87 -30
- package/dist/adapters/AdapterFactory.js +5 -25
- package/dist/adapters/DatabaseAdapter.d.ts +17 -2
- package/dist/adapters/LegacyAdapter.d.ts +2 -1
- package/dist/adapters/LegacyAdapter.js +212 -16
- package/dist/adapters/TablesDBAdapter.d.ts +2 -12
- package/dist/adapters/TablesDBAdapter.js +261 -57
- package/dist/cli/commands/databaseCommands.js +4 -3
- package/dist/cli/commands/functionCommands.js +17 -8
- package/dist/collections/attributes.js +447 -125
- package/dist/collections/methods.js +197 -186
- package/dist/collections/tableOperations.d.ts +86 -0
- package/dist/collections/tableOperations.js +434 -0
- package/dist/collections/transferOperations.d.ts +3 -2
- package/dist/collections/transferOperations.js +93 -12
- package/dist/config/yamlConfig.d.ts +221 -88
- package/dist/examples/yamlTerminologyExample.d.ts +1 -1
- package/dist/examples/yamlTerminologyExample.js +6 -3
- package/dist/functions/fnConfigDiscovery.d.ts +3 -0
- package/dist/functions/fnConfigDiscovery.js +108 -0
- package/dist/interactiveCLI.js +18 -15
- package/dist/main.js +211 -73
- package/dist/migrations/appwriteToX.d.ts +88 -23
- package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
- package/dist/migrations/comprehensiveTransfer.js +83 -6
- package/dist/migrations/dataLoader.d.ts +227 -69
- package/dist/migrations/dataLoader.js +3 -3
- package/dist/migrations/importController.js +3 -3
- package/dist/migrations/relationships.d.ts +8 -2
- package/dist/migrations/services/ImportOrchestrator.js +3 -3
- package/dist/migrations/transfer.js +159 -37
- package/dist/shared/attributeMapper.d.ts +20 -0
- package/dist/shared/attributeMapper.js +203 -0
- package/dist/shared/selectionDialogs.js +8 -4
- package/dist/storage/schemas.d.ts +354 -92
- package/dist/utils/configDiscovery.js +4 -3
- package/dist/utils/versionDetection.d.ts +0 -4
- package/dist/utils/versionDetection.js +41 -173
- package/dist/utils/yamlConverter.js +89 -16
- package/dist/utils/yamlLoader.d.ts +1 -1
- package/dist/utils/yamlLoader.js +6 -2
- package/dist/utilsController.js +56 -19
- package/package.json +4 -4
- package/src/adapters/AdapterFactory.ts +119 -143
- package/src/adapters/DatabaseAdapter.ts +18 -3
- package/src/adapters/LegacyAdapter.ts +236 -105
- package/src/adapters/TablesDBAdapter.ts +773 -643
- package/src/cli/commands/databaseCommands.ts +13 -12
- package/src/cli/commands/functionCommands.ts +23 -14
- package/src/collections/attributes.ts +2054 -1611
- package/src/collections/methods.ts +208 -293
- package/src/collections/tableOperations.ts +506 -0
- package/src/collections/transferOperations.ts +218 -144
- package/src/examples/yamlTerminologyExample.ts +10 -5
- package/src/functions/fnConfigDiscovery.ts +103 -0
- package/src/interactiveCLI.ts +25 -20
- package/src/main.ts +549 -194
- package/src/migrations/comprehensiveTransfer.ts +126 -50
- package/src/migrations/dataLoader.ts +3 -3
- package/src/migrations/importController.ts +3 -3
- package/src/migrations/services/ImportOrchestrator.ts +3 -3
- package/src/migrations/transfer.ts +148 -131
- package/src/shared/attributeMapper.ts +229 -0
- package/src/shared/selectionDialogs.ts +29 -25
- package/src/utils/configDiscovery.ts +9 -3
- package/src/utils/versionDetection.ts +74 -228
- package/src/utils/yamlConverter.ts +94 -17
- package/src/utils/yamlLoader.ts +11 -4
- package/src/utilsController.ts +80 -30
|
@@ -17,8 +17,7 @@ import {
|
|
|
17
17
|
markCollectionProcessed
|
|
18
18
|
} from "../shared/operationQueue.js";
|
|
19
19
|
import { logger } from "../shared/logging.js";
|
|
20
|
-
|
|
21
|
-
import { createOrUpdateIndexesWithStatusCheck } from "./indexes.js";
|
|
20
|
+
// Legacy attribute/index helpers removed in favor of unified adapter path
|
|
22
21
|
import { SchemaGenerator } from "../shared/schemaGenerator.js";
|
|
23
22
|
import {
|
|
24
23
|
isNull,
|
|
@@ -27,9 +26,11 @@ import {
|
|
|
27
26
|
isPlainObject,
|
|
28
27
|
isString,
|
|
29
28
|
} from "es-toolkit";
|
|
30
|
-
import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
31
|
-
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
32
|
-
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
29
|
+
import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
30
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
31
|
+
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
32
|
+
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
|
|
33
|
+
import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
|
|
33
34
|
|
|
34
35
|
// Re-export wipe operations
|
|
35
36
|
export {
|
|
@@ -183,203 +184,26 @@ export const generateSchemas = async (
|
|
|
183
184
|
schemaGenerator.generateSchemas();
|
|
184
185
|
};
|
|
185
186
|
|
|
186
|
-
export const createOrUpdateCollections = async (
|
|
187
|
-
database: Databases,
|
|
188
|
-
databaseId: string,
|
|
189
|
-
config: AppwriteConfig,
|
|
190
|
-
deletedCollections?: { collectionId: string; collectionName: string }[],
|
|
191
|
-
selectedCollections: Models.Collection[] = []
|
|
192
|
-
): Promise<void> => {
|
|
193
|
-
// Clear processing state at the start of a new operation
|
|
194
|
-
clearProcessingState();
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const collectionsToProcess =
|
|
207
|
-
selectedCollections.length > 0 ? selectedCollections : config.collections;
|
|
208
|
-
if (!collectionsToProcess) {
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const usedIds = new Set();
|
|
212
|
-
|
|
213
|
-
MessageFormatter.info(`Processing ${collectionsToProcess.length} collections with intelligent state management`, { prefix: "Collections" });
|
|
214
|
-
|
|
215
|
-
for (const collection of collectionsToProcess) {
|
|
216
|
-
const { attributes, indexes, ...collectionData } = collection;
|
|
217
|
-
|
|
218
|
-
// Check if this collection has already been processed in this session
|
|
219
|
-
if (collectionData.$id && isCollectionProcessed(collectionData.$id)) {
|
|
220
|
-
MessageFormatter.info(`Collection '${collectionData.name}' already processed, skipping`, { prefix: "Collections" });
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Prepare permissions for the collection
|
|
225
|
-
const permissions: string[] = [];
|
|
226
|
-
if (collection.$permissions && collection.$permissions.length > 0) {
|
|
227
|
-
for (const permission of collection.$permissions) {
|
|
228
|
-
if (typeof permission === "string") {
|
|
229
|
-
permissions.push(permission);
|
|
230
|
-
} else {
|
|
231
|
-
switch (permission.permission) {
|
|
232
|
-
case "read":
|
|
233
|
-
permissions.push(Permission.read(permission.target));
|
|
234
|
-
break;
|
|
235
|
-
case "create":
|
|
236
|
-
permissions.push(Permission.create(permission.target));
|
|
237
|
-
break;
|
|
238
|
-
case "update":
|
|
239
|
-
permissions.push(Permission.update(permission.target));
|
|
240
|
-
break;
|
|
241
|
-
case "delete":
|
|
242
|
-
permissions.push(Permission.delete(permission.target));
|
|
243
|
-
break;
|
|
244
|
-
case "write":
|
|
245
|
-
permissions.push(Permission.write(permission.target));
|
|
246
|
-
break;
|
|
247
|
-
default:
|
|
248
|
-
MessageFormatter.warning(`Unknown permission: ${permission.permission}`, { prefix: "Collections" });
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Check if the collection already exists by name
|
|
256
|
-
let collectionsFound = await tryAwaitWithRetry(
|
|
257
|
-
async () =>
|
|
258
|
-
await database.listCollections(databaseId, [
|
|
259
|
-
Query.equal("name", collectionData.name),
|
|
260
|
-
])
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
let collectionToUse =
|
|
264
|
-
collectionsFound.total > 0 ? collectionsFound.collections[0] : null;
|
|
265
|
-
|
|
266
|
-
// Determine the correct ID for the collection
|
|
267
|
-
let collectionId: string;
|
|
268
|
-
if (!collectionToUse) {
|
|
269
|
-
MessageFormatter.info(`Creating collection: ${collectionData.name}`, { prefix: "Collections" });
|
|
270
|
-
let foundColl = deletedCollections?.find(
|
|
271
|
-
(coll) =>
|
|
272
|
-
coll.collectionName.toLowerCase().trim().replace(" ", "") ===
|
|
273
|
-
collectionData.name.toLowerCase().trim().replace(" ", "")
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
if (collectionData.$id) {
|
|
277
|
-
collectionId = collectionData.$id;
|
|
278
|
-
} else if (foundColl && !usedIds.has(foundColl.collectionId)) {
|
|
279
|
-
collectionId = foundColl.collectionId;
|
|
280
|
-
} else {
|
|
281
|
-
collectionId = ID.unique();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
usedIds.add(collectionId);
|
|
285
|
-
|
|
286
|
-
// Create the collection with the determined ID
|
|
287
|
-
try {
|
|
288
|
-
collectionToUse = await tryAwaitWithRetry(
|
|
289
|
-
async () =>
|
|
290
|
-
await database.createCollection(
|
|
291
|
-
databaseId,
|
|
292
|
-
collectionId,
|
|
293
|
-
collectionData.name,
|
|
294
|
-
permissions,
|
|
295
|
-
collectionData.documentSecurity ?? false,
|
|
296
|
-
collectionData.enabled ?? true
|
|
297
|
-
)
|
|
298
|
-
);
|
|
299
|
-
collectionData.$id = collectionToUse!.$id;
|
|
300
|
-
nameToIdMapping.set(collectionData.name, collectionToUse!.$id);
|
|
301
|
-
} catch (error) {
|
|
302
|
-
MessageFormatter.error(
|
|
303
|
-
`Failed to create collection ${collectionData.name} with ID ${collectionId}`,
|
|
304
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
305
|
-
{ prefix: "Collections" }
|
|
306
|
-
);
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
} else {
|
|
310
|
-
MessageFormatter.info(`Collection ${collectionData.name} exists, updating it`, { prefix: "Collections" });
|
|
311
|
-
await tryAwaitWithRetry(
|
|
312
|
-
async () =>
|
|
313
|
-
await database.updateCollection(
|
|
314
|
-
databaseId,
|
|
315
|
-
collectionToUse!.$id,
|
|
316
|
-
collectionData.name,
|
|
317
|
-
permissions,
|
|
318
|
-
collectionData.documentSecurity ?? false,
|
|
319
|
-
collectionData.enabled ?? true
|
|
320
|
-
)
|
|
321
|
-
);
|
|
322
|
-
// Cache the existing collection ID
|
|
323
|
-
nameToIdMapping.set(collectionData.name, collectionToUse.$id);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Add delay after creating/updating collection
|
|
327
|
-
await delay(250);
|
|
328
|
-
|
|
329
|
-
// Update attributes and indexes for the collection
|
|
330
|
-
MessageFormatter.progress("Creating Attributes", { prefix: "Collections" });
|
|
331
|
-
await createUpdateCollectionAttributesWithStatusCheck(
|
|
332
|
-
database,
|
|
333
|
-
databaseId,
|
|
334
|
-
collectionToUse!,
|
|
335
|
-
// @ts-expect-error
|
|
336
|
-
attributes
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
// Add delay after creating attributes
|
|
340
|
-
await delay(250);
|
|
341
|
-
|
|
342
|
-
// Prefer local config indexes, but fall back to collection's own indexes if no local config exists
|
|
343
|
-
const localCollectionConfig = config.collections?.find(
|
|
344
|
-
c => c.name === collectionData.name || c.$id === collectionData.$id
|
|
345
|
-
);
|
|
346
|
-
const indexesToUse = localCollectionConfig?.indexes ?? indexes ?? [];
|
|
347
|
-
|
|
348
|
-
MessageFormatter.progress("Creating Indexes", { prefix: "Collections" });
|
|
349
|
-
await createOrUpdateIndexesWithStatusCheck(
|
|
350
|
-
databaseId,
|
|
351
|
-
database,
|
|
352
|
-
collectionToUse!.$id,
|
|
353
|
-
collectionToUse!,
|
|
354
|
-
indexesToUse as Indexes
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
// Delete indexes that exist on server but not in local config
|
|
358
|
-
const { deleteObsoleteIndexes } = await import('../shared/indexManager.js');
|
|
359
|
-
await deleteObsoleteIndexes(
|
|
360
|
-
database,
|
|
361
|
-
databaseId,
|
|
362
|
-
collectionToUse!,
|
|
363
|
-
{ indexes: indexesToUse } as any,
|
|
364
|
-
{ verbose: true }
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
// Mark this collection as fully processed to prevent re-processing
|
|
368
|
-
markCollectionProcessed(collectionToUse!.$id, collectionData.name);
|
|
369
|
-
|
|
370
|
-
// Add delay after creating indexes
|
|
371
|
-
await delay(250);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Process any remaining relationship attributes in the queue
|
|
375
|
-
// This surgical approach only processes specific attributes, not entire collections
|
|
376
|
-
if (queuedOperations.length > 0) {
|
|
377
|
-
MessageFormatter.info(`🔧 Processing ${queuedOperations.length} queued relationship attributes (surgical approach)`, { prefix: "Collections" });
|
|
378
|
-
await processQueue(database, databaseId);
|
|
379
|
-
} else {
|
|
380
|
-
MessageFormatter.info("✅ No queued relationship attributes to process", { prefix: "Collections" });
|
|
381
|
-
}
|
|
382
|
-
};
|
|
187
|
+
export const createOrUpdateCollections = async (
|
|
188
|
+
database: Databases,
|
|
189
|
+
databaseId: string,
|
|
190
|
+
config: AppwriteConfig,
|
|
191
|
+
deletedCollections?: { collectionId: string; collectionName: string }[],
|
|
192
|
+
selectedCollections: Models.Collection[] = []
|
|
193
|
+
): Promise<void> => {
|
|
194
|
+
// Clear processing state at the start of a new operation
|
|
195
|
+
clearProcessingState();
|
|
196
|
+
|
|
197
|
+
// Always use adapter path (LegacyAdapter translates when pre-1.8)
|
|
198
|
+
const { adapter } = await getAdapterFromConfig(config);
|
|
199
|
+
await createOrUpdateCollectionsViaAdapter(
|
|
200
|
+
adapter,
|
|
201
|
+
databaseId,
|
|
202
|
+
config,
|
|
203
|
+
deletedCollections,
|
|
204
|
+
selectedCollections
|
|
205
|
+
);
|
|
206
|
+
};
|
|
383
207
|
|
|
384
208
|
// New: Adapter-based implementation for TablesDB with state management
|
|
385
209
|
export const createOrUpdateCollectionsViaAdapter = async (
|
|
@@ -394,33 +218,19 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
394
218
|
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
|
395
219
|
|
|
396
220
|
const usedIds = new Set<string>();
|
|
397
|
-
MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
const createAttr = async (tableId: string, attr: Attribute) => {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
min: (attr as any).min,
|
|
411
|
-
max: (attr as any).max,
|
|
412
|
-
elements: (attr as any).elements,
|
|
413
|
-
encrypt: (attr as any).encrypted,
|
|
414
|
-
relatedCollection: (attr as any).relatedCollection,
|
|
415
|
-
relationType: (attr as any).relationType,
|
|
416
|
-
twoWay: (attr as any).twoWay,
|
|
417
|
-
twoWayKey: (attr as any).twoWayKey,
|
|
418
|
-
onDelete: (attr as any).onDelete,
|
|
419
|
-
side: (attr as any).side,
|
|
420
|
-
};
|
|
421
|
-
await adapter.createAttribute(base);
|
|
422
|
-
await delay(150);
|
|
423
|
-
};
|
|
221
|
+
MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
|
|
222
|
+
|
|
223
|
+
// Helpers for attribute operations through adapter
|
|
224
|
+
const createAttr = async (tableId: string, attr: Attribute) => {
|
|
225
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId, tableId });
|
|
226
|
+
await adapter.createAttribute(params);
|
|
227
|
+
await delay(150);
|
|
228
|
+
};
|
|
229
|
+
const updateAttr = async (tableId: string, attr: Attribute) => {
|
|
230
|
+
const params = mapToUpdateAttributeParams(attr as any, { databaseId, tableId }) as any;
|
|
231
|
+
await adapter.updateAttribute(params);
|
|
232
|
+
await delay(150);
|
|
233
|
+
};
|
|
424
234
|
|
|
425
235
|
// Local queue for unresolved relationships
|
|
426
236
|
const relQueue: { tableId: string; attr: Attribute }[] = [];
|
|
@@ -495,39 +305,79 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
495
305
|
// Add small delay after table create/update
|
|
496
306
|
await delay(250);
|
|
497
307
|
|
|
498
|
-
// Create attributes: non-relationship first
|
|
499
|
-
const nonRel = (attributes || []).filter((a: Attribute) => a.type !== 'relationship');
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
308
|
+
// Create/Update attributes: non-relationship first using enhanced planning
|
|
309
|
+
const nonRel = (attributes || []).filter((a: Attribute) => a.type !== 'relationship');
|
|
310
|
+
if (nonRel.length > 0) {
|
|
311
|
+
// Fetch existing columns once
|
|
312
|
+
const tableInfo = await adapter.getTable({ databaseId, tableId });
|
|
313
|
+
const existingCols: any[] = (tableInfo as any).data?.columns || (tableInfo as any).data?.attributes || [];
|
|
314
|
+
|
|
315
|
+
// Plan with icons
|
|
316
|
+
const plan = diffColumnsDetailed(nonRel as any, existingCols);
|
|
317
|
+
const plus = plan.toCreate.map((a: any) => a.key);
|
|
318
|
+
const plusminus = plan.toUpdate.map((u: any) => (u.attribute as any).key);
|
|
319
|
+
const minus = plan.toRecreate.map((r: any) => (r.newAttribute as any).key);
|
|
320
|
+
const skip = plan.unchanged;
|
|
321
|
+
|
|
322
|
+
const parts: string[] = [];
|
|
323
|
+
if (plus.length) parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
|
|
324
|
+
if (plusminus.length) parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
|
|
325
|
+
if (minus.length) parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
|
|
326
|
+
if (skip.length) parts.push(`⏭️ ${skip.length}`);
|
|
327
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
|
|
328
|
+
|
|
329
|
+
// Execute
|
|
330
|
+
const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
|
|
331
|
+
|
|
332
|
+
if (colResults.success.length > 0) {
|
|
333
|
+
MessageFormatter.success(`Processed ${colResults.success.length} ops`, { prefix: 'Attributes' });
|
|
334
|
+
}
|
|
335
|
+
if (colResults.errors.length > 0) {
|
|
336
|
+
MessageFormatter.error(`${colResults.errors.length} attribute operations failed:`, undefined, { prefix: 'Attributes' });
|
|
337
|
+
for (const err of colResults.errors) {
|
|
338
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Attributes' });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
MessageFormatter.info(
|
|
342
|
+
`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`,
|
|
343
|
+
{ prefix: 'Attributes' }
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update
|
|
348
|
+
const rels = (attributes || []).filter((a: Attribute) => a.type === 'relationship') as any[];
|
|
349
|
+
if (rels.length > 0) {
|
|
350
|
+
for (const attr of rels) {
|
|
351
|
+
const relNameOrId = attr.relatedCollection as string | undefined;
|
|
352
|
+
if (!relNameOrId) continue;
|
|
353
|
+
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
|
354
|
+
if (!nameToIdMapping.has(relNameOrId)) {
|
|
355
|
+
try {
|
|
356
|
+
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
|
357
|
+
const relItems: any[] = (relList as any).tables || [];
|
|
358
|
+
if (relItems[0]?.$id) {
|
|
359
|
+
relId = relItems[0].$id;
|
|
360
|
+
nameToIdMapping.set(relNameOrId, relId);
|
|
361
|
+
}
|
|
362
|
+
} catch {}
|
|
363
|
+
}
|
|
364
|
+
if (relId && typeof relId === 'string') attr.relatedCollection = relId;
|
|
365
|
+
}
|
|
366
|
+
const tableInfo2 = await adapter.getTable({ databaseId, tableId });
|
|
367
|
+
const existingCols2: any[] = (tableInfo2 as any).data?.columns || (tableInfo2 as any).data?.attributes || [];
|
|
368
|
+
const { toCreate: relCreate, toUpdate: relUpdate, unchanged: relUnchanged } = diffTableColumns(existingCols2, rels as any);
|
|
369
|
+
|
|
370
|
+
// Relationship plan with icons
|
|
371
|
+
{
|
|
372
|
+
const parts: string[] = [];
|
|
373
|
+
if (relCreate.length) parts.push(`➕ ${relCreate.length} (${relCreate.map((a:any)=>a.key).join(', ')})`);
|
|
374
|
+
if (relUpdate.length) parts.push(`🔧 ${relUpdate.length} (${relUpdate.map((a:any)=>a.key).join(', ')})`);
|
|
375
|
+
if (relUnchanged.length) parts.push(`⏭️ ${relUnchanged.length}`);
|
|
376
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
|
|
377
|
+
}
|
|
378
|
+
for (const attr of relUpdate) { try { await updateAttr(tableId, attr as Attribute); } catch (e) { MessageFormatter.error(`Failed to update relationship ${(attr as any).key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' }); } }
|
|
379
|
+
for (const attr of relCreate) { try { await createAttr(tableId, attr as Attribute); } catch (e) { MessageFormatter.error(`Failed to create relationship ${(attr as any).key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' }); } }
|
|
380
|
+
}
|
|
531
381
|
|
|
532
382
|
// Wait for all attributes to become available before creating indexes
|
|
533
383
|
const allAttrKeys = [
|
|
@@ -535,22 +385,22 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
535
385
|
...rels.filter((a: any) => a.relatedCollection).map((a: any) => a.key)
|
|
536
386
|
];
|
|
537
387
|
|
|
538
|
-
if (allAttrKeys.length > 0) {
|
|
539
|
-
for (const attrKey of allAttrKeys) {
|
|
540
|
-
const maxWait = 60000; // 60 seconds
|
|
541
|
-
const startTime = Date.now();
|
|
542
|
-
let lastStatus = '';
|
|
543
|
-
|
|
544
|
-
while (Date.now() - startTime < maxWait) {
|
|
545
|
-
try {
|
|
546
|
-
const tableData = await adapter.getTable({ databaseId, tableId });
|
|
547
|
-
const attrs = (tableData as any).attributes || [];
|
|
548
|
-
const attr = attrs.find((a: any) => a.key === attrKey);
|
|
549
|
-
|
|
550
|
-
if (attr) {
|
|
551
|
-
if (attr.status === 'available') {
|
|
552
|
-
break; // Attribute is ready
|
|
553
|
-
}
|
|
388
|
+
if (allAttrKeys.length > 0) {
|
|
389
|
+
for (const attrKey of allAttrKeys) {
|
|
390
|
+
const maxWait = 60000; // 60 seconds
|
|
391
|
+
const startTime = Date.now();
|
|
392
|
+
let lastStatus = '';
|
|
393
|
+
|
|
394
|
+
while (Date.now() - startTime < maxWait) {
|
|
395
|
+
try {
|
|
396
|
+
const tableData = await adapter.getTable({ databaseId, tableId });
|
|
397
|
+
const attrs = (tableData as any).data?.columns || (tableData as any).data?.attributes || [];
|
|
398
|
+
const attr = attrs.find((a: any) => a.key === attrKey);
|
|
399
|
+
|
|
400
|
+
if (attr) {
|
|
401
|
+
if (attr.status === 'available') {
|
|
402
|
+
break; // Attribute is ready
|
|
403
|
+
}
|
|
554
404
|
if (attr.status === 'failed' || attr.status === 'stuck') {
|
|
555
405
|
throw new Error(`Attribute ${attrKey} failed to create: ${attr.error || 'unknown error'}`);
|
|
556
406
|
}
|
|
@@ -580,21 +430,86 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
580
430
|
c => c.name === collectionData.name || c.$id === collectionData.$id
|
|
581
431
|
);
|
|
582
432
|
const idxs = (localTableConfig?.indexes ?? indexes ?? []) as any[];
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
433
|
+
// Compare with existing indexes and create/update accordingly with status checks
|
|
434
|
+
try {
|
|
435
|
+
const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
436
|
+
const existingIdx: any[] = (existingIdxRes as any).data || (existingIdxRes as any).indexes || [];
|
|
437
|
+
MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i:any)=>i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
|
|
438
|
+
// Show a concise plan with icons before executing
|
|
439
|
+
const idxPlanPlus: string[] = [];
|
|
440
|
+
const idxPlanPlusMinus: string[] = [];
|
|
441
|
+
const idxPlanSkip: string[] = [];
|
|
442
|
+
for (const idx of idxs) {
|
|
443
|
+
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
444
|
+
if (found) {
|
|
445
|
+
if (isIndexEqualToIndex(found, idx)) idxPlanSkip.push(idx.key);
|
|
446
|
+
else idxPlanPlusMinus.push(idx.key);
|
|
447
|
+
} else idxPlanPlus.push(idx.key);
|
|
448
|
+
}
|
|
449
|
+
const planParts: string[] = [];
|
|
450
|
+
if (idxPlanPlus.length) planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
|
|
451
|
+
if (idxPlanPlusMinus.length) planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
|
|
452
|
+
if (idxPlanSkip.length) planParts.push(`⏭️ ${idxPlanSkip.length}`);
|
|
453
|
+
MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
|
|
454
|
+
const created: string[] = [];
|
|
455
|
+
const updated: string[] = [];
|
|
456
|
+
const skipped: string[] = [];
|
|
457
|
+
for (const idx of idxs) {
|
|
458
|
+
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
459
|
+
if (found) {
|
|
460
|
+
if (isIndexEqualToIndex(found, idx)) {
|
|
461
|
+
MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
|
|
462
|
+
skipped.push(idx.key);
|
|
463
|
+
} else {
|
|
464
|
+
try { await adapter.deleteIndex({ databaseId, tableId, key: idx.key }); await delay(100); } catch {}
|
|
465
|
+
try {
|
|
466
|
+
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
467
|
+
updated.push(idx.key);
|
|
468
|
+
} catch (e: any) {
|
|
469
|
+
const msg = (e?.message || '').toString().toLowerCase();
|
|
470
|
+
if (msg.includes('already exists')) {
|
|
471
|
+
MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
|
|
472
|
+
skipped.push(idx.key);
|
|
473
|
+
} else {
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
try {
|
|
480
|
+
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
481
|
+
created.push(idx.key);
|
|
482
|
+
} catch (e: any) {
|
|
483
|
+
const msg = (e?.message || '').toString().toLowerCase();
|
|
484
|
+
if (msg.includes('already exists')) {
|
|
485
|
+
MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
|
|
486
|
+
skipped.push(idx.key);
|
|
487
|
+
} else {
|
|
488
|
+
throw e;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Wait for index availability
|
|
493
|
+
const maxWait = 60000; const start = Date.now(); let lastStatus = '';
|
|
494
|
+
while (Date.now() - start < maxWait) {
|
|
495
|
+
try {
|
|
496
|
+
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
497
|
+
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
498
|
+
const cur = list.find((i: any) => i.key === idx.key);
|
|
499
|
+
if (cur) {
|
|
500
|
+
if (cur.status === 'available') break;
|
|
501
|
+
if (cur.status === 'failed' || cur.status === 'stuck') { throw new Error(cur.error || `Index ${idx.key} failed`); }
|
|
502
|
+
lastStatus = cur.status;
|
|
503
|
+
}
|
|
504
|
+
await delay(2000);
|
|
505
|
+
} catch { await delay(2000); }
|
|
506
|
+
}
|
|
507
|
+
await delay(150);
|
|
508
|
+
}
|
|
509
|
+
MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}` , { prefix: 'Indexes' });
|
|
510
|
+
} catch (e) {
|
|
511
|
+
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
512
|
+
}
|
|
598
513
|
|
|
599
514
|
// Mark this table as fully processed to prevent re-processing
|
|
600
515
|
markCollectionProcessed(tableId, collectionData.name);
|