appwrite-utils-cli 1.9.5 → 1.9.6
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/collections/methods.js +79 -27
- package/dist/utilsController.js +10 -1
- package/package.json +1 -1
- package/src/collections/methods.ts +475 -405
- package/src/utilsController.ts +10 -1
|
@@ -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";
|
|
@@ -287,63 +287,106 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
287
287
|
}
|
|
288
288
|
MessageFormatter.info(`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`, { prefix: 'Attributes' });
|
|
289
289
|
}
|
|
290
|
-
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
290
|
+
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
|
|
291
|
+
const relsAll = (attributes || []).filter((a) => a.type === 'relationship');
|
|
292
|
+
if (relsAll.length > 0) {
|
|
293
|
+
const relsResolved = [];
|
|
294
|
+
const relsDeferred = [];
|
|
295
|
+
// Resolve related collections (names -> IDs) using cache or lookup.
|
|
296
|
+
// If not resolvable yet (target table created later in the same push), queue for later.
|
|
297
|
+
for (const attr of relsAll) {
|
|
294
298
|
const relNameOrId = attr.relatedCollection;
|
|
295
299
|
if (!relNameOrId)
|
|
296
300
|
continue;
|
|
297
301
|
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
|
298
|
-
|
|
302
|
+
let resolved = false;
|
|
303
|
+
if (nameToIdMapping.has(relNameOrId)) {
|
|
304
|
+
resolved = true;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// Try resolve by name
|
|
299
308
|
try {
|
|
300
309
|
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
|
301
310
|
const relItems = relList.tables || [];
|
|
302
311
|
if (relItems[0]?.$id) {
|
|
303
312
|
relId = relItems[0].$id;
|
|
304
313
|
nameToIdMapping.set(relNameOrId, relId);
|
|
314
|
+
resolved = true;
|
|
305
315
|
}
|
|
306
316
|
}
|
|
307
317
|
catch { }
|
|
318
|
+
// If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
|
|
319
|
+
if (!resolved && relNameOrId && relNameOrId.length >= 10) {
|
|
320
|
+
try {
|
|
321
|
+
const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
|
|
322
|
+
if (probe.data?.$id) {
|
|
323
|
+
nameToIdMapping.set(relNameOrId, relNameOrId);
|
|
324
|
+
relId = relNameOrId;
|
|
325
|
+
resolved = true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
}
|
|
308
330
|
}
|
|
309
|
-
if (relId && typeof relId === 'string')
|
|
331
|
+
if (resolved && relId && typeof relId === 'string') {
|
|
310
332
|
attr.relatedCollection = relId;
|
|
333
|
+
relsResolved.push(attr);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Defer until related table exists; queue a surgical operation
|
|
337
|
+
enqueueOperation({
|
|
338
|
+
type: 'attribute',
|
|
339
|
+
collectionId: tableId,
|
|
340
|
+
attribute: attr,
|
|
341
|
+
dependencies: [relNameOrId]
|
|
342
|
+
});
|
|
343
|
+
relsDeferred.push(attr);
|
|
344
|
+
}
|
|
311
345
|
}
|
|
346
|
+
// Compute a detailed plan for immediately resolvable relationships
|
|
312
347
|
const tableInfo2 = await adapter.getTable({ databaseId, tableId });
|
|
313
348
|
const existingCols2 = tableInfo2.data?.columns || tableInfo2.data?.attributes || [];
|
|
314
|
-
const
|
|
315
|
-
// Relationship plan with icons
|
|
349
|
+
const relPlan = diffColumnsDetailed(relsResolved, existingCols2);
|
|
350
|
+
// Relationship plan with icons (includes recreates)
|
|
316
351
|
{
|
|
317
352
|
const parts = [];
|
|
318
|
-
if (
|
|
319
|
-
parts.push(`➕ ${
|
|
320
|
-
if (
|
|
321
|
-
parts.push(`🔧 ${
|
|
322
|
-
if (
|
|
323
|
-
parts.push(
|
|
353
|
+
if (relPlan.toCreate.length)
|
|
354
|
+
parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a) => a.key).join(', ')})`);
|
|
355
|
+
if (relPlan.toUpdate.length)
|
|
356
|
+
parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u) => u.attribute?.key ?? u.key).join(', ')})`);
|
|
357
|
+
if (relPlan.toRecreate.length)
|
|
358
|
+
parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r) => r.newAttribute?.key ?? r?.key).join(', ')})`);
|
|
359
|
+
if (relPlan.unchanged.length)
|
|
360
|
+
parts.push(`⏭️ ${relPlan.unchanged.length}`);
|
|
324
361
|
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
|
|
325
362
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
363
|
+
// Execute plan using the same operation executor to properly handle deletes/recreates
|
|
364
|
+
const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
|
|
365
|
+
if (relResults.success.length > 0) {
|
|
366
|
+
const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
|
|
367
|
+
const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
|
|
368
|
+
if (relResults.success.length !== activeRelationships) {
|
|
369
|
+
// Show both counts when they differ (usually due to recreations)
|
|
370
|
+
MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
329
371
|
}
|
|
330
|
-
|
|
331
|
-
MessageFormatter.
|
|
372
|
+
else {
|
|
373
|
+
MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
332
374
|
}
|
|
333
375
|
}
|
|
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' });
|
|
376
|
+
if (relResults.errors.length > 0) {
|
|
377
|
+
MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
|
|
378
|
+
for (const err of relResults.errors) {
|
|
379
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
|
|
340
380
|
}
|
|
341
381
|
}
|
|
382
|
+
if (relsDeferred.length > 0) {
|
|
383
|
+
MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
|
|
384
|
+
}
|
|
342
385
|
}
|
|
343
386
|
// Wait for all attributes to become available before creating indexes
|
|
344
387
|
const allAttrKeys = [
|
|
345
388
|
...nonRel.map((a) => a.key),
|
|
346
|
-
...
|
|
389
|
+
...relsAll.filter((a) => a.relatedCollection).map((a) => a.key)
|
|
347
390
|
];
|
|
348
391
|
if (allAttrKeys.length > 0) {
|
|
349
392
|
for (const attrKey of allAttrKeys) {
|
|
@@ -647,6 +690,15 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
647
690
|
}
|
|
648
691
|
}
|
|
649
692
|
}
|
|
693
|
+
// Process any remaining queued operations to complete relationship sync
|
|
694
|
+
try {
|
|
695
|
+
MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
|
|
696
|
+
await processQueue(adapter, databaseId);
|
|
697
|
+
MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
|
|
701
|
+
}
|
|
650
702
|
};
|
|
651
703
|
export const generateMockData = async (database, databaseId, configCollections) => {
|
|
652
704
|
for (const { collection, mockFunction } of configCollections) {
|
package/dist/utilsController.js
CHANGED
|
@@ -30,6 +30,7 @@ import { createImportSchemas } from "./migrations/yaml/generateImportSchemas.js"
|
|
|
30
30
|
import { validateCollectionsTablesConfig, reportValidationResults, validateWithStrictMode } from "./config/configValidation.js";
|
|
31
31
|
import { ConfigManager } from "./config/ConfigManager.js";
|
|
32
32
|
import { ClientFactory } from "./utils/ClientFactory.js";
|
|
33
|
+
import { clearProcessingState, processQueue } from "./shared/operationQueue.js";
|
|
33
34
|
export class UtilsController {
|
|
34
35
|
// ──────────────────────────────────────────────────
|
|
35
36
|
// SINGLETON PATTERN
|
|
@@ -458,7 +459,6 @@ export class UtilsController {
|
|
|
458
459
|
// Ensure we don't carry state between databases in a multi-db push
|
|
459
460
|
// This resets processed sets and name->id mapping per database
|
|
460
461
|
try {
|
|
461
|
-
const { clearProcessingState } = await import('./shared/operationQueue.js');
|
|
462
462
|
clearProcessingState();
|
|
463
463
|
}
|
|
464
464
|
catch { }
|
|
@@ -475,6 +475,15 @@ export class UtilsController {
|
|
|
475
475
|
logger.debug("Adapter unavailable, falling back to legacy Databases path", { prefix: "UtilsController" });
|
|
476
476
|
await createOrUpdateCollections(this.database, database.$id, this.config, deletedCollections, collections);
|
|
477
477
|
}
|
|
478
|
+
// Safety net: Process any remaining queued operations to complete relationship sync
|
|
479
|
+
try {
|
|
480
|
+
MessageFormatter.info(`🔄 Processing final operation queue for database ${database.$id}`, { prefix: "UtilsController" });
|
|
481
|
+
await processQueue(this.adapter || this.database, database.$id);
|
|
482
|
+
MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "UtilsController" });
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'UtilsController' });
|
|
486
|
+
}
|
|
478
487
|
}
|
|
479
488
|
async generateSchemas() {
|
|
480
489
|
// Schema generation doesn't need Appwrite connection, just config
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appwrite-utils-cli",
|
|
3
3
|
"description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.6",
|
|
5
5
|
"main": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
queuedOperations,
|
|
15
15
|
clearProcessingState,
|
|
16
16
|
isCollectionProcessed,
|
|
17
|
-
markCollectionProcessed
|
|
17
|
+
markCollectionProcessed,
|
|
18
|
+
enqueueOperation
|
|
18
19
|
} from "../shared/operationQueue.js";
|
|
19
20
|
import { logger } from "../shared/logging.js";
|
|
20
|
-
// Legacy attribute/index helpers removed in favor of unified adapter path
|
|
21
|
+
// Legacy attribute/index helpers removed in favor of unified adapter path
|
|
21
22
|
import { SchemaGenerator } from "../shared/schemaGenerator.js";
|
|
22
23
|
import {
|
|
23
24
|
isNull,
|
|
@@ -26,11 +27,11 @@ import {
|
|
|
26
27
|
isPlainObject,
|
|
27
28
|
isString,
|
|
28
29
|
} from "es-toolkit";
|
|
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";
|
|
30
|
+
import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
31
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
32
|
+
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
33
|
+
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
|
|
34
|
+
import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
|
|
34
35
|
|
|
35
36
|
// Re-export wipe operations
|
|
36
37
|
export {
|
|
@@ -176,34 +177,34 @@ export const fetchAndCacheCollectionByName = async (
|
|
|
176
177
|
}
|
|
177
178
|
};
|
|
178
179
|
|
|
179
|
-
export const generateSchemas = async (
|
|
180
|
-
config: AppwriteConfig,
|
|
181
|
-
appwriteFolderPath: string
|
|
182
|
-
): Promise<void> => {
|
|
183
|
-
const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
|
|
184
|
-
await schemaGenerator.generateSchemas();
|
|
185
|
-
};
|
|
186
|
-
|
|
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
|
-
};
|
|
180
|
+
export const generateSchemas = async (
|
|
181
|
+
config: AppwriteConfig,
|
|
182
|
+
appwriteFolderPath: string
|
|
183
|
+
): Promise<void> => {
|
|
184
|
+
const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
|
|
185
|
+
await schemaGenerator.generateSchemas();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const createOrUpdateCollections = async (
|
|
189
|
+
database: Databases,
|
|
190
|
+
databaseId: string,
|
|
191
|
+
config: AppwriteConfig,
|
|
192
|
+
deletedCollections?: { collectionId: string; collectionName: string }[],
|
|
193
|
+
selectedCollections: Models.Collection[] = []
|
|
194
|
+
): Promise<void> => {
|
|
195
|
+
// Clear processing state at the start of a new operation
|
|
196
|
+
clearProcessingState();
|
|
197
|
+
|
|
198
|
+
// Always use adapter path (LegacyAdapter translates when pre-1.8)
|
|
199
|
+
const { adapter } = await getAdapterFromConfig(config);
|
|
200
|
+
await createOrUpdateCollectionsViaAdapter(
|
|
201
|
+
adapter,
|
|
202
|
+
databaseId,
|
|
203
|
+
config,
|
|
204
|
+
deletedCollections,
|
|
205
|
+
selectedCollections
|
|
206
|
+
);
|
|
207
|
+
};
|
|
207
208
|
|
|
208
209
|
// New: Adapter-based implementation for TablesDB with state management
|
|
209
210
|
export const createOrUpdateCollectionsViaAdapter = async (
|
|
@@ -215,22 +216,22 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
215
216
|
): Promise<void> => {
|
|
216
217
|
const collectionsToProcess =
|
|
217
218
|
selectedCollections.length > 0 ? selectedCollections : (config.collections || []);
|
|
218
|
-
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
|
219
|
+
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
|
219
220
|
|
|
220
221
|
const usedIds = new Set<string>();
|
|
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
|
-
};
|
|
222
|
+
MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
|
|
223
|
+
|
|
224
|
+
// Helpers for attribute operations through adapter
|
|
225
|
+
const createAttr = async (tableId: string, attr: Attribute) => {
|
|
226
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId, tableId });
|
|
227
|
+
await adapter.createAttribute(params);
|
|
228
|
+
await delay(150);
|
|
229
|
+
};
|
|
230
|
+
const updateAttr = async (tableId: string, attr: Attribute) => {
|
|
231
|
+
const params = mapToUpdateAttributeParams(attr as any, { databaseId, tableId }) as any;
|
|
232
|
+
await adapter.updateAttribute(params);
|
|
233
|
+
await delay(150);
|
|
234
|
+
};
|
|
234
235
|
|
|
235
236
|
// Local queue for unresolved relationships
|
|
236
237
|
const relQueue: { tableId: string; attr: Attribute }[] = [];
|
|
@@ -238,11 +239,11 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
238
239
|
for (const collection of collectionsToProcess) {
|
|
239
240
|
const { attributes, indexes, ...collectionData } = collection as any;
|
|
240
241
|
|
|
241
|
-
// Check if this table has already been processed in this session (per database)
|
|
242
|
-
if (collectionData.$id && isCollectionProcessed(collectionData.$id, databaseId)) {
|
|
243
|
-
MessageFormatter.info(`Table '${collectionData.name}' already processed, skipping`, { prefix: "Tables" });
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
242
|
+
// Check if this table has already been processed in this session (per database)
|
|
243
|
+
if (collectionData.$id && isCollectionProcessed(collectionData.$id, databaseId)) {
|
|
244
|
+
MessageFormatter.info(`Table '${collectionData.name}' already processed, skipping`, { prefix: "Tables" });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
246
247
|
|
|
247
248
|
// Prepare permissions as strings (reuse Permission helper)
|
|
248
249
|
const permissions: string[] = [];
|
|
@@ -262,176 +263,236 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
// Find existing table — prefer lookup by ID (if provided), then by name
|
|
266
|
-
let table: any | undefined;
|
|
267
|
-
let tableId: string;
|
|
268
|
-
|
|
269
|
-
// 1) Try by explicit $id first (handles rename scenarios)
|
|
270
|
-
if (collectionData.$id) {
|
|
271
|
-
try {
|
|
272
|
-
const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
|
|
273
|
-
table = (byId as any).data || (byId as any).tables?.[0];
|
|
274
|
-
if (table?.$id) {
|
|
275
|
-
MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
// Not found by ID; fall back to name lookup
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 2) If not found by ID, try by name
|
|
283
|
-
if (!table) {
|
|
284
|
-
const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
|
|
285
|
-
const items: any[] = (list as any).tables || [];
|
|
286
|
-
table = items[0];
|
|
287
|
-
if (table?.$id) {
|
|
288
|
-
// If local has $id that differs from remote, prefer remote (IDs are immutable)
|
|
289
|
-
if (collectionData.$id && collectionData.$id !== table.$id) {
|
|
290
|
-
MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (!table) {
|
|
296
|
-
// Determine ID (prefer provided $id or re-use deleted one)
|
|
297
|
-
let foundColl = deletedCollections?.find(
|
|
298
|
-
(coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", "")
|
|
299
|
-
);
|
|
300
|
-
if (collectionData.$id) tableId = collectionData.$id;
|
|
301
|
-
else if (foundColl && !usedIds.has(foundColl.collectionId)) tableId = foundColl.collectionId;
|
|
302
|
-
else tableId = ID.unique();
|
|
303
|
-
usedIds.add(tableId);
|
|
304
|
-
|
|
305
|
-
const res = await adapter.createTable({
|
|
306
|
-
databaseId,
|
|
307
|
-
id: tableId,
|
|
308
|
-
name: collectionData.name,
|
|
309
|
-
permissions,
|
|
310
|
-
documentSecurity: !!collectionData.documentSecurity,
|
|
311
|
-
enabled: collectionData.enabled !== false
|
|
312
|
-
});
|
|
313
|
-
table = (res as any).data || res;
|
|
314
|
-
nameToIdMapping.set(collectionData.name, tableId);
|
|
315
|
-
} else {
|
|
316
|
-
tableId = table.$id;
|
|
317
|
-
await adapter.updateTable({
|
|
318
|
-
databaseId,
|
|
319
|
-
id: tableId,
|
|
320
|
-
name: collectionData.name,
|
|
321
|
-
permissions,
|
|
322
|
-
documentSecurity: !!collectionData.documentSecurity,
|
|
323
|
-
enabled: collectionData.enabled !== false
|
|
324
|
-
});
|
|
325
|
-
// Cache the existing table ID
|
|
326
|
-
nameToIdMapping.set(collectionData.name, tableId);
|
|
327
|
-
}
|
|
266
|
+
// Find existing table — prefer lookup by ID (if provided), then by name
|
|
267
|
+
let table: any | undefined;
|
|
268
|
+
let tableId: string;
|
|
269
|
+
|
|
270
|
+
// 1) Try by explicit $id first (handles rename scenarios)
|
|
271
|
+
if (collectionData.$id) {
|
|
272
|
+
try {
|
|
273
|
+
const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
|
|
274
|
+
table = (byId as any).data || (byId as any).tables?.[0];
|
|
275
|
+
if (table?.$id) {
|
|
276
|
+
MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Not found by ID; fall back to name lookup
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 2) If not found by ID, try by name
|
|
284
|
+
if (!table) {
|
|
285
|
+
const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
|
|
286
|
+
const items: any[] = (list as any).tables || [];
|
|
287
|
+
table = items[0];
|
|
288
|
+
if (table?.$id) {
|
|
289
|
+
// If local has $id that differs from remote, prefer remote (IDs are immutable)
|
|
290
|
+
if (collectionData.$id && collectionData.$id !== table.$id) {
|
|
291
|
+
MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!table) {
|
|
297
|
+
// Determine ID (prefer provided $id or re-use deleted one)
|
|
298
|
+
let foundColl = deletedCollections?.find(
|
|
299
|
+
(coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", "")
|
|
300
|
+
);
|
|
301
|
+
if (collectionData.$id) tableId = collectionData.$id;
|
|
302
|
+
else if (foundColl && !usedIds.has(foundColl.collectionId)) tableId = foundColl.collectionId;
|
|
303
|
+
else tableId = ID.unique();
|
|
304
|
+
usedIds.add(tableId);
|
|
305
|
+
|
|
306
|
+
const res = await adapter.createTable({
|
|
307
|
+
databaseId,
|
|
308
|
+
id: tableId,
|
|
309
|
+
name: collectionData.name,
|
|
310
|
+
permissions,
|
|
311
|
+
documentSecurity: !!collectionData.documentSecurity,
|
|
312
|
+
enabled: collectionData.enabled !== false
|
|
313
|
+
});
|
|
314
|
+
table = (res as any).data || res;
|
|
315
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
|
316
|
+
} else {
|
|
317
|
+
tableId = table.$id;
|
|
318
|
+
await adapter.updateTable({
|
|
319
|
+
databaseId,
|
|
320
|
+
id: tableId,
|
|
321
|
+
name: collectionData.name,
|
|
322
|
+
permissions,
|
|
323
|
+
documentSecurity: !!collectionData.documentSecurity,
|
|
324
|
+
enabled: collectionData.enabled !== false
|
|
325
|
+
});
|
|
326
|
+
// Cache the existing table ID
|
|
327
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
|
328
|
+
}
|
|
328
329
|
|
|
329
330
|
// Add small delay after table create/update
|
|
330
331
|
await delay(250);
|
|
331
332
|
|
|
332
|
-
// Create/Update attributes: non-relationship first using enhanced planning
|
|
333
|
-
const nonRel = (attributes || []).filter((a: Attribute) => a.type !== 'relationship');
|
|
334
|
-
if (nonRel.length > 0) {
|
|
335
|
-
// Fetch existing columns once
|
|
336
|
-
const tableInfo = await adapter.getTable({ databaseId, tableId });
|
|
337
|
-
const existingCols: any[] = (tableInfo as any).data?.columns || (tableInfo as any).data?.attributes || [];
|
|
338
|
-
|
|
339
|
-
// Plan with icons
|
|
340
|
-
const plan = diffColumnsDetailed(nonRel as any, existingCols);
|
|
341
|
-
const plus = plan.toCreate.map((a: any) => a.key);
|
|
342
|
-
const plusminus = plan.toUpdate.map((u: any) => (u.attribute as any).key);
|
|
343
|
-
const minus = plan.toRecreate.map((r: any) => (r.newAttribute as any).key);
|
|
344
|
-
const skip = plan.unchanged;
|
|
345
|
-
|
|
346
|
-
// Compute deletions (remote extras not present locally)
|
|
347
|
-
const desiredKeysForDelete = new Set((attributes || []).map((a: any) => a.key));
|
|
348
|
-
const extraRemoteKeys = (existingCols || [])
|
|
349
|
-
.map((c: any) => c?.key)
|
|
350
|
-
.filter((k: any): k is string => !!k && !desiredKeysForDelete.has(k));
|
|
351
|
-
|
|
352
|
-
const parts: string[] = [];
|
|
353
|
-
if (plus.length) parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
|
|
354
|
-
if (plusminus.length) parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
|
|
355
|
-
if (minus.length) parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
|
|
356
|
-
if (skip.length) parts.push(`⏭️ ${skip.length}`);
|
|
357
|
-
parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
|
|
358
|
-
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
|
|
359
|
-
|
|
360
|
-
// Execute
|
|
361
|
-
const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
|
|
362
|
-
|
|
363
|
-
if (colResults.success.length > 0) {
|
|
364
|
-
MessageFormatter.success(`Processed ${colResults.success.length} ops`, { prefix: 'Attributes' });
|
|
365
|
-
}
|
|
366
|
-
if (colResults.errors.length > 0) {
|
|
367
|
-
MessageFormatter.error(`${colResults.errors.length} attribute operations failed:`, undefined, { prefix: 'Attributes' });
|
|
368
|
-
for (const err of colResults.errors) {
|
|
369
|
-
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Attributes' });
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
MessageFormatter.info(
|
|
373
|
-
`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`,
|
|
374
|
-
{ prefix: 'Attributes' }
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
333
|
+
// Create/Update attributes: non-relationship first using enhanced planning
|
|
334
|
+
const nonRel = (attributes || []).filter((a: Attribute) => a.type !== 'relationship');
|
|
335
|
+
if (nonRel.length > 0) {
|
|
336
|
+
// Fetch existing columns once
|
|
337
|
+
const tableInfo = await adapter.getTable({ databaseId, tableId });
|
|
338
|
+
const existingCols: any[] = (tableInfo as any).data?.columns || (tableInfo as any).data?.attributes || [];
|
|
339
|
+
|
|
340
|
+
// Plan with icons
|
|
341
|
+
const plan = diffColumnsDetailed(nonRel as any, existingCols);
|
|
342
|
+
const plus = plan.toCreate.map((a: any) => a.key);
|
|
343
|
+
const plusminus = plan.toUpdate.map((u: any) => (u.attribute as any).key);
|
|
344
|
+
const minus = plan.toRecreate.map((r: any) => (r.newAttribute as any).key);
|
|
345
|
+
const skip = plan.unchanged;
|
|
346
|
+
|
|
347
|
+
// Compute deletions (remote extras not present locally)
|
|
348
|
+
const desiredKeysForDelete = new Set((attributes || []).map((a: any) => a.key));
|
|
349
|
+
const extraRemoteKeys = (existingCols || [])
|
|
350
|
+
.map((c: any) => c?.key)
|
|
351
|
+
.filter((k: any): k is string => !!k && !desiredKeysForDelete.has(k));
|
|
352
|
+
|
|
353
|
+
const parts: string[] = [];
|
|
354
|
+
if (plus.length) parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
|
|
355
|
+
if (plusminus.length) parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
|
|
356
|
+
if (minus.length) parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
|
|
357
|
+
if (skip.length) parts.push(`⏭️ ${skip.length}`);
|
|
358
|
+
parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
|
|
359
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
|
|
360
|
+
|
|
361
|
+
// Execute
|
|
362
|
+
const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
|
|
363
|
+
|
|
364
|
+
if (colResults.success.length > 0) {
|
|
365
|
+
MessageFormatter.success(`Processed ${colResults.success.length} ops`, { prefix: 'Attributes' });
|
|
366
|
+
}
|
|
367
|
+
if (colResults.errors.length > 0) {
|
|
368
|
+
MessageFormatter.error(`${colResults.errors.length} attribute operations failed:`, undefined, { prefix: 'Attributes' });
|
|
369
|
+
for (const err of colResults.errors) {
|
|
370
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Attributes' });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
MessageFormatter.info(
|
|
374
|
+
`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`,
|
|
375
|
+
{ prefix: 'Attributes' }
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
|
|
380
|
+
const relsAll = (attributes || []).filter((a: Attribute) => a.type === 'relationship') as any[];
|
|
381
|
+
if (relsAll.length > 0) {
|
|
382
|
+
const relsResolved: any[] = [];
|
|
383
|
+
const relsDeferred: any[] = [];
|
|
384
|
+
|
|
385
|
+
// Resolve related collections (names -> IDs) using cache or lookup.
|
|
386
|
+
// If not resolvable yet (target table created later in the same push), queue for later.
|
|
387
|
+
for (const attr of relsAll) {
|
|
388
|
+
const relNameOrId = attr.relatedCollection as string | undefined;
|
|
389
|
+
if (!relNameOrId) continue;
|
|
390
|
+
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
|
391
|
+
let resolved = false;
|
|
392
|
+
if (nameToIdMapping.has(relNameOrId)) {
|
|
393
|
+
resolved = true;
|
|
394
|
+
} else {
|
|
395
|
+
// Try resolve by name
|
|
396
|
+
try {
|
|
397
|
+
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
|
398
|
+
const relItems: any[] = (relList as any).tables || [];
|
|
399
|
+
if (relItems[0]?.$id) {
|
|
400
|
+
relId = relItems[0].$id;
|
|
401
|
+
nameToIdMapping.set(relNameOrId, relId);
|
|
402
|
+
resolved = true;
|
|
403
|
+
}
|
|
404
|
+
} catch {}
|
|
405
|
+
|
|
406
|
+
// If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
|
|
407
|
+
if (!resolved && relNameOrId && relNameOrId.length >= 10) {
|
|
408
|
+
try {
|
|
409
|
+
const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
|
|
410
|
+
if ((probe as any).data?.$id) {
|
|
411
|
+
nameToIdMapping.set(relNameOrId, relNameOrId);
|
|
412
|
+
relId = relNameOrId;
|
|
413
|
+
resolved = true;
|
|
414
|
+
}
|
|
415
|
+
} catch {}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (resolved && relId && typeof relId === 'string') {
|
|
420
|
+
attr.relatedCollection = relId;
|
|
421
|
+
relsResolved.push(attr);
|
|
422
|
+
} else {
|
|
423
|
+
// Defer until related table exists; queue a surgical operation
|
|
424
|
+
enqueueOperation({
|
|
425
|
+
type: 'attribute',
|
|
426
|
+
collectionId: tableId,
|
|
427
|
+
attribute: attr,
|
|
428
|
+
dependencies: [relNameOrId]
|
|
429
|
+
});
|
|
430
|
+
relsDeferred.push(attr);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Compute a detailed plan for immediately resolvable relationships
|
|
435
|
+
const tableInfo2 = await adapter.getTable({ databaseId, tableId });
|
|
436
|
+
const existingCols2: any[] = (tableInfo2 as any).data?.columns || (tableInfo2 as any).data?.attributes || [];
|
|
437
|
+
const relPlan = diffColumnsDetailed(relsResolved as any, existingCols2);
|
|
438
|
+
|
|
439
|
+
// Relationship plan with icons (includes recreates)
|
|
440
|
+
{
|
|
441
|
+
const parts: string[] = [];
|
|
442
|
+
if (relPlan.toCreate.length) parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a:any)=>a.key).join(', ')})`);
|
|
443
|
+
if (relPlan.toUpdate.length) parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u:any)=>u.attribute?.key ?? u.key).join(', ')})`);
|
|
444
|
+
if (relPlan.toRecreate.length) parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r:any)=>r.newAttribute?.key ?? r?.key).join(', ')})`);
|
|
445
|
+
if (relPlan.unchanged.length) parts.push(`⏭️ ${relPlan.unchanged.length}`);
|
|
446
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Execute plan using the same operation executor to properly handle deletes/recreates
|
|
450
|
+
const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
|
|
451
|
+
if (relResults.success.length > 0) {
|
|
452
|
+
const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
|
|
453
|
+
const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
|
|
454
|
+
|
|
455
|
+
if (relResults.success.length !== activeRelationships) {
|
|
456
|
+
// Show both counts when they differ (usually due to recreations)
|
|
457
|
+
MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
458
|
+
} else {
|
|
459
|
+
MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (relResults.errors.length > 0) {
|
|
463
|
+
MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
|
|
464
|
+
for (const err of relResults.errors) {
|
|
465
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (relsDeferred.length > 0) {
|
|
470
|
+
MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
412
473
|
|
|
413
474
|
// Wait for all attributes to become available before creating indexes
|
|
414
475
|
const allAttrKeys = [
|
|
415
476
|
...nonRel.map((a: any) => a.key),
|
|
416
|
-
...
|
|
477
|
+
...relsAll.filter((a: any) => a.relatedCollection).map((a: any) => a.key)
|
|
417
478
|
];
|
|
418
479
|
|
|
419
|
-
if (allAttrKeys.length > 0) {
|
|
420
|
-
for (const attrKey of allAttrKeys) {
|
|
421
|
-
const maxWait = 60000; // 60 seconds
|
|
422
|
-
const startTime = Date.now();
|
|
423
|
-
let lastStatus = '';
|
|
424
|
-
|
|
425
|
-
while (Date.now() - startTime < maxWait) {
|
|
426
|
-
try {
|
|
427
|
-
const tableData = await adapter.getTable({ databaseId, tableId });
|
|
428
|
-
const attrs = (tableData as any).data?.columns || (tableData as any).data?.attributes || [];
|
|
429
|
-
const attr = attrs.find((a: any) => a.key === attrKey);
|
|
430
|
-
|
|
431
|
-
if (attr) {
|
|
432
|
-
if (attr.status === 'available') {
|
|
433
|
-
break; // Attribute is ready
|
|
434
|
-
}
|
|
480
|
+
if (allAttrKeys.length > 0) {
|
|
481
|
+
for (const attrKey of allAttrKeys) {
|
|
482
|
+
const maxWait = 60000; // 60 seconds
|
|
483
|
+
const startTime = Date.now();
|
|
484
|
+
let lastStatus = '';
|
|
485
|
+
|
|
486
|
+
while (Date.now() - startTime < maxWait) {
|
|
487
|
+
try {
|
|
488
|
+
const tableData = await adapter.getTable({ databaseId, tableId });
|
|
489
|
+
const attrs = (tableData as any).data?.columns || (tableData as any).data?.attributes || [];
|
|
490
|
+
const attr = attrs.find((a: any) => a.key === attrKey);
|
|
491
|
+
|
|
492
|
+
if (attr) {
|
|
493
|
+
if (attr.status === 'available') {
|
|
494
|
+
break; // Attribute is ready
|
|
495
|
+
}
|
|
435
496
|
if (attr.status === 'failed' || attr.status === 'stuck') {
|
|
436
497
|
throw new Error(`Attribute ${attrKey} failed to create: ${attr.error || 'unknown error'}`);
|
|
437
498
|
}
|
|
@@ -461,197 +522,197 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
461
522
|
c => c.name === collectionData.name || c.$id === collectionData.$id
|
|
462
523
|
);
|
|
463
524
|
const idxs = (localTableConfig?.indexes ?? indexes ?? []) as any[];
|
|
464
|
-
// Compare with existing indexes and create/update accordingly with status checks
|
|
465
|
-
try {
|
|
466
|
-
const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
467
|
-
const existingIdx: any[] = (existingIdxRes as any).data || (existingIdxRes as any).indexes || [];
|
|
468
|
-
MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i:any)=>i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
|
|
469
|
-
// Show a concise plan with icons before executing
|
|
470
|
-
const idxPlanPlus: string[] = [];
|
|
471
|
-
const idxPlanPlusMinus: string[] = [];
|
|
472
|
-
const idxPlanSkip: string[] = [];
|
|
473
|
-
for (const idx of idxs) {
|
|
474
|
-
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
475
|
-
if (found) {
|
|
476
|
-
if (isIndexEqualToIndex(found, idx)) idxPlanSkip.push(idx.key);
|
|
477
|
-
else idxPlanPlusMinus.push(idx.key);
|
|
478
|
-
} else idxPlanPlus.push(idx.key);
|
|
479
|
-
}
|
|
480
|
-
const planParts: string[] = [];
|
|
481
|
-
if (idxPlanPlus.length) planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
|
|
482
|
-
if (idxPlanPlusMinus.length) planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
|
|
483
|
-
if (idxPlanSkip.length) planParts.push(`⏭️ ${idxPlanSkip.length}`);
|
|
484
|
-
MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
|
|
485
|
-
const created: string[] = [];
|
|
486
|
-
const updated: string[] = [];
|
|
487
|
-
const skipped: string[] = [];
|
|
488
|
-
for (const idx of idxs) {
|
|
489
|
-
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
490
|
-
if (found) {
|
|
491
|
-
if (isIndexEqualToIndex(found, idx)) {
|
|
492
|
-
MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
|
|
493
|
-
skipped.push(idx.key);
|
|
494
|
-
} else {
|
|
495
|
-
try { await adapter.deleteIndex({ databaseId, tableId, key: idx.key }); await delay(100); } catch {}
|
|
496
|
-
try {
|
|
497
|
-
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
498
|
-
updated.push(idx.key);
|
|
499
|
-
} catch (e: any) {
|
|
500
|
-
const msg = (e?.message || '').toString().toLowerCase();
|
|
501
|
-
if (msg.includes('already exists')) {
|
|
502
|
-
MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
|
|
503
|
-
skipped.push(idx.key);
|
|
504
|
-
} else {
|
|
505
|
-
throw e;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
try {
|
|
511
|
-
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
512
|
-
created.push(idx.key);
|
|
513
|
-
} catch (e: any) {
|
|
514
|
-
const msg = (e?.message || '').toString().toLowerCase();
|
|
515
|
-
if (msg.includes('already exists')) {
|
|
516
|
-
MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
|
|
517
|
-
skipped.push(idx.key);
|
|
518
|
-
} else {
|
|
519
|
-
throw e;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
// Wait for index availability
|
|
524
|
-
const maxWait = 60000; const start = Date.now(); let lastStatus = '';
|
|
525
|
-
while (Date.now() - start < maxWait) {
|
|
526
|
-
try {
|
|
527
|
-
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
528
|
-
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
529
|
-
const cur = list.find((i: any) => i.key === idx.key);
|
|
530
|
-
if (cur) {
|
|
531
|
-
if (cur.status === 'available') break;
|
|
532
|
-
if (cur.status === 'failed' || cur.status === 'stuck') { throw new Error(cur.error || `Index ${idx.key} failed`); }
|
|
533
|
-
lastStatus = cur.status;
|
|
534
|
-
}
|
|
535
|
-
await delay(2000);
|
|
536
|
-
} catch { await delay(2000); }
|
|
537
|
-
}
|
|
538
|
-
await delay(150);
|
|
539
|
-
}
|
|
540
|
-
MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}` , { prefix: 'Indexes' });
|
|
541
|
-
} catch (e) {
|
|
542
|
-
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Deletions for indexes: remove remote indexes not declared in YAML/config
|
|
546
|
-
try {
|
|
547
|
-
const desiredIndexKeys = new Set((indexes || []).map((i: any) => i.key));
|
|
548
|
-
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
549
|
-
const existingIdx: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
550
|
-
const extraIdx = existingIdx
|
|
551
|
-
.filter((i: any) => i?.key && !desiredIndexKeys.has(i.key))
|
|
552
|
-
.map((i: any) => i.key as string);
|
|
553
|
-
if (extraIdx.length > 0) {
|
|
554
|
-
MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
|
|
555
|
-
const deleted: string[] = [];
|
|
556
|
-
const errors: Array<{ key: string; error: string }> = [];
|
|
557
|
-
for (const key of extraIdx) {
|
|
558
|
-
try {
|
|
559
|
-
await adapter.deleteIndex({ databaseId, tableId, key });
|
|
560
|
-
// Optionally wait for index to disappear
|
|
561
|
-
const start = Date.now();
|
|
562
|
-
const maxWait = 30000;
|
|
563
|
-
while (Date.now() - start < maxWait) {
|
|
564
|
-
try {
|
|
565
|
-
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
566
|
-
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
567
|
-
if (!list.find((ix: any) => ix.key === key)) break;
|
|
568
|
-
} catch {}
|
|
569
|
-
await delay(1000);
|
|
570
|
-
}
|
|
571
|
-
deleted.push(key);
|
|
572
|
-
} catch (e: any) {
|
|
573
|
-
errors.push({ key, error: e?.message || String(e) });
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (deleted.length) {
|
|
577
|
-
MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
|
|
578
|
-
}
|
|
579
|
-
if (errors.length) {
|
|
580
|
-
MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
|
|
581
|
-
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
|
|
582
|
-
}
|
|
583
|
-
} else {
|
|
584
|
-
MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
|
|
585
|
-
}
|
|
586
|
-
} catch (e) {
|
|
587
|
-
MessageFormatter.warning(`Could not evaluate index deletions: ${(e as Error)?.message || e}`, { prefix: 'Indexes' });
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
591
|
-
try {
|
|
592
|
-
const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
|
|
593
|
-
const tableInfo3 = await adapter.getTable({ databaseId, tableId });
|
|
594
|
-
const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
|
|
595
|
-
const toDelete = existingCols3
|
|
596
|
-
.filter((col: any) => col?.key && !desiredKeys.has(col.key))
|
|
597
|
-
.map((col: any) => col.key as string);
|
|
598
|
-
|
|
599
|
-
if (toDelete.length > 0) {
|
|
600
|
-
MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
|
|
601
|
-
const deleted: string[] = [];
|
|
602
|
-
const errors: Array<{ key: string; error: string }> = [];
|
|
603
|
-
for (const key of toDelete) {
|
|
604
|
-
try {
|
|
605
|
-
// Drop any indexes that reference this attribute to avoid server errors
|
|
606
|
-
try {
|
|
607
|
-
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
608
|
-
const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
609
|
-
for (const idx of ilist) {
|
|
610
|
-
const attrs: string[] = Array.isArray(idx.attributes) ? idx.attributes : [];
|
|
611
|
-
if (attrs.includes(key)) {
|
|
612
|
-
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
613
|
-
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
614
|
-
await delay(500);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
} catch {}
|
|
618
|
-
|
|
619
|
-
await adapter.deleteAttribute({ databaseId, tableId, key });
|
|
620
|
-
// Wait briefly for deletion to settle
|
|
621
|
-
const start = Date.now();
|
|
622
|
-
const maxWaitMs = 60000;
|
|
623
|
-
while (Date.now() - start < maxWaitMs) {
|
|
624
|
-
try {
|
|
625
|
-
const tinfo = await adapter.getTable({ databaseId, tableId });
|
|
626
|
-
const cols = (tinfo as any).data?.columns || (tinfo as any).data?.attributes || [];
|
|
627
|
-
const found = cols.find((c: any) => c.key === key);
|
|
628
|
-
if (!found) break;
|
|
629
|
-
if (found.status && found.status !== 'deleting') break;
|
|
630
|
-
} catch {}
|
|
631
|
-
await delay(1000);
|
|
632
|
-
}
|
|
633
|
-
deleted.push(key);
|
|
634
|
-
} catch (e: any) {
|
|
635
|
-
errors.push({ key, error: e?.message || String(e) });
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (deleted.length) {
|
|
639
|
-
MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
|
|
640
|
-
}
|
|
641
|
-
if (errors.length) {
|
|
642
|
-
MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
|
|
643
|
-
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
|
|
644
|
-
}
|
|
645
|
-
} else {
|
|
646
|
-
MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
|
|
647
|
-
}
|
|
648
|
-
} catch (e) {
|
|
649
|
-
MessageFormatter.warning(`Could not evaluate deletions: ${(e as Error)?.message || e}`, { prefix: 'Attributes' });
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Mark this table as fully processed for this database to prevent re-processing in the same DB only
|
|
653
|
-
markCollectionProcessed(tableId, collectionData.name, databaseId);
|
|
654
|
-
}
|
|
525
|
+
// Compare with existing indexes and create/update accordingly with status checks
|
|
526
|
+
try {
|
|
527
|
+
const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
528
|
+
const existingIdx: any[] = (existingIdxRes as any).data || (existingIdxRes as any).indexes || [];
|
|
529
|
+
MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i:any)=>i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
|
|
530
|
+
// Show a concise plan with icons before executing
|
|
531
|
+
const idxPlanPlus: string[] = [];
|
|
532
|
+
const idxPlanPlusMinus: string[] = [];
|
|
533
|
+
const idxPlanSkip: string[] = [];
|
|
534
|
+
for (const idx of idxs) {
|
|
535
|
+
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
536
|
+
if (found) {
|
|
537
|
+
if (isIndexEqualToIndex(found, idx)) idxPlanSkip.push(idx.key);
|
|
538
|
+
else idxPlanPlusMinus.push(idx.key);
|
|
539
|
+
} else idxPlanPlus.push(idx.key);
|
|
540
|
+
}
|
|
541
|
+
const planParts: string[] = [];
|
|
542
|
+
if (idxPlanPlus.length) planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
|
|
543
|
+
if (idxPlanPlusMinus.length) planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
|
|
544
|
+
if (idxPlanSkip.length) planParts.push(`⏭️ ${idxPlanSkip.length}`);
|
|
545
|
+
MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
|
|
546
|
+
const created: string[] = [];
|
|
547
|
+
const updated: string[] = [];
|
|
548
|
+
const skipped: string[] = [];
|
|
549
|
+
for (const idx of idxs) {
|
|
550
|
+
const found = existingIdx.find((i: any) => i.key === idx.key);
|
|
551
|
+
if (found) {
|
|
552
|
+
if (isIndexEqualToIndex(found, idx)) {
|
|
553
|
+
MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
|
|
554
|
+
skipped.push(idx.key);
|
|
555
|
+
} else {
|
|
556
|
+
try { await adapter.deleteIndex({ databaseId, tableId, key: idx.key }); await delay(100); } catch {}
|
|
557
|
+
try {
|
|
558
|
+
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
559
|
+
updated.push(idx.key);
|
|
560
|
+
} catch (e: any) {
|
|
561
|
+
const msg = (e?.message || '').toString().toLowerCase();
|
|
562
|
+
if (msg.includes('already exists')) {
|
|
563
|
+
MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
|
|
564
|
+
skipped.push(idx.key);
|
|
565
|
+
} else {
|
|
566
|
+
throw e;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
try {
|
|
572
|
+
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
573
|
+
created.push(idx.key);
|
|
574
|
+
} catch (e: any) {
|
|
575
|
+
const msg = (e?.message || '').toString().toLowerCase();
|
|
576
|
+
if (msg.includes('already exists')) {
|
|
577
|
+
MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
|
|
578
|
+
skipped.push(idx.key);
|
|
579
|
+
} else {
|
|
580
|
+
throw e;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Wait for index availability
|
|
585
|
+
const maxWait = 60000; const start = Date.now(); let lastStatus = '';
|
|
586
|
+
while (Date.now() - start < maxWait) {
|
|
587
|
+
try {
|
|
588
|
+
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
589
|
+
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
590
|
+
const cur = list.find((i: any) => i.key === idx.key);
|
|
591
|
+
if (cur) {
|
|
592
|
+
if (cur.status === 'available') break;
|
|
593
|
+
if (cur.status === 'failed' || cur.status === 'stuck') { throw new Error(cur.error || `Index ${idx.key} failed`); }
|
|
594
|
+
lastStatus = cur.status;
|
|
595
|
+
}
|
|
596
|
+
await delay(2000);
|
|
597
|
+
} catch { await delay(2000); }
|
|
598
|
+
}
|
|
599
|
+
await delay(150);
|
|
600
|
+
}
|
|
601
|
+
MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}` , { prefix: 'Indexes' });
|
|
602
|
+
} catch (e) {
|
|
603
|
+
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Deletions for indexes: remove remote indexes not declared in YAML/config
|
|
607
|
+
try {
|
|
608
|
+
const desiredIndexKeys = new Set((indexes || []).map((i: any) => i.key));
|
|
609
|
+
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
610
|
+
const existingIdx: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
611
|
+
const extraIdx = existingIdx
|
|
612
|
+
.filter((i: any) => i?.key && !desiredIndexKeys.has(i.key))
|
|
613
|
+
.map((i: any) => i.key as string);
|
|
614
|
+
if (extraIdx.length > 0) {
|
|
615
|
+
MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
|
|
616
|
+
const deleted: string[] = [];
|
|
617
|
+
const errors: Array<{ key: string; error: string }> = [];
|
|
618
|
+
for (const key of extraIdx) {
|
|
619
|
+
try {
|
|
620
|
+
await adapter.deleteIndex({ databaseId, tableId, key });
|
|
621
|
+
// Optionally wait for index to disappear
|
|
622
|
+
const start = Date.now();
|
|
623
|
+
const maxWait = 30000;
|
|
624
|
+
while (Date.now() - start < maxWait) {
|
|
625
|
+
try {
|
|
626
|
+
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
627
|
+
const list: any[] = (li as any).data || (li as any).indexes || [];
|
|
628
|
+
if (!list.find((ix: any) => ix.key === key)) break;
|
|
629
|
+
} catch {}
|
|
630
|
+
await delay(1000);
|
|
631
|
+
}
|
|
632
|
+
deleted.push(key);
|
|
633
|
+
} catch (e: any) {
|
|
634
|
+
errors.push({ key, error: e?.message || String(e) });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (deleted.length) {
|
|
638
|
+
MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
|
|
639
|
+
}
|
|
640
|
+
if (errors.length) {
|
|
641
|
+
MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
|
|
642
|
+
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
|
|
646
|
+
}
|
|
647
|
+
} catch (e) {
|
|
648
|
+
MessageFormatter.warning(`Could not evaluate index deletions: ${(e as Error)?.message || e}`, { prefix: 'Indexes' });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
652
|
+
try {
|
|
653
|
+
const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
|
|
654
|
+
const tableInfo3 = await adapter.getTable({ databaseId, tableId });
|
|
655
|
+
const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
|
|
656
|
+
const toDelete = existingCols3
|
|
657
|
+
.filter((col: any) => col?.key && !desiredKeys.has(col.key))
|
|
658
|
+
.map((col: any) => col.key as string);
|
|
659
|
+
|
|
660
|
+
if (toDelete.length > 0) {
|
|
661
|
+
MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
|
|
662
|
+
const deleted: string[] = [];
|
|
663
|
+
const errors: Array<{ key: string; error: string }> = [];
|
|
664
|
+
for (const key of toDelete) {
|
|
665
|
+
try {
|
|
666
|
+
// Drop any indexes that reference this attribute to avoid server errors
|
|
667
|
+
try {
|
|
668
|
+
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
669
|
+
const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
670
|
+
for (const idx of ilist) {
|
|
671
|
+
const attrs: string[] = Array.isArray(idx.attributes) ? idx.attributes : [];
|
|
672
|
+
if (attrs.includes(key)) {
|
|
673
|
+
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
674
|
+
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
675
|
+
await delay(500);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
} catch {}
|
|
679
|
+
|
|
680
|
+
await adapter.deleteAttribute({ databaseId, tableId, key });
|
|
681
|
+
// Wait briefly for deletion to settle
|
|
682
|
+
const start = Date.now();
|
|
683
|
+
const maxWaitMs = 60000;
|
|
684
|
+
while (Date.now() - start < maxWaitMs) {
|
|
685
|
+
try {
|
|
686
|
+
const tinfo = await adapter.getTable({ databaseId, tableId });
|
|
687
|
+
const cols = (tinfo as any).data?.columns || (tinfo as any).data?.attributes || [];
|
|
688
|
+
const found = cols.find((c: any) => c.key === key);
|
|
689
|
+
if (!found) break;
|
|
690
|
+
if (found.status && found.status !== 'deleting') break;
|
|
691
|
+
} catch {}
|
|
692
|
+
await delay(1000);
|
|
693
|
+
}
|
|
694
|
+
deleted.push(key);
|
|
695
|
+
} catch (e: any) {
|
|
696
|
+
errors.push({ key, error: e?.message || String(e) });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (deleted.length) {
|
|
700
|
+
MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
|
|
701
|
+
}
|
|
702
|
+
if (errors.length) {
|
|
703
|
+
MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
|
|
704
|
+
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
|
|
708
|
+
}
|
|
709
|
+
} catch (e) {
|
|
710
|
+
MessageFormatter.warning(`Could not evaluate deletions: ${(e as Error)?.message || e}`, { prefix: 'Attributes' });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Mark this table as fully processed for this database to prevent re-processing in the same DB only
|
|
714
|
+
markCollectionProcessed(tableId, collectionData.name, databaseId);
|
|
715
|
+
}
|
|
655
716
|
|
|
656
717
|
// Process queued relationships once mapping likely populated
|
|
657
718
|
if (relQueue.length > 0) {
|
|
@@ -692,6 +753,15 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
692
753
|
}
|
|
693
754
|
}
|
|
694
755
|
}
|
|
756
|
+
|
|
757
|
+
// Process any remaining queued operations to complete relationship sync
|
|
758
|
+
try {
|
|
759
|
+
MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
|
|
760
|
+
await processQueue(adapter, databaseId);
|
|
761
|
+
MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
|
|
762
|
+
} catch (error) {
|
|
763
|
+
MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
|
|
764
|
+
}
|
|
695
765
|
};
|
|
696
766
|
|
|
697
767
|
export const generateMockData = async (
|
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() {
|