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
|
@@ -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,12 @@ 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";
|
|
35
|
+
import { createOrUpdateIndexesViaAdapter, deleteObsoleteIndexesViaAdapter } from "../tables/indexManager.js";
|
|
34
36
|
|
|
35
37
|
// Re-export wipe operations
|
|
36
38
|
export {
|
|
@@ -176,34 +178,34 @@ export const fetchAndCacheCollectionByName = async (
|
|
|
176
178
|
}
|
|
177
179
|
};
|
|
178
180
|
|
|
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
|
-
};
|
|
181
|
+
export const generateSchemas = async (
|
|
182
|
+
config: AppwriteConfig,
|
|
183
|
+
appwriteFolderPath: string
|
|
184
|
+
): Promise<void> => {
|
|
185
|
+
const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
|
|
186
|
+
await schemaGenerator.generateSchemas();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const createOrUpdateCollections = async (
|
|
190
|
+
database: Databases,
|
|
191
|
+
databaseId: string,
|
|
192
|
+
config: AppwriteConfig,
|
|
193
|
+
deletedCollections?: { collectionId: string; collectionName: string }[],
|
|
194
|
+
selectedCollections: Models.Collection[] = []
|
|
195
|
+
): Promise<void> => {
|
|
196
|
+
// Clear processing state at the start of a new operation
|
|
197
|
+
clearProcessingState();
|
|
198
|
+
|
|
199
|
+
// Always use adapter path (LegacyAdapter translates when pre-1.8)
|
|
200
|
+
const { adapter } = await getAdapterFromConfig(config);
|
|
201
|
+
await createOrUpdateCollectionsViaAdapter(
|
|
202
|
+
adapter,
|
|
203
|
+
databaseId,
|
|
204
|
+
config,
|
|
205
|
+
deletedCollections,
|
|
206
|
+
selectedCollections
|
|
207
|
+
);
|
|
208
|
+
};
|
|
207
209
|
|
|
208
210
|
// New: Adapter-based implementation for TablesDB with state management
|
|
209
211
|
export const createOrUpdateCollectionsViaAdapter = async (
|
|
@@ -215,22 +217,22 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
215
217
|
): Promise<void> => {
|
|
216
218
|
const collectionsToProcess =
|
|
217
219
|
selectedCollections.length > 0 ? selectedCollections : (config.collections || []);
|
|
218
|
-
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
|
220
|
+
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
|
219
221
|
|
|
220
222
|
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
|
-
};
|
|
223
|
+
MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
|
|
224
|
+
|
|
225
|
+
// Helpers for attribute operations through adapter
|
|
226
|
+
const createAttr = async (tableId: string, attr: Attribute) => {
|
|
227
|
+
const params = mapToCreateAttributeParams(attr as any, { databaseId, tableId });
|
|
228
|
+
await adapter.createAttribute(params);
|
|
229
|
+
await delay(150);
|
|
230
|
+
};
|
|
231
|
+
const updateAttr = async (tableId: string, attr: Attribute) => {
|
|
232
|
+
const params = mapToUpdateAttributeParams(attr as any, { databaseId, tableId }) as any;
|
|
233
|
+
await adapter.updateAttribute(params);
|
|
234
|
+
await delay(150);
|
|
235
|
+
};
|
|
234
236
|
|
|
235
237
|
// Local queue for unresolved relationships
|
|
236
238
|
const relQueue: { tableId: string; attr: Attribute }[] = [];
|
|
@@ -238,11 +240,11 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
238
240
|
for (const collection of collectionsToProcess) {
|
|
239
241
|
const { attributes, indexes, ...collectionData } = collection as any;
|
|
240
242
|
|
|
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
|
-
}
|
|
243
|
+
// Check if this table has already been processed in this session (per database)
|
|
244
|
+
if (collectionData.$id && isCollectionProcessed(collectionData.$id, databaseId)) {
|
|
245
|
+
MessageFormatter.info(`Table '${collectionData.name}' already processed, skipping`, { prefix: "Tables" });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
246
248
|
|
|
247
249
|
// Prepare permissions as strings (reuse Permission helper)
|
|
248
250
|
const permissions: string[] = [];
|
|
@@ -262,176 +264,236 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
|
|
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
|
-
}
|
|
267
|
+
// Find existing table — prefer lookup by ID (if provided), then by name
|
|
268
|
+
let table: any | undefined;
|
|
269
|
+
let tableId: string;
|
|
270
|
+
|
|
271
|
+
// 1) Try by explicit $id first (handles rename scenarios)
|
|
272
|
+
if (collectionData.$id) {
|
|
273
|
+
try {
|
|
274
|
+
const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
|
|
275
|
+
table = (byId as any).data || (byId as any).tables?.[0];
|
|
276
|
+
if (table?.$id) {
|
|
277
|
+
MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// Not found by ID; fall back to name lookup
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2) If not found by ID, try by name
|
|
285
|
+
if (!table) {
|
|
286
|
+
const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
|
|
287
|
+
const items: any[] = (list as any).tables || [];
|
|
288
|
+
table = items[0];
|
|
289
|
+
if (table?.$id) {
|
|
290
|
+
// If local has $id that differs from remote, prefer remote (IDs are immutable)
|
|
291
|
+
if (collectionData.$id && collectionData.$id !== table.$id) {
|
|
292
|
+
MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!table) {
|
|
298
|
+
// Determine ID (prefer provided $id or re-use deleted one)
|
|
299
|
+
let foundColl = deletedCollections?.find(
|
|
300
|
+
(coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", "")
|
|
301
|
+
);
|
|
302
|
+
if (collectionData.$id) tableId = collectionData.$id;
|
|
303
|
+
else if (foundColl && !usedIds.has(foundColl.collectionId)) tableId = foundColl.collectionId;
|
|
304
|
+
else tableId = ID.unique();
|
|
305
|
+
usedIds.add(tableId);
|
|
306
|
+
|
|
307
|
+
const res = await adapter.createTable({
|
|
308
|
+
databaseId,
|
|
309
|
+
id: tableId,
|
|
310
|
+
name: collectionData.name,
|
|
311
|
+
permissions,
|
|
312
|
+
documentSecurity: !!collectionData.documentSecurity,
|
|
313
|
+
enabled: collectionData.enabled !== false
|
|
314
|
+
});
|
|
315
|
+
table = (res as any).data || res;
|
|
316
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
|
317
|
+
} else {
|
|
318
|
+
tableId = table.$id;
|
|
319
|
+
await adapter.updateTable({
|
|
320
|
+
databaseId,
|
|
321
|
+
id: tableId,
|
|
322
|
+
name: collectionData.name,
|
|
323
|
+
permissions,
|
|
324
|
+
documentSecurity: !!collectionData.documentSecurity,
|
|
325
|
+
enabled: collectionData.enabled !== false
|
|
326
|
+
});
|
|
327
|
+
// Cache the existing table ID
|
|
328
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
|
329
|
+
}
|
|
328
330
|
|
|
329
331
|
// Add small delay after table create/update
|
|
330
332
|
await delay(250);
|
|
331
333
|
|
|
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
|
-
|
|
334
|
+
// Create/Update attributes: non-relationship first using enhanced planning
|
|
335
|
+
const nonRel = (attributes || []).filter((a: Attribute) => a.type !== 'relationship');
|
|
336
|
+
if (nonRel.length > 0) {
|
|
337
|
+
// Fetch existing columns once
|
|
338
|
+
const tableInfo = await adapter.getTable({ databaseId, tableId });
|
|
339
|
+
const existingCols: any[] = (tableInfo as any).data?.columns || (tableInfo as any).data?.attributes || [];
|
|
340
|
+
|
|
341
|
+
// Plan with icons
|
|
342
|
+
const plan = diffColumnsDetailed(nonRel as any, existingCols);
|
|
343
|
+
const plus = plan.toCreate.map((a: any) => a.key);
|
|
344
|
+
const plusminus = plan.toUpdate.map((u: any) => (u.attribute as any).key);
|
|
345
|
+
const minus = plan.toRecreate.map((r: any) => (r.newAttribute as any).key);
|
|
346
|
+
const skip = plan.unchanged;
|
|
347
|
+
|
|
348
|
+
// Compute deletions (remote extras not present locally)
|
|
349
|
+
const desiredKeysForDelete = new Set((attributes || []).map((a: any) => a.key));
|
|
350
|
+
const extraRemoteKeys = (existingCols || [])
|
|
351
|
+
.map((c: any) => c?.key)
|
|
352
|
+
.filter((k: any): k is string => !!k && !desiredKeysForDelete.has(k));
|
|
353
|
+
|
|
354
|
+
const parts: string[] = [];
|
|
355
|
+
if (plus.length) parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
|
|
356
|
+
if (plusminus.length) parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
|
|
357
|
+
if (minus.length) parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
|
|
358
|
+
if (skip.length) parts.push(`⏭️ ${skip.length}`);
|
|
359
|
+
parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
|
|
360
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
|
|
361
|
+
|
|
362
|
+
// Execute
|
|
363
|
+
const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
|
|
364
|
+
|
|
365
|
+
if (colResults.success.length > 0) {
|
|
366
|
+
MessageFormatter.success(`Processed ${colResults.success.length} ops`, { prefix: 'Attributes' });
|
|
367
|
+
}
|
|
368
|
+
if (colResults.errors.length > 0) {
|
|
369
|
+
MessageFormatter.error(`${colResults.errors.length} attribute operations failed:`, undefined, { prefix: 'Attributes' });
|
|
370
|
+
for (const err of colResults.errors) {
|
|
371
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Attributes' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
MessageFormatter.info(
|
|
375
|
+
`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`,
|
|
376
|
+
{ prefix: 'Attributes' }
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
|
|
381
|
+
const relsAll = (attributes || []).filter((a: Attribute) => a.type === 'relationship') as any[];
|
|
382
|
+
if (relsAll.length > 0) {
|
|
383
|
+
const relsResolved: any[] = [];
|
|
384
|
+
const relsDeferred: any[] = [];
|
|
385
|
+
|
|
386
|
+
// Resolve related collections (names -> IDs) using cache or lookup.
|
|
387
|
+
// If not resolvable yet (target table created later in the same push), queue for later.
|
|
388
|
+
for (const attr of relsAll) {
|
|
389
|
+
const relNameOrId = attr.relatedCollection as string | undefined;
|
|
390
|
+
if (!relNameOrId) continue;
|
|
391
|
+
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
|
392
|
+
let resolved = false;
|
|
393
|
+
if (nameToIdMapping.has(relNameOrId)) {
|
|
394
|
+
resolved = true;
|
|
395
|
+
} else {
|
|
396
|
+
// Try resolve by name
|
|
397
|
+
try {
|
|
398
|
+
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
|
399
|
+
const relItems: any[] = (relList as any).tables || [];
|
|
400
|
+
if (relItems[0]?.$id) {
|
|
401
|
+
relId = relItems[0].$id;
|
|
402
|
+
nameToIdMapping.set(relNameOrId, relId);
|
|
403
|
+
resolved = true;
|
|
404
|
+
}
|
|
405
|
+
} catch {}
|
|
406
|
+
|
|
407
|
+
// If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
|
|
408
|
+
if (!resolved && relNameOrId && relNameOrId.length >= 10) {
|
|
409
|
+
try {
|
|
410
|
+
const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
|
|
411
|
+
if ((probe as any).data?.$id) {
|
|
412
|
+
nameToIdMapping.set(relNameOrId, relNameOrId);
|
|
413
|
+
relId = relNameOrId;
|
|
414
|
+
resolved = true;
|
|
415
|
+
}
|
|
416
|
+
} catch {}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (resolved && relId && typeof relId === 'string') {
|
|
421
|
+
attr.relatedCollection = relId;
|
|
422
|
+
relsResolved.push(attr);
|
|
423
|
+
} else {
|
|
424
|
+
// Defer until related table exists; queue a surgical operation
|
|
425
|
+
enqueueOperation({
|
|
426
|
+
type: 'attribute',
|
|
427
|
+
collectionId: tableId,
|
|
428
|
+
attribute: attr,
|
|
429
|
+
dependencies: [relNameOrId]
|
|
430
|
+
});
|
|
431
|
+
relsDeferred.push(attr);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Compute a detailed plan for immediately resolvable relationships
|
|
436
|
+
const tableInfo2 = await adapter.getTable({ databaseId, tableId });
|
|
437
|
+
const existingCols2: any[] = (tableInfo2 as any).data?.columns || (tableInfo2 as any).data?.attributes || [];
|
|
438
|
+
const relPlan = diffColumnsDetailed(relsResolved as any, existingCols2);
|
|
439
|
+
|
|
440
|
+
// Relationship plan with icons (includes recreates)
|
|
441
|
+
{
|
|
442
|
+
const parts: string[] = [];
|
|
443
|
+
if (relPlan.toCreate.length) parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a:any)=>a.key).join(', ')})`);
|
|
444
|
+
if (relPlan.toUpdate.length) parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u:any)=>u.attribute?.key ?? u.key).join(', ')})`);
|
|
445
|
+
if (relPlan.toRecreate.length) parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r:any)=>r.newAttribute?.key ?? r?.key).join(', ')})`);
|
|
446
|
+
if (relPlan.unchanged.length) parts.push(`⏭️ ${relPlan.unchanged.length}`);
|
|
447
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Execute plan using the same operation executor to properly handle deletes/recreates
|
|
451
|
+
const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
|
|
452
|
+
if (relResults.success.length > 0) {
|
|
453
|
+
const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
|
|
454
|
+
const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
|
|
455
|
+
|
|
456
|
+
if (relResults.success.length !== activeRelationships) {
|
|
457
|
+
// Show both counts when they differ (usually due to recreations)
|
|
458
|
+
MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
459
|
+
} else {
|
|
460
|
+
MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (relResults.errors.length > 0) {
|
|
464
|
+
MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
|
|
465
|
+
for (const err of relResults.errors) {
|
|
466
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (relsDeferred.length > 0) {
|
|
471
|
+
MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
412
474
|
|
|
413
475
|
// Wait for all attributes to become available before creating indexes
|
|
414
476
|
const allAttrKeys = [
|
|
415
477
|
...nonRel.map((a: any) => a.key),
|
|
416
|
-
...
|
|
478
|
+
...relsAll.filter((a: any) => a.relatedCollection).map((a: any) => a.key)
|
|
417
479
|
];
|
|
418
480
|
|
|
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
|
-
}
|
|
481
|
+
if (allAttrKeys.length > 0) {
|
|
482
|
+
for (const attrKey of allAttrKeys) {
|
|
483
|
+
const maxWait = 60000; // 60 seconds
|
|
484
|
+
const startTime = Date.now();
|
|
485
|
+
let lastStatus = '';
|
|
486
|
+
|
|
487
|
+
while (Date.now() - startTime < maxWait) {
|
|
488
|
+
try {
|
|
489
|
+
const tableData = await adapter.getTable({ databaseId, tableId });
|
|
490
|
+
const attrs = (tableData as any).data?.columns || (tableData as any).data?.attributes || [];
|
|
491
|
+
const attr = attrs.find((a: any) => a.key === attrKey);
|
|
492
|
+
|
|
493
|
+
if (attr) {
|
|
494
|
+
if (attr.status === 'available') {
|
|
495
|
+
break; // Attribute is ready
|
|
496
|
+
}
|
|
435
497
|
if (attr.status === 'failed' || attr.status === 'stuck') {
|
|
436
498
|
throw new Error(`Attribute ${attrKey} failed to create: ${attr.error || 'unknown error'}`);
|
|
437
499
|
}
|
|
@@ -456,202 +518,86 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
456
518
|
}
|
|
457
519
|
}
|
|
458
520
|
|
|
459
|
-
//
|
|
521
|
+
// Index management: create/update indexes using clean adapter-based system
|
|
460
522
|
const localTableConfig = config.collections?.find(
|
|
461
523
|
c => c.name === collectionData.name || c.$id === collectionData.$id
|
|
462
524
|
);
|
|
463
525
|
const idxs = (localTableConfig?.indexes ?? indexes ?? []) as any[];
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
}
|
|
526
|
+
|
|
527
|
+
// Create/update indexes with proper planning and execution
|
|
528
|
+
await createOrUpdateIndexesViaAdapter(adapter, databaseId, tableId, idxs, indexes);
|
|
529
|
+
|
|
530
|
+
// Handle obsolete index deletions
|
|
531
|
+
const desiredIndexKeys: Set<string> = new Set((indexes || []).map((i: any) => i.key as string));
|
|
532
|
+
await deleteObsoleteIndexesViaAdapter(adapter, databaseId, tableId, desiredIndexKeys);
|
|
533
|
+
|
|
534
|
+
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
535
|
+
try {
|
|
536
|
+
const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
|
|
537
|
+
const tableInfo3 = await adapter.getTable({ databaseId, tableId });
|
|
538
|
+
const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
|
|
539
|
+
const toDelete = existingCols3
|
|
540
|
+
.filter((col: any) => col?.key && !desiredKeys.has(col.key))
|
|
541
|
+
.map((col: any) => col.key as string);
|
|
542
|
+
|
|
543
|
+
if (toDelete.length > 0) {
|
|
544
|
+
MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
|
|
545
|
+
const deleted: string[] = [];
|
|
546
|
+
const errors: Array<{ key: string; error: string }> = [];
|
|
547
|
+
for (const key of toDelete) {
|
|
548
|
+
try {
|
|
549
|
+
// Drop any indexes that reference this attribute to avoid server errors
|
|
550
|
+
try {
|
|
551
|
+
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
552
|
+
const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
553
|
+
for (const idx of ilist) {
|
|
554
|
+
const attrs: string[] = Array.isArray(idx.attributes)
|
|
555
|
+
? idx.attributes
|
|
556
|
+
: (Array.isArray((idx as any).columns) ? (idx as any).columns : []);
|
|
557
|
+
if (attrs.includes(key)) {
|
|
558
|
+
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
559
|
+
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
560
|
+
await delay(500);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} catch {}
|
|
564
|
+
|
|
565
|
+
await adapter.deleteAttribute({ databaseId, tableId, key });
|
|
566
|
+
// Wait briefly for deletion to settle
|
|
567
|
+
const start = Date.now();
|
|
568
|
+
const maxWaitMs = 60000;
|
|
569
|
+
while (Date.now() - start < maxWaitMs) {
|
|
570
|
+
try {
|
|
571
|
+
const tinfo = await adapter.getTable({ databaseId, tableId });
|
|
572
|
+
const cols = (tinfo as any).data?.columns || (tinfo as any).data?.attributes || [];
|
|
573
|
+
const found = cols.find((c: any) => c.key === key);
|
|
574
|
+
if (!found) break;
|
|
575
|
+
if (found.status && found.status !== 'deleting') break;
|
|
576
|
+
} catch {}
|
|
577
|
+
await delay(1000);
|
|
578
|
+
}
|
|
579
|
+
deleted.push(key);
|
|
580
|
+
} catch (e: any) {
|
|
581
|
+
errors.push({ key, error: e?.message || String(e) });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (deleted.length) {
|
|
585
|
+
MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
|
|
586
|
+
}
|
|
587
|
+
if (errors.length) {
|
|
588
|
+
MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
|
|
589
|
+
errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
|
|
593
|
+
}
|
|
594
|
+
} catch (e) {
|
|
595
|
+
MessageFormatter.warning(`Could not evaluate deletions: ${(e as Error)?.message || e}`, { prefix: 'Attributes' });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Mark this table as fully processed for this database to prevent re-processing in the same DB only
|
|
599
|
+
markCollectionProcessed(tableId, collectionData.name, databaseId);
|
|
600
|
+
}
|
|
655
601
|
|
|
656
602
|
// Process queued relationships once mapping likely populated
|
|
657
603
|
if (relQueue.length > 0) {
|
|
@@ -692,6 +638,15 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
692
638
|
}
|
|
693
639
|
}
|
|
694
640
|
}
|
|
641
|
+
|
|
642
|
+
// Process any remaining queued operations to complete relationship sync
|
|
643
|
+
try {
|
|
644
|
+
MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
|
|
645
|
+
await processQueue(adapter, databaseId);
|
|
646
|
+
MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
|
|
647
|
+
} catch (error) {
|
|
648
|
+
MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
|
|
649
|
+
}
|
|
695
650
|
};
|
|
696
651
|
|
|
697
652
|
export const generateMockData = async (
|