@njdamstra/appwrite-utils-cli 1.8.9 → 1.10.0
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 +2 -35
- package/dist/collections/indexes.js +1 -3
- 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 +55 -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 +63 -9
- package/dist/main.js +130 -177
- package/dist/migrations/afterImportActions.js +2 -3
- package/dist/migrations/appwriteToX.d.ts +1 -1
- package/dist/migrations/appwriteToX.js +9 -7
- package/dist/migrations/comprehensiveTransfer.js +3 -5
- 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 +2 -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 +7 -3
- 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 +3 -39
- package/src/collections/indexes.ts +2 -4
- package/src/collections/methods.ts +115 -150
- package/src/collections/tableOperations.ts +57 -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 +17 -4
- package/src/init.ts +1 -1
- package/src/interactiveCLI.ts +75 -10
- 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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { MessageFormatter } from
|
|
5
|
-
import { SchemaGenerator } from
|
|
4
|
+
import { MessageFormatter } from '@njdamstra/appwrite-utils-helpers';
|
|
5
|
+
import { SchemaGenerator } from '@njdamstra/appwrite-utils-helpers';
|
|
6
6
|
import { setupDirsFiles } from "../../utils/setupFiles.js";
|
|
7
7
|
import { fetchAllDatabases } from "../../databases/methods.js";
|
|
8
8
|
import type { InteractiveCLI } from "../../interactiveCLI.js";
|
|
@@ -126,7 +126,7 @@ export const schemaCommands = {
|
|
|
126
126
|
]);
|
|
127
127
|
|
|
128
128
|
try {
|
|
129
|
-
const { ConstantsGenerator } = await import("
|
|
129
|
+
const { ConstantsGenerator } = await import("@njdamstra/appwrite-utils-helpers");
|
|
130
130
|
const generator = new ConstantsGenerator((cli as any).controller.config);
|
|
131
131
|
|
|
132
132
|
const include = {
|
|
@@ -2,10 +2,9 @@ import inquirer from "inquirer";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { Storage, Permission, Role, Compression, type Models } from "node-appwrite";
|
|
4
4
|
import type { InteractiveCLI } from "../../interactiveCLI.js";
|
|
5
|
-
import { MessageFormatter } from
|
|
5
|
+
import { MessageFormatter } from '@njdamstra/appwrite-utils-helpers';
|
|
6
6
|
import { listBuckets, createBucket as createBucketApi, deleteBucket as deleteBucketApi } from "../../storage/methods.js";
|
|
7
|
-
import { writeYamlConfig } from "
|
|
8
|
-
import { ConfigManager } from "../../config/ConfigManager.js";
|
|
7
|
+
import { writeYamlConfig, ConfigManager } from "@njdamstra/appwrite-utils-helpers";
|
|
9
8
|
|
|
10
9
|
export const storageCommands = {
|
|
11
10
|
async createBucket(cli: InteractiveCLI): Promise<void> {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
2
|
import { Databases, Storage } from "node-appwrite";
|
|
3
|
-
import { MessageFormatter } from
|
|
3
|
+
import { MessageFormatter } from '@njdamstra/appwrite-utils-helpers';
|
|
4
4
|
import { fetchAllDatabases } from "../../databases/methods.js";
|
|
5
5
|
import { listBuckets } from "../../storage/methods.js";
|
|
6
|
-
import { getClient } from "
|
|
6
|
+
import { getClient } from "@njdamstra/appwrite-utils-helpers";
|
|
7
7
|
import { ComprehensiveTransfer, type ComprehensiveTransferOptions } from "../../migrations/comprehensiveTransfer.js";
|
|
8
8
|
import type { TransferOptions } from "../../migrations/transfer.js";
|
|
9
9
|
import type { InteractiveCLI } from "../../interactiveCLI.js";
|
|
@@ -145,10 +145,7 @@ export const transferCommands = {
|
|
|
145
145
|
fromDb,
|
|
146
146
|
targetDb,
|
|
147
147
|
isRemote,
|
|
148
|
-
collections:
|
|
149
|
-
selectedCollections.length > 0
|
|
150
|
-
? selectedCollections.map((c: any) => c.$id)
|
|
151
|
-
: undefined,
|
|
148
|
+
collections: selectedCollections.map((c: any) => c.$id),
|
|
152
149
|
sourceBucket,
|
|
153
150
|
targetBucket,
|
|
154
151
|
};
|
|
@@ -14,13 +14,11 @@ import {
|
|
|
14
14
|
delay,
|
|
15
15
|
tryAwaitWithRetry,
|
|
16
16
|
calculateExponentialBackoff,
|
|
17
|
-
} from "
|
|
17
|
+
} from "@njdamstra/appwrite-utils-helpers";
|
|
18
18
|
import chalk from "chalk";
|
|
19
19
|
import { Decimal } from "decimal.js";
|
|
20
|
-
import type { DatabaseAdapter, CreateAttributeParams, UpdateAttributeParams, DeleteAttributeParams } from "
|
|
21
|
-
import { logger } from "
|
|
22
|
-
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
23
|
-
import { isDatabaseAdapter } from "../utils/typeGuards.js";
|
|
20
|
+
import type { DatabaseAdapter, CreateAttributeParams, UpdateAttributeParams, DeleteAttributeParams } from "@njdamstra/appwrite-utils-helpers";
|
|
21
|
+
import { logger, MessageFormatter, isDatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
|
|
24
22
|
|
|
25
23
|
// Extreme values that Appwrite may return, which should be treated as undefined
|
|
26
24
|
const EXTREME_MIN_INTEGER = -9223372036854776000;
|
|
@@ -651,13 +649,6 @@ const updateLegacyAttribute = async (
|
|
|
651
649
|
collectionId: string,
|
|
652
650
|
attribute: Attribute
|
|
653
651
|
): Promise<void> => {
|
|
654
|
-
console.log(`DEBUG updateLegacyAttribute before normalizeMinMaxValues:`, {
|
|
655
|
-
key: attribute.key,
|
|
656
|
-
type: attribute.type,
|
|
657
|
-
min: (attribute as any).min,
|
|
658
|
-
max: (attribute as any).max
|
|
659
|
-
});
|
|
660
|
-
|
|
661
652
|
const { min: normalizedMin, max: normalizedMax } =
|
|
662
653
|
normalizeMinMaxValues(attribute);
|
|
663
654
|
|
|
@@ -1515,37 +1506,10 @@ export const createOrUpdateAttribute = async (
|
|
|
1515
1506
|
// `Updating attribute with same key ${attribute.key} but different values`
|
|
1516
1507
|
// );
|
|
1517
1508
|
|
|
1518
|
-
// DEBUG: Log before object merge to detect corruption
|
|
1519
|
-
if ((attribute.key === 'conversationType' || attribute.key === 'messageStreakCount')) {
|
|
1520
|
-
console.log(`[DEBUG] MERGE - key="${attribute.key}"`, {
|
|
1521
|
-
found: {
|
|
1522
|
-
elements: (foundAttribute as any)?.elements,
|
|
1523
|
-
min: (foundAttribute as any)?.min,
|
|
1524
|
-
max: (foundAttribute as any)?.max
|
|
1525
|
-
},
|
|
1526
|
-
desired: {
|
|
1527
|
-
elements: (attribute as any)?.elements,
|
|
1528
|
-
min: (attribute as any)?.min,
|
|
1529
|
-
max: (attribute as any)?.max
|
|
1530
|
-
}
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
1509
|
finalAttribute = {
|
|
1535
1510
|
...foundAttribute,
|
|
1536
1511
|
...attribute,
|
|
1537
1512
|
};
|
|
1538
|
-
|
|
1539
|
-
// DEBUG: Log after object merge to detect corruption
|
|
1540
|
-
if ((finalAttribute.key === 'conversationType' || finalAttribute.key === 'messageStreakCount')) {
|
|
1541
|
-
console.log(`[DEBUG] AFTER_MERGE - key="${finalAttribute.key}"`, {
|
|
1542
|
-
merged: {
|
|
1543
|
-
elements: finalAttribute?.elements,
|
|
1544
|
-
min: (finalAttribute as any)?.min,
|
|
1545
|
-
max: (finalAttribute as any)?.max
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
1513
|
action = "update";
|
|
1550
1514
|
} else if (
|
|
1551
1515
|
!updateEnabled &&
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { indexSchema, type Index } from "@njdamstra/appwrite-utils";
|
|
2
2
|
import { Databases, IndexType, Query, type Models } from "node-appwrite";
|
|
3
|
-
import type { DatabaseAdapter } from "
|
|
4
|
-
import { delay, tryAwaitWithRetry, calculateExponentialBackoff } from "
|
|
5
|
-
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
6
|
-
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
3
|
+
import type { DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
|
|
4
|
+
import { delay, tryAwaitWithRetry, calculateExponentialBackoff, isLegacyDatabases, MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
|
|
7
5
|
|
|
8
6
|
// System attributes that are always available for indexing in Appwrite
|
|
9
7
|
const SYSTEM_ATTRIBUTES = ['$id', '$createdAt', '$updatedAt', '$permissions'];
|
|
@@ -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
|
|
|
@@ -143,10 +143,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
export function normalizeColumnToComparable(col: any): ComparableColumn {
|
|
146
|
-
// Detect enum surfaced as string+elements from server and normalize to enum for comparison
|
|
146
|
+
// Detect enum surfaced as string+elements or string+format:enum from server and normalize to enum for comparison
|
|
147
147
|
let t = String((col?.type ?? col?.columnType ?? '')).toLowerCase();
|
|
148
148
|
const hasElements = Array.isArray(col?.elements) && (col.elements as any[]).length > 0;
|
|
149
|
-
|
|
149
|
+
const hasEnumFormat = (col?.format === 'enum');
|
|
150
|
+
if (t === 'string' && (hasElements || hasEnumFormat)) {
|
|
151
|
+
t = 'enum';
|
|
152
|
+
}
|
|
150
153
|
const base: ComparableColumn = {
|
|
151
154
|
key: col?.key,
|
|
152
155
|
type: t,
|
|
@@ -227,21 +230,29 @@ export function isIndexEqualToIndex(a: any, b: any): boolean {
|
|
|
227
230
|
if (String(a.type).toLowerCase() !== String(b.type).toLowerCase()) return false;
|
|
228
231
|
|
|
229
232
|
// Compare attributes as sets (order-insensitive)
|
|
230
|
-
|
|
231
|
-
const
|
|
233
|
+
// Support TablesDB which returns 'columns' instead of 'attributes'
|
|
234
|
+
const attrsAraw = Array.isArray(a.attributes)
|
|
235
|
+
? a.attributes
|
|
236
|
+
: (Array.isArray((a as any).columns) ? (a as any).columns : []);
|
|
237
|
+
const attrsA = [...attrsAraw].sort();
|
|
238
|
+
const attrsB = Array.isArray(b.attributes)
|
|
239
|
+
? [...b.attributes].sort()
|
|
240
|
+
: (Array.isArray((b as any).columns) ? [...(b as any).columns].sort() : []);
|
|
232
241
|
if (attrsA.length !== attrsB.length) return false;
|
|
233
242
|
for (let i = 0; i < attrsA.length; i++) if (attrsA[i] !== attrsB[i]) return false;
|
|
234
243
|
|
|
235
|
-
// Orders are only considered if
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
239
|
-
|
|
244
|
+
// Orders are only considered if CONFIG (b) has orders defined
|
|
245
|
+
// This prevents false positives when Appwrite returns orders but user didn't specify them
|
|
246
|
+
const hasConfigOrders = Array.isArray(b.orders) && b.orders.length > 0;
|
|
247
|
+
if (hasConfigOrders) {
|
|
248
|
+
// Some APIs may expose 'directions' instead of 'orders'
|
|
249
|
+
const ordersA = Array.isArray(a.orders)
|
|
250
|
+
? [...a.orders].sort()
|
|
251
|
+
: (Array.isArray((a as any).directions) ? [...(a as any).directions].sort() : []);
|
|
240
252
|
const ordersB = [...b.orders].sort();
|
|
241
253
|
if (ordersA.length !== ordersB.length) return false;
|
|
242
254
|
for (let i = 0; i < ordersA.length; i++) if (ordersA[i] !== ordersB[i]) return false;
|
|
243
255
|
}
|
|
244
|
-
// If only one side has orders, treat as equal (orders unspecified by user)
|
|
245
256
|
return true;
|
|
246
257
|
}
|
|
247
258
|
|
|
@@ -255,6 +266,8 @@ function compareColumnProperties(
|
|
|
255
266
|
): ColumnPropertyChange[] {
|
|
256
267
|
const changes: ColumnPropertyChange[] = [];
|
|
257
268
|
const t = String(columnType || (newAttribute as any).type || '').toLowerCase();
|
|
269
|
+
const key = newAttribute?.key || 'unknown';
|
|
270
|
+
|
|
258
271
|
const mutableProps = (MUTABLE_PROPERTIES as any)[t] || [];
|
|
259
272
|
const immutableProps = (IMMUTABLE_PROPERTIES as any)[t] || [];
|
|
260
273
|
|
|
@@ -274,7 +287,9 @@ function compareColumnProperties(
|
|
|
274
287
|
let newValue = getNewVal(prop);
|
|
275
288
|
// Special-case: enum elements empty/missing should not trigger updates
|
|
276
289
|
if (t === 'enum' && prop === 'elements') {
|
|
277
|
-
if (!Array.isArray(newValue) || newValue.length === 0)
|
|
290
|
+
if (!Array.isArray(newValue) || newValue.length === 0) {
|
|
291
|
+
newValue = oldValue;
|
|
292
|
+
}
|
|
278
293
|
}
|
|
279
294
|
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
|
280
295
|
if (oldValue.length !== newValue.length || oldValue.some((v: any, i: number) => v !== newValue[i])) {
|
|
@@ -300,11 +315,11 @@ function compareColumnProperties(
|
|
|
300
315
|
// Type change requires recreate (normalize string+elements to enum on old side)
|
|
301
316
|
const oldTypeRaw = String(oldColumn?.type || oldColumn?.columnType || '').toLowerCase();
|
|
302
317
|
const oldHasElements = Array.isArray(oldColumn?.elements) && (oldColumn.elements as any[]).length > 0;
|
|
303
|
-
const
|
|
318
|
+
const oldHasEnumFormat = (oldColumn?.format === 'enum');
|
|
319
|
+
const oldType = oldTypeRaw === 'string' && (oldHasElements || oldHasEnumFormat) ? 'enum' : oldTypeRaw;
|
|
304
320
|
if (oldType && t && oldType !== t && TYPE_CHANGE_REQUIRES_RECREATE.includes(oldType)) {
|
|
305
321
|
changes.push({ property: 'type', oldValue: oldType, newValue: t, requiresRecreate: true });
|
|
306
322
|
}
|
|
307
|
-
|
|
308
323
|
return changes;
|
|
309
324
|
}
|
|
310
325
|
|
|
@@ -348,32 +363,53 @@ function analyzeColumnChanges(
|
|
|
348
363
|
/**
|
|
349
364
|
* Enhanced version of columns diff with detailed change analysis
|
|
350
365
|
* Order: desired first, then existing (matches internal usage here)
|
|
366
|
+
* Handles case-insensitive key matches as renames (recreates)
|
|
351
367
|
*/
|
|
352
368
|
export function diffColumnsDetailed(
|
|
353
369
|
desiredAttributes: Attribute[],
|
|
354
370
|
existingColumns: any[]
|
|
355
371
|
): ColumnOperationPlan {
|
|
372
|
+
// Exact key lookup (case-sensitive)
|
|
356
373
|
const byKey = new Map((existingColumns || []).map((col: any) => [col?.key, col] as const));
|
|
374
|
+
// Case-insensitive key lookup for detecting renames
|
|
375
|
+
const byKeyLower = new Map((existingColumns || []).map((col: any) => [col?.key?.toLowerCase(), col] as const));
|
|
357
376
|
|
|
358
377
|
const toCreate: Attribute[] = [];
|
|
359
378
|
const toUpdate: Array<{ attribute: Attribute; changes: ColumnPropertyChange[] }> = [];
|
|
360
379
|
const toRecreate: Array<{ oldAttribute: any; newAttribute: Attribute }> = [];
|
|
361
380
|
const unchanged: string[] = [];
|
|
381
|
+
const handledExistingKeys = new Set<string>(); // Track which existing columns we've handled
|
|
362
382
|
|
|
363
383
|
for (const attr of desiredAttributes || []) {
|
|
364
384
|
const key = (attr as any)?.key;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
385
|
+
if (!key) continue;
|
|
386
|
+
|
|
387
|
+
// First try exact match
|
|
388
|
+
const exactMatch = byKey.get(key);
|
|
389
|
+
if (exactMatch) {
|
|
390
|
+
handledExistingKeys.add(key);
|
|
391
|
+
const analysis = analyzeColumnChanges(exactMatch, attr);
|
|
392
|
+
if (!analysis.hasChanges) unchanged.push(analysis.columnKey);
|
|
393
|
+
else if (analysis.requiresRecreate) toRecreate.push({ oldAttribute: exactMatch, newAttribute: attr });
|
|
394
|
+
else toUpdate.push({ attribute: attr, changes: analysis.changes });
|
|
368
395
|
continue;
|
|
369
396
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
397
|
+
|
|
398
|
+
// Check for case-insensitive match (rename scenario like oAuthAccounts -> oauthAccounts)
|
|
399
|
+
const caseInsensitiveMatch = byKeyLower.get(key.toLowerCase());
|
|
400
|
+
if (caseInsensitiveMatch && caseInsensitiveMatch.key !== key) {
|
|
401
|
+
// This is a rename - treat as recreate (delete old, create new)
|
|
402
|
+
handledExistingKeys.add(caseInsensitiveMatch.key);
|
|
403
|
+
toRecreate.push({ oldAttribute: caseInsensitiveMatch, newAttribute: attr });
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// No match - it's a new attribute
|
|
408
|
+
toCreate.push(attr);
|
|
374
409
|
}
|
|
375
410
|
|
|
376
411
|
// Note: we keep toDelete empty for now (conservative behavior)
|
|
412
|
+
// Deletions are handled separately in methods.ts
|
|
377
413
|
return { toCreate, toUpdate, toRecreate, toDelete: [], unchanged };
|
|
378
414
|
}
|
|
379
415
|
|