@njdamstra/appwrite-utils-cli 1.8.9 → 1.10.1
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 +16 -0
- package/CONFIG_TODO.md +1189 -0
- package/SELECTION_DIALOGS.md +146 -0
- package/SERVICE_IMPLEMENTATION_REPORT.md +462 -0
- package/dist/adapters/index.d.ts +7 -8
- package/dist/adapters/index.js +7 -9
- package/dist/backups/operations/bucketBackup.js +2 -2
- package/dist/backups/operations/collectionBackup.d.ts +1 -1
- package/dist/backups/operations/collectionBackup.js +3 -3
- package/dist/backups/operations/comprehensiveBackup.d.ts +1 -1
- package/dist/backups/operations/comprehensiveBackup.js +2 -2
- package/dist/backups/tracking/centralizedTracking.d.ts +1 -1
- package/dist/backups/tracking/centralizedTracking.js +2 -2
- package/dist/cli/commands/configCommands.js +51 -7
- package/dist/cli/commands/databaseCommands.d.ts +1 -0
- package/dist/cli/commands/databaseCommands.js +119 -9
- package/dist/cli/commands/functionCommands.js +3 -3
- package/dist/cli/commands/importFileCommands.d.ts +7 -0
- package/dist/cli/commands/importFileCommands.js +674 -0
- package/dist/cli/commands/schemaCommands.js +3 -3
- package/dist/cli/commands/storageCommands.js +2 -3
- package/dist/cli/commands/transferCommands.js +3 -5
- package/dist/collections/attributes.d.ts +1 -1
- package/dist/collections/attributes.js +85 -35
- package/dist/collections/indexes.js +2 -4
- package/dist/collections/methods.d.ts +1 -1
- package/dist/collections/methods.js +111 -192
- package/dist/collections/tableOperations.d.ts +1 -0
- package/dist/collections/tableOperations.js +90 -23
- package/dist/collections/transferOperations.d.ts +1 -1
- package/dist/collections/transferOperations.js +3 -4
- package/dist/collections/wipeOperations.d.ts +4 -3
- package/dist/collections/wipeOperations.js +112 -39
- package/dist/databases/methods.js +2 -2
- package/dist/databases/setup.js +2 -2
- package/dist/examples/yamlTerminologyExample.js +2 -2
- package/dist/functions/deployments.d.ts +1 -1
- package/dist/functions/deployments.js +5 -5
- package/dist/functions/fnConfigDiscovery.js +2 -2
- package/dist/functions/methods.js +16 -4
- package/dist/init.js +1 -1
- package/dist/interactiveCLI.d.ts +6 -1
- package/dist/interactiveCLI.js +64 -10
- package/dist/main.js +130 -177
- package/dist/migrations/afterImportActions.js +2 -3
- package/dist/migrations/appwriteToX.d.ts +97 -1
- package/dist/migrations/appwriteToX.js +9 -7
- package/dist/migrations/comprehensiveTransfer.js +3 -5
- package/dist/migrations/dataLoader.d.ts +194 -2
- package/dist/migrations/dataLoader.js +2 -5
- package/dist/migrations/importController.js +3 -4
- package/dist/migrations/importDataActions.js +3 -3
- package/dist/migrations/relationships.js +1 -2
- package/dist/migrations/services/DataTransformationService.js +2 -2
- package/dist/migrations/services/FileHandlerService.js +1 -1
- package/dist/migrations/services/ImportOrchestrator.js +4 -4
- package/dist/migrations/services/RateLimitManager.js +1 -1
- package/dist/migrations/services/RelationshipResolver.js +1 -1
- package/dist/migrations/services/UserMappingService.js +1 -1
- package/dist/migrations/services/ValidationService.js +1 -1
- package/dist/migrations/transfer.d.ts +8 -4
- package/dist/migrations/transfer.js +106 -55
- package/dist/migrations/yaml/YamlImportConfigLoader.js +1 -1
- package/dist/migrations/yaml/YamlImportIntegration.js +2 -2
- package/dist/migrations/yaml/generateImportSchemas.js +1 -1
- package/dist/setupCommands.d.ts +1 -1
- package/dist/setupCommands.js +5 -6
- package/dist/setupController.js +1 -1
- package/dist/shared/backupTracking.d.ts +1 -1
- package/dist/shared/backupTracking.js +2 -2
- package/dist/shared/confirmationDialogs.js +1 -1
- package/dist/shared/migrationHelpers.d.ts +1 -1
- package/dist/shared/migrationHelpers.js +3 -3
- package/dist/shared/operationQueue.d.ts +1 -1
- package/dist/shared/operationQueue.js +2 -3
- package/dist/shared/operationsTable.d.ts +1 -1
- package/dist/shared/operationsTable.js +2 -2
- package/dist/shared/progressManager.js +1 -1
- package/dist/shared/selectionDialogs.js +9 -8
- package/dist/storage/methods.js +4 -4
- package/dist/storage/schemas.d.ts +386 -2
- package/dist/tables/indexManager.d.ts +65 -0
- package/dist/tables/indexManager.js +294 -0
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/users/methods.js +2 -3
- package/dist/utils/configMigration.js +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/loadConfigs.d.ts +2 -2
- package/dist/utils/loadConfigs.js +6 -7
- package/dist/utils/setupFiles.js +5 -7
- package/dist/utilsController.d.ts +15 -8
- package/dist/utilsController.js +57 -28
- package/package.json +8 -4
- package/src/adapters/index.ts +8 -34
- package/src/backups/operations/bucketBackup.ts +2 -2
- package/src/backups/operations/collectionBackup.ts +4 -4
- package/src/backups/operations/comprehensiveBackup.ts +3 -3
- package/src/backups/tracking/centralizedTracking.ts +3 -3
- package/src/cli/commands/configCommands.ts +72 -8
- package/src/cli/commands/databaseCommands.ts +161 -9
- package/src/cli/commands/functionCommands.ts +4 -3
- package/src/cli/commands/importFileCommands.ts +815 -0
- package/src/cli/commands/schemaCommands.ts +3 -3
- package/src/cli/commands/storageCommands.ts +2 -3
- package/src/cli/commands/transferCommands.ts +3 -6
- package/src/collections/attributes.ts +155 -39
- package/src/collections/indexes.ts +5 -7
- package/src/collections/methods.ts +115 -150
- package/src/collections/tableOperations.ts +92 -21
- package/src/collections/transferOperations.ts +4 -5
- package/src/collections/wipeOperations.ts +154 -51
- package/src/databases/methods.ts +2 -2
- package/src/databases/setup.ts +2 -2
- package/src/examples/yamlTerminologyExample.ts +2 -2
- package/src/functions/deployments.ts +6 -5
- package/src/functions/fnConfigDiscovery.ts +2 -2
- package/src/functions/methods.ts +19 -6
- package/src/init.ts +1 -1
- package/src/interactiveCLI.ts +78 -13
- package/src/main.ts +143 -287
- package/src/migrations/afterImportActions.ts +2 -3
- package/src/migrations/appwriteToX.ts +12 -8
- package/src/migrations/comprehensiveTransfer.ts +6 -6
- package/src/migrations/dataLoader.ts +2 -5
- package/src/migrations/importController.ts +3 -4
- package/src/migrations/importDataActions.ts +3 -3
- package/src/migrations/relationships.ts +1 -2
- package/src/migrations/services/DataTransformationService.ts +2 -2
- package/src/migrations/services/FileHandlerService.ts +1 -1
- package/src/migrations/services/ImportOrchestrator.ts +4 -4
- package/src/migrations/services/RateLimitManager.ts +1 -1
- package/src/migrations/services/RelationshipResolver.ts +1 -1
- package/src/migrations/services/UserMappingService.ts +1 -1
- package/src/migrations/services/ValidationService.ts +1 -1
- package/src/migrations/transfer.ts +126 -83
- package/src/migrations/yaml/YamlImportConfigLoader.ts +1 -1
- package/src/migrations/yaml/YamlImportIntegration.ts +2 -2
- package/src/migrations/yaml/generateImportSchemas.ts +1 -1
- package/src/setupCommands.ts +5 -6
- package/src/setupController.ts +1 -1
- package/src/shared/backupTracking.ts +3 -3
- package/src/shared/confirmationDialogs.ts +1 -1
- package/src/shared/migrationHelpers.ts +4 -4
- package/src/shared/operationQueue.ts +3 -4
- package/src/shared/operationsTable.ts +3 -3
- package/src/shared/progressManager.ts +1 -1
- package/src/shared/selectionDialogs.ts +9 -8
- package/src/storage/methods.ts +4 -4
- package/src/tables/indexManager.ts +409 -0
- package/src/types.ts +2 -2
- package/src/users/methods.ts +2 -3
- package/src/utils/configMigration.ts +1 -1
- package/src/utils/index.ts +1 -1
- package/src/utils/loadConfigs.ts +15 -7
- package/src/utils/setupFiles.ts +5 -7
- package/src/utilsController.ts +86 -32
- package/dist/adapters/AdapterFactory.d.ts +0 -94
- package/dist/adapters/AdapterFactory.js +0 -405
- package/dist/adapters/DatabaseAdapter.d.ts +0 -233
- package/dist/adapters/DatabaseAdapter.js +0 -50
- package/dist/adapters/LegacyAdapter.d.ts +0 -50
- package/dist/adapters/LegacyAdapter.js +0 -612
- package/dist/adapters/TablesDBAdapter.d.ts +0 -45
- package/dist/adapters/TablesDBAdapter.js +0 -571
- package/dist/config/ConfigManager.d.ts +0 -445
- package/dist/config/ConfigManager.js +0 -625
- package/dist/config/configMigration.d.ts +0 -87
- package/dist/config/configMigration.js +0 -390
- package/dist/config/configValidation.d.ts +0 -66
- package/dist/config/configValidation.js +0 -358
- package/dist/config/index.d.ts +0 -8
- package/dist/config/index.js +0 -7
- package/dist/config/services/ConfigDiscoveryService.d.ts +0 -126
- package/dist/config/services/ConfigDiscoveryService.js +0 -374
- package/dist/config/services/ConfigLoaderService.d.ts +0 -129
- package/dist/config/services/ConfigLoaderService.js +0 -540
- package/dist/config/services/ConfigMergeService.d.ts +0 -208
- package/dist/config/services/ConfigMergeService.js +0 -308
- package/dist/config/services/ConfigValidationService.d.ts +0 -214
- package/dist/config/services/ConfigValidationService.js +0 -310
- package/dist/config/services/SessionAuthService.d.ts +0 -225
- package/dist/config/services/SessionAuthService.js +0 -456
- package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +0 -1
- package/dist/config/services/__tests__/ConfigMergeService.test.js +0 -271
- package/dist/config/services/index.d.ts +0 -13
- package/dist/config/services/index.js +0 -10
- package/dist/config/yamlConfig.d.ts +0 -722
- package/dist/config/yamlConfig.js +0 -702
- package/dist/functions/pathResolution.d.ts +0 -37
- package/dist/functions/pathResolution.js +0 -185
- package/dist/shared/attributeMapper.d.ts +0 -20
- package/dist/shared/attributeMapper.js +0 -203
- package/dist/shared/errorUtils.d.ts +0 -54
- package/dist/shared/errorUtils.js +0 -95
- package/dist/shared/functionManager.d.ts +0 -48
- package/dist/shared/functionManager.js +0 -336
- package/dist/shared/indexManager.d.ts +0 -24
- package/dist/shared/indexManager.js +0 -151
- package/dist/shared/jsonSchemaGenerator.d.ts +0 -50
- package/dist/shared/jsonSchemaGenerator.js +0 -290
- package/dist/shared/logging.d.ts +0 -61
- package/dist/shared/logging.js +0 -116
- package/dist/shared/messageFormatter.d.ts +0 -39
- package/dist/shared/messageFormatter.js +0 -162
- package/dist/shared/pydanticModelGenerator.d.ts +0 -17
- package/dist/shared/pydanticModelGenerator.js +0 -615
- package/dist/shared/schemaGenerator.d.ts +0 -40
- package/dist/shared/schemaGenerator.js +0 -556
- package/dist/utils/ClientFactory.d.ts +0 -87
- package/dist/utils/ClientFactory.js +0 -212
- package/dist/utils/configDiscovery.d.ts +0 -78
- package/dist/utils/configDiscovery.js +0 -472
- package/dist/utils/constantsGenerator.d.ts +0 -31
- package/dist/utils/constantsGenerator.js +0 -321
- package/dist/utils/dataConverters.d.ts +0 -46
- package/dist/utils/dataConverters.js +0 -139
- package/dist/utils/directoryUtils.d.ts +0 -22
- package/dist/utils/directoryUtils.js +0 -59
- package/dist/utils/getClientFromConfig.d.ts +0 -39
- package/dist/utils/getClientFromConfig.js +0 -199
- package/dist/utils/helperFunctions.d.ts +0 -63
- package/dist/utils/helperFunctions.js +0 -156
- package/dist/utils/pathResolvers.d.ts +0 -53
- package/dist/utils/pathResolvers.js +0 -72
- package/dist/utils/projectConfig.d.ts +0 -119
- package/dist/utils/projectConfig.js +0 -171
- package/dist/utils/retryFailedPromises.d.ts +0 -2
- package/dist/utils/retryFailedPromises.js +0 -23
- package/dist/utils/sessionAuth.d.ts +0 -48
- package/dist/utils/sessionAuth.js +0 -164
- package/dist/utils/typeGuards.d.ts +0 -35
- package/dist/utils/typeGuards.js +0 -57
- package/dist/utils/validationRules.d.ts +0 -43
- package/dist/utils/validationRules.js +0 -42
- package/dist/utils/versionDetection.d.ts +0 -58
- package/dist/utils/versionDetection.js +0 -251
- package/dist/utils/yamlConverter.d.ts +0 -100
- package/dist/utils/yamlConverter.js +0 -428
- package/dist/utils/yamlLoader.d.ts +0 -70
- package/dist/utils/yamlLoader.js +0 -267
- package/src/adapters/AdapterFactory.ts +0 -510
- package/src/adapters/DatabaseAdapter.ts +0 -306
- package/src/adapters/LegacyAdapter.ts +0 -841
- package/src/adapters/TablesDBAdapter.ts +0 -773
- package/src/config/ConfigManager.ts +0 -808
- package/src/config/README.md +0 -274
- package/src/config/configMigration.ts +0 -575
- package/src/config/configValidation.ts +0 -445
- package/src/config/index.ts +0 -10
- package/src/config/services/ConfigDiscoveryService.ts +0 -463
- package/src/config/services/ConfigLoaderService.ts +0 -740
- package/src/config/services/ConfigMergeService.ts +0 -388
- package/src/config/services/ConfigValidationService.ts +0 -394
- package/src/config/services/SessionAuthService.ts +0 -565
- package/src/config/services/__tests__/ConfigMergeService.test.ts +0 -351
- package/src/config/services/index.ts +0 -29
- package/src/config/yamlConfig.ts +0 -761
- package/src/functions/pathResolution.ts +0 -227
- package/src/shared/attributeMapper.ts +0 -229
- package/src/shared/errorUtils.ts +0 -110
- package/src/shared/functionManager.ts +0 -525
- package/src/shared/indexManager.ts +0 -254
- package/src/shared/jsonSchemaGenerator.ts +0 -383
- package/src/shared/logging.ts +0 -149
- package/src/shared/messageFormatter.ts +0 -208
- package/src/shared/pydanticModelGenerator.ts +0 -618
- package/src/shared/schemaGenerator.ts +0 -644
- package/src/utils/ClientFactory.ts +0 -240
- package/src/utils/configDiscovery.ts +0 -557
- package/src/utils/constantsGenerator.ts +0 -369
- package/src/utils/dataConverters.ts +0 -159
- package/src/utils/directoryUtils.ts +0 -61
- package/src/utils/getClientFromConfig.ts +0 -257
- package/src/utils/helperFunctions.ts +0 -228
- package/src/utils/pathResolvers.ts +0 -81
- package/src/utils/projectConfig.ts +0 -299
- package/src/utils/retryFailedPromises.ts +0 -29
- package/src/utils/sessionAuth.ts +0 -230
- package/src/utils/typeGuards.ts +0 -65
- package/src/utils/validationRules.ts +0 -88
- package/src/utils/versionDetection.ts +0 -292
- package/src/utils/yamlConverter.ts +0 -542
- package/src/utils/yamlLoader.ts +0 -371
|
@@ -6,19 +6,19 @@ import {
|
|
|
6
6
|
type Models,
|
|
7
7
|
} from "node-appwrite";
|
|
8
8
|
import type { AppwriteConfig, CollectionCreate, Indexes, Attribute } from "@njdamstra/appwrite-utils";
|
|
9
|
-
import type { DatabaseAdapter } from "
|
|
10
|
-
import { getAdapterFromConfig } from "
|
|
9
|
+
import type { DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
|
|
10
|
+
import { getAdapterFromConfig } from "@njdamstra/appwrite-utils-helpers";
|
|
11
11
|
import {
|
|
12
12
|
nameToIdMapping,
|
|
13
13
|
processQueue,
|
|
14
14
|
queuedOperations,
|
|
15
15
|
clearProcessingState,
|
|
16
16
|
isCollectionProcessed,
|
|
17
|
-
markCollectionProcessed
|
|
17
|
+
markCollectionProcessed,
|
|
18
|
+
enqueueOperation
|
|
18
19
|
} from "../shared/operationQueue.js";
|
|
19
|
-
import { logger } from "
|
|
20
|
+
import { logger, SchemaGenerator } from "@njdamstra/appwrite-utils-helpers";
|
|
20
21
|
// Legacy attribute/index helpers removed in favor of unified adapter path
|
|
21
|
-
import { SchemaGenerator } from "../shared/schemaGenerator.js";
|
|
22
22
|
import {
|
|
23
23
|
isNull,
|
|
24
24
|
isUndefined,
|
|
@@ -26,11 +26,11 @@ import {
|
|
|
26
26
|
isPlainObject,
|
|
27
27
|
isString,
|
|
28
28
|
} from "es-toolkit";
|
|
29
|
-
import { delay, tryAwaitWithRetry } from "
|
|
30
|
-
import { MessageFormatter } from "
|
|
31
|
-
import { isLegacyDatabases } from "
|
|
32
|
-
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "../shared/attributeMapper.js";
|
|
29
|
+
import { delay, tryAwaitWithRetry } from "@njdamstra/appwrite-utils-helpers";
|
|
30
|
+
import { MessageFormatter, mapToCreateAttributeParams, mapToUpdateAttributeParams } from "@njdamstra/appwrite-utils-helpers";
|
|
31
|
+
import { isLegacyDatabases } from "@njdamstra/appwrite-utils-helpers";
|
|
33
32
|
import { diffTableColumns, isIndexEqualToIndex, diffColumnsDetailed, executeColumnOperations } from "./tableOperations.js";
|
|
33
|
+
import { createOrUpdateIndexesViaAdapter, deleteObsoleteIndexesViaAdapter } from "../tables/indexManager.js";
|
|
34
34
|
|
|
35
35
|
// Re-export wipe operations
|
|
36
36
|
export {
|
|
@@ -375,45 +375,105 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
375
375
|
);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
378
|
+
// Relationship attributes — resolve relatedCollection to ID, then diff and create/update with recreate support
|
|
379
|
+
const relsAll = (attributes || []).filter((a: Attribute) => a.type === 'relationship') as any[];
|
|
380
|
+
if (relsAll.length > 0) {
|
|
381
|
+
const relsResolved: any[] = [];
|
|
382
|
+
const relsDeferred: any[] = [];
|
|
383
|
+
|
|
384
|
+
// Resolve related collections (names -> IDs) using cache or lookup.
|
|
385
|
+
// If not resolvable yet (target table created later in the same push), queue for later.
|
|
386
|
+
for (const attr of relsAll) {
|
|
382
387
|
const relNameOrId = attr.relatedCollection as string | undefined;
|
|
383
388
|
if (!relNameOrId) continue;
|
|
384
389
|
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
|
385
|
-
|
|
390
|
+
let resolved = false;
|
|
391
|
+
if (nameToIdMapping.has(relNameOrId)) {
|
|
392
|
+
resolved = true;
|
|
393
|
+
} else {
|
|
394
|
+
// Try resolve by name
|
|
386
395
|
try {
|
|
387
396
|
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
|
388
397
|
const relItems: any[] = (relList as any).tables || [];
|
|
389
398
|
if (relItems[0]?.$id) {
|
|
390
399
|
relId = relItems[0].$id;
|
|
391
400
|
nameToIdMapping.set(relNameOrId, relId);
|
|
401
|
+
resolved = true;
|
|
392
402
|
}
|
|
393
403
|
} catch {}
|
|
404
|
+
|
|
405
|
+
// If the relNameOrId looks like an ID but isn't resolved yet, attempt a direct get
|
|
406
|
+
if (!resolved && relNameOrId && relNameOrId.length >= 10) {
|
|
407
|
+
try {
|
|
408
|
+
const probe = await adapter.getTable({ databaseId, tableId: relNameOrId });
|
|
409
|
+
if ((probe as any).data?.$id) {
|
|
410
|
+
nameToIdMapping.set(relNameOrId, relNameOrId);
|
|
411
|
+
relId = relNameOrId;
|
|
412
|
+
resolved = true;
|
|
413
|
+
}
|
|
414
|
+
} catch {}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (resolved && relId && typeof relId === 'string') {
|
|
419
|
+
attr.relatedCollection = relId;
|
|
420
|
+
relsResolved.push(attr);
|
|
421
|
+
} else {
|
|
422
|
+
// Defer until related table exists; queue a surgical operation
|
|
423
|
+
enqueueOperation({
|
|
424
|
+
type: 'attribute',
|
|
425
|
+
collectionId: tableId,
|
|
426
|
+
attribute: attr,
|
|
427
|
+
dependencies: [relNameOrId]
|
|
428
|
+
});
|
|
429
|
+
relsDeferred.push(attr);
|
|
394
430
|
}
|
|
395
|
-
if (relId && typeof relId === 'string') attr.relatedCollection = relId;
|
|
396
431
|
}
|
|
432
|
+
|
|
433
|
+
// Compute a detailed plan for immediately resolvable relationships
|
|
397
434
|
const tableInfo2 = await adapter.getTable({ databaseId, tableId });
|
|
398
435
|
const existingCols2: any[] = (tableInfo2 as any).data?.columns || (tableInfo2 as any).data?.attributes || [];
|
|
399
|
-
const
|
|
436
|
+
const relPlan = diffColumnsDetailed(relsResolved as any, existingCols2);
|
|
400
437
|
|
|
401
|
-
// Relationship plan with icons
|
|
438
|
+
// Relationship plan with icons (includes recreates)
|
|
402
439
|
{
|
|
403
440
|
const parts: string[] = [];
|
|
404
|
-
if (
|
|
405
|
-
if (
|
|
406
|
-
if (
|
|
441
|
+
if (relPlan.toCreate.length) parts.push(`➕ ${relPlan.toCreate.length} (${relPlan.toCreate.map((a:any)=>a.key).join(', ')})`);
|
|
442
|
+
if (relPlan.toUpdate.length) parts.push(`🔧 ${relPlan.toUpdate.length} (${relPlan.toUpdate.map((u:any)=>u.attribute?.key ?? u.key).join(', ')})`);
|
|
443
|
+
if (relPlan.toRecreate.length) parts.push(`♻️ ${relPlan.toRecreate.length} (${relPlan.toRecreate.map((r:any)=>r.newAttribute?.key ?? r?.key).join(', ')})`);
|
|
444
|
+
if (relPlan.unchanged.length) parts.push(`⏭️ ${relPlan.unchanged.length}`);
|
|
407
445
|
MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Relationships' });
|
|
408
446
|
}
|
|
409
|
-
|
|
410
|
-
|
|
447
|
+
|
|
448
|
+
// Execute plan using the same operation executor to properly handle deletes/recreates
|
|
449
|
+
const relResults = await executeColumnOperations(adapter, databaseId, tableId, relPlan);
|
|
450
|
+
if (relResults.success.length > 0) {
|
|
451
|
+
const totalRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length + relPlan.unchanged.length;
|
|
452
|
+
const activeRelationships = relPlan.toCreate.length + relPlan.toUpdate.length + relPlan.toRecreate.length;
|
|
453
|
+
|
|
454
|
+
if (relResults.success.length !== activeRelationships) {
|
|
455
|
+
// Show both counts when they differ (usually due to recreations)
|
|
456
|
+
MessageFormatter.success(`Processed ${relResults.success.length} operations for ${activeRelationships} relationship${activeRelationships === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
457
|
+
} else {
|
|
458
|
+
MessageFormatter.success(`Processed ${relResults.success.length} relationship${relResults.success.length === 1 ? '' : 's'}`, { prefix: 'Relationships' });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (relResults.errors.length > 0) {
|
|
462
|
+
MessageFormatter.error(`${relResults.errors.length} relationship operations failed:`, undefined, { prefix: 'Relationships' });
|
|
463
|
+
for (const err of relResults.errors) {
|
|
464
|
+
MessageFormatter.error(` ${err.column}: ${err.error}`, undefined, { prefix: 'Relationships' });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (relsDeferred.length > 0) {
|
|
469
|
+
MessageFormatter.info(`Deferred ${relsDeferred.length} relationship(s) until related tables become available`, { prefix: 'Relationships' });
|
|
470
|
+
}
|
|
411
471
|
}
|
|
412
472
|
|
|
413
473
|
// Wait for all attributes to become available before creating indexes
|
|
414
474
|
const allAttrKeys = [
|
|
415
475
|
...nonRel.map((a: any) => a.key),
|
|
416
|
-
...
|
|
476
|
+
...relsAll.filter((a: any) => a.relatedCollection).map((a: any) => a.key)
|
|
417
477
|
];
|
|
418
478
|
|
|
419
479
|
if (allAttrKeys.length > 0) {
|
|
@@ -456,144 +516,38 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
456
516
|
}
|
|
457
517
|
}
|
|
458
518
|
|
|
459
|
-
//
|
|
519
|
+
// Index management: create/update indexes using clean adapter-based system
|
|
460
520
|
const localTableConfig = config.collections?.find(
|
|
461
521
|
c => c.name === collectionData.name || c.$id === collectionData.$id
|
|
462
522
|
);
|
|
463
523
|
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
524
|
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
}
|
|
525
|
+
// Create/update indexes with proper planning and execution
|
|
526
|
+
await createOrUpdateIndexesViaAdapter(adapter, databaseId, tableId, idxs, indexes);
|
|
527
|
+
|
|
528
|
+
// Handle obsolete index deletions
|
|
529
|
+
const desiredIndexKeys: Set<string> = new Set((indexes || []).map((i: any) => i.key as string));
|
|
530
|
+
await deleteObsoleteIndexesViaAdapter(adapter, databaseId, tableId, desiredIndexKeys);
|
|
589
531
|
|
|
590
532
|
// Deletions: remove columns/attributes that are present remotely but not in desired config
|
|
591
533
|
try {
|
|
592
534
|
const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
|
|
535
|
+
// Also track case-insensitive keys to avoid double-deletion of renames (handled as recreates)
|
|
536
|
+
const desiredKeysLower = new Set((attributes || []).map((a: any) => a.key?.toLowerCase()));
|
|
593
537
|
const tableInfo3 = await adapter.getTable({ databaseId, tableId });
|
|
594
538
|
const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
|
|
595
539
|
const toDelete = existingCols3
|
|
596
|
-
.filter((col: any) =>
|
|
540
|
+
.filter((col: any) => {
|
|
541
|
+
if (!col?.key) return false;
|
|
542
|
+
// Exact match - keep it
|
|
543
|
+
if (desiredKeys.has(col.key)) return false;
|
|
544
|
+
// Case-insensitive match (rename scenario) - already handled as recreate, don't delete again
|
|
545
|
+
if (desiredKeysLower.has(col.key?.toLowerCase())) return false;
|
|
546
|
+
// Don't delete child-side relationship attributes - they're auto-managed by Appwrite
|
|
547
|
+
// for two-way relationships and deleting them would break the parent relationship
|
|
548
|
+
if (col.type === 'relationship' && col.side === 'child') return false;
|
|
549
|
+
return true;
|
|
550
|
+
})
|
|
597
551
|
.map((col: any) => col.key as string);
|
|
598
552
|
|
|
599
553
|
if (toDelete.length > 0) {
|
|
@@ -607,7 +561,9 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
607
561
|
const idxRes = await adapter.listIndexes({ databaseId, tableId });
|
|
608
562
|
const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
|
|
609
563
|
for (const idx of ilist) {
|
|
610
|
-
const attrs: string[] = Array.isArray(idx.attributes)
|
|
564
|
+
const attrs: string[] = Array.isArray(idx.attributes)
|
|
565
|
+
? idx.attributes
|
|
566
|
+
: (Array.isArray((idx as any).columns) ? (idx as any).columns : []);
|
|
611
567
|
if (attrs.includes(key)) {
|
|
612
568
|
MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
|
|
613
569
|
await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
|
|
@@ -692,6 +648,15 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
692
648
|
}
|
|
693
649
|
}
|
|
694
650
|
}
|
|
651
|
+
|
|
652
|
+
// Process any remaining queued operations to complete relationship sync
|
|
653
|
+
try {
|
|
654
|
+
MessageFormatter.info(`🔄 Processing final operation queue for database ${databaseId}`, { prefix: "Tables" });
|
|
655
|
+
await processQueue(adapter, databaseId);
|
|
656
|
+
MessageFormatter.info(`✅ Operation queue processing completed`, { prefix: "Tables" });
|
|
657
|
+
} catch (error) {
|
|
658
|
+
MessageFormatter.error(`Failed to process operation queue`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Tables' });
|
|
659
|
+
}
|
|
695
660
|
};
|
|
696
661
|
|
|
697
662
|
export const generateMockData = async (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Attribute } from "@njdamstra/appwrite-utils";
|
|
2
|
-
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "
|
|
2
|
+
import { mapToCreateAttributeParams, mapToUpdateAttributeParams } from "@njdamstra/appwrite-utils-helpers";
|
|
3
3
|
import { Decimal } from "decimal.js";
|
|
4
4
|
const EXTREME_BOUND = new Decimal('1e12');
|
|
5
5
|
|
|
@@ -32,6 +32,10 @@ interface ColumnOperationPlan {
|
|
|
32
32
|
// Property configuration for different column types
|
|
33
33
|
const MUTABLE_PROPERTIES = {
|
|
34
34
|
string: ["required", "default", "size", "array"],
|
|
35
|
+
varchar: ["required", "default", "size", "array"],
|
|
36
|
+
text: ["required", "default", "array"],
|
|
37
|
+
mediumtext: ["required", "default", "array"],
|
|
38
|
+
longtext: ["required", "default", "array"],
|
|
35
39
|
integer: ["required", "default", "min", "max", "array"],
|
|
36
40
|
float: ["required", "default", "min", "max", "array"],
|
|
37
41
|
double: ["required", "default", "min", "max", "array"],
|
|
@@ -41,11 +45,18 @@ const MUTABLE_PROPERTIES = {
|
|
|
41
45
|
ip: ["required", "default", "array"],
|
|
42
46
|
url: ["required", "default", "array"],
|
|
43
47
|
enum: ["required", "default", "elements", "array"],
|
|
48
|
+
point: ["required", "default"],
|
|
49
|
+
line: ["required", "default"],
|
|
50
|
+
polygon: ["required", "default"],
|
|
44
51
|
relationship: ["required", "default"],
|
|
45
52
|
} as const;
|
|
46
53
|
|
|
47
54
|
const IMMUTABLE_PROPERTIES = {
|
|
48
55
|
string: ["encrypt", "key"],
|
|
56
|
+
varchar: ["encrypt", "key"],
|
|
57
|
+
text: ["encrypt", "key"],
|
|
58
|
+
mediumtext: ["encrypt", "key"],
|
|
59
|
+
longtext: ["encrypt", "key"],
|
|
49
60
|
integer: ["encrypt", "key"],
|
|
50
61
|
float: ["encrypt", "key"],
|
|
51
62
|
double: ["encrypt", "key"],
|
|
@@ -55,11 +66,18 @@ const IMMUTABLE_PROPERTIES = {
|
|
|
55
66
|
ip: ["key"],
|
|
56
67
|
url: ["key"],
|
|
57
68
|
enum: ["key"],
|
|
69
|
+
point: ["key"],
|
|
70
|
+
line: ["key"],
|
|
71
|
+
polygon: ["key"],
|
|
58
72
|
relationship: ["key", "relatedCollection", "relationType", "twoWay", "twoWayKey", "onDelete"],
|
|
59
73
|
} as const;
|
|
60
74
|
|
|
61
75
|
const TYPE_CHANGE_REQUIRES_RECREATE = [
|
|
62
76
|
"string",
|
|
77
|
+
"varchar",
|
|
78
|
+
"text",
|
|
79
|
+
"mediumtext",
|
|
80
|
+
"longtext",
|
|
63
81
|
"integer",
|
|
64
82
|
"float",
|
|
65
83
|
"double",
|
|
@@ -69,6 +87,9 @@ const TYPE_CHANGE_REQUIRES_RECREATE = [
|
|
|
69
87
|
"ip",
|
|
70
88
|
"url",
|
|
71
89
|
"enum",
|
|
90
|
+
"point",
|
|
91
|
+
"line",
|
|
92
|
+
"polygon",
|
|
72
93
|
"relationship",
|
|
73
94
|
];
|
|
74
95
|
|
|
@@ -117,6 +138,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
|
|
|
117
138
|
base.size = (attr as any).size ?? 255;
|
|
118
139
|
base.encrypt = !!((attr as any).encrypt);
|
|
119
140
|
}
|
|
141
|
+
if (t === 'varchar') {
|
|
142
|
+
base.size = (attr as any).size ?? 255;
|
|
143
|
+
base.encrypt = !!((attr as any).encrypt);
|
|
144
|
+
}
|
|
145
|
+
if (t === 'text' || t === 'mediumtext' || t === 'longtext') {
|
|
146
|
+
base.encrypt = !!((attr as any).encrypt);
|
|
147
|
+
}
|
|
120
148
|
if (t === 'integer' || t === 'float' || t === 'double') {
|
|
121
149
|
const min = toNumber((attr as any).min);
|
|
122
150
|
const max = toNumber((attr as any).max);
|
|
@@ -143,10 +171,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
|
|
|
143
171
|
}
|
|
144
172
|
|
|
145
173
|
export function normalizeColumnToComparable(col: any): ComparableColumn {
|
|
146
|
-
// Detect enum surfaced as string+elements from server and normalize to enum for comparison
|
|
174
|
+
// Detect enum surfaced as string+elements or string+format:enum from server and normalize to enum for comparison
|
|
147
175
|
let t = String((col?.type ?? col?.columnType ?? '')).toLowerCase();
|
|
148
176
|
const hasElements = Array.isArray(col?.elements) && (col.elements as any[]).length > 0;
|
|
149
|
-
|
|
177
|
+
const hasEnumFormat = (col?.format === 'enum');
|
|
178
|
+
if (t === 'string' && (hasElements || hasEnumFormat)) {
|
|
179
|
+
t = 'enum';
|
|
180
|
+
}
|
|
150
181
|
const base: ComparableColumn = {
|
|
151
182
|
key: col?.key,
|
|
152
183
|
type: t,
|
|
@@ -159,6 +190,13 @@ export function normalizeColumnToComparable(col: any): ComparableColumn {
|
|
|
159
190
|
base.size = typeof col?.size === 'number' ? col.size : undefined;
|
|
160
191
|
base.encrypt = !!col?.encrypt;
|
|
161
192
|
}
|
|
193
|
+
if (t === 'varchar') {
|
|
194
|
+
base.size = typeof col?.size === 'number' ? col.size : undefined;
|
|
195
|
+
base.encrypt = !!col?.encrypt;
|
|
196
|
+
}
|
|
197
|
+
if (t === 'text' || t === 'mediumtext' || t === 'longtext') {
|
|
198
|
+
base.encrypt = !!col?.encrypt;
|
|
199
|
+
}
|
|
162
200
|
if (t === 'integer' || t === 'float' || t === 'double') {
|
|
163
201
|
// Preserve raw min/max without forcing extremes; compare with Decimal in shallowEqual
|
|
164
202
|
const rawMin = (col as any)?.min;
|
|
@@ -227,21 +265,29 @@ export function isIndexEqualToIndex(a: any, b: any): boolean {
|
|
|
227
265
|
if (String(a.type).toLowerCase() !== String(b.type).toLowerCase()) return false;
|
|
228
266
|
|
|
229
267
|
// Compare attributes as sets (order-insensitive)
|
|
230
|
-
|
|
231
|
-
const
|
|
268
|
+
// Support TablesDB which returns 'columns' instead of 'attributes'
|
|
269
|
+
const attrsAraw = Array.isArray(a.attributes)
|
|
270
|
+
? a.attributes
|
|
271
|
+
: (Array.isArray((a as any).columns) ? (a as any).columns : []);
|
|
272
|
+
const attrsA = [...attrsAraw].sort();
|
|
273
|
+
const attrsB = Array.isArray(b.attributes)
|
|
274
|
+
? [...b.attributes].sort()
|
|
275
|
+
: (Array.isArray((b as any).columns) ? [...(b as any).columns].sort() : []);
|
|
232
276
|
if (attrsA.length !== attrsB.length) return false;
|
|
233
277
|
for (let i = 0; i < attrsA.length; i++) if (attrsA[i] !== attrsB[i]) return false;
|
|
234
278
|
|
|
235
|
-
// Orders are only considered if
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
|
|
279
|
+
// Orders are only considered if CONFIG (b) has orders defined
|
|
280
|
+
// This prevents false positives when Appwrite returns orders but user didn't specify them
|
|
281
|
+
const hasConfigOrders = Array.isArray(b.orders) && b.orders.length > 0;
|
|
282
|
+
if (hasConfigOrders) {
|
|
283
|
+
// Some APIs may expose 'directions' instead of 'orders'
|
|
284
|
+
const ordersA = Array.isArray(a.orders)
|
|
285
|
+
? [...a.orders].sort()
|
|
286
|
+
: (Array.isArray((a as any).directions) ? [...(a as any).directions].sort() : []);
|
|
240
287
|
const ordersB = [...b.orders].sort();
|
|
241
288
|
if (ordersA.length !== ordersB.length) return false;
|
|
242
289
|
for (let i = 0; i < ordersA.length; i++) if (ordersA[i] !== ordersB[i]) return false;
|
|
243
290
|
}
|
|
244
|
-
// If only one side has orders, treat as equal (orders unspecified by user)
|
|
245
291
|
return true;
|
|
246
292
|
}
|
|
247
293
|
|
|
@@ -255,6 +301,8 @@ function compareColumnProperties(
|
|
|
255
301
|
): ColumnPropertyChange[] {
|
|
256
302
|
const changes: ColumnPropertyChange[] = [];
|
|
257
303
|
const t = String(columnType || (newAttribute as any).type || '').toLowerCase();
|
|
304
|
+
const key = newAttribute?.key || 'unknown';
|
|
305
|
+
|
|
258
306
|
const mutableProps = (MUTABLE_PROPERTIES as any)[t] || [];
|
|
259
307
|
const immutableProps = (IMMUTABLE_PROPERTIES as any)[t] || [];
|
|
260
308
|
|
|
@@ -274,7 +322,9 @@ function compareColumnProperties(
|
|
|
274
322
|
let newValue = getNewVal(prop);
|
|
275
323
|
// Special-case: enum elements empty/missing should not trigger updates
|
|
276
324
|
if (t === 'enum' && prop === 'elements') {
|
|
277
|
-
if (!Array.isArray(newValue) || newValue.length === 0)
|
|
325
|
+
if (!Array.isArray(newValue) || newValue.length === 0) {
|
|
326
|
+
newValue = oldValue;
|
|
327
|
+
}
|
|
278
328
|
}
|
|
279
329
|
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
|
280
330
|
if (oldValue.length !== newValue.length || oldValue.some((v: any, i: number) => v !== newValue[i])) {
|
|
@@ -300,11 +350,11 @@ function compareColumnProperties(
|
|
|
300
350
|
// Type change requires recreate (normalize string+elements to enum on old side)
|
|
301
351
|
const oldTypeRaw = String(oldColumn?.type || oldColumn?.columnType || '').toLowerCase();
|
|
302
352
|
const oldHasElements = Array.isArray(oldColumn?.elements) && (oldColumn.elements as any[]).length > 0;
|
|
303
|
-
const
|
|
353
|
+
const oldHasEnumFormat = (oldColumn?.format === 'enum');
|
|
354
|
+
const oldType = oldTypeRaw === 'string' && (oldHasElements || oldHasEnumFormat) ? 'enum' : oldTypeRaw;
|
|
304
355
|
if (oldType && t && oldType !== t && TYPE_CHANGE_REQUIRES_RECREATE.includes(oldType)) {
|
|
305
356
|
changes.push({ property: 'type', oldValue: oldType, newValue: t, requiresRecreate: true });
|
|
306
357
|
}
|
|
307
|
-
|
|
308
358
|
return changes;
|
|
309
359
|
}
|
|
310
360
|
|
|
@@ -348,32 +398,53 @@ function analyzeColumnChanges(
|
|
|
348
398
|
/**
|
|
349
399
|
* Enhanced version of columns diff with detailed change analysis
|
|
350
400
|
* Order: desired first, then existing (matches internal usage here)
|
|
401
|
+
* Handles case-insensitive key matches as renames (recreates)
|
|
351
402
|
*/
|
|
352
403
|
export function diffColumnsDetailed(
|
|
353
404
|
desiredAttributes: Attribute[],
|
|
354
405
|
existingColumns: any[]
|
|
355
406
|
): ColumnOperationPlan {
|
|
407
|
+
// Exact key lookup (case-sensitive)
|
|
356
408
|
const byKey = new Map((existingColumns || []).map((col: any) => [col?.key, col] as const));
|
|
409
|
+
// Case-insensitive key lookup for detecting renames
|
|
410
|
+
const byKeyLower = new Map((existingColumns || []).map((col: any) => [col?.key?.toLowerCase(), col] as const));
|
|
357
411
|
|
|
358
412
|
const toCreate: Attribute[] = [];
|
|
359
413
|
const toUpdate: Array<{ attribute: Attribute; changes: ColumnPropertyChange[] }> = [];
|
|
360
414
|
const toRecreate: Array<{ oldAttribute: any; newAttribute: Attribute }> = [];
|
|
361
415
|
const unchanged: string[] = [];
|
|
416
|
+
const handledExistingKeys = new Set<string>(); // Track which existing columns we've handled
|
|
362
417
|
|
|
363
418
|
for (const attr of desiredAttributes || []) {
|
|
364
419
|
const key = (attr as any)?.key;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
420
|
+
if (!key) continue;
|
|
421
|
+
|
|
422
|
+
// First try exact match
|
|
423
|
+
const exactMatch = byKey.get(key);
|
|
424
|
+
if (exactMatch) {
|
|
425
|
+
handledExistingKeys.add(key);
|
|
426
|
+
const analysis = analyzeColumnChanges(exactMatch, attr);
|
|
427
|
+
if (!analysis.hasChanges) unchanged.push(analysis.columnKey);
|
|
428
|
+
else if (analysis.requiresRecreate) toRecreate.push({ oldAttribute: exactMatch, newAttribute: attr });
|
|
429
|
+
else toUpdate.push({ attribute: attr, changes: analysis.changes });
|
|
368
430
|
continue;
|
|
369
431
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
432
|
+
|
|
433
|
+
// Check for case-insensitive match (rename scenario like oAuthAccounts -> oauthAccounts)
|
|
434
|
+
const caseInsensitiveMatch = byKeyLower.get(key.toLowerCase());
|
|
435
|
+
if (caseInsensitiveMatch && caseInsensitiveMatch.key !== key) {
|
|
436
|
+
// This is a rename - treat as recreate (delete old, create new)
|
|
437
|
+
handledExistingKeys.add(caseInsensitiveMatch.key);
|
|
438
|
+
toRecreate.push({ oldAttribute: caseInsensitiveMatch, newAttribute: attr });
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// No match - it's a new attribute
|
|
443
|
+
toCreate.push(attr);
|
|
374
444
|
}
|
|
375
445
|
|
|
376
446
|
// Note: we keep toDelete empty for now (conservative behavior)
|
|
447
|
+
// Deletions are handled separately in methods.ts
|
|
377
448
|
return { toCreate, toUpdate, toRecreate, toDelete: [], unchanged };
|
|
378
449
|
}
|
|
379
450
|
|
|
@@ -4,12 +4,11 @@ import {
|
|
|
4
4
|
ID,
|
|
5
5
|
Query,
|
|
6
6
|
} from "node-appwrite";
|
|
7
|
-
import { tryAwaitWithRetry, delay, calculateExponentialBackoff } from "
|
|
8
|
-
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
7
|
+
import { tryAwaitWithRetry, delay, calculateExponentialBackoff, MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
|
|
9
8
|
import { chunk } from "es-toolkit";
|
|
10
|
-
import type { DatabaseAdapter } from "
|
|
11
|
-
import { isLegacyDatabases } from "
|
|
12
|
-
import { getAdapter } from "
|
|
9
|
+
import type { DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
|
|
10
|
+
import { isLegacyDatabases } from "@njdamstra/appwrite-utils-helpers";
|
|
11
|
+
import { getAdapter } from "@njdamstra/appwrite-utils-helpers";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Transfers all documents from one collection to another in a different database
|