appwrite-utils-cli 1.7.9 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -199
- package/README.md +87 -30
- package/dist/adapters/AdapterFactory.js +5 -25
- package/dist/adapters/DatabaseAdapter.d.ts +17 -2
- package/dist/adapters/LegacyAdapter.d.ts +2 -1
- package/dist/adapters/LegacyAdapter.js +212 -16
- package/dist/adapters/TablesDBAdapter.d.ts +2 -12
- package/dist/adapters/TablesDBAdapter.js +261 -57
- package/dist/cli/commands/databaseCommands.js +4 -3
- package/dist/cli/commands/functionCommands.js +17 -8
- package/dist/collections/attributes.js +447 -125
- package/dist/collections/methods.js +197 -186
- package/dist/collections/tableOperations.d.ts +86 -0
- package/dist/collections/tableOperations.js +434 -0
- package/dist/collections/transferOperations.d.ts +3 -2
- package/dist/collections/transferOperations.js +93 -12
- package/dist/config/yamlConfig.d.ts +221 -88
- package/dist/examples/yamlTerminologyExample.d.ts +1 -1
- package/dist/examples/yamlTerminologyExample.js +6 -3
- package/dist/functions/fnConfigDiscovery.d.ts +3 -0
- package/dist/functions/fnConfigDiscovery.js +108 -0
- package/dist/interactiveCLI.js +18 -15
- package/dist/main.js +211 -73
- package/dist/migrations/appwriteToX.d.ts +88 -23
- package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
- package/dist/migrations/comprehensiveTransfer.js +83 -6
- package/dist/migrations/dataLoader.d.ts +227 -69
- package/dist/migrations/dataLoader.js +3 -3
- package/dist/migrations/importController.js +3 -3
- package/dist/migrations/relationships.d.ts +8 -2
- package/dist/migrations/services/ImportOrchestrator.js +3 -3
- package/dist/migrations/transfer.js +159 -37
- package/dist/shared/attributeMapper.d.ts +20 -0
- package/dist/shared/attributeMapper.js +203 -0
- package/dist/shared/selectionDialogs.js +8 -4
- package/dist/storage/schemas.d.ts +354 -92
- package/dist/utils/configDiscovery.js +4 -3
- package/dist/utils/versionDetection.d.ts +0 -4
- package/dist/utils/versionDetection.js +41 -173
- package/dist/utils/yamlConverter.js +89 -16
- package/dist/utils/yamlLoader.d.ts +1 -1
- package/dist/utils/yamlLoader.js +6 -2
- package/dist/utilsController.js +56 -19
- package/package.json +4 -4
- package/src/adapters/AdapterFactory.ts +119 -143
- package/src/adapters/DatabaseAdapter.ts +18 -3
- package/src/adapters/LegacyAdapter.ts +236 -105
- package/src/adapters/TablesDBAdapter.ts +773 -643
- package/src/cli/commands/databaseCommands.ts +13 -12
- package/src/cli/commands/functionCommands.ts +23 -14
- package/src/collections/attributes.ts +2054 -1611
- package/src/collections/methods.ts +208 -293
- package/src/collections/tableOperations.ts +506 -0
- package/src/collections/transferOperations.ts +218 -144
- package/src/examples/yamlTerminologyExample.ts +10 -5
- package/src/functions/fnConfigDiscovery.ts +103 -0
- package/src/interactiveCLI.ts +25 -20
- package/src/main.ts +549 -194
- package/src/migrations/comprehensiveTransfer.ts +126 -50
- package/src/migrations/dataLoader.ts +3 -3
- package/src/migrations/importController.ts +3 -3
- package/src/migrations/services/ImportOrchestrator.ts +3 -3
- package/src/migrations/transfer.ts +148 -131
- package/src/shared/attributeMapper.ts +229 -0
- package/src/shared/selectionDialogs.ts +29 -25
- package/src/utils/configDiscovery.ts +9 -3
- package/src/utils/versionDetection.ts +74 -228
- package/src/utils/yamlConverter.ts +94 -17
- package/src/utils/yamlLoader.ts +11 -4
- package/src/utilsController.ts +80 -30
|
@@ -2,13 +2,14 @@ import { Databases, ID, Permission, Query, } from "node-appwrite";
|
|
|
2
2
|
import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
|
|
3
3
|
import { nameToIdMapping, processQueue, queuedOperations, clearProcessingState, isCollectionProcessed, markCollectionProcessed } from "../shared/operationQueue.js";
|
|
4
4
|
import { logger } from "../shared/logging.js";
|
|
5
|
-
|
|
6
|
-
import { createOrUpdateIndexesWithStatusCheck } from "./indexes.js";
|
|
5
|
+
// Legacy attribute/index helpers removed in favor of unified adapter path
|
|
7
6
|
import { SchemaGenerator } from "../shared/schemaGenerator.js";
|
|
8
7
|
import { isNull, isUndefined, isNil, isPlainObject, isString, } from "es-toolkit";
|
|
9
8
|
import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
10
9
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
11
10
|
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
11
|
+
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
|
|
12
|
+
import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
|
|
12
13
|
// Re-export wipe operations
|
|
13
14
|
export { wipeDatabase, wipeCollection, wipeAllTables, wipeTableRows, } from "./wipeOperations.js";
|
|
14
15
|
// Re-export transfer operations
|
|
@@ -122,130 +123,9 @@ export const generateSchemas = async (config, appwriteFolderPath) => {
|
|
|
122
123
|
export const createOrUpdateCollections = async (database, databaseId, config, deletedCollections, selectedCollections = []) => {
|
|
123
124
|
// Clear processing state at the start of a new operation
|
|
124
125
|
clearProcessingState();
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (apiMode === 'tablesdb') {
|
|
129
|
-
await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
// Fallback to legacy path below
|
|
135
|
-
}
|
|
136
|
-
const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : config.collections;
|
|
137
|
-
if (!collectionsToProcess) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const usedIds = new Set();
|
|
141
|
-
MessageFormatter.info(`Processing ${collectionsToProcess.length} collections with intelligent state management`, { prefix: "Collections" });
|
|
142
|
-
for (const collection of collectionsToProcess) {
|
|
143
|
-
const { attributes, indexes, ...collectionData } = collection;
|
|
144
|
-
// Check if this collection has already been processed in this session
|
|
145
|
-
if (collectionData.$id && isCollectionProcessed(collectionData.$id)) {
|
|
146
|
-
MessageFormatter.info(`Collection '${collectionData.name}' already processed, skipping`, { prefix: "Collections" });
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
// Prepare permissions for the collection
|
|
150
|
-
const permissions = [];
|
|
151
|
-
if (collection.$permissions && collection.$permissions.length > 0) {
|
|
152
|
-
for (const permission of collection.$permissions) {
|
|
153
|
-
if (typeof permission === "string") {
|
|
154
|
-
permissions.push(permission);
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
switch (permission.permission) {
|
|
158
|
-
case "read":
|
|
159
|
-
permissions.push(Permission.read(permission.target));
|
|
160
|
-
break;
|
|
161
|
-
case "create":
|
|
162
|
-
permissions.push(Permission.create(permission.target));
|
|
163
|
-
break;
|
|
164
|
-
case "update":
|
|
165
|
-
permissions.push(Permission.update(permission.target));
|
|
166
|
-
break;
|
|
167
|
-
case "delete":
|
|
168
|
-
permissions.push(Permission.delete(permission.target));
|
|
169
|
-
break;
|
|
170
|
-
case "write":
|
|
171
|
-
permissions.push(Permission.write(permission.target));
|
|
172
|
-
break;
|
|
173
|
-
default:
|
|
174
|
-
MessageFormatter.warning(`Unknown permission: ${permission.permission}`, { prefix: "Collections" });
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
// Check if the collection already exists by name
|
|
181
|
-
let collectionsFound = await tryAwaitWithRetry(async () => await database.listCollections(databaseId, [
|
|
182
|
-
Query.equal("name", collectionData.name),
|
|
183
|
-
]));
|
|
184
|
-
let collectionToUse = collectionsFound.total > 0 ? collectionsFound.collections[0] : null;
|
|
185
|
-
// Determine the correct ID for the collection
|
|
186
|
-
let collectionId;
|
|
187
|
-
if (!collectionToUse) {
|
|
188
|
-
MessageFormatter.info(`Creating collection: ${collectionData.name}`, { prefix: "Collections" });
|
|
189
|
-
let foundColl = deletedCollections?.find((coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") ===
|
|
190
|
-
collectionData.name.toLowerCase().trim().replace(" ", ""));
|
|
191
|
-
if (collectionData.$id) {
|
|
192
|
-
collectionId = collectionData.$id;
|
|
193
|
-
}
|
|
194
|
-
else if (foundColl && !usedIds.has(foundColl.collectionId)) {
|
|
195
|
-
collectionId = foundColl.collectionId;
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
collectionId = ID.unique();
|
|
199
|
-
}
|
|
200
|
-
usedIds.add(collectionId);
|
|
201
|
-
// Create the collection with the determined ID
|
|
202
|
-
try {
|
|
203
|
-
collectionToUse = await tryAwaitWithRetry(async () => await database.createCollection(databaseId, collectionId, collectionData.name, permissions, collectionData.documentSecurity ?? false, collectionData.enabled ?? true));
|
|
204
|
-
collectionData.$id = collectionToUse.$id;
|
|
205
|
-
nameToIdMapping.set(collectionData.name, collectionToUse.$id);
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
MessageFormatter.error(`Failed to create collection ${collectionData.name} with ID ${collectionId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Collections" });
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
MessageFormatter.info(`Collection ${collectionData.name} exists, updating it`, { prefix: "Collections" });
|
|
214
|
-
await tryAwaitWithRetry(async () => await database.updateCollection(databaseId, collectionToUse.$id, collectionData.name, permissions, collectionData.documentSecurity ?? false, collectionData.enabled ?? true));
|
|
215
|
-
// Cache the existing collection ID
|
|
216
|
-
nameToIdMapping.set(collectionData.name, collectionToUse.$id);
|
|
217
|
-
}
|
|
218
|
-
// Add delay after creating/updating collection
|
|
219
|
-
await delay(250);
|
|
220
|
-
// Update attributes and indexes for the collection
|
|
221
|
-
MessageFormatter.progress("Creating Attributes", { prefix: "Collections" });
|
|
222
|
-
await createUpdateCollectionAttributesWithStatusCheck(database, databaseId, collectionToUse,
|
|
223
|
-
// @ts-expect-error
|
|
224
|
-
attributes);
|
|
225
|
-
// Add delay after creating attributes
|
|
226
|
-
await delay(250);
|
|
227
|
-
// Prefer local config indexes, but fall back to collection's own indexes if no local config exists
|
|
228
|
-
const localCollectionConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
|
|
229
|
-
const indexesToUse = localCollectionConfig?.indexes ?? indexes ?? [];
|
|
230
|
-
MessageFormatter.progress("Creating Indexes", { prefix: "Collections" });
|
|
231
|
-
await createOrUpdateIndexesWithStatusCheck(databaseId, database, collectionToUse.$id, collectionToUse, indexesToUse);
|
|
232
|
-
// Delete indexes that exist on server but not in local config
|
|
233
|
-
const { deleteObsoleteIndexes } = await import('../shared/indexManager.js');
|
|
234
|
-
await deleteObsoleteIndexes(database, databaseId, collectionToUse, { indexes: indexesToUse }, { verbose: true });
|
|
235
|
-
// Mark this collection as fully processed to prevent re-processing
|
|
236
|
-
markCollectionProcessed(collectionToUse.$id, collectionData.name);
|
|
237
|
-
// Add delay after creating indexes
|
|
238
|
-
await delay(250);
|
|
239
|
-
}
|
|
240
|
-
// Process any remaining relationship attributes in the queue
|
|
241
|
-
// This surgical approach only processes specific attributes, not entire collections
|
|
242
|
-
if (queuedOperations.length > 0) {
|
|
243
|
-
MessageFormatter.info(`🔧 Processing ${queuedOperations.length} queued relationship attributes (surgical approach)`, { prefix: "Collections" });
|
|
244
|
-
await processQueue(database, databaseId);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
MessageFormatter.info("✅ No queued relationship attributes to process", { prefix: "Collections" });
|
|
248
|
-
}
|
|
126
|
+
// Always use adapter path (LegacyAdapter translates when pre-1.8)
|
|
127
|
+
const { adapter } = await getAdapterFromConfig(config);
|
|
128
|
+
await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
|
|
249
129
|
};
|
|
250
130
|
// New: Adapter-based implementation for TablesDB with state management
|
|
251
131
|
export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, config, deletedCollections, selectedCollections = []) => {
|
|
@@ -254,29 +134,15 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
254
134
|
return;
|
|
255
135
|
const usedIds = new Set();
|
|
256
136
|
MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
|
|
257
|
-
//
|
|
137
|
+
// Helpers for attribute operations through adapter
|
|
258
138
|
const createAttr = async (tableId, attr) => {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
default: attr.xdefault,
|
|
267
|
-
array: !!attr.array,
|
|
268
|
-
min: attr.min,
|
|
269
|
-
max: attr.max,
|
|
270
|
-
elements: attr.elements,
|
|
271
|
-
encrypt: attr.encrypted,
|
|
272
|
-
relatedCollection: attr.relatedCollection,
|
|
273
|
-
relationType: attr.relationType,
|
|
274
|
-
twoWay: attr.twoWay,
|
|
275
|
-
twoWayKey: attr.twoWayKey,
|
|
276
|
-
onDelete: attr.onDelete,
|
|
277
|
-
side: attr.side,
|
|
278
|
-
};
|
|
279
|
-
await adapter.createAttribute(base);
|
|
139
|
+
const params = mapToCreateAttributeParams(attr, { databaseId, tableId });
|
|
140
|
+
await adapter.createAttribute(params);
|
|
141
|
+
await delay(150);
|
|
142
|
+
};
|
|
143
|
+
const updateAttr = async (tableId, attr) => {
|
|
144
|
+
const params = mapToUpdateAttributeParams(attr, { databaseId, tableId });
|
|
145
|
+
await adapter.updateAttribute(params);
|
|
280
146
|
await delay(150);
|
|
281
147
|
};
|
|
282
148
|
// Local queue for unresolved relationships
|
|
@@ -357,37 +223,92 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
357
223
|
}
|
|
358
224
|
// Add small delay after table create/update
|
|
359
225
|
await delay(250);
|
|
360
|
-
// Create attributes: non-relationship first
|
|
226
|
+
// Create/Update attributes: non-relationship first using enhanced planning
|
|
361
227
|
const nonRel = (attributes || []).filter((a) => a.type !== 'relationship');
|
|
362
|
-
|
|
363
|
-
|
|
228
|
+
if (nonRel.length > 0) {
|
|
229
|
+
// Fetch existing columns once
|
|
230
|
+
const tableInfo = await adapter.getTable({ databaseId, tableId });
|
|
231
|
+
const existingCols = tableInfo.data?.columns || tableInfo.data?.attributes || [];
|
|
232
|
+
// Plan with icons
|
|
233
|
+
const plan = diffColumnsDetailed(nonRel, existingCols);
|
|
234
|
+
const plus = plan.toCreate.map((a) => a.key);
|
|
235
|
+
const plusminus = plan.toUpdate.map((u) => u.attribute.key);
|
|
236
|
+
const minus = plan.toRecreate.map((r) => r.newAttribute.key);
|
|
237
|
+
const skip = plan.unchanged;
|
|
238
|
+
const parts = [];
|
|
239
|
+
if (plus.length)
|
|
240
|
+
parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
|
|
241
|
+
if (plusminus.length)
|
|
242
|
+
parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
|
|
243
|
+
if (minus.length)
|
|
244
|
+
parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
|
|
245
|
+
if (skip.length)
|
|
246
|
+
parts.push(`⏭️ ${skip.length}`);
|
|
247
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
|
|
248
|
+
// Execute
|
|
249
|
+
const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
|
|
250
|
+
if (colResults.success.length > 0) {
|
|
251
|
+
MessageFormatter.success(`Processed ${colResults.success.length} ops`, { prefix: 'Attributes' });
|
|
252
|
+
}
|
|
253
|
+
if (colResults.errors.length > 0) {
|
|
254
|
+
MessageFormatter.error(`${colResults.errors.length} attribute operations failed:`, undefined, { prefix: 'Attributes' });
|
|
255
|
+
for (const err of colResults.errors) {
|
|
256
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Attributes' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
MessageFormatter.info(`Summary → ➕ ${plan.toCreate.length} | 🔧 ${plan.toUpdate.length} | ♻️ ${plan.toRecreate.length} | ⏭️ ${plan.unchanged.length}`, { prefix: 'Attributes' });
|
|
364
260
|
}
|
|
365
|
-
// Relationship attributes — resolve relatedCollection to ID
|
|
261
|
+
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update
|
|
366
262
|
const rels = (attributes || []).filter((a) => a.type === 'relationship');
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
263
|
+
if (rels.length > 0) {
|
|
264
|
+
for (const attr of rels) {
|
|
265
|
+
const relNameOrId = attr.relatedCollection;
|
|
266
|
+
if (!relNameOrId)
|
|
267
|
+
continue;
|
|
268
|
+
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
|
269
|
+
if (!nameToIdMapping.has(relNameOrId)) {
|
|
270
|
+
try {
|
|
271
|
+
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
|
272
|
+
const relItems = relList.tables || [];
|
|
273
|
+
if (relItems[0]?.$id) {
|
|
274
|
+
relId = relItems[0].$id;
|
|
275
|
+
nameToIdMapping.set(relNameOrId, relId);
|
|
276
|
+
}
|
|
380
277
|
}
|
|
278
|
+
catch { }
|
|
381
279
|
}
|
|
382
|
-
|
|
280
|
+
if (relId && typeof relId === 'string')
|
|
281
|
+
attr.relatedCollection = relId;
|
|
383
282
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
283
|
+
const tableInfo2 = await adapter.getTable({ databaseId, tableId });
|
|
284
|
+
const existingCols2 = tableInfo2.data?.columns || tableInfo2.data?.attributes || [];
|
|
285
|
+
const { toCreate: relCreate, toUpdate: relUpdate, unchanged: relUnchanged } = diffTableColumns(existingCols2, rels);
|
|
286
|
+
// Relationship plan with icons
|
|
287
|
+
{
|
|
288
|
+
const parts = [];
|
|
289
|
+
if (relCreate.length)
|
|
290
|
+
parts.push(`➕ ${relCreate.length} (${relCreate.map((a) => a.key).join(', ')})`);
|
|
291
|
+
if (relUpdate.length)
|
|
292
|
+
parts.push(`🔧 ${relUpdate.length} (${relUpdate.map((a) => a.key).join(', ')})`);
|
|
293
|
+
if (relUnchanged.length)
|
|
294
|
+
parts.push(`⏭️ ${relUnchanged.length}`);
|
|
295
|
+
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
|
|
387
296
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
297
|
+
for (const attr of relUpdate) {
|
|
298
|
+
try {
|
|
299
|
+
await updateAttr(tableId, attr);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
MessageFormatter.error(`Failed to update relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const attr of relCreate) {
|
|
306
|
+
try {
|
|
307
|
+
await createAttr(tableId, attr);
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
MessageFormatter.error(`Failed to create relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
|
|
311
|
+
}
|
|
391
312
|
}
|
|
392
313
|
}
|
|
393
314
|
// Wait for all attributes to become available before creating indexes
|
|
@@ -403,7 +324,7 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
403
324
|
while (Date.now() - startTime < maxWait) {
|
|
404
325
|
try {
|
|
405
326
|
const tableData = await adapter.getTable({ databaseId, tableId });
|
|
406
|
-
const attrs = tableData.attributes || [];
|
|
327
|
+
const attrs = tableData.data?.columns || tableData.data?.attributes || [];
|
|
407
328
|
const attr = attrs.find((a) => a.key === attrKey);
|
|
408
329
|
if (attr) {
|
|
409
330
|
if (attr.status === 'available') {
|
|
@@ -431,21 +352,111 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
431
352
|
// Prefer local config indexes, but fall back to collection's own indexes if no local config exists (TablesDB path)
|
|
432
353
|
const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
|
|
433
354
|
const idxs = (localTableConfig?.indexes ?? indexes ?? []);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
355
|
+
// Compare with existing indexes and create/update accordingly with status checks
|
|
356
|
+
try {
|
|
357
|
+
const existingIdxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
358
|
+
const existingIdx = existingIdxRes.data || existingIdxRes.indexes || [];
|
|
359
|
+
MessageFormatter.debug(`Existing index keys: ${existingIdx.map((i) => i.key).join(', ')}`, undefined, { prefix: 'Indexes' });
|
|
360
|
+
// Show a concise plan with icons before executing
|
|
361
|
+
const idxPlanPlus = [];
|
|
362
|
+
const idxPlanPlusMinus = [];
|
|
363
|
+
const idxPlanSkip = [];
|
|
364
|
+
for (const idx of idxs) {
|
|
365
|
+
const found = existingIdx.find((i) => i.key === idx.key);
|
|
366
|
+
if (found) {
|
|
367
|
+
if (isIndexEqualToIndex(found, idx))
|
|
368
|
+
idxPlanSkip.push(idx.key);
|
|
369
|
+
else
|
|
370
|
+
idxPlanPlusMinus.push(idx.key);
|
|
371
|
+
}
|
|
372
|
+
else
|
|
373
|
+
idxPlanPlus.push(idx.key);
|
|
445
374
|
}
|
|
446
|
-
|
|
447
|
-
|
|
375
|
+
const planParts = [];
|
|
376
|
+
if (idxPlanPlus.length)
|
|
377
|
+
planParts.push(`➕ ${idxPlanPlus.length} (${idxPlanPlus.join(', ')})`);
|
|
378
|
+
if (idxPlanPlusMinus.length)
|
|
379
|
+
planParts.push(`🔧 ${idxPlanPlusMinus.length} (${idxPlanPlusMinus.join(', ')})`);
|
|
380
|
+
if (idxPlanSkip.length)
|
|
381
|
+
planParts.push(`⏭️ ${idxPlanSkip.length}`);
|
|
382
|
+
MessageFormatter.info(`Plan → ${planParts.join(' | ') || 'no changes'}`, { prefix: 'Indexes' });
|
|
383
|
+
const created = [];
|
|
384
|
+
const updated = [];
|
|
385
|
+
const skipped = [];
|
|
386
|
+
for (const idx of idxs) {
|
|
387
|
+
const found = existingIdx.find((i) => i.key === idx.key);
|
|
388
|
+
if (found) {
|
|
389
|
+
if (isIndexEqualToIndex(found, idx)) {
|
|
390
|
+
MessageFormatter.info(`Index ${idx.key} unchanged`, { prefix: 'Indexes' });
|
|
391
|
+
skipped.push(idx.key);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
try {
|
|
395
|
+
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
396
|
+
await delay(100);
|
|
397
|
+
}
|
|
398
|
+
catch { }
|
|
399
|
+
try {
|
|
400
|
+
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
401
|
+
updated.push(idx.key);
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
const msg = (e?.message || '').toString().toLowerCase();
|
|
405
|
+
if (msg.includes('already exists')) {
|
|
406
|
+
MessageFormatter.info(`Index ${idx.key} already exists after delete attempt, skipping`, { prefix: 'Indexes' });
|
|
407
|
+
skipped.push(idx.key);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
throw e;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
try {
|
|
417
|
+
await adapter.createIndex({ databaseId, tableId, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] });
|
|
418
|
+
created.push(idx.key);
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
const msg = (e?.message || '').toString().toLowerCase();
|
|
422
|
+
if (msg.includes('already exists')) {
|
|
423
|
+
MessageFormatter.info(`Index ${idx.key} already exists (create), skipping`, { prefix: 'Indexes' });
|
|
424
|
+
skipped.push(idx.key);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
throw e;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Wait for index availability
|
|
432
|
+
const maxWait = 60000;
|
|
433
|
+
const start = Date.now();
|
|
434
|
+
let lastStatus = '';
|
|
435
|
+
while (Date.now() - start < maxWait) {
|
|
436
|
+
try {
|
|
437
|
+
const li = await adapter.listIndexes({ databaseId, tableId });
|
|
438
|
+
const list = li.data || li.indexes || [];
|
|
439
|
+
const cur = list.find((i) => i.key === idx.key);
|
|
440
|
+
if (cur) {
|
|
441
|
+
if (cur.status === 'available')
|
|
442
|
+
break;
|
|
443
|
+
if (cur.status === 'failed' || cur.status === 'stuck') {
|
|
444
|
+
throw new Error(cur.error || `Index ${idx.key} failed`);
|
|
445
|
+
}
|
|
446
|
+
lastStatus = cur.status;
|
|
447
|
+
}
|
|
448
|
+
await delay(2000);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
await delay(2000);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
await delay(150);
|
|
448
455
|
}
|
|
456
|
+
MessageFormatter.info(`Summary → ➕ ${created.length} | 🔧 ${updated.length} | ⏭️ ${skipped.length}`, { prefix: 'Indexes' });
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
|
449
460
|
}
|
|
450
461
|
// Mark this table as fully processed to prevent re-processing
|
|
451
462
|
markCollectionProcessed(tableId, collectionData.name);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Attribute } from "appwrite-utils";
|
|
2
|
+
interface ColumnPropertyChange {
|
|
3
|
+
property: string;
|
|
4
|
+
oldValue: any;
|
|
5
|
+
newValue: any;
|
|
6
|
+
requiresRecreate: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface ColumnOperationPlan {
|
|
9
|
+
toCreate: Attribute[];
|
|
10
|
+
toUpdate: Array<{
|
|
11
|
+
attribute: Attribute;
|
|
12
|
+
changes: ColumnPropertyChange[];
|
|
13
|
+
}>;
|
|
14
|
+
toRecreate: Array<{
|
|
15
|
+
oldAttribute: any;
|
|
16
|
+
newAttribute: Attribute;
|
|
17
|
+
}>;
|
|
18
|
+
toDelete: Array<{
|
|
19
|
+
attribute: any;
|
|
20
|
+
}>;
|
|
21
|
+
unchanged: string[];
|
|
22
|
+
}
|
|
23
|
+
type ComparableColumn = {
|
|
24
|
+
key: string;
|
|
25
|
+
type: string;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
array?: boolean;
|
|
28
|
+
default?: any;
|
|
29
|
+
size?: number;
|
|
30
|
+
min?: number;
|
|
31
|
+
max?: number;
|
|
32
|
+
elements?: string[];
|
|
33
|
+
encrypt?: boolean;
|
|
34
|
+
relatedCollection?: string;
|
|
35
|
+
relationType?: string;
|
|
36
|
+
twoWay?: boolean;
|
|
37
|
+
twoWayKey?: string;
|
|
38
|
+
onDelete?: string;
|
|
39
|
+
side?: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function normalizeAttributeToComparable(attr: Attribute): ComparableColumn;
|
|
42
|
+
export declare function normalizeColumnToComparable(col: any): ComparableColumn;
|
|
43
|
+
export declare function isColumnEqualToColumn(a: any, b: any): boolean;
|
|
44
|
+
export declare function isIndexEqualToIndex(a: any, b: any): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Enhanced version of columns diff with detailed change analysis
|
|
47
|
+
* Order: desired first, then existing (matches internal usage here)
|
|
48
|
+
*/
|
|
49
|
+
export declare function diffColumnsDetailed(desiredAttributes: Attribute[], existingColumns: any[]): ColumnOperationPlan;
|
|
50
|
+
/**
|
|
51
|
+
* Returns true if there is any difference between existing columns and desired attributes
|
|
52
|
+
*/
|
|
53
|
+
export declare function areTableColumnsDiff(existingColumns: any[], desired: Attribute[]): boolean;
|
|
54
|
+
export declare function diffTableColumns(existingColumns: any[], desired: Attribute[]): {
|
|
55
|
+
toCreate: Attribute[];
|
|
56
|
+
toUpdate: Attribute[];
|
|
57
|
+
unchanged: string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Execute the column operation plan using the adapter
|
|
61
|
+
*/
|
|
62
|
+
export declare function executeColumnOperations(adapter: any, databaseId: string, tableId: string, plan: ColumnOperationPlan): Promise<{
|
|
63
|
+
success: string[];
|
|
64
|
+
errors: Array<{
|
|
65
|
+
column: string;
|
|
66
|
+
error: string;
|
|
67
|
+
}>;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Integration function for methods.ts - processes columns using enhanced logic
|
|
71
|
+
*/
|
|
72
|
+
export declare function processTableColumns(adapter: any, databaseId: string, tableId: string, desiredAttributes: Attribute[], existingColumns?: any[]): Promise<{
|
|
73
|
+
totalProcessed: number;
|
|
74
|
+
success: string[];
|
|
75
|
+
errors: Array<{
|
|
76
|
+
column: string;
|
|
77
|
+
error: string;
|
|
78
|
+
}>;
|
|
79
|
+
summary: {
|
|
80
|
+
created: number;
|
|
81
|
+
updated: number;
|
|
82
|
+
recreated: number;
|
|
83
|
+
unchanged: number;
|
|
84
|
+
};
|
|
85
|
+
}>;
|
|
86
|
+
export {};
|